imperal-sdk 4.2.14__tar.gz → 4.2.16__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 (212) hide show
  1. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/CHANGELOG.md +62 -0
  2. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/PKG-INFO +1 -1
  3. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/__init__.py +1 -1
  4. imperal_sdk-4.2.16/src/imperal_sdk/chat/exceptions.py +14 -0
  5. imperal_sdk-4.2.16/src/imperal_sdk/chat/execution.py +330 -0
  6. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/chat/guards.py +102 -0
  7. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/chat/handler.py +11 -294
  8. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_handler_p2.py +51 -0
  9. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/.github/workflows/identity-contract.yml +0 -0
  10. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/.github/workflows/publish.yml +0 -0
  11. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/.github/workflows/test.yml +0 -0
  12. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/.gitignore +0 -0
  13. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/LICENSE +0 -0
  14. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/README.md +0 -0
  15. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/api_surface.json +0 -0
  16. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/pyproject.toml +0 -0
  17. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/.codebase-index-cache.pkl +0 -0
  18. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/ai/__init__.py +0 -0
  19. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/ai/client.py +0 -0
  20. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/auth/__init__.py +0 -0
  21. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/auth/client.py +0 -0
  22. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/auth/middleware.py +0 -0
  23. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/billing/__init__.py +0 -0
  24. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/billing/client.py +0 -0
  25. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/cache/__init__.py +0 -0
  26. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/cache/client.py +0 -0
  27. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/cache/protocol.py +0 -0
  28. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/chat/__init__.py +0 -0
  29. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/chat/action_result.py +0 -0
  30. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/chat/error_codes.py +0 -0
  31. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/chat/extension.py +0 -0
  32. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/chat/filters.py +0 -0
  33. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/chat/kernel_primitives.py +0 -0
  34. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/chat/narration.py +0 -0
  35. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/chat/narration_guard.py +0 -0
  36. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/chat/prompt.py +0 -0
  37. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/chat/refusal.py +0 -0
  38. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/chat/retry.py +0 -0
  39. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/cli/__init__.py +0 -0
  40. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/cli/main.py +0 -0
  41. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/config/__init__.py +0 -0
  42. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/config/client.py +0 -0
  43. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/context.py +0 -0
  44. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/db/__init__.py +0 -0
  45. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/db/client.py +0 -0
  46. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/errors.py +0 -0
  47. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/extension.py +0 -0
  48. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/extensions/__init__.py +0 -0
  49. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/extensions/client.py +0 -0
  50. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/http/__init__.py +0 -0
  51. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/http/client.py +0 -0
  52. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/manifest.py +0 -0
  53. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/manifest_schema.py +0 -0
  54. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/notify/__init__.py +0 -0
  55. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/notify/client.py +0 -0
  56. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/prompts/__init__.py +0 -0
  57. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/prompts/icnli_integrity_rules.txt +0 -0
  58. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/prompts/kernel_formatting_rule.txt +0 -0
  59. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/prompts/kernel_proactivity_rule.txt +0 -0
  60. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/protocols.py +0 -0
  61. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/rpc/__init__.py +0 -0
  62. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/rpc/codec.py +0 -0
  63. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/rpc/contract.py +0 -0
  64. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/runtime/__init__.py +0 -0
  65. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/runtime/executor.py +0 -0
  66. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/runtime/llm_provider.py +0 -0
  67. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/runtime/message_adapter.py +0 -0
  68. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/schemas/action_result.schema.json +0 -0
  69. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/schemas/balance_info.schema.json +0 -0
  70. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/schemas/chat_result.schema.json +0 -0
  71. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/schemas/completion_result.schema.json +0 -0
  72. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/schemas/document.schema.json +0 -0
  73. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/schemas/event.schema.json +0 -0
  74. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/schemas/file_info.schema.json +0 -0
  75. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/schemas/function_call.schema.json +0 -0
  76. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/schemas/http_response.schema.json +0 -0
  77. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/schemas/imperal.schema.json +0 -0
  78. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/schemas/limits_result.schema.json +0 -0
  79. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/schemas/subscription_info.schema.json +0 -0
  80. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/secrets/__init__.py +0 -0
  81. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/secrets/client.py +0 -0
  82. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/secrets/exceptions.py +0 -0
  83. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/secrets/panel_handler.py +0 -0
  84. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/secrets/spec.py +0 -0
  85. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/security/__init__.py +0 -0
  86. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/security/call_token.py +0 -0
  87. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/skeleton/__init__.py +0 -0
  88. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/skeleton/client.py +0 -0
  89. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/storage/__init__.py +0 -0
  90. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/storage/client.py +0 -0
  91. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/store/__init__.py +0 -0
  92. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/store/client.py +0 -0
  93. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/store/exceptions.py +0 -0
  94. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/testing/__init__.py +0 -0
  95. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/testing/mock_context.py +0 -0
  96. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/testing/mock_secrets.py +0 -0
  97. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/tools/__init__.py +0 -0
  98. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/tools/client.py +0 -0
  99. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/tools/generate_api_surface.py +0 -0
  100. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/tools/validate_identity_contract.py +0 -0
  101. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/types/__init__.py +0 -0
  102. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/types/action_result.py +0 -0
  103. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/types/chat_result.py +0 -0
  104. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/types/client_contracts.py +0 -0
  105. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/types/contracts.py +0 -0
  106. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/types/contributions.py +0 -0
  107. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/types/events.py +0 -0
  108. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/types/health.py +0 -0
  109. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/types/identity.py +0 -0
  110. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/types/models.py +0 -0
  111. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/types/pagination.py +0 -0
  112. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/types/store_contracts.py +0 -0
  113. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/ui/__init__.py +0 -0
  114. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/ui/actions.py +0 -0
  115. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/ui/base.py +0 -0
  116. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/ui/data.py +0 -0
  117. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/ui/display.py +0 -0
  118. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/ui/feedback.py +0 -0
  119. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/ui/graph.py +0 -0
  120. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/ui/input_components.py +0 -0
  121. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/ui/interactive.py +0 -0
  122. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/ui/layout.py +0 -0
  123. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/ui/theme.py +0 -0
  124. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/validator.py +0 -0
  125. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/src/imperal_sdk/validator_v1_6_0.py +0 -0
  126. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/conftest.py +0 -0
  127. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/contracts/__init__.py +0 -0
  128. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/contracts/test_store_contracts.py +0 -0
  129. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/fixtures/openapi/auth-gateway.json +0 -0
  130. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/fixtures/openapi/registry.json +0 -0
  131. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/fixtures/openapi/sharelock-cases.json +0 -0
  132. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/rpc/__init__.py +0 -0
  133. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/rpc/test_codec.py +0 -0
  134. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/rpc/test_contract.py +0 -0
  135. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/runtime/__init__.py +0 -0
  136. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/runtime/test_llm_provider_config_store.py +0 -0
  137. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/runtime/test_llm_provider_ctx_injection.py +0 -0
  138. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/store/__init__.py +0 -0
  139. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/store/test_list_users_client.py +0 -0
  140. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/store/test_query_all_client.py +0 -0
  141. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_action_result_typed.py +0 -0
  142. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_as_user.py +0 -0
  143. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_auth.py +0 -0
  144. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_billing.py +0 -0
  145. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_cache_client.py +0 -0
  146. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_cache_model.py +0 -0
  147. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_call_token.py +0 -0
  148. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_chat_extension_deprecation.py +0 -0
  149. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_chat_filters.py +0 -0
  150. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_chat_function_background_flag.py +0 -0
  151. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_chat_guards.py +0 -0
  152. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_chat_guards_bleed.py +0 -0
  153. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_chat_prompt.py +0 -0
  154. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_chat_pydantic_retry.py +0 -0
  155. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_chat_result.py +0 -0
  156. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_cli.py +0 -0
  157. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_client_contracts.py +0 -0
  158. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_config_client.py +0 -0
  159. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_context.py +0 -0
  160. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_context_background_task.py +0 -0
  161. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_context_deliver_chat_message.py +0 -0
  162. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_context_guards.py +0 -0
  163. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_contracts.py +0 -0
  164. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_contracts_live.py +0 -0
  165. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_contributions.py +0 -0
  166. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_document_contract.py +0 -0
  167. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_emits_decorator.py +0 -0
  168. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_error_codes.py +0 -0
  169. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_errors.py +0 -0
  170. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_event_schema_v2.py +0 -0
  171. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_events_health.py +0 -0
  172. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_extension.py +0 -0
  173. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_extension_v2.py +0 -0
  174. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_extensions_emit.py +0 -0
  175. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_http_timeout_override.py +0 -0
  176. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_id_shape_guard.py +0 -0
  177. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_identity_contract.py +0 -0
  178. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_imperal_schema_v2.py +0 -0
  179. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_kernel_primitives.py +0 -0
  180. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_manifest.py +0 -0
  181. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_manifest_roundtrip_gate.py +0 -0
  182. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_manifest_schema.py +0 -0
  183. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_manifest_v2_events.py +0 -0
  184. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_manifest_v2_other_sections.py +0 -0
  185. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_manifest_v2_webhooks.py +0 -0
  186. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_manifest_validator_v2.py +0 -0
  187. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_mock_context.py +0 -0
  188. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_models.py +0 -0
  189. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_narration_emission.py +0 -0
  190. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_narration_guard.py +0 -0
  191. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_openai_max_completion_tokens.py +0 -0
  192. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_pagination.py +0 -0
  193. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_panel_rendering_contract.py +0 -0
  194. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_panels.py +0 -0
  195. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_skeleton_decorator.py +0 -0
  196. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_spec_validation.py +0 -0
  197. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_tools_client.py +0 -0
  198. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_ui.py +0 -0
  199. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_ui_fileupload_enhanced.py +0 -0
  200. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_ui_html.py +0 -0
  201. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_ui_image_enhanced.py +0 -0
  202. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_ui_open.py +0 -0
  203. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_ui_theme.py +0 -0
  204. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_user.py +0 -0
  205. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_v7_emit_refusal.py +0 -0
  206. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_validator.py +0 -0
  207. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_validator_drift.py +0 -0
  208. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_validator_pep563.py +0 -0
  209. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_validator_v1_6_0_rules.py +0 -0
  210. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/test_write_arg_bleed.py +0 -0
  211. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/tools/__init__.py +0 -0
  212. {imperal_sdk-4.2.14 → imperal_sdk-4.2.16}/tests/tools/test_generate_api_surface.py +0 -0
@@ -2,6 +2,68 @@
2
2
 
3
3
  All notable changes to `imperal-sdk` are documented here.
4
4
 
5
+ ## 4.2.16 — 2026-05-15
6
+
7
+ Enriched tool_use log with `UNKNOWN_FUNCTION(will-reject)` marker when the
8
+ LLM hallucinates a tool name not in the extension's `_functions` schema.
9
+ Caught at `handler.py:185` guard with `UNKNOWN_SUB_FUNCTION` error_code;
10
+ this change makes the rejection visible operator-side. Soak monitoring
11
+ can grep `UNKNOWN_FUNCTION(will-reject)` to track LLM hallucination rate.
12
+
13
+ Behavior change: log line format only. No federal contract impact. No
14
+ SDK API surface change.
15
+
16
+ Closes: sql-db isolation investigation false-positive (operator reading
17
+ journals saw `tool_sql_db_chat (round 2): send(...)` and assumed an
18
+ isolation breach; actual cause was LLM hallucination caught by existing
19
+ UNKNOWN_SUB_FUNCTION guard).
20
+
21
+ ## 4.2.15 — 2026-05-14
22
+
23
+ **Feat: federal placeholder-args guard (I-PARAMS-NO-PLACEHOLDER-VALUES)**
24
+
25
+ New ChatExtension guard that rejects any tool call whose arg values look
26
+ like LLM-emitted placeholder sentinels — e.g. `<UNKNOWN>`, `<TODO>`,
27
+ `<MISSING>`, `<EMAIL>`, `<PASSWORD>`, `<USER_ID>`. Runs **before**
28
+ write-arg-bleed, target-scope, and 2-step confirmation guards so the
29
+ dispatch is short-circuited before any billing-charged work or audit-ledger
30
+ pollution. Friendly instruction-to-LLM rejection text feeds back through
31
+ the chat loop as a synthetic tool_result so the LLM can self-correct
32
+ and ask the user a clarifying question.
33
+
34
+ Motivating incident (2026-05-14): admin extension's `tool_admin_chat`
35
+ ChatExtension wrapper LLM produced `create_user({'email': '<UNKNOWN>',
36
+ 'password': '<UNKNOWN>'})` when the user had not yet provided concrete
37
+ values. The anti-fab response-side layer correctly caught the drift
38
+ (`server did not reflect 'email': requested '<UNKNOWN>', got None`) but by
39
+ then the dispatch had already wasted billing, polluted `action_ledger`
40
+ with `target=<UNKNOWN>` rows, and produced an opaque user-visible failure.
41
+ This guard fails fast on the request side.
42
+
43
+ ### Added
44
+
45
+ - **`check_placeholder_args(tu, action_type) -> str | None`** in
46
+ `imperal_sdk.chat.guards` — recursive scan of `tu.input` (dict/list/str)
47
+ for values matching `^<[A-Z][A-Z0-9_]*>$`. Tight regex — narrow
48
+ false-positive surface: matches only uppercase-ASCII sentinel tokens,
49
+ ignores prose containing `<UNKNOWN>` as a substring (e.g. error message
50
+ bodies). Whitespace-tolerant via `.strip()`.
51
+
52
+ - **`_PLACEHOLDER_RE`** and **`_scan_for_placeholders(value)`** helpers
53
+ (module-private; recursive over dict values and list/tuple items).
54
+
55
+ - Integration into `check_guards()` orchestrator before
56
+ `check_write_arg_bleed`. Federal invariant
57
+ **I-PARAMS-NO-PLACEHOLDER-VALUES** registered in kernel
58
+ `tests/federal/_invariant_assertions.py`.
59
+
60
+ ### Why a PATCH bump (not MINOR)
61
+
62
+ Strictly additive — new guard with default-allow surface. Existing
63
+ extensions emit no placeholder sentinels in legitimate flows, so the
64
+ guard is a no-op for every real call. No manifest schema change, no API
65
+ break, no rebuild required for downstream extensions.
66
+
5
67
  ## 4.2.14 — 2026-05-14
6
68
 
7
69
  **Fix: regenerate static `imperal.schema.json` to match runtime `Manifest` model**
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: imperal-sdk
3
- Version: 4.2.14
3
+ Version: 4.2.16
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
@@ -39,7 +39,7 @@ from imperal_sdk.secrets import (
39
39
  SecretValueTooLarge, SecretDeclarationConflict,
40
40
  )
41
41
 
42
- __version__ = "4.2.14"
42
+ __version__ = "4.2.16"
43
43
 
44
44
  __all__ = [
45
45
  # Core
@@ -0,0 +1,14 @@
1
+ # Copyright (c) 2026 Imperal, Inc., Valentin Scerbacov, and contributors
2
+ # Licensed under the AGPL-3.0 License. See LICENSE file for details.
3
+ """Shared chat-loop exceptions.
4
+
5
+ Lives in a leaf module so both `imperal_sdk.chat.handler` and
6
+ `imperal_sdk.chat.execution` can import without creating an import
7
+ cycle. handler.py re-exports `TaskCancelled` for back-compat with any
8
+ caller doing `from imperal_sdk.chat.handler import TaskCancelled`.
9
+ """
10
+ from __future__ import annotations
11
+
12
+
13
+ class TaskCancelled(Exception):
14
+ """Raised by ctx.progress() when the user cancels a task."""
@@ -0,0 +1,330 @@
1
+ # Copyright (c) 2026 Imperal, Inc., Valentin Scerbacov, and contributors
2
+ # Licensed under the AGPL-3.0 License. See LICENSE file for details.
3
+ """Single-function-call executor for ChatExtension tool-use loop.
4
+
5
+ Extracted from imperal_sdk.chat.handler in v5-27 god-file split (handler.py
6
+ 717 LOC → ~425 LOC). `_execute_function` runs ONE tool_use block end-to-end:
7
+ Pydantic validation (with bounded retry feedback loop per
8
+ I-PYDANTIC-RETRY-BUDGET), guard pipeline, the actual handler call, and
9
+ return-shape normalisation. Module-private — no external callers.
10
+
11
+ Federal hooks preserved verbatim:
12
+ * P2 Task 20 — structured error_code on PydanticValidationError + unknown
13
+ sub-function early exit (no raw str(e) into tool_result).
14
+ * I-PYDANTIC-RETRY-BUDGET / I-PYDANTIC-FEEDBACK-STRUCTURED — bounded retry
15
+ with structured prose feedback piped through chat/retry helpers.
16
+ """
17
+ from __future__ import annotations
18
+ import json
19
+ import logging
20
+ from typing import TYPE_CHECKING
21
+
22
+ from pydantic import ValidationError as PydanticValidationError
23
+
24
+ from imperal_sdk.chat.filters import trim_tool_result
25
+ from imperal_sdk.chat.action_result import ActionResult
26
+ from imperal_sdk.chat.exceptions import TaskCancelled
27
+ from imperal_sdk.chat.retry import (
28
+ format_pydantic_for_llm,
29
+ _emit_retry_outcome,
30
+ _RETRY_BUDGET,
31
+ _validation_missing_field_response,
32
+ )
33
+
34
+ if TYPE_CHECKING:
35
+ from imperal_sdk.chat.extension import ChatExtension
36
+
37
+ log = logging.getLogger(__name__)
38
+
39
+
40
+ async def _execute_function(
41
+ chat_ext: ChatExtension, ctx, tu, action_type: str, cfg: dict,
42
+ *,
43
+ retry_ctx: dict | None = None,
44
+ ) -> str:
45
+ """Execute a single function call and return the tool result content string.
46
+
47
+ When ``retry_ctx`` is provided AND the function uses a Pydantic params
48
+ model, ``PydanticValidationError`` triggers up to ``_RETRY_BUDGET=2``
49
+ retries with structured prose feedback to the LLM. Without ``retry_ctx``
50
+ (or for legacy ``**kwargs`` extensions), behavior is exactly the
51
+ pre-feature implementation.
52
+
53
+ ``retry_ctx`` shape (passed by handle_message tool-use loop):
54
+ client, messages, _system, _exec_cfg, _tools_for_llm,
55
+ _tool_use_mt, _api_kwargs
56
+
57
+ Pre-guards (UNKNOWN_SUB_FUNCTION, I-AH-1 fabricated_id) run BEFORE the
58
+ retry loop and short-circuit with their own fc-append.
59
+
60
+ I-AH-1 federal: fabricated-id re-check fires on every retry attempt
61
+ (security guard remains effective across retries).
62
+
63
+ Error contract (I-ERR-CODE-1): every failure surfaces in BOTH the
64
+ JSON-encoded content AND ``_functions_called[-1]["result"]`` as a dict
65
+ carrying an ``error_code`` drawn from
66
+ :mod:`imperal_sdk.chat.error_codes`. No raw ``str(exception)`` output.
67
+ """
68
+ # ── Unknown sub-function early exit ──────────────────────────────────
69
+ if tu.name not in chat_ext._functions:
70
+ available = list(chat_ext._functions.keys())
71
+ content = json.dumps({
72
+ "RESULT": "ERROR",
73
+ "error_code": "UNKNOWN_SUB_FUNCTION",
74
+ "detail": f"'{tu.name}' not in this extension. Available: {available}",
75
+ })
76
+ chat_ext._functions_called.append({
77
+ "name": tu.name, "params": tu.input, "action_type": action_type,
78
+ "success": False, "intercepted": False, "event": "",
79
+ "result": {"error_code": "UNKNOWN_SUB_FUNCTION"},
80
+ })
81
+ return trim_tool_result(content, cfg["max_result_tokens"], cfg["list_truncate_items"], cfg["string_truncate_chars"])
82
+
83
+ _func_def = chat_ext._functions[tu.name]
84
+
85
+ # I-AH-1 L3: pre-validation shape guard — reject empirically observed
86
+ # fabricated message_id slug shapes BEFORE Pydantic coercion so error
87
+ # feedback to the LLM is specific ("FABRICATED_ID_SHAPE") rather than
88
+ # generic ("VALIDATION_MISSING_FIELD"). Closes Bug-1 from prod chat
89
+ # 2026-05-01.
90
+ from imperal_sdk.chat.guards import check_id_shape_fabrication
91
+ _id_rejection = check_id_shape_fabrication(tu.input or {})
92
+ if _id_rejection is not None:
93
+ log.warning(
94
+ "ChatExtension I-AH-1 reject %s field=%s value=%r",
95
+ tu.name, _id_rejection["field"], _id_rejection["value"],
96
+ )
97
+ content = json.dumps({"RESULT": "ERROR", **_id_rejection})
98
+ chat_ext._functions_called.append({
99
+ "name": tu.name, "params": tu.input, "action_type": action_type,
100
+ "success": False, "intercepted": True, "event": "",
101
+ "result": _id_rejection,
102
+ })
103
+ return trim_tool_result(
104
+ content, cfg["max_result_tokens"],
105
+ cfg["list_truncate_items"], cfg["string_truncate_chars"],
106
+ )
107
+
108
+ # === Pydantic-aware retry loop (SPEC2-LLM-ARGS-QUALITY, v4.1.0) ===
109
+ current_tu = tu
110
+ retry_count = 0
111
+ _ext_name = chat_ext.tool_name
112
+
113
+ # Eligibility for retry: retry_ctx provided AND function uses a Pydantic
114
+ # params model (legacy **kwargs paths cannot raise PydanticValidationError).
115
+ _retry_eligible = (
116
+ retry_ctx is not None
117
+ and bool(_func_def._pydantic_model)
118
+ and bool(_func_def._pydantic_param)
119
+ )
120
+
121
+ while True:
122
+ try:
123
+ # LONGRUN-V1 Component D (v4.2.13+) — declarative background-task sugar.
124
+ # When the handler is decorated with @chat.function(background=True),
125
+ # the SDK auto-wraps the call in ctx.background_task() and returns an
126
+ # immediate ack envelope to the LLM. The actual handler runs detached;
127
+ # the platform delivers its returned ActionResult as a fresh bot turn.
128
+ if getattr(_func_def, "background", False):
129
+ _bg_pydantic_model = _func_def._pydantic_model
130
+ _bg_pydantic_param = _func_def._pydantic_param
131
+ _bg_input = current_tu.input or {}
132
+ _bg_fn = _func_def.func
133
+
134
+ async def _bg_coro():
135
+ if _bg_pydantic_model and _bg_pydantic_param:
136
+ _mi = _bg_pydantic_model(**_bg_input)
137
+ return await _bg_fn(ctx, **{_bg_pydantic_param: _mi})
138
+ return await _bg_fn(ctx, **_bg_input)
139
+
140
+ _bg_task_id = await ctx.background_task(
141
+ _bg_coro(),
142
+ long_running=bool(getattr(_func_def, "long_running", False)),
143
+ name=current_tu.name,
144
+ )
145
+ result = ActionResult.success(
146
+ summary=(
147
+ f"Started '{current_tu.name}' in background — "
148
+ "the result will be sent to chat when it finishes."
149
+ ),
150
+ data={"task_id": _bg_task_id, "background": True},
151
+ )
152
+ elif _func_def._pydantic_model and _func_def._pydantic_param:
153
+ _model_instance = _func_def._pydantic_model(**(current_tu.input or {}))
154
+ result = await _func_def.func(ctx, **{_func_def._pydantic_param: _model_instance})
155
+ else:
156
+ result = await _func_def.func(ctx, **current_tu.input)
157
+
158
+ # === SUCCESS path ===
159
+ _is_action_result = isinstance(result, ActionResult)
160
+ if _is_action_result:
161
+ content = json.dumps(result.to_dict(), default=str, ensure_ascii=False)
162
+ else:
163
+ content = json.dumps(result, default=str, ensure_ascii=False)
164
+ if _func_def.event:
165
+ log.warning(
166
+ f"ChatExtension {chat_ext.tool_name}: function '{current_tu.name}' "
167
+ f"has event='{_func_def.event}' but returned dict, not ActionResult"
168
+ )
169
+ content = trim_tool_result(
170
+ content, cfg["max_result_tokens"],
171
+ cfg["list_truncate_items"], cfg["string_truncate_chars"],
172
+ )
173
+
174
+ if _is_action_result:
175
+ success = result.status == "success"
176
+ else:
177
+ success = True
178
+ if isinstance(result, dict):
179
+ if result.get("RESULT") == "ERROR" or result.get("error"):
180
+ success = False
181
+ elif "success" in result:
182
+ success = bool(result["success"])
183
+
184
+ chat_ext._functions_called.append({
185
+ "name": current_tu.name, "params": current_tu.input,
186
+ "action_type": action_type, "success": success,
187
+ "intercepted": False,
188
+ "event": _func_def.event if _is_action_result else "",
189
+ "result": result if _is_action_result else None,
190
+ })
191
+ _emit_retry_outcome(
192
+ tool=current_tu.name, ext=_ext_name,
193
+ outcome=("no_retry" if retry_count == 0 else "success"),
194
+ retry_count=retry_count,
195
+ )
196
+ return content
197
+
198
+ except TaskCancelled:
199
+ raise
200
+
201
+ except PydanticValidationError as e:
202
+ if not _retry_eligible or retry_count >= _RETRY_BUDGET:
203
+ # Exhausted OR not eligible for retry — existing failure handling.
204
+ content = _validation_missing_field_response(
205
+ e=e, chat_ext=chat_ext, tu=current_tu,
206
+ action_type=action_type, cfg=cfg,
207
+ )
208
+ if _retry_eligible:
209
+ _emit_retry_outcome(
210
+ tool=current_tu.name, ext=_ext_name,
211
+ outcome="exhausted", retry_count=retry_count,
212
+ )
213
+ return content
214
+
215
+ # Retry path: re-prompt LLM with structured prose feedback.
216
+ prose = format_pydantic_for_llm(e)
217
+ log.info(
218
+ f"chat_handler validation_retry tool={current_tu.name} "
219
+ f"retry_count={retry_count + 1}/{_RETRY_BUDGET}"
220
+ )
221
+
222
+ tmp_messages = list(retry_ctx["messages"]) + [
223
+ {"role": "assistant", "content": [current_tu]},
224
+ {"role": "user", "content": [
225
+ {"type": "tool_result", "tool_use_id": current_tu.id, "content": prose}
226
+ ]},
227
+ ]
228
+ retry_resp = await retry_ctx["client"].create_message(
229
+ max_tokens=retry_ctx["_tool_use_mt"],
230
+ system=retry_ctx["_system"],
231
+ messages=tmp_messages,
232
+ tools=retry_ctx["_tools_for_llm"],
233
+ cfg=retry_ctx["_exec_cfg"],
234
+ **retry_ctx["_api_kwargs"],
235
+ )
236
+ # Mirror the main loop's usage callback for retry LLM calls
237
+ # (handler.py:411-426). Without this, retry token cost is silently
238
+ # dropped from billing/observability.
239
+ _usage_cb = getattr(ctx, "_llm_usage_callback", None)
240
+ if _usage_cb and hasattr(retry_resp, "usage") and retry_resp.usage is not None:
241
+ try:
242
+ from imperal_sdk.runtime.llm_provider import LLMUsage
243
+ _exec_cfg = retry_ctx["_exec_cfg"]
244
+ _uid = str(getattr(ctx.user, "id", "")) if hasattr(ctx, "user") and ctx.user else ""
245
+ _usage = LLMUsage(
246
+ provider=_exec_cfg.provider,
247
+ model=_exec_cfg.model,
248
+ input_tokens=getattr(retry_resp.usage, "input_tokens", 0) or 0,
249
+ output_tokens=getattr(retry_resp.usage, "output_tokens", 0) or 0,
250
+ is_byollm=_exec_cfg.is_byollm,
251
+ purpose="execution",
252
+ user_id=_uid,
253
+ )
254
+ await _usage_cb(_usage)
255
+ except Exception as _e:
256
+ log.debug(f"retry usage callback failed: {_e}") # NEVER raise
257
+ new_tools = [b for b in retry_resp.content if getattr(b, "type", None) == "tool_use"]
258
+ new_tu = next((b for b in new_tools if b.name == current_tu.name), None)
259
+ if new_tu is None:
260
+ # LLM gave up (final text or different tool). Existing failure shape.
261
+ content = _validation_missing_field_response(
262
+ e=e, chat_ext=chat_ext, tu=current_tu,
263
+ action_type=action_type, cfg=cfg,
264
+ )
265
+ _emit_retry_outcome(
266
+ tool=current_tu.name, ext=_ext_name,
267
+ outcome="llm_gave_up", retry_count=retry_count,
268
+ )
269
+ return content
270
+
271
+ # I-AH-1 federal re-check on retry (spec section 8 E15).
272
+ _ret_id_rejection = check_id_shape_fabrication(new_tu.input or {})
273
+ if _ret_id_rejection is not None:
274
+ log.warning(
275
+ "ChatExtension I-AH-1 reject-on-retry %s field=%s value=%r",
276
+ new_tu.name, _ret_id_rejection["field"], _ret_id_rejection["value"],
277
+ )
278
+ content = json.dumps({"RESULT": "ERROR", **_ret_id_rejection})
279
+ chat_ext._functions_called.append({
280
+ "name": new_tu.name, "params": new_tu.input,
281
+ "action_type": action_type, "success": False,
282
+ "intercepted": True, "event": "",
283
+ "result": _ret_id_rejection,
284
+ })
285
+ _emit_retry_outcome(
286
+ tool=current_tu.name, ext=_ext_name,
287
+ outcome="fabricated_id_on_retry", retry_count=retry_count + 1,
288
+ )
289
+ return trim_tool_result(
290
+ content, cfg["max_result_tokens"],
291
+ cfg["list_truncate_items"], cfg["string_truncate_chars"],
292
+ )
293
+
294
+ # Redundant retry detection (byte-identical args).
295
+ if json.dumps(new_tu.input or {}, sort_keys=True) == json.dumps(current_tu.input or {}, sort_keys=True):
296
+ _emit_retry_outcome(
297
+ tool=current_tu.name, ext=_ext_name,
298
+ outcome="redundant", retry_count=retry_count + 1,
299
+ )
300
+ log.warning(
301
+ "chat_handler validation_retry_redundant tool=%s args_unchanged=true retry_count=%d",
302
+ current_tu.name, retry_count + 1,
303
+ )
304
+
305
+ current_tu = new_tu
306
+ retry_count += 1
307
+ continue # while True
308
+
309
+ except Exception as e:
310
+ log.error(f"ChatExtension internal error {current_tu.name}: {e}", exc_info=True)
311
+ content = json.dumps({
312
+ "RESULT": "ERROR",
313
+ "error_code": "INTERNAL",
314
+ "error_class": type(e).__name__,
315
+ })
316
+ chat_ext._functions_called.append({
317
+ "name": current_tu.name, "params": current_tu.input,
318
+ "action_type": action_type, "success": False,
319
+ "intercepted": False, "event": "",
320
+ "result": {"error_code": "INTERNAL", "error_class": type(e).__name__},
321
+ })
322
+ return trim_tool_result(
323
+ content, cfg["max_result_tokens"],
324
+ cfg["list_truncate_items"], cfg["string_truncate_chars"],
325
+ )
326
+
327
+
328
+ # ---------------------------------------------------------------------------
329
+ # Main entry point
330
+ # ---------------------------------------------------------------------------
@@ -118,6 +118,22 @@ def check_guards(
118
118
  # Do not append to _functions_called; do not return a verdict — fall
119
119
  # through to target_scope + confirmation guards below.
120
120
 
121
+ # ── Placeholder-args guard (I-PARAMS-NO-PLACEHOLDER-VALUES) ───
122
+ # Defence-in-depth: reject ANY tool call whose arg values are LLM-emitted
123
+ # placeholder sentinels (`<UNKNOWN>`, `<TODO>`, `<MISSING>`, etc).
124
+ # Runs BEFORE write-arg-bleed + target-scope + confirmation guards so
125
+ # we fail fast on poisoned inputs and never waste a billing-charged
126
+ # dispatch or pollute the audit ledger with `target=<UNKNOWN>` rows.
127
+ # See federal invariant **I-PARAMS-NO-PLACEHOLDER-VALUES**.
128
+ placeholder_reason = check_placeholder_args(tu, action_type)
129
+ if placeholder_reason is not None:
130
+ chat_ext._functions_called.append({
131
+ "name": tu.name, "params": tu.input,
132
+ "action_type": action_type, "success": False, "intercepted": False,
133
+ "event": "", "result": None,
134
+ })
135
+ return json.dumps({"RESULT": "BLOCKED", "error": placeholder_reason})
136
+
121
137
  # ── Write-arg-bleed guard (I-WRITE-ARG-NO-BLEED) ──────────────
122
138
  # Defence-in-depth: reject any write/destructive call whose args contain
123
139
  # substrings of prior ERROR_TAXONOMY codes, even if the LLM paraphrased
@@ -145,6 +161,92 @@ def check_guards(
145
161
  return None
146
162
 
147
163
 
164
+ # Federal invariant **I-PARAMS-NO-PLACEHOLDER-VALUES** sentinel pattern.
165
+ # Matches LLM-emitted placeholder values of the form `<NAME>` where NAME is
166
+ # uppercase ASCII + digits + underscores (e.g. `<UNKNOWN>`, `<TODO>`,
167
+ # `<MISSING>`, `<EMAIL>`, `<PASSWORD>`, `<USER_ID>`). The pattern is
168
+ # deliberately tight — narrow false-positive surface — and only flags values
169
+ # that are EXACTLY one sentinel token after `.strip()`. Real prose containing
170
+ # `<UNKNOWN>` as a substring (e.g. error messages, comments) does NOT match.
171
+ _PLACEHOLDER_RE = re.compile(r"^<[A-Z][A-Z0-9_]*>$")
172
+
173
+
174
+ def _scan_for_placeholders(value: Any, _path: str = "") -> list[tuple[str, str]]:
175
+ """Recursive scan for placeholder values.
176
+
177
+ Returns a list of ``(json-path, matched-value)`` tuples; empty list when
178
+ nothing matched. Recurses into dict values and list/tuple items. Caps
179
+ recursion depth implicitly via JSON shape (no cycles in tool_use input).
180
+ """
181
+ hits: list[tuple[str, str]] = []
182
+ if isinstance(value, str):
183
+ s = value.strip()
184
+ if _PLACEHOLDER_RE.match(s):
185
+ hits.append((_path or "$", s))
186
+ elif isinstance(value, dict):
187
+ for k, v in value.items():
188
+ hits.extend(_scan_for_placeholders(v, f"{_path}.{k}" if _path else str(k)))
189
+ elif isinstance(value, (list, tuple)):
190
+ for i, v in enumerate(value):
191
+ hits.extend(_scan_for_placeholders(v, f"{_path}[{i}]"))
192
+ return hits
193
+
194
+
195
+ def check_placeholder_args(tu, action_type: str) -> str | None:
196
+ """Reject any tool call whose arg values look like LLM-emitted placeholder
197
+ sentinels (e.g. ``<UNKNOWN>``, ``<TODO>``, ``<MISSING>``).
198
+
199
+ Federal invariant **I-PARAMS-NO-PLACEHOLDER-VALUES**: when the LLM does
200
+ not have a real value for a required field, it sometimes substitutes a
201
+ placeholder token instead of asking the user. The downstream anti-fab
202
+ layer catches the drift on the response side (``server did not reflect
203
+ 'email': requested '<UNKNOWN>', got None``), but by then the dispatch has
204
+ already wasted billing tokens, polluted the audit ledger with
205
+ ``target=<UNKNOWN>`` rows, and produced an opaque user-visible failure.
206
+ This guard fails fast on the request side and surfaces a friendly ask
207
+ back to the LLM so it can clarify with the user.
208
+
209
+ Applies to **all** ``action_type`` values (read/write/destructive) —
210
+ placeholder values are never legitimate.
211
+
212
+ Returns
213
+ -------
214
+ str | None
215
+ ``None`` to allow the dispatch through, or a human-readable rejection
216
+ reason that the caller wraps into the standard
217
+ ``{"RESULT": "BLOCKED", "error": ...}`` envelope. The reason is
218
+ deliberately framed as an instruction to the LLM (not the end user)
219
+ because the SDK chat loop feeds it back as a synthetic tool_result so
220
+ the LLM can self-correct and emit a clarifying question.
221
+ """
222
+ payload = getattr(tu, "input", None)
223
+ if not payload:
224
+ return None
225
+ try:
226
+ hits = _scan_for_placeholders(payload)
227
+ except Exception:
228
+ # Defensive — never block on the scanner itself raising on exotic
229
+ # payloads. Real placeholder values are str-scalar and trivially
230
+ # serialisable; if scan blows up it is on something that cannot
231
+ # contain a placeholder anyway.
232
+ return None
233
+ if not hits:
234
+ return None
235
+ field_list = ", ".join(f"`{f}`" for f, _ in hits[:5])
236
+ log.warning(
237
+ f"ChatExtension guard: PLACEHOLDER_ARGS blocked {getattr(tu, 'name', '?')} "
238
+ f"action={action_type} fields={[f for f, _ in hits]}"
239
+ )
240
+ return (
241
+ f"PLACEHOLDER_ARGS rejected: tool call '{getattr(tu, 'name', '?')}' "
242
+ f"contains placeholder value(s) in: {field_list}. The user did not "
243
+ f"provide concrete values for these fields and you emitted sentinel "
244
+ f"tokens (e.g. <UNKNOWN>) instead. Do not dispatch with placeholders. "
245
+ f"Ask the user a clarifying question to obtain the missing values, "
246
+ f"then retry with the real data."
247
+ )
248
+
249
+
148
250
  def check_write_arg_bleed(
149
251
  tu,
150
252
  functions_called: Iterable[dict[str, Any]],