controlzero 1.9.3__tar.gz → 1.9.4__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 (235) hide show
  1. {controlzero-1.9.3 → controlzero-1.9.4}/CHANGELOG.md +21 -1
  2. {controlzero-1.9.3 → controlzero-1.9.4}/PKG-INFO +1 -1
  3. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/__init__.py +1 -1
  4. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/client.py +8 -2
  5. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/error_codes.yaml +10 -5
  6. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/errors.py +105 -2
  7. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/hosted_policy.py +17 -6
  8. {controlzero-1.9.3 → controlzero-1.9.4}/pyproject.toml +1 -1
  9. controlzero-1.9.4/tests/test_epic_1247_bryan_acceptance.py +529 -0
  10. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_errors_e_codes.py +93 -0
  11. {controlzero-1.9.3 → controlzero-1.9.4}/.gitignore +0 -0
  12. {controlzero-1.9.3 → controlzero-1.9.4}/Dockerfile.test +0 -0
  13. {controlzero-1.9.3 → controlzero-1.9.4}/LICENSE +0 -0
  14. {controlzero-1.9.3 → controlzero-1.9.4}/README.md +0 -0
  15. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/_internal/__init__.py +0 -0
  16. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/_internal/action_aliases.py +0 -0
  17. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/_internal/action_validator.py +0 -0
  18. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/_internal/bundle.py +0 -0
  19. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/_internal/credential_hook.py +0 -0
  20. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/_internal/credential_scanner.py +0 -0
  21. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/_internal/credentials_data/__init__.py +0 -0
  22. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/_internal/credentials_data/built_in.yaml +0 -0
  23. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/_internal/dlp_scanner.py +0 -0
  24. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/_internal/enforcer.py +0 -0
  25. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/_internal/hook_extractors.py +0 -0
  26. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/_internal/tool_extractors.json +0 -0
  27. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/_internal/types.py +0 -0
  28. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/audit_local.py +0 -0
  29. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/audit_remote.py +0 -0
  30. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/canonical.py +0 -0
  31. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/__init__.py +0 -0
  32. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/_secrets.py +0 -0
  33. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/console.py +0 -0
  34. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/debug_bundle.py +0 -0
  35. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/doctor.py +0 -0
  36. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/hosts/__init__.py +0 -0
  37. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/hosts/antigravity.py +0 -0
  38. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/hosts/base.py +0 -0
  39. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/hosts/claude_code.py +0 -0
  40. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/hosts/codex_cli.py +0 -0
  41. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/hosts/gemini_cli.py +0 -0
  42. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/hosts/kiro.py +0 -0
  43. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/hosts/unknown.py +0 -0
  44. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/kiro_adapter.py +0 -0
  45. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/main.py +0 -0
  46. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/migrate.py +0 -0
  47. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/spool_cmd.py +0 -0
  48. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/telemetry_consent.py +0 -0
  49. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/templates/antigravity/hooks.json +0 -0
  50. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/templates/antigravity.yaml +0 -0
  51. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/templates/autogen.yaml +0 -0
  52. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/templates/claude-code.yaml +0 -0
  53. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/templates/codex-cli.yaml +0 -0
  54. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/templates/cost-cap.yaml +0 -0
  55. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/templates/crewai.yaml +0 -0
  56. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/templates/cursor.yaml +0 -0
  57. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/templates/gemini-cli.yaml +0 -0
  58. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/templates/generic.yaml +0 -0
  59. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook +0 -0
  60. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/templates/kiro/ide-pre-tool-use.kiro.hook +0 -0
  61. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/templates/kiro/ide-prompt-submit.kiro.hook +0 -0
  62. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/templates/kiro/kiro.yaml +0 -0
  63. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/templates/langchain.yaml +0 -0
  64. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/templates/mcp.yaml +0 -0
  65. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/cli/templates/rag.yaml +0 -0
  66. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/device.py +0 -0
  67. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/enrollment.py +0 -0
  68. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/error_codes.py +0 -0
  69. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/hitl/__init__.py +0 -0
  70. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/hitl/grant_protocol.py +0 -0
  71. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/hitl/mock.py +0 -0
  72. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/hitl/pending_approval.py +0 -0
  73. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/hitl/secret_leak_guard.py +0 -0
  74. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/hitl/status.py +0 -0
  75. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/hooks/__init__.py +0 -0
  76. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/hooks/tool_output_handler.py +0 -0
  77. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/integrations/__init__.py +0 -0
  78. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/integrations/anthropic.py +0 -0
  79. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/integrations/autogen.py +0 -0
  80. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/integrations/braintrust.py +0 -0
  81. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/integrations/crewai/__init__.py +0 -0
  82. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/integrations/crewai/agent.py +0 -0
  83. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/integrations/crewai/crew.py +0 -0
  84. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/integrations/crewai/task.py +0 -0
  85. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/integrations/crewai/tool.py +0 -0
  86. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/integrations/google.py +0 -0
  87. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/integrations/google_adk/__init__.py +0 -0
  88. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/integrations/google_adk/agent.py +0 -0
  89. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/integrations/google_adk/tool.py +0 -0
  90. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/integrations/langchain/__init__.py +0 -0
  91. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/integrations/langchain/agent.py +0 -0
  92. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/integrations/langchain/callbacks.py +0 -0
  93. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/integrations/langchain/chain.py +0 -0
  94. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/integrations/langchain/graph.py +0 -0
  95. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/integrations/langchain/modern.py +0 -0
  96. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/integrations/langchain/tool.py +0 -0
  97. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/integrations/langfuse.py +0 -0
  98. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/integrations/litellm.py +0 -0
  99. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/integrations/openai.py +0 -0
  100. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/integrations/pydantic_ai.py +0 -0
  101. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/integrations/vercel_ai.py +0 -0
  102. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/layout_migration.py +0 -0
  103. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/policy_loader.py +0 -0
  104. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/spool/__init__.py +0 -0
  105. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/spool/_compress.py +0 -0
  106. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/spool/_constants.py +0 -0
  107. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/spool/_crc32c.py +0 -0
  108. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/spool/_crypto.py +0 -0
  109. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/spool/_frame.py +0 -0
  110. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/spool/_keyring.py +0 -0
  111. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/spool/_metrics.py +0 -0
  112. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/spool/_spool.py +0 -0
  113. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/spool/_state.py +0 -0
  114. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/spool/_uploader.py +0 -0
  115. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/spool/cz-audit-v1.dict +0 -0
  116. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/tamper.py +0 -0
  117. {controlzero-1.9.3 → controlzero-1.9.4}/controlzero/tracecontext.py +0 -0
  118. {controlzero-1.9.3 → controlzero-1.9.4}/examples/hello_world.py +0 -0
  119. {controlzero-1.9.3 → controlzero-1.9.4}/tests/_fixtures/jcs_args_hash_vectors.json +0 -0
  120. {controlzero-1.9.3 → controlzero-1.9.4}/tests/conftest.py +0 -0
  121. {controlzero-1.9.3 → controlzero-1.9.4}/tests/integrations/__init__.py +0 -0
  122. {controlzero-1.9.3 → controlzero-1.9.4}/tests/integrations/test_google.py +0 -0
  123. {controlzero-1.9.3 → controlzero-1.9.4}/tests/parity/action_aliases.json +0 -0
  124. {controlzero-1.9.3 → controlzero-1.9.4}/tests/spool/__init__.py +0 -0
  125. {controlzero-1.9.3 → controlzero-1.9.4}/tests/spool/conftest.py +0 -0
  126. {controlzero-1.9.3 → controlzero-1.9.4}/tests/spool/test_spool_cli.py +0 -0
  127. {controlzero-1.9.3 → controlzero-1.9.4}/tests/spool/test_spool_concurrency.py +0 -0
  128. {controlzero-1.9.3 → controlzero-1.9.4}/tests/spool/test_spool_conformance.py +0 -0
  129. {controlzero-1.9.3 → controlzero-1.9.4}/tests/spool/test_spool_core.py +0 -0
  130. {controlzero-1.9.3 → controlzero-1.9.4}/tests/spool/test_spool_crash.py +0 -0
  131. {controlzero-1.9.3 → controlzero-1.9.4}/tests/spool/test_spool_diskfull.py +0 -0
  132. {controlzero-1.9.3 → controlzero-1.9.4}/tests/spool/test_spool_durable_default_tamper.py +0 -0
  133. {controlzero-1.9.3 → controlzero-1.9.4}/tests/spool/test_spool_keychain_dek.py +0 -0
  134. {controlzero-1.9.3 → controlzero-1.9.4}/tests/spool/test_spool_sink_wiring.py +0 -0
  135. {controlzero-1.9.3 → controlzero-1.9.4}/tests/spool/test_spool_transcript_localack.py +0 -0
  136. {controlzero-1.9.3 → controlzero-1.9.4}/tests/spool/test_spool_uploader.py +0 -0
  137. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_action_aliases.py +0 -0
  138. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_action_canonicalization.py +0 -0
  139. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_action_validator_t86.py +0 -0
  140. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_agent_name_env.py +0 -0
  141. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_antigravity_adapter.py +0 -0
  142. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_antigravity_hook_check.py +0 -0
  143. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_antigravity_install.py +0 -0
  144. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_api_key_mask.py +0 -0
  145. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_audit_remote.py +0 -0
  146. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_audit_remote_sdk_version.py +0 -0
  147. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_audit_sink_isolation.py +0 -0
  148. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_bundle_parser.py +0 -0
  149. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_bundle_translate.py +0 -0
  150. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_canonical_phase1a.py +0 -0
  151. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_cli_carve_out.py +0 -0
  152. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_cli_debug_bundle.py +0 -0
  153. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_cli_extractor_integration.py +0 -0
  154. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_cli_hook.py +0 -0
  155. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_cli_hosted_refresh.py +0 -0
  156. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_cli_init.py +0 -0
  157. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_cli_init_templates.py +0 -0
  158. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_cli_tail.py +0 -0
  159. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_cli_test.py +0 -0
  160. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_cli_validate.py +0 -0
  161. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_coding_agent_hooks.py +0 -0
  162. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_conditions.py +0 -0
  163. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_conformance.py +0 -0
  164. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_console.py +0 -0
  165. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_credential_hook.py +0 -0
  166. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_default_action.py +0 -0
  167. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_device.py +0 -0
  168. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_dlp_scanner.py +0 -0
  169. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_doctor.py +0 -0
  170. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_engine_version_consistency.py +0 -0
  171. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_enrollment.py +0 -0
  172. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_env_dump_438.py +0 -0
  173. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_error_codes.py +0 -0
  174. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_fail_closed_eval.py +0 -0
  175. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_glob_matching.py +0 -0
  176. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_hitl_5d_email_install.py +0 -0
  177. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_hitl_6a_cli_flag.py +0 -0
  178. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_hitl_6a_exceptions.py +0 -0
  179. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_hitl_6a_get_secret_hitl.py +0 -0
  180. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_hitl_6a_mock_backend.py +0 -0
  181. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_hitl_6a_pending_approval.py +0 -0
  182. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_hitl_6a_request_approval.py +0 -0
  183. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_hitl_6a_secret_leak_guard.py +0 -0
  184. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_hitl_6a_wait.py +0 -0
  185. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_hitl_conformance.py +0 -0
  186. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_hitl_phase2b_protocol.py +0 -0
  187. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_hitl_reason_codes.py +0 -0
  188. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_hitl_validator_keys.py +0 -0
  189. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_hook_extractors.py +0 -0
  190. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_hosted_local_audit_1247.py +0 -0
  191. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_hosted_policy_e2e.py +0 -0
  192. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_hosts_adapter.py +0 -0
  193. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_hybrid_mode_strict.py +0 -0
  194. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_hybrid_mode_warn.py +0 -0
  195. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_install_hook_command.py +0 -0
  196. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_install_hooks.py +0 -0
  197. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_kiro_adapter.py +0 -0
  198. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_kiro_hook_templates.py +0 -0
  199. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_kiro_install.py +0 -0
  200. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_layout_migration_t101.py +0 -0
  201. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_layout_parity_t102.py +0 -0
  202. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_local_mode_dict.py +0 -0
  203. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_local_mode_file_json.py +0 -0
  204. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_local_mode_file_yaml.py +0 -0
  205. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_log_fallback_stderr.py +0 -0
  206. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_log_options_ignored_hosted.py +0 -0
  207. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_log_rotation.py +0 -0
  208. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_migrate.py +0 -0
  209. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_min_sdk_version_gate.py +0 -0
  210. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_multi_client_per_project_175.py +0 -0
  211. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_no_policy_no_key.py +0 -0
  212. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_observe_mode_1247.py +0 -0
  213. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_package_rename_shim.py +0 -0
  214. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_policy_engine_version_phase1b.py +0 -0
  215. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_policy_freshness.py +0 -0
  216. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_policy_settings.py +0 -0
  217. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_policy_source_audit.py +0 -0
  218. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_quarantine.py +0 -0
  219. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_reason_code.py +0 -0
  220. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_refresh.py +0 -0
  221. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_secrets.py +0 -0
  222. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_sql_semantic_class.py +0 -0
  223. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_synthetic_policy_id_t79.py +0 -0
  224. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_t103_precedence.py +0 -0
  225. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_t104_cache_gc.py +0 -0
  226. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_t108_local_override_audit.py +0 -0
  227. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_t96_single_audit_log.py +0 -0
  228. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_t99_install_prefetch_bundle.py +0 -0
  229. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_tamper.py +0 -0
  230. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_tamper_behavior.py +0 -0
  231. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_tamper_hook.py +0 -0
  232. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_telemetry_consent.py +0 -0
  233. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_tracecontext.py +0 -0
  234. {controlzero-1.9.3 → controlzero-1.9.4}/tests/test_unsafe_int_boundary.py +0 -0
  235. {controlzero-1.9.3 → controlzero-1.9.4}/tools/cz-kiro-adapter +0 -0
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.4 -- 2026-06-16 (actionable E1101 / API-key-rejected message, #1254, epic gh#1247)
4
+
5
+ ### Fixed
6
+
7
+ - **(#1254) `HostedAuthError` (E1101) is now actionable instead of an
8
+ opaque "API key rejected (401)".** A dead/revoked/expired/placeholder API key
9
+ in hosted mode used to surface a bare message that told the user neither WHY
10
+ nor what to do. The message now always states the key is invalid/revoked/
11
+ expired and how to fix it: "Generate a new API key in the dashboard (Settings
12
+ -> API Keys, https://app.controlzero.ai/settings/api-keys) and set
13
+ CONTROLZERO_API_KEY to it." When the backend sends a structured 401 body, the
14
+ exception preserves the coarse machine-readable `reason` (e.g.
15
+ `invalid_or_revoked`) and the `remediation` on the exception so programmatic
16
+ callers can branch while humans read the fix. The `E1101` code is unchanged.
17
+ All four 401 raise sites (bootstrap, bundle pull, approval request, get
18
+ secret) now parse the body via `HostedAuthError.from_response(...)`.
19
+ Enumeration-safe: the backend keeps the reason coarse (not_found / revoked /
20
+ expired / placeholder all collapse to `invalid_or_revoked`) so a 401 cannot be
21
+ used to probe which keys exist. Tests:
22
+ `tests/test_errors_e_codes.py::TestHostedAuthErrorActionable`.
23
+
3
24
  ## 1.9.3 -- 2026-06-16 (posture release: empty-bundle OBSERVE + self-explaining no-rule-match deny, epic gh#1247)
4
25
 
5
26
  The consolidated **posture release** for epic gh#1247 (customer Bryan).
@@ -76,7 +97,6 @@ second-opinion). It is deliberately **narrow and gated**:
76
97
  bundle translator -> `PolicySettings`. Local YAML policies may set
77
98
  `settings.default_on_empty`. The dashboard observe-mode indicator +
78
99
  CLI `status` line remain a separate frontend/CLI follow-up.
79
-
80
100
  ## 1.9.1 -- 2026-06-16 (hosted-mode local audit log P0, epic gh#1247)
81
101
 
82
102
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: controlzero
3
- Version: 1.9.3
3
+ Version: 1.9.4
4
4
  Summary: AI agent governance: policies, audit, and observability for tool calls. Works locally with no signup.
5
5
  Project-URL: Homepage, https://controlzero.ai
6
6
  Project-URL: Documentation, https://docs.controlzero.ai
@@ -40,7 +40,7 @@ from controlzero.hitl.grant_protocol import (
40
40
  )
41
41
  from controlzero.policy_loader import load_policy
42
42
 
43
- __version__ = "1.9.3"
43
+ __version__ = "1.9.4"
44
44
 
45
45
  __all__ = [
46
46
  "Client",
@@ -676,9 +676,11 @@ class Client:
676
676
  # Non-2xx: map status + body code to the right typed exception.
677
677
  body_code = ""
678
678
  body_msg = ""
679
+ err_body_dict: dict = {}
679
680
  try:
680
681
  err_body = resp.json()
681
682
  if isinstance(err_body, dict):
683
+ err_body_dict = err_body
682
684
  body_code = str(err_body.get("code", "") or "")
683
685
  body_msg = str(err_body.get("message", "") or err_body.get("error", "") or "")
684
686
  except ValueError:
@@ -717,7 +719,7 @@ class Client:
717
719
  body_msg or "Requestor identity claim rejected by backend"
718
720
  )
719
721
  if status == 401:
720
- raise HostedAuthError(body_msg or "hosted API key rejected by backend")
722
+ raise HostedAuthError.from_response(status, err_body_dict)
721
723
  if status == 404:
722
724
  raise HITLNotConfiguredError(
723
725
  body_msg or "Approvals not configured for this org"
@@ -885,7 +887,11 @@ class Client:
885
887
  if status == 404:
886
888
  raise SecretNotFound(f"secret {name!r} not found")
887
889
  if status == 401:
888
- raise HostedAuthError("hosted API key rejected by backend")
890
+ try:
891
+ secret_err_body = resp.json()
892
+ except ValueError:
893
+ secret_err_body = None
894
+ raise HostedAuthError.from_response(status, secret_err_body)
889
895
  if not (200 <= status < 300):
890
896
  raise HITLBackendUnreachableError(
891
897
  f"GET /api/secrets/{name} returned HTTP {status}"
@@ -95,11 +95,16 @@ codes:
95
95
  - code: E1101
96
96
  title: API key rejected (401)
97
97
  what: >-
98
- The hosted backend rejected the API key. The key is either revoked,
99
- from a different environment (test vs live), or never existed.
100
- fix: >-
101
- Check your dashboard for the current key under Settings API Keys.
102
- Make sure CONTROLZERO_API_KEY matches the key shown there.
98
+ The hosted backend rejected the API key with HTTP 401. The key is
99
+ unknown, revoked, expired, or a never-activated placeholder. For
100
+ safety the backend does not say which (so a 401 cannot be used to
101
+ probe which keys exist), but in every case the same fix applies:
102
+ the key you are using is no longer valid.
103
+ fix: >-
104
+ Generate a fresh key in the dashboard under Settings -> API Keys
105
+ (https://app.controlzero.ai/settings/api-keys) and set
106
+ CONTROLZERO_API_KEY to it. If you just rotated a key, make sure the
107
+ environment your agent runs in picked up the new value.
103
108
  doc: E1101-key-rejected
104
109
 
105
110
  - code: E1102
@@ -28,6 +28,18 @@ from controlzero._internal.enforcer import PolicyDeniedError, PolicyDecision
28
28
  from controlzero.error_codes import ErrorCode, get as _get_error_code
29
29
 
30
30
 
31
+ def _first_str(*values: object) -> Optional[str]:
32
+ """Return the first value that is a non-empty string, else None.
33
+
34
+ Used to read a field that the backend may place at the top level or
35
+ nested under ``error`` without crashing on unexpected types.
36
+ """
37
+ for v in values:
38
+ if isinstance(v, str) and v.strip():
39
+ return v
40
+ return None
41
+
42
+
31
43
  def _augment_with_code(message: str, code_key: Optional[str]) -> str:
32
44
  """Append the catalog's fix + docs URL to a raw error message.
33
45
 
@@ -137,6 +149,12 @@ class BundleSignatureError(_CZErrorMixin, Exception):
137
149
  super().__init__(_augment_with_code(message, self.E_CODE))
138
150
 
139
151
 
152
+ _DEFAULT_AUTH_REMEDIATION = (
153
+ "Generate a new API key in the dashboard (Settings -> API Keys, "
154
+ "https://app.controlzero.ai/settings/api-keys) and set CONTROLZERO_API_KEY to it."
155
+ )
156
+
157
+
140
158
  class HostedAuthError(_CZErrorMixin, RuntimeError):
141
159
  """Raised when the project API key is rejected by the backend
142
160
  (401/403). Maps to E1101.
@@ -144,12 +162,97 @@ class HostedAuthError(_CZErrorMixin, RuntimeError):
144
162
  This is a permanent failure: the caller supplied an invalid or
145
163
  revoked API key. Retrying with the same key will not help. The
146
164
  SDK surfaces this so the user can correct their configuration.
165
+
166
+ #1254: the bare "API key rejected (401)" gave the user no
167
+ idea WHY or what to do. The message now always tells the user the
168
+ key is invalid/revoked and how to fix it. When the backend supplies
169
+ a structured 401 body (``reason`` + ``remediation``), those are
170
+ preserved on the exception so programmatic callers can branch on
171
+ :attr:`reason` while humans read the actionable message.
172
+
173
+ The backend keeps ``reason`` coarse on purpose (not_found / revoked /
174
+ expired / placeholder all collapse to ``invalid_or_revoked``) so a
175
+ 401 is never an enumeration oracle; the SDK does not try to refine it.
176
+
177
+ Attributes:
178
+ reason: coarse machine-readable reason from the backend, e.g.
179
+ ``"invalid_or_revoked"``. ``None`` when the backend did not
180
+ send a structured body (older backend).
181
+ remediation: copy-pasteable next step shown to the user.
147
182
  """
148
183
 
149
184
  E_CODE = "E1101"
150
185
 
151
- def __init__(self, message: str = "hosted API key rejected by backend"):
152
- super().__init__(_augment_with_code(message, self.E_CODE))
186
+ def __init__(
187
+ self,
188
+ message: Optional[str] = None,
189
+ *,
190
+ reason: Optional[str] = None,
191
+ remediation: Optional[str] = None,
192
+ ):
193
+ self.reason = reason
194
+ self.remediation = remediation or _DEFAULT_AUTH_REMEDIATION
195
+ if message is None:
196
+ message = (
197
+ "Your Control Zero API key was rejected by the backend "
198
+ "(invalid, revoked, or expired)."
199
+ )
200
+ full = f"{message} {self.remediation}"
201
+ super().__init__(_augment_with_code(full, self.E_CODE))
202
+
203
+ @classmethod
204
+ def from_response(
205
+ cls,
206
+ status_code: int,
207
+ body: object = None,
208
+ *,
209
+ context: str = "",
210
+ ) -> "HostedAuthError":
211
+ """Build a HostedAuthError from a backend 401/403 response.
212
+
213
+ ``body`` is the parsed JSON (a dict) when available, else None.
214
+ The backend may put the structured fields either at the top
215
+ level (``body["reason"]``) or nested under ``body["error"]``;
216
+ we read both. ``context`` is an optional short phrase like
217
+ "during bundle pull" appended to the headline so logs say which
218
+ request failed. Never raises on a malformed body -- a bad body
219
+ just yields the actionable default message.
220
+ """
221
+ reason: Optional[str] = None
222
+ remediation: Optional[str] = None
223
+ backend_msg: Optional[str] = None
224
+ if isinstance(body, dict):
225
+ err = body.get("error")
226
+ err_dict = err if isinstance(err, dict) else {}
227
+ reason = _first_str(body.get("reason"), err_dict.get("reason"))
228
+ remediation = _first_str(
229
+ body.get("remediation"), err_dict.get("remediation")
230
+ )
231
+ # The backend message can be body["message"], the nested
232
+ # error.message, or -- for back-compat with non-structured
233
+ # backends -- a plain string in the top-level "error" key
234
+ # (e.g. {"error": "Unauthorized"}).
235
+ err_str = err if isinstance(err, str) else None
236
+ backend_msg = _first_str(
237
+ body.get("message"), err_dict.get("message"), err_str
238
+ )
239
+
240
+ headline = (
241
+ "Your Control Zero API key was rejected by the backend "
242
+ "(invalid, revoked, or expired)."
243
+ )
244
+ if context:
245
+ headline = (
246
+ f"Your Control Zero API key was rejected by the backend "
247
+ f"{context} (invalid, revoked, or expired)."
248
+ )
249
+ # Prefer the backend's own human message when it is more specific
250
+ # than our generic headline, but always keep the headline so the
251
+ # user sees the "what to do" framing even with an old backend.
252
+ message = headline
253
+ if backend_msg and backend_msg.lower() not in ("invalid api key", ""):
254
+ message = f"{headline} (backend: {backend_msg})"
255
+ return cls(message, reason=reason, remediation=remediation)
153
256
 
154
257
 
155
258
  class HostedBootstrapError(_CZErrorMixin, RuntimeError):
@@ -66,6 +66,20 @@ PULL_TIMEOUT_S = 5.0
66
66
  MAX_BUNDLE_BYTES = 16 * 1024 * 1024
67
67
 
68
68
 
69
+ def _try_json(resp: object) -> object:
70
+ """Best-effort parse of an httpx response body as JSON.
71
+
72
+ Returns the decoded object on success, or ``None`` on any failure
73
+ (non-JSON body, empty body, missing ``.json``). Never raises -- a
74
+ structured 401 body is a bonus, not a requirement, so a malformed
75
+ body must not mask the underlying auth error (#1254).
76
+ """
77
+ try:
78
+ return resp.json() # type: ignore[attr-defined]
79
+ except Exception: # noqa: BLE001
80
+ return None
81
+
82
+
69
83
  def get_api_url() -> str:
70
84
  """Resolve the Control Zero API base URL."""
71
85
  return os.environ.get("CONTROLZERO_API_URL", DEFAULT_API_URL).rstrip("/")
@@ -230,10 +244,7 @@ def fetch_bootstrap(api_key: str, api_url: Optional[str] = None) -> BootstrapKey
230
244
  ) from exc
231
245
 
232
246
  if resp.status_code in (401, 403):
233
- raise HostedAuthError(
234
- "Control Zero API key rejected by backend. Check that "
235
- "CONTROLZERO_API_KEY is a valid cz_live_ or cz_test_ project key."
236
- )
247
+ raise HostedAuthError.from_response(resp.status_code, _try_json(resp))
237
248
  if resp.status_code >= 400:
238
249
  raise HostedBootstrapError(
239
250
  f"bootstrap returned HTTP {resp.status_code}: {resp.text[:500]}"
@@ -370,8 +381,8 @@ def pull_bundle(
370
381
  if resp.status_code == 304:
371
382
  return None
372
383
  if resp.status_code in (401, 403):
373
- raise HostedAuthError(
374
- "Control Zero API key rejected during bundle pull."
384
+ raise HostedAuthError.from_response(
385
+ resp.status_code, _try_json(resp), context="during bundle pull"
375
386
  )
376
387
  if resp.status_code >= 400:
377
388
  raise HostedBootstrapError(
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "controlzero"
7
- version = "1.9.3"
7
+ version = "1.9.4"
8
8
  description = "AI agent governance: policies, audit, and observability for tool calls. Works locally with no signup."
9
9
  readme = "README.md"
10
10
  license = {text = "Apache-2.0"}