controlzero 1.9.0__tar.gz → 1.9.2__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.0 → controlzero-1.9.2}/CHANGELOG.md +73 -0
  2. {controlzero-1.9.0 → controlzero-1.9.2}/PKG-INFO +1 -1
  3. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/__init__.py +1 -1
  4. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/audit_remote.py +71 -11
  5. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/spool_cmd.py +15 -2
  6. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/client.py +108 -24
  7. controlzero-1.9.2/controlzero/spool/_keyring.py +311 -0
  8. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/spool/_spool.py +38 -3
  9. controlzero-1.9.2/controlzero/spool/_state.py +356 -0
  10. {controlzero-1.9.0 → controlzero-1.9.2}/pyproject.toml +1 -1
  11. {controlzero-1.9.0 → controlzero-1.9.2}/tests/conftest.py +24 -0
  12. controlzero-1.9.2/tests/spool/test_spool_durable_default_tamper.py +354 -0
  13. controlzero-1.9.2/tests/spool/test_spool_keychain_dek.py +309 -0
  14. {controlzero-1.9.0 → controlzero-1.9.2}/tests/spool/test_spool_sink_wiring.py +55 -6
  15. controlzero-1.9.2/tests/test_hosted_local_audit_1247.py +217 -0
  16. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hosted_policy_e2e.py +16 -3
  17. controlzero-1.9.2/tests/test_log_options_ignored_hosted.py +50 -0
  18. controlzero-1.9.0/controlzero/spool/_state.py +0 -154
  19. controlzero-1.9.0/tests/test_log_options_ignored_hosted.py +0 -35
  20. {controlzero-1.9.0 → controlzero-1.9.2}/.gitignore +0 -0
  21. {controlzero-1.9.0 → controlzero-1.9.2}/Dockerfile.test +0 -0
  22. {controlzero-1.9.0 → controlzero-1.9.2}/LICENSE +0 -0
  23. {controlzero-1.9.0 → controlzero-1.9.2}/README.md +0 -0
  24. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/_internal/__init__.py +0 -0
  25. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/_internal/action_aliases.py +0 -0
  26. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/_internal/action_validator.py +0 -0
  27. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/_internal/bundle.py +0 -0
  28. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/_internal/credential_hook.py +0 -0
  29. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/_internal/credential_scanner.py +0 -0
  30. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/_internal/credentials_data/__init__.py +0 -0
  31. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/_internal/credentials_data/built_in.yaml +0 -0
  32. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/_internal/dlp_scanner.py +0 -0
  33. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/_internal/enforcer.py +0 -0
  34. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/_internal/hook_extractors.py +0 -0
  35. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/_internal/tool_extractors.json +0 -0
  36. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/_internal/types.py +0 -0
  37. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/audit_local.py +0 -0
  38. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/canonical.py +0 -0
  39. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/__init__.py +0 -0
  40. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/_secrets.py +0 -0
  41. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/console.py +0 -0
  42. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/debug_bundle.py +0 -0
  43. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/doctor.py +0 -0
  44. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/hosts/__init__.py +0 -0
  45. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/hosts/antigravity.py +0 -0
  46. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/hosts/base.py +0 -0
  47. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/hosts/claude_code.py +0 -0
  48. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/hosts/codex_cli.py +0 -0
  49. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/hosts/gemini_cli.py +0 -0
  50. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/hosts/kiro.py +0 -0
  51. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/hosts/unknown.py +0 -0
  52. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/kiro_adapter.py +0 -0
  53. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/main.py +0 -0
  54. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/migrate.py +0 -0
  55. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/telemetry_consent.py +0 -0
  56. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/antigravity/hooks.json +0 -0
  57. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/antigravity.yaml +0 -0
  58. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/autogen.yaml +0 -0
  59. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/claude-code.yaml +0 -0
  60. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/codex-cli.yaml +0 -0
  61. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/cost-cap.yaml +0 -0
  62. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/crewai.yaml +0 -0
  63. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/cursor.yaml +0 -0
  64. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/gemini-cli.yaml +0 -0
  65. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/generic.yaml +0 -0
  66. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook +0 -0
  67. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/kiro/ide-pre-tool-use.kiro.hook +0 -0
  68. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/kiro/ide-prompt-submit.kiro.hook +0 -0
  69. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/kiro/kiro.yaml +0 -0
  70. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/langchain.yaml +0 -0
  71. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/mcp.yaml +0 -0
  72. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/rag.yaml +0 -0
  73. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/device.py +0 -0
  74. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/enrollment.py +0 -0
  75. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/error_codes.py +0 -0
  76. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/error_codes.yaml +0 -0
  77. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/errors.py +0 -0
  78. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/hitl/__init__.py +0 -0
  79. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/hitl/grant_protocol.py +0 -0
  80. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/hitl/mock.py +0 -0
  81. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/hitl/pending_approval.py +0 -0
  82. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/hitl/secret_leak_guard.py +0 -0
  83. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/hitl/status.py +0 -0
  84. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/hooks/__init__.py +0 -0
  85. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/hooks/tool_output_handler.py +0 -0
  86. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/hosted_policy.py +0 -0
  87. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/__init__.py +0 -0
  88. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/anthropic.py +0 -0
  89. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/autogen.py +0 -0
  90. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/braintrust.py +0 -0
  91. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/crewai/__init__.py +0 -0
  92. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/crewai/agent.py +0 -0
  93. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/crewai/crew.py +0 -0
  94. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/crewai/task.py +0 -0
  95. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/crewai/tool.py +0 -0
  96. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/google.py +0 -0
  97. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/google_adk/__init__.py +0 -0
  98. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/google_adk/agent.py +0 -0
  99. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/google_adk/tool.py +0 -0
  100. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/langchain/__init__.py +0 -0
  101. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/langchain/agent.py +0 -0
  102. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/langchain/callbacks.py +0 -0
  103. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/langchain/chain.py +0 -0
  104. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/langchain/graph.py +0 -0
  105. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/langchain/modern.py +0 -0
  106. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/langchain/tool.py +0 -0
  107. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/langfuse.py +0 -0
  108. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/litellm.py +0 -0
  109. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/openai.py +0 -0
  110. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/pydantic_ai.py +0 -0
  111. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/vercel_ai.py +0 -0
  112. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/layout_migration.py +0 -0
  113. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/policy_loader.py +0 -0
  114. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/spool/__init__.py +0 -0
  115. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/spool/_compress.py +0 -0
  116. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/spool/_constants.py +0 -0
  117. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/spool/_crc32c.py +0 -0
  118. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/spool/_crypto.py +0 -0
  119. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/spool/_frame.py +0 -0
  120. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/spool/_metrics.py +0 -0
  121. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/spool/_uploader.py +0 -0
  122. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/spool/cz-audit-v1.dict +0 -0
  123. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/tamper.py +0 -0
  124. {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/tracecontext.py +0 -0
  125. {controlzero-1.9.0 → controlzero-1.9.2}/examples/hello_world.py +0 -0
  126. {controlzero-1.9.0 → controlzero-1.9.2}/tests/_fixtures/jcs_args_hash_vectors.json +0 -0
  127. {controlzero-1.9.0 → controlzero-1.9.2}/tests/integrations/__init__.py +0 -0
  128. {controlzero-1.9.0 → controlzero-1.9.2}/tests/integrations/test_google.py +0 -0
  129. {controlzero-1.9.0 → controlzero-1.9.2}/tests/parity/action_aliases.json +0 -0
  130. {controlzero-1.9.0 → controlzero-1.9.2}/tests/spool/__init__.py +0 -0
  131. {controlzero-1.9.0 → controlzero-1.9.2}/tests/spool/conftest.py +0 -0
  132. {controlzero-1.9.0 → controlzero-1.9.2}/tests/spool/test_spool_cli.py +0 -0
  133. {controlzero-1.9.0 → controlzero-1.9.2}/tests/spool/test_spool_concurrency.py +0 -0
  134. {controlzero-1.9.0 → controlzero-1.9.2}/tests/spool/test_spool_conformance.py +0 -0
  135. {controlzero-1.9.0 → controlzero-1.9.2}/tests/spool/test_spool_core.py +0 -0
  136. {controlzero-1.9.0 → controlzero-1.9.2}/tests/spool/test_spool_crash.py +0 -0
  137. {controlzero-1.9.0 → controlzero-1.9.2}/tests/spool/test_spool_diskfull.py +0 -0
  138. {controlzero-1.9.0 → controlzero-1.9.2}/tests/spool/test_spool_transcript_localack.py +0 -0
  139. {controlzero-1.9.0 → controlzero-1.9.2}/tests/spool/test_spool_uploader.py +0 -0
  140. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_action_aliases.py +0 -0
  141. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_action_canonicalization.py +0 -0
  142. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_action_validator_t86.py +0 -0
  143. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_agent_name_env.py +0 -0
  144. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_antigravity_adapter.py +0 -0
  145. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_antigravity_hook_check.py +0 -0
  146. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_antigravity_install.py +0 -0
  147. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_api_key_mask.py +0 -0
  148. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_audit_remote.py +0 -0
  149. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_audit_remote_sdk_version.py +0 -0
  150. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_audit_sink_isolation.py +0 -0
  151. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_bundle_parser.py +0 -0
  152. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_bundle_translate.py +0 -0
  153. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_canonical_phase1a.py +0 -0
  154. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_cli_carve_out.py +0 -0
  155. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_cli_debug_bundle.py +0 -0
  156. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_cli_extractor_integration.py +0 -0
  157. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_cli_hook.py +0 -0
  158. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_cli_hosted_refresh.py +0 -0
  159. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_cli_init.py +0 -0
  160. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_cli_init_templates.py +0 -0
  161. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_cli_tail.py +0 -0
  162. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_cli_test.py +0 -0
  163. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_cli_validate.py +0 -0
  164. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_coding_agent_hooks.py +0 -0
  165. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_conditions.py +0 -0
  166. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_conformance.py +0 -0
  167. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_console.py +0 -0
  168. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_credential_hook.py +0 -0
  169. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_default_action.py +0 -0
  170. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_device.py +0 -0
  171. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_dlp_scanner.py +0 -0
  172. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_doctor.py +0 -0
  173. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_engine_version_consistency.py +0 -0
  174. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_enrollment.py +0 -0
  175. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_env_dump_438.py +0 -0
  176. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_error_codes.py +0 -0
  177. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_errors_e_codes.py +0 -0
  178. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_fail_closed_eval.py +0 -0
  179. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_glob_matching.py +0 -0
  180. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hitl_5d_email_install.py +0 -0
  181. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hitl_6a_cli_flag.py +0 -0
  182. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hitl_6a_exceptions.py +0 -0
  183. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hitl_6a_get_secret_hitl.py +0 -0
  184. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hitl_6a_mock_backend.py +0 -0
  185. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hitl_6a_pending_approval.py +0 -0
  186. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hitl_6a_request_approval.py +0 -0
  187. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hitl_6a_secret_leak_guard.py +0 -0
  188. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hitl_6a_wait.py +0 -0
  189. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hitl_conformance.py +0 -0
  190. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hitl_phase2b_protocol.py +0 -0
  191. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hitl_reason_codes.py +0 -0
  192. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hitl_validator_keys.py +0 -0
  193. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hook_extractors.py +0 -0
  194. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hosts_adapter.py +0 -0
  195. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hybrid_mode_strict.py +0 -0
  196. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hybrid_mode_warn.py +0 -0
  197. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_install_hook_command.py +0 -0
  198. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_install_hooks.py +0 -0
  199. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_kiro_adapter.py +0 -0
  200. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_kiro_hook_templates.py +0 -0
  201. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_kiro_install.py +0 -0
  202. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_layout_migration_t101.py +0 -0
  203. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_layout_parity_t102.py +0 -0
  204. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_local_mode_dict.py +0 -0
  205. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_local_mode_file_json.py +0 -0
  206. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_local_mode_file_yaml.py +0 -0
  207. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_log_fallback_stderr.py +0 -0
  208. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_log_rotation.py +0 -0
  209. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_migrate.py +0 -0
  210. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_min_sdk_version_gate.py +0 -0
  211. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_multi_client_per_project_175.py +0 -0
  212. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_no_policy_no_key.py +0 -0
  213. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_package_rename_shim.py +0 -0
  214. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_policy_engine_version_phase1b.py +0 -0
  215. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_policy_freshness.py +0 -0
  216. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_policy_settings.py +0 -0
  217. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_policy_source_audit.py +0 -0
  218. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_quarantine.py +0 -0
  219. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_reason_code.py +0 -0
  220. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_refresh.py +0 -0
  221. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_secrets.py +0 -0
  222. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_sql_semantic_class.py +0 -0
  223. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_synthetic_policy_id_t79.py +0 -0
  224. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_t103_precedence.py +0 -0
  225. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_t104_cache_gc.py +0 -0
  226. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_t108_local_override_audit.py +0 -0
  227. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_t96_single_audit_log.py +0 -0
  228. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_t99_install_prefetch_bundle.py +0 -0
  229. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_tamper.py +0 -0
  230. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_tamper_behavior.py +0 -0
  231. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_tamper_hook.py +0 -0
  232. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_telemetry_consent.py +0 -0
  233. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_tracecontext.py +0 -0
  234. {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_unsafe_int_boundary.py +0 -0
  235. {controlzero-1.9.0 → controlzero-1.9.2}/tools/cz-kiro-adapter +0 -0
@@ -1,5 +1,78 @@
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
+
32
+ ## 1.9.2 -- 2026-06-16 (durable-by-default + keychain-DEK audit spool, epic gh#1247)
33
+
34
+ ### Changed
35
+
36
+ - **Hosted-mode audit is now durable by default.** When a `Client` is
37
+ constructed with an API key (hosted mode) and `CONTROLZERO_SPOOL` is
38
+ unset, the audit sink now defaults to the **durable encrypted spool**:
39
+ every decision is serialized, encrypted, and fsynced to an append-only
40
+ on-disk WAL **before** any network send, then drained opportunistically
41
+ in a background thread. Audit is no longer lost on a backend outage. An
42
+ explicit `CONTROLZERO_SPOOL` value (including `off`) still wins, and the
43
+ enrolled-machine sink keeps its prior off-by-default. Sink init is
44
+ fail-soft: any open/IO error degrades to the prior in-memory path,
45
+ emits `spool_init_degraded_total`, and never crashes or blocks the
46
+ PreToolUse hook.
47
+
48
+ ### Security
49
+
50
+ - **Spool encryption key (DEK) defaults to the OS keystore.** The DEK now
51
+ lives in the macOS Keychain (a `security` generic-password item) or the
52
+ Linux Secret Service (`secret-tool`/libsecret) by default when one is
53
+ available; the on-disk `spool.key` then holds only a sentinel, so a
54
+ file-read of the spool directory can neither **decrypt** nor **forge**
55
+ spooled audit records (the AES-GCM tag and hash chain are keyed on the
56
+ DEK). Keystore access is strictly **non-interactive** and
57
+ hard-timeout-bounded -- a prompt risk, locked keystore, or missing CLI
58
+ degrades to the legacy 0600 `spool.key` (key hardening never costs audit
59
+ durability and never blocks the hook). A pre-existing on-disk DEK is
60
+ migrated into the keystore on first keystore-enabled open. Force the
61
+ legacy file DEK with `CONTROLZERO_SPOOL_KEYCHAIN=0` or
62
+ `CONTROLZERO_SPOOL_KEYCHAIN_DISABLE=1`; require the keystore with
63
+ `CONTROLZERO_SPOOL_KEYCHAIN=1`.
64
+
65
+ ### Tests
66
+
67
+ - New `tests/spool/test_spool_keychain_dek.py` (keystore round-trip,
68
+ sentinel-on-disk, prompt-risk fallback, file opt-out, file->keystore
69
+ migration) and `tests/spool/test_spool_durable_default_tamper.py`
70
+ (hosted durable default, encrypted-WAL-before-send, **tampered-frame
71
+ rejection surfaced via `spool_tamper_records_total{gcm_auth}`**,
72
+ graceful init-failure degrade, WAL append latency budget). Spool
73
+ isolation is enforced suite-wide so tests never touch the real home dir
74
+ or OS keychain.
75
+
3
76
  ## 1.9.0 -- 2026-06-15 (Antigravity install CLI, epic gh#925)
4
77
 
5
78
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: controlzero
3
- Version: 1.9.0
3
+ Version: 1.9.2
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.2"
44
44
 
45
45
  __all__ = [
46
46
  "Client",
@@ -261,26 +261,74 @@ class _SpoolWiringMixin:
261
261
  _spool = None
262
262
  _spool_mode = "off"
263
263
 
264
- def _init_spool(self, stream_key: str) -> None:
264
+ def _init_spool(self, stream_key: str, default_mode: str = "off") -> None:
265
+ """Open the offline audit spool for this sink.
266
+
267
+ ``default_mode`` is the mode used when ``CONTROLZERO_SPOOL`` is
268
+ UNSET. The hosted (API-key) sink passes ``"durable"`` so audit is
269
+ never lost on a backend outage even when the operator has not set
270
+ the env knob -- the founder requirement that hosted-mode audit
271
+ WALs to encrypted disk before send by default. The enrolled sink
272
+ keeps ``"off"`` (no behavior change). An explicit ``CONTROLZERO_SPOOL``
273
+ value ALWAYS wins (operators can force ``off``/``spool_only``).
274
+
275
+ Non-blocking guarantee (founder constraint): this runs inside the
276
+ PreToolUse hook hot path. Every failure mode -- spool import
277
+ error, keystore prompt risk, IO error, ENOSPC -- DEGRADES to the
278
+ prior in-memory behavior and emits ``spool_init_degraded_total``;
279
+ it never crashes or blocks the agent. Spool open itself is
280
+ bounded (one recovery scan + the 200 ms seq.lock budget); it does
281
+ no network IO.
282
+ """
265
283
  self._drain_state_lock = threading.Lock()
266
284
  self._drain_inflight = False
267
285
  self._drain_again = False
268
286
  self._drain_auth_blocked = False
269
287
  self._spool = None
270
288
  self._spool_mode = "off"
271
- # Fast path: flag off AND no spool directory on disk -- skip the
272
- # spool import entirely so the default path stays byte-identical.
273
- mode_raw = (os.environ.get("CONTROLZERO_SPOOL") or "").strip().lower()
289
+ # Fast path: flag off/unset, NO hosted default, AND no spool
290
+ # directory on disk -- skip the spool import entirely so the
291
+ # legacy default path stays byte-identical. When default_mode is
292
+ # durable (hosted), we must NOT take this shortcut: the whole
293
+ # point is to open a durable spool with no env set.
294
+ env_present = os.environ.get("CONTROLZERO_SPOOL")
295
+ mode_raw = (env_present or "").strip().lower()
274
296
  spool_dir = os.path.expanduser(
275
297
  os.environ.get("CONTROLZERO_SPOOL_DIR") or _SPOOL_DEFAULT_DIR)
276
- if mode_raw in ("", "off") and not os.path.isdir(spool_dir):
298
+ # "Unset" means the env var is absent or an empty/whitespace
299
+ # string. An EXPLICIT "off" is a deliberate operator choice and
300
+ # MUST win over the sink default -- it is NOT treated as unset.
301
+ env_unset = (env_present is None) or (mode_raw == "")
302
+ env_off = mode_raw == "off"
303
+ # Effective mode is "off" when: env explicitly off, OR env unset
304
+ # AND this sink's default is off. In that case, with no spool on
305
+ # disk, skip the spool import entirely (byte-identical legacy).
306
+ effective_off = env_off or (env_unset and default_mode in ("", "off"))
307
+ if effective_off and not os.path.isdir(spool_dir):
277
308
  return
278
309
  try:
279
- from controlzero.spool import Spool, get_mode
280
-
281
- self._spool_mode = get_mode()
282
- self._spool = Spool.maybe_open(stream_key)
310
+ from controlzero.spool import Spool, SpoolConfig, get_mode
311
+
312
+ # Resolve the effective mode: an explicit env value (incl.
313
+ # "off") wins; only a truly UNSET/blank env falls back to
314
+ # this sink's default_mode.
315
+ resolved = get_mode() # "off" when unset/blank or unknown
316
+ if env_unset and default_mode not in ("", "off"):
317
+ resolved = default_mode
318
+ cfg = SpoolConfig.from_env()
319
+ cfg.mode = resolved
320
+ self._spool_mode = resolved
321
+ self._spool = Spool.maybe_open(stream_key, config=cfg)
322
+ # If the spool opened but immediately degraded to memory-only
323
+ # (e.g. ENOSPC during the open-time recovery/cleanup), surface
324
+ # it as a metric. The sink still functions -- WAL just buffers
325
+ # in memory until disk frees up -- so this is a warning, not a
326
+ # crash (C15).
327
+ if (self._spool is not None
328
+ and getattr(self._spool, "in_memory_mode", False)):
329
+ self._spool_init_degraded("memory_mode")
283
330
  except Exception as exc: # noqa: BLE001
331
+ self._spool_init_degraded("exception")
284
332
  logger.warning(
285
333
  "controlzero: audit spool unavailable (%s); "
286
334
  "falling back to in-memory buffering",
@@ -289,6 +337,15 @@ class _SpoolWiringMixin:
289
337
  self._spool = None
290
338
  self._spool_mode = "off"
291
339
 
340
+ @staticmethod
341
+ def _spool_init_degraded(reason: str) -> None:
342
+ """Emit the spool-init degrade metric. Never raises."""
343
+ try:
344
+ from controlzero.spool import metrics as _m
345
+ _m.incr("spool_init_degraded_total", reason)
346
+ except Exception: # noqa: BLE001
347
+ pass
348
+
292
349
  @property
293
350
  def _spool_wal(self) -> bool:
294
351
  """True when log() must take the spool-first WAL path."""
@@ -660,8 +717,11 @@ class BearerAuditSink(_SpoolWiringMixin):
660
717
  self._closed = False
661
718
 
662
719
  # Offline audit spool (Phase 2): the stream is keyed by the api
663
- # key fingerprint, exactly the spec's stream identity.
664
- self._init_spool(api_key)
720
+ # key fingerprint, exactly the spec's stream identity. HOSTED
721
+ # mode defaults to durable WAL-to-encrypted-disk so audit is
722
+ # never lost on a backend outage even with no env set (founder
723
+ # requirement). An explicit CONTROLZERO_SPOOL value still wins.
724
+ self._init_spool(api_key, default_mode=_SPOOL_MODE_DURABLE)
665
725
 
666
726
  self._start_flush_timer()
667
727
  # Drain-only rule (plan section 10): an existing non-empty spool
@@ -203,9 +203,22 @@ def spool_verify(as_json):
203
203
  click.echo("error: spool.key missing; cannot verify", err=True)
204
204
  sys.exit(2)
205
205
  from controlzero.spool import assess_segment, derive_key
206
- from controlzero.spool._state import load_or_create_dek, secure_read
206
+ from controlzero.spool._state import (
207
+ SpoolKeyUnavailable,
208
+ load_or_create_dek,
209
+ secure_read,
210
+ )
207
211
 
208
- dek = load_or_create_dek(root)
212
+ try:
213
+ dek = load_or_create_dek(root)
214
+ except SpoolKeyUnavailable:
215
+ click.echo(
216
+ "error: spool DEK lives in the OS keystore but the keystore is "
217
+ "not readable right now (locked, or run on a different machine). "
218
+ "Unlock the keystore (or run on the originating host) and retry.",
219
+ err=True,
220
+ )
221
+ sys.exit(2)
209
222
 
210
223
  def key_provider(header):
211
224
  return derive_key(dek, header.device_id, header.stream_fp,
@@ -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: