controlzero 1.9.0__tar.gz → 1.9.1__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 (231) hide show
  1. {controlzero-1.9.0 → controlzero-1.9.1}/CHANGELOG.md +29 -0
  2. {controlzero-1.9.0 → controlzero-1.9.1}/PKG-INFO +1 -1
  3. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/__init__.py +1 -1
  4. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/client.py +108 -24
  5. {controlzero-1.9.0 → controlzero-1.9.1}/pyproject.toml +1 -1
  6. controlzero-1.9.1/tests/test_hosted_local_audit_1247.py +217 -0
  7. controlzero-1.9.1/tests/test_log_options_ignored_hosted.py +50 -0
  8. controlzero-1.9.0/tests/test_log_options_ignored_hosted.py +0 -35
  9. {controlzero-1.9.0 → controlzero-1.9.1}/.gitignore +0 -0
  10. {controlzero-1.9.0 → controlzero-1.9.1}/Dockerfile.test +0 -0
  11. {controlzero-1.9.0 → controlzero-1.9.1}/LICENSE +0 -0
  12. {controlzero-1.9.0 → controlzero-1.9.1}/README.md +0 -0
  13. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/_internal/__init__.py +0 -0
  14. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/_internal/action_aliases.py +0 -0
  15. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/_internal/action_validator.py +0 -0
  16. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/_internal/bundle.py +0 -0
  17. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/_internal/credential_hook.py +0 -0
  18. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/_internal/credential_scanner.py +0 -0
  19. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/_internal/credentials_data/__init__.py +0 -0
  20. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/_internal/credentials_data/built_in.yaml +0 -0
  21. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/_internal/dlp_scanner.py +0 -0
  22. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/_internal/enforcer.py +0 -0
  23. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/_internal/hook_extractors.py +0 -0
  24. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/_internal/tool_extractors.json +0 -0
  25. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/_internal/types.py +0 -0
  26. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/audit_local.py +0 -0
  27. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/audit_remote.py +0 -0
  28. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/canonical.py +0 -0
  29. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/__init__.py +0 -0
  30. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/_secrets.py +0 -0
  31. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/console.py +0 -0
  32. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/debug_bundle.py +0 -0
  33. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/doctor.py +0 -0
  34. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/hosts/__init__.py +0 -0
  35. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/hosts/antigravity.py +0 -0
  36. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/hosts/base.py +0 -0
  37. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/hosts/claude_code.py +0 -0
  38. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/hosts/codex_cli.py +0 -0
  39. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/hosts/gemini_cli.py +0 -0
  40. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/hosts/kiro.py +0 -0
  41. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/hosts/unknown.py +0 -0
  42. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/kiro_adapter.py +0 -0
  43. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/main.py +0 -0
  44. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/migrate.py +0 -0
  45. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/spool_cmd.py +0 -0
  46. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/telemetry_consent.py +0 -0
  47. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/antigravity/hooks.json +0 -0
  48. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/antigravity.yaml +0 -0
  49. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/autogen.yaml +0 -0
  50. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/claude-code.yaml +0 -0
  51. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/codex-cli.yaml +0 -0
  52. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/cost-cap.yaml +0 -0
  53. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/crewai.yaml +0 -0
  54. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/cursor.yaml +0 -0
  55. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/gemini-cli.yaml +0 -0
  56. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/generic.yaml +0 -0
  57. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook +0 -0
  58. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/kiro/ide-pre-tool-use.kiro.hook +0 -0
  59. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/kiro/ide-prompt-submit.kiro.hook +0 -0
  60. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/kiro/kiro.yaml +0 -0
  61. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/langchain.yaml +0 -0
  62. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/mcp.yaml +0 -0
  63. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/cli/templates/rag.yaml +0 -0
  64. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/device.py +0 -0
  65. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/enrollment.py +0 -0
  66. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/error_codes.py +0 -0
  67. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/error_codes.yaml +0 -0
  68. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/errors.py +0 -0
  69. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/hitl/__init__.py +0 -0
  70. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/hitl/grant_protocol.py +0 -0
  71. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/hitl/mock.py +0 -0
  72. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/hitl/pending_approval.py +0 -0
  73. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/hitl/secret_leak_guard.py +0 -0
  74. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/hitl/status.py +0 -0
  75. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/hooks/__init__.py +0 -0
  76. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/hooks/tool_output_handler.py +0 -0
  77. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/hosted_policy.py +0 -0
  78. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/__init__.py +0 -0
  79. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/anthropic.py +0 -0
  80. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/autogen.py +0 -0
  81. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/braintrust.py +0 -0
  82. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/crewai/__init__.py +0 -0
  83. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/crewai/agent.py +0 -0
  84. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/crewai/crew.py +0 -0
  85. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/crewai/task.py +0 -0
  86. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/crewai/tool.py +0 -0
  87. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/google.py +0 -0
  88. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/google_adk/__init__.py +0 -0
  89. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/google_adk/agent.py +0 -0
  90. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/google_adk/tool.py +0 -0
  91. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/langchain/__init__.py +0 -0
  92. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/langchain/agent.py +0 -0
  93. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/langchain/callbacks.py +0 -0
  94. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/langchain/chain.py +0 -0
  95. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/langchain/graph.py +0 -0
  96. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/langchain/modern.py +0 -0
  97. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/langchain/tool.py +0 -0
  98. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/langfuse.py +0 -0
  99. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/litellm.py +0 -0
  100. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/openai.py +0 -0
  101. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/pydantic_ai.py +0 -0
  102. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/integrations/vercel_ai.py +0 -0
  103. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/layout_migration.py +0 -0
  104. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/policy_loader.py +0 -0
  105. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/spool/__init__.py +0 -0
  106. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/spool/_compress.py +0 -0
  107. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/spool/_constants.py +0 -0
  108. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/spool/_crc32c.py +0 -0
  109. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/spool/_crypto.py +0 -0
  110. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/spool/_frame.py +0 -0
  111. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/spool/_metrics.py +0 -0
  112. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/spool/_spool.py +0 -0
  113. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/spool/_state.py +0 -0
  114. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/spool/_uploader.py +0 -0
  115. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/spool/cz-audit-v1.dict +0 -0
  116. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/tamper.py +0 -0
  117. {controlzero-1.9.0 → controlzero-1.9.1}/controlzero/tracecontext.py +0 -0
  118. {controlzero-1.9.0 → controlzero-1.9.1}/examples/hello_world.py +0 -0
  119. {controlzero-1.9.0 → controlzero-1.9.1}/tests/_fixtures/jcs_args_hash_vectors.json +0 -0
  120. {controlzero-1.9.0 → controlzero-1.9.1}/tests/conftest.py +0 -0
  121. {controlzero-1.9.0 → controlzero-1.9.1}/tests/integrations/__init__.py +0 -0
  122. {controlzero-1.9.0 → controlzero-1.9.1}/tests/integrations/test_google.py +0 -0
  123. {controlzero-1.9.0 → controlzero-1.9.1}/tests/parity/action_aliases.json +0 -0
  124. {controlzero-1.9.0 → controlzero-1.9.1}/tests/spool/__init__.py +0 -0
  125. {controlzero-1.9.0 → controlzero-1.9.1}/tests/spool/conftest.py +0 -0
  126. {controlzero-1.9.0 → controlzero-1.9.1}/tests/spool/test_spool_cli.py +0 -0
  127. {controlzero-1.9.0 → controlzero-1.9.1}/tests/spool/test_spool_concurrency.py +0 -0
  128. {controlzero-1.9.0 → controlzero-1.9.1}/tests/spool/test_spool_conformance.py +0 -0
  129. {controlzero-1.9.0 → controlzero-1.9.1}/tests/spool/test_spool_core.py +0 -0
  130. {controlzero-1.9.0 → controlzero-1.9.1}/tests/spool/test_spool_crash.py +0 -0
  131. {controlzero-1.9.0 → controlzero-1.9.1}/tests/spool/test_spool_diskfull.py +0 -0
  132. {controlzero-1.9.0 → controlzero-1.9.1}/tests/spool/test_spool_sink_wiring.py +0 -0
  133. {controlzero-1.9.0 → controlzero-1.9.1}/tests/spool/test_spool_transcript_localack.py +0 -0
  134. {controlzero-1.9.0 → controlzero-1.9.1}/tests/spool/test_spool_uploader.py +0 -0
  135. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_action_aliases.py +0 -0
  136. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_action_canonicalization.py +0 -0
  137. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_action_validator_t86.py +0 -0
  138. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_agent_name_env.py +0 -0
  139. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_antigravity_adapter.py +0 -0
  140. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_antigravity_hook_check.py +0 -0
  141. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_antigravity_install.py +0 -0
  142. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_api_key_mask.py +0 -0
  143. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_audit_remote.py +0 -0
  144. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_audit_remote_sdk_version.py +0 -0
  145. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_audit_sink_isolation.py +0 -0
  146. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_bundle_parser.py +0 -0
  147. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_bundle_translate.py +0 -0
  148. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_canonical_phase1a.py +0 -0
  149. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_cli_carve_out.py +0 -0
  150. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_cli_debug_bundle.py +0 -0
  151. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_cli_extractor_integration.py +0 -0
  152. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_cli_hook.py +0 -0
  153. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_cli_hosted_refresh.py +0 -0
  154. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_cli_init.py +0 -0
  155. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_cli_init_templates.py +0 -0
  156. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_cli_tail.py +0 -0
  157. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_cli_test.py +0 -0
  158. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_cli_validate.py +0 -0
  159. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_coding_agent_hooks.py +0 -0
  160. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_conditions.py +0 -0
  161. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_conformance.py +0 -0
  162. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_console.py +0 -0
  163. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_credential_hook.py +0 -0
  164. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_default_action.py +0 -0
  165. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_device.py +0 -0
  166. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_dlp_scanner.py +0 -0
  167. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_doctor.py +0 -0
  168. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_engine_version_consistency.py +0 -0
  169. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_enrollment.py +0 -0
  170. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_env_dump_438.py +0 -0
  171. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_error_codes.py +0 -0
  172. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_errors_e_codes.py +0 -0
  173. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_fail_closed_eval.py +0 -0
  174. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_glob_matching.py +0 -0
  175. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hitl_5d_email_install.py +0 -0
  176. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hitl_6a_cli_flag.py +0 -0
  177. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hitl_6a_exceptions.py +0 -0
  178. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hitl_6a_get_secret_hitl.py +0 -0
  179. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hitl_6a_mock_backend.py +0 -0
  180. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hitl_6a_pending_approval.py +0 -0
  181. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hitl_6a_request_approval.py +0 -0
  182. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hitl_6a_secret_leak_guard.py +0 -0
  183. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hitl_6a_wait.py +0 -0
  184. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hitl_conformance.py +0 -0
  185. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hitl_phase2b_protocol.py +0 -0
  186. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hitl_reason_codes.py +0 -0
  187. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hitl_validator_keys.py +0 -0
  188. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hook_extractors.py +0 -0
  189. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hosted_policy_e2e.py +0 -0
  190. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hosts_adapter.py +0 -0
  191. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hybrid_mode_strict.py +0 -0
  192. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_hybrid_mode_warn.py +0 -0
  193. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_install_hook_command.py +0 -0
  194. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_install_hooks.py +0 -0
  195. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_kiro_adapter.py +0 -0
  196. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_kiro_hook_templates.py +0 -0
  197. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_kiro_install.py +0 -0
  198. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_layout_migration_t101.py +0 -0
  199. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_layout_parity_t102.py +0 -0
  200. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_local_mode_dict.py +0 -0
  201. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_local_mode_file_json.py +0 -0
  202. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_local_mode_file_yaml.py +0 -0
  203. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_log_fallback_stderr.py +0 -0
  204. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_log_rotation.py +0 -0
  205. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_migrate.py +0 -0
  206. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_min_sdk_version_gate.py +0 -0
  207. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_multi_client_per_project_175.py +0 -0
  208. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_no_policy_no_key.py +0 -0
  209. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_package_rename_shim.py +0 -0
  210. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_policy_engine_version_phase1b.py +0 -0
  211. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_policy_freshness.py +0 -0
  212. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_policy_settings.py +0 -0
  213. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_policy_source_audit.py +0 -0
  214. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_quarantine.py +0 -0
  215. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_reason_code.py +0 -0
  216. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_refresh.py +0 -0
  217. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_secrets.py +0 -0
  218. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_sql_semantic_class.py +0 -0
  219. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_synthetic_policy_id_t79.py +0 -0
  220. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_t103_precedence.py +0 -0
  221. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_t104_cache_gc.py +0 -0
  222. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_t108_local_override_audit.py +0 -0
  223. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_t96_single_audit_log.py +0 -0
  224. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_t99_install_prefetch_bundle.py +0 -0
  225. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_tamper.py +0 -0
  226. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_tamper_behavior.py +0 -0
  227. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_tamper_hook.py +0 -0
  228. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_telemetry_consent.py +0 -0
  229. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_tracecontext.py +0 -0
  230. {controlzero-1.9.0 → controlzero-1.9.1}/tests/test_unsafe_int_boundary.py +0 -0
  231. {controlzero-1.9.0 → controlzero-1.9.1}/tools/cz-kiro-adapter +0 -0
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.1 -- 2026-06-16 (hosted-mode local audit log P0, epic gh#1247)
4
+
5
+ ### Fixed
6
+
7
+ - **(P0) Hosted mode never wrote the local `~/.controlzero/audit.log`.** After
8
+ `controlzero install claude-code --api-key cz_live_...` (hosted mode), tool
9
+ calls reached the dashboard (remote audit) but the local audit log stayed
10
+ frozen indefinitely -- even though the claude-code template and the install
11
+ success message both promise that every tool call is logged to
12
+ `~/.controlzero/audit.log`. Root cause: `Client.__init__` constructed the
13
+ `LocalAuditLogger` ONLY when no API key was set, so hosted mode left the
14
+ local sink `None` and `_audit_decision()` skipped the local write (allow AND
15
+ deny). The local log is now written in EVERY mode; the remote sink is layered
16
+ on top, not instead. A blocked (deny) call is now locally recorded too -- the
17
+ exact case a customer hit (Claude Code `Bash` calls denied by no-rule-match).
18
+ This also restores `cz debug-bundle` and the tamper hash-chain, which read
19
+ the local log. Regression test: `tests/test_hosted_local_audit_1247.py`.
20
+
21
+ ### Security
22
+
23
+ - **Local audit log redacts PII/financial DLP plaintext.** Now that hosted mode
24
+ writes the local plaintext audit log, a DLP finding's raw `matched_text` (which
25
+ is plaintext for the pii/financial categories; the secret category is already
26
+ SHA-256 hashed) is stripped from the LOCAL row -- it would otherwise expose
27
+ PII/financial data to anyone who can read the file, content hosted mode kept
28
+ server-side only. The finding metadata (rule_id/category/location) is
29
+ preserved so the local log still records THAT a rule fired, and the remote
30
+ sink keeps full fidelity.
31
+
3
32
  ## 1.9.0 -- 2026-06-15 (Antigravity install CLI, epic gh#925)
4
33
 
5
34
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: controlzero
3
- Version: 1.9.0
3
+ Version: 1.9.1
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.0"
43
+ __version__ = "1.9.1"
44
44
 
45
45
  __all__ = [
46
46
  "Client",
@@ -36,7 +36,6 @@ import sys
36
36
  import threading
37
37
  import time
38
38
  import uuid
39
- import warnings
40
39
  import asyncio
41
40
  from datetime import datetime, timezone
42
41
  from pathlib import Path
@@ -120,6 +119,53 @@ def _mask_api_key(api_key: Optional[str]) -> str:
120
119
  return "***"
121
120
 
122
121
 
122
+ # DLP categories whose ``matched_text`` is PLAINTEXT in a finding (the secret
123
+ # category is already SHA-256 hashed by the scanner, so it is safe to persist
124
+ # locally). Anything in this set must have its raw matched value stripped
125
+ # before the finding is written to the local plaintext audit log. See
126
+ # ``_redact_local_dlp`` and the #1247 review note in ``_audit_decision``.
127
+ _DLP_PLAINTEXT_CATEGORIES = ("pii", "financial")
128
+
129
+
130
+ def _redact_local_dlp(entry: dict) -> dict:
131
+ """Return a copy of an audit entry safe to write to the LOCAL plaintext log.
132
+
133
+ The only field that carries raw argument-derived sensitive content is
134
+ ``dlp_findings[*].matched_text`` for pii/financial categories. We strip the
135
+ raw value (replacing it with a redaction marker) while preserving every
136
+ other field of the finding -- rule_id, category, location, count, etc. --
137
+ so the local log still records THAT a DLP rule fired, just not the matched
138
+ value. Entries without DLP findings are returned unchanged (a cheap
139
+ identity for the overwhelmingly common case).
140
+ """
141
+ findings = entry.get("dlp_findings")
142
+ if not findings:
143
+ return entry
144
+ needs_redaction = any(
145
+ isinstance(f, dict)
146
+ and f.get("category") in _DLP_PLAINTEXT_CATEGORIES
147
+ and "matched_text" in f
148
+ for f in findings
149
+ )
150
+ if not needs_redaction:
151
+ return entry
152
+ safe = dict(entry)
153
+ redacted_findings = []
154
+ for f in findings:
155
+ if (
156
+ isinstance(f, dict)
157
+ and f.get("category") in _DLP_PLAINTEXT_CATEGORIES
158
+ and "matched_text" in f
159
+ ):
160
+ rf = dict(f)
161
+ rf["matched_text"] = "[redacted-local]"
162
+ redacted_findings.append(rf)
163
+ else:
164
+ redacted_findings.append(f)
165
+ safe["dlp_findings"] = redacted_findings
166
+ return safe
167
+
168
+
123
169
  class Client:
124
170
  """The ControlZero policy client.
125
171
 
@@ -302,35 +348,60 @@ class Client:
302
348
 
303
349
  self._tamper_state_dir = Path.home() / ".controlzero"
304
350
 
305
- # Set up local audit logger ONLY in pure-local mode.
306
- # When hosted, audit goes through the remote forwarder (not implemented in
307
- # this skinny client; see hosted SDK in the legacy package for now).
351
+ # Set up the local audit logger in EVERY mode.
352
+ #
353
+ # P0 regression (epic #1247, 2026-06-15): a customer who ran
354
+ # `controlzero install claude-code --api-key cz_live_...` saw audit
355
+ # rows reach the dashboard (remote bearer sink) but their local
356
+ # ~/.controlzero/audit.log stayed frozen -- it was last appended weeks
357
+ # earlier. Root cause: this block used to create the LocalAuditLogger
358
+ # ONLY when `not self._has_api_key`, so hosted mode left ``self._audit``
359
+ # None and ``_audit_decision`` skipped the local write entirely.
360
+ #
361
+ # That silently broke the day-one value prop the claude-code template
362
+ # AND the install command both promise verbatim:
363
+ # "every Claude Code tool call is logged to ~/.controlzero/audit.log"
364
+ # "Audit log: ~/.controlzero/audit.log"
365
+ # The promise is mode-independent, so the local sink must be too. Local
366
+ # audit is now ALWAYS on (allow AND deny, local AND hosted); the remote
367
+ # bearer/enrolled sink is layered ON TOP in hosted mode, not instead of
368
+ # the local file. The local log is also what `cz debug-bundle` and the
369
+ # tamper hash-chain depend on, so a frozen file degraded those too.
308
370
  self._audit: Optional[LocalAuditLogger] = None
309
- if not self._has_api_key:
310
- # log_* options are honored
371
+ # In hosted mode the caller (e.g. the hook-check CLI) may not pass a
372
+ # log_path; default it to the canonical global audit log so a hosted
373
+ # install still writes the file the template/install message advertise.
374
+ effective_log_path = log_path
375
+ if self._has_api_key and log_path == "./controlzero.log":
376
+ effective_log_path = str(Path.home() / ".controlzero" / "audit.log")
377
+ try:
311
378
  self._audit = LocalAuditLogger(
312
- log_path=log_path,
379
+ log_path=effective_log_path,
313
380
  rotation=log_rotation,
314
381
  retention=log_retention,
315
382
  compression=log_compression,
316
383
  log_format=log_format,
317
384
  )
318
- else:
319
- # Hosted: log_* options are ignored. Warn if user tried to set them.
320
- user_set_log_opts = (
321
- log_path != "./controlzero.log"
322
- or log_rotation != "daily"
323
- or log_retention != "30 days"
324
- or log_compression is not None
325
- or log_format != "json"
385
+ except (OSError, PermissionError) as exc:
386
+ # The local audit log must never crash the client when the FAILURE
387
+ # IS ENVIRONMENTAL: an unwritable log path (read-only HOME, sandbox,
388
+ # full disk) falls back to no local sink. In hosted mode the remote
389
+ # sink still carries the trail; in pure-local mode there may be no
390
+ # remote sink, but the alternative -- crashing every guard() call --
391
+ # is worse, and the warning surfaces the cause.
392
+ #
393
+ # #1247 review (codex): this catch is deliberately NARROW. A broad
394
+ # ``except Exception`` here would also swallow a genuine logger bug
395
+ # or bad config and silently set _audit=None, which in pure-local
396
+ # mode would make audit vanish with no signal. Programming errors
397
+ # must propagate so they are caught in tests/CI, not in production.
398
+ self._audit = None
399
+ import logging as _logging
400
+ _logging.getLogger("controlzero.client").warning(
401
+ "controlzero: local audit log path unavailable (%s); "
402
+ "audit will be recorded remotely only",
403
+ exc,
326
404
  )
327
- if user_set_log_opts:
328
- warnings.warn(
329
- "controlzero: log_* options are ignored when an API key is set "
330
- "(audit is managed server-side).",
331
- UserWarning,
332
- stacklevel=2,
333
- )
334
405
 
335
406
  # --- Hosted policy periodic refresh state --------------------------
336
407
  #
@@ -1746,9 +1817,22 @@ class Client:
1746
1817
  if "host_tool_name" in context:
1747
1818
  entry["host_tool_name"] = context["host_tool_name"]
1748
1819
 
1749
- # Local file first (when local audit is enabled)
1820
+ # Local file first (when local audit is enabled).
1821
+ #
1822
+ # #1247 review (codex): now that the local audit log is written in
1823
+ # hosted mode too, we must NOT regress the data-exposure posture.
1824
+ # ``dlp_findings[*].matched_text`` is PLAINTEXT for pii/financial
1825
+ # categories (only the secret category is already SHA-256 hashed --
1826
+ # see dlp_scanner). Previously hosted mode kept that argument-derived
1827
+ # sensitive content server-side only; persisting it to the local
1828
+ # plaintext ~/.controlzero/audit.log would leak PII/financial data to
1829
+ # anyone who can read the file. So the LOCAL row carries DLP findings
1830
+ # with the raw plaintext stripped (rule_id / category / location /
1831
+ # count are preserved so the local log still shows THAT a match fired,
1832
+ # just not the matched value). The REMOTE sinks keep full fidelity --
1833
+ # they ship over TLS to server-side storage exactly as before.
1750
1834
  if self._audit is not None:
1751
- self._audit.log(entry)
1835
+ self._audit.log(_redact_local_dlp(entry))
1752
1836
 
1753
1837
  # Remote sink (enrolled machines: signed-request auth).
1754
1838
  if self._remote_sink is not None:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "controlzero"
7
- version = "1.9.0"
7
+ version = "1.9.1"
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"}
@@ -0,0 +1,217 @@
1
+ """Regression guard for epic #1247: hosted mode MUST write the local audit log.
2
+
3
+ P0 customer regression (2026-06-15): after
4
+ ``controlzero install claude-code --api-key cz_live_...`` (hosted mode), a
5
+ customer's Claude Code tool calls reached the dashboard (remote bearer sink)
6
+ but their local ``~/.controlzero/audit.log`` stayed frozen for weeks. Root
7
+ cause: ``Client.__init__`` only constructed the ``LocalAuditLogger`` when NO
8
+ API key was set, so hosted mode left ``self._audit`` ``None`` and
9
+ ``_audit_decision`` skipped the local write entirely.
10
+
11
+ That silently broke the day-one value prop the claude-code template AND the
12
+ ``controlzero install`` message both promise verbatim: every tool call is
13
+ logged to ``~/.controlzero/audit.log``. The promise is mode-independent, so the
14
+ local sink must be too.
15
+
16
+ These tests pin the contract so the core audit path cannot silently regress
17
+ again:
18
+
19
+ * Hosted mode (api_key set) writes a LOCAL audit row for an ALLOW decision.
20
+ * Hosted mode writes a LOCAL audit row for a DENY decision (a blocked call
21
+ MUST be locally recorded -- this is the exact shape the customer hit:
22
+ Claude Code Bash calls denied by the policy engine).
23
+ * Hosted mode still ALSO ships the row remotely (bearer sink), i.e. the fix
24
+ layers local audit ON TOP of remote, it does not trade one for the other.
25
+ * Both rows carry ``client_name == "claude_code"`` so the dashboard + local
26
+ log attribute the call to the right host adapter.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import json
32
+ from unittest.mock import MagicMock, patch
33
+
34
+ from controlzero import Client
35
+
36
+
37
+ def _make_fake_bearer_sink():
38
+ """A BearerAuditSink stand-in that records every entry instead of POSTing."""
39
+ sink = MagicMock()
40
+ sink.logged = []
41
+ sink.log.side_effect = lambda entry: sink.logged.append(entry)
42
+ sink.close = MagicMock()
43
+ return sink
44
+
45
+
46
+ def _read_local_audit_rows(log_path):
47
+ """Parse the local audit log into a list of decoded JSON rows.
48
+
49
+ The LocalAuditLogger writes one JSON object per line (loguru ``{message}``
50
+ sink). Lines that are not pure JSON (loguru may not be installed, in which
51
+ case it falls back to a formatted stderr line) are skipped.
52
+ """
53
+ rows = []
54
+ if not log_path.exists():
55
+ return rows
56
+ for line in log_path.read_text(encoding="utf-8").splitlines():
57
+ line = line.strip()
58
+ if not line:
59
+ continue
60
+ try:
61
+ rows.append(json.loads(line))
62
+ except json.JSONDecodeError:
63
+ continue
64
+ return rows
65
+
66
+
67
+ def _hosted_client(monkeypatch, tmp_path, *, allow: bool, fake_sink):
68
+ """Build a hosted-mode Client whose evaluator returns a fixed allow/deny.
69
+
70
+ Patches the hosted bundle load + the bearer sink so no network is touched.
71
+ The evaluator is stubbed to return the requested effect deterministically,
72
+ independent of whatever the (mocked) bundle contains.
73
+ """
74
+ monkeypatch.setenv("CONTROLZERO_API_KEY", "cz_live_1247_regression")
75
+ monkeypatch.delenv("CONTROLZERO_LOCAL_OVERRIDE", raising=False)
76
+
77
+ fake_parsed = MagicMock()
78
+ fake_parsed.payload = {"project_id": "proj-1247"}
79
+
80
+ log_path = tmp_path / "audit.log"
81
+
82
+ with patch(
83
+ "controlzero.audit_remote.BearerAuditSink", return_value=fake_sink
84
+ ), patch(
85
+ "controlzero.hosted_policy.load_hosted_policy",
86
+ return_value=({"version": "1", "rules": [{"allow": "*"}]}, fake_parsed),
87
+ ), patch(
88
+ "controlzero.hosted_policy.load_cached_bundle",
89
+ return_value=None,
90
+ ), patch(
91
+ "controlzero.client.load_policy",
92
+ return_value=MagicMock(rules=[], settings=MagicMock()),
93
+ ):
94
+ cz = Client(log_path=str(log_path))
95
+
96
+ # Stub the evaluator so the decision is deterministic regardless of bundle.
97
+ from controlzero._internal.enforcer import PolicyDecision
98
+
99
+ effect = "allow" if allow else "deny"
100
+ decision = PolicyDecision(
101
+ effect=effect,
102
+ reason="ok" if allow else "blocked by policy (no rule match)",
103
+ policy_id="rule-1" if allow else "synthetic:NO_RULE_MATCH",
104
+ )
105
+ cz._evaluator = MagicMock()
106
+ cz._evaluator.evaluate.return_value = decision
107
+ return cz, log_path
108
+
109
+
110
+ def test_hosted_mode_writes_local_audit_on_allow(monkeypatch, tmp_path):
111
+ fake_sink = _make_fake_bearer_sink()
112
+ cz, log_path = _hosted_client(
113
+ monkeypatch, tmp_path, allow=True, fake_sink=fake_sink
114
+ )
115
+ try:
116
+ # Hosted Client MUST have a live local audit sink (the regression was
117
+ # this being None whenever an API key was set).
118
+ assert cz._audit is not None, (
119
+ "hosted-mode Client must construct a local audit logger "
120
+ "(#1247: it used to be None when api_key was set)"
121
+ )
122
+ cz.guard(
123
+ "Bash",
124
+ args={"command": "echo hi"},
125
+ method="echo",
126
+ context={"client_name": "claude_code"},
127
+ )
128
+ finally:
129
+ cz.close()
130
+
131
+ rows = _read_local_audit_rows(log_path)
132
+ allow_rows = [r for r in rows if r.get("decision") == "allow"]
133
+ assert allow_rows, f"expected a local ALLOW audit row, got rows: {rows}"
134
+ row = allow_rows[-1]
135
+ assert row["tool"] == "Bash", row
136
+ assert row["client_name"] == "claude_code", row
137
+ assert row["policy_source"] == "hosted", row
138
+
139
+ # The fix layers local ON TOP of remote: the bearer sink still got the row.
140
+ remote_rows = [e for e in fake_sink.logged if e.get("mode") != "lifecycle"]
141
+ assert remote_rows, "hosted mode must still ship the row to the remote sink"
142
+
143
+
144
+ def test_hosted_mode_writes_local_audit_on_deny(monkeypatch, tmp_path):
145
+ """The exact customer shape: a Claude Code call DENIED in hosted mode must
146
+ still produce a local audit row. A blocked call that leaves no local trace
147
+ is the worst case -- it is precisely the action an auditor needs to see."""
148
+ fake_sink = _make_fake_bearer_sink()
149
+ cz, log_path = _hosted_client(
150
+ monkeypatch, tmp_path, allow=False, fake_sink=fake_sink
151
+ )
152
+ try:
153
+ assert cz._audit is not None, "hosted-mode Client must have a local audit logger"
154
+ decision = cz.guard(
155
+ "Bash",
156
+ args={"command": "rm -rf /tmp/x"},
157
+ method="rm",
158
+ context={"client_name": "claude_code"},
159
+ )
160
+ assert decision.denied, "test setup expected a deny decision"
161
+ finally:
162
+ cz.close()
163
+
164
+ rows = _read_local_audit_rows(log_path)
165
+ deny_rows = [r for r in rows if r.get("decision") == "deny"]
166
+ assert deny_rows, f"expected a local DENY audit row, got rows: {rows}"
167
+ row = deny_rows[-1]
168
+ assert row["tool"] == "Bash", row
169
+ assert row["client_name"] == "claude_code", row
170
+ assert row["policy_source"] == "hosted", row
171
+
172
+ remote_rows = [e for e in fake_sink.logged if e.get("mode") != "lifecycle"]
173
+ assert remote_rows, "hosted mode must still ship the deny row to the remote sink"
174
+
175
+
176
+ def test_local_dlp_redaction_strips_plaintext_but_remote_keeps_it():
177
+ """#1247 review (codex): the local plaintext audit row must NOT carry raw
178
+ pii/financial ``matched_text``. The remote sink keeps full fidelity.
179
+
180
+ Now that hosted mode writes the local audit.log, persisting a DLP match's
181
+ raw plaintext value to ~/.controlzero/audit.log would leak PII/financial
182
+ data to anyone who can read the file -- content hosted mode previously
183
+ kept server-side only. The redaction preserves the finding's metadata
184
+ (rule_id/category/location) so the local log still shows a match fired.
185
+ """
186
+ from controlzero.client import _redact_local_dlp
187
+
188
+ entry = {
189
+ "decision": "deny",
190
+ "tool": "Bash",
191
+ "dlp_findings": [
192
+ {"rule_id": "ssn", "category": "pii", "matched_text": "123-45-6789", "location": "command"},
193
+ {"rule_id": "ccn", "category": "financial", "matched_text": "4111111111111111"},
194
+ # secret category is ALREADY hashed by the scanner -> safe, keep as-is.
195
+ {"rule_id": "aws", "category": "secret", "matched_text": "sha256:deadbeef"},
196
+ ],
197
+ }
198
+
199
+ safe = _redact_local_dlp(entry)
200
+
201
+ # Local copy: pii/financial plaintext stripped, secret hash preserved.
202
+ by_rule = {f["rule_id"]: f for f in safe["dlp_findings"]}
203
+ assert by_rule["ssn"]["matched_text"] == "[redacted-local]"
204
+ assert by_rule["ccn"]["matched_text"] == "[redacted-local]"
205
+ assert by_rule["aws"]["matched_text"] == "sha256:deadbeef"
206
+ # Metadata preserved so the local log still records THAT a rule fired.
207
+ assert by_rule["ssn"]["category"] == "pii"
208
+ assert by_rule["ssn"]["location"] == "command"
209
+
210
+ # The ORIGINAL entry (which the remote sinks consume) is untouched: full
211
+ # fidelity ships to server-side storage exactly as before.
212
+ assert entry["dlp_findings"][0]["matched_text"] == "123-45-6789"
213
+ assert entry["dlp_findings"][1]["matched_text"] == "4111111111111111"
214
+
215
+ # No DLP findings -> identity (cheap common path).
216
+ plain = {"decision": "allow", "tool": "Read"}
217
+ assert _redact_local_dlp(plain) is plain
@@ -0,0 +1,50 @@
1
+ """Audit-log option behavior across hosted + pure-local modes.
2
+
3
+ History: before epic #1247 (2026-06-15), hosted mode (API key set) DISABLED the
4
+ local audit log entirely and warned that ``log_*`` options were ignored. That
5
+ was the regression -- a customer's ``~/.controlzero/audit.log`` froze while the
6
+ dashboard kept receiving rows. The fix makes the local audit log ALWAYS active
7
+ (see ``Client.__init__`` and ``tests/test_hosted_local_audit_1247.py``), so the
8
+ ``log_*`` options are now HONORED in hosted mode and the old "ignored" warning
9
+ no longer fires.
10
+ """
11
+
12
+ import warnings
13
+
14
+ from controlzero import Client
15
+
16
+
17
+ def test_log_options_honored_in_hosted_mode(monkeypatch, tmp_path):
18
+ """Hosted mode (api key set) now WRITES the local audit log, so the
19
+ log_* options take effect and the legacy 'ignored' warning is gone."""
20
+ monkeypatch.setenv("CONTROLZERO_API_KEY", "cz_test_fakekey")
21
+ custom_log = tmp_path / "custom.log"
22
+ with warnings.catch_warnings(record=True) as w:
23
+ warnings.simplefilter("always")
24
+ cz = Client(
25
+ policy={"rules": [{"allow": "*"}]},
26
+ log_path=str(custom_log),
27
+ log_rotation="1 MB",
28
+ )
29
+ log_warnings = [x for x in w if "log_*" in str(x.message)]
30
+ assert not log_warnings, (
31
+ "#1247: hosted mode now honors log_* options (local audit always "
32
+ f"on); no 'ignored' warning expected, got: {[str(x.message) for x in log_warnings]}"
33
+ )
34
+ # A local audit sink must exist in hosted mode now.
35
+ assert cz._audit is not None
36
+ cz.close()
37
+
38
+
39
+ def test_log_options_silent_in_pure_local(tmp_log):
40
+ with warnings.catch_warnings(record=True) as w:
41
+ warnings.simplefilter("always")
42
+ cz = Client(
43
+ policy={"rules": [{"allow": "*"}]},
44
+ log_path=str(tmp_log),
45
+ log_rotation="1 MB",
46
+ )
47
+ # No warning expected
48
+ log_warnings = [x for x in w if "log_*" in str(x.message)]
49
+ assert not log_warnings
50
+ cz.close()
@@ -1,35 +0,0 @@
1
- """When API key is set (hybrid mode), log_* options are ignored with a warning."""
2
-
3
- import warnings
4
-
5
- from controlzero import Client
6
-
7
-
8
- def test_log_options_warn_in_hybrid_mode(monkeypatch):
9
- """Hybrid mode (api key + local policy): log_* options have no effect
10
- because audit ships to remote, so we warn the user."""
11
- monkeypatch.setenv("CONTROLZERO_API_KEY", "cz_test_fakekey")
12
- with warnings.catch_warnings(record=True) as w:
13
- warnings.simplefilter("always")
14
- Client(
15
- policy={"rules": [{"allow": "*"}]},
16
- log_path="/tmp/custom.log",
17
- log_rotation="1 MB",
18
- )
19
- assert any(
20
- "log_*" in str(warning.message) for warning in w
21
- ), f"expected log_* warning, got: {[str(x.message) for x in w]}"
22
-
23
-
24
- def test_log_options_silent_in_pure_local(tmp_log):
25
- with warnings.catch_warnings(record=True) as w:
26
- warnings.simplefilter("always")
27
- cz = Client(
28
- policy={"rules": [{"allow": "*"}]},
29
- log_path=str(tmp_log),
30
- log_rotation="1 MB",
31
- )
32
- # No warning expected
33
- log_warnings = [x for x in w if "log_*" in str(x.message)]
34
- assert not log_warnings
35
- cz.close()
File without changes
File without changes
File without changes
File without changes