controlzero 1.9.7__tar.gz → 1.9.9__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 (241) hide show
  1. {controlzero-1.9.7 → controlzero-1.9.9}/CHANGELOG.md +61 -0
  2. {controlzero-1.9.7 → controlzero-1.9.9}/PKG-INFO +9 -7
  3. {controlzero-1.9.7 → controlzero-1.9.9}/README.md +5 -3
  4. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/__init__.py +1 -1
  5. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/_internal/bundle.py +48 -5
  6. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/_internal/hook_extractors.py +24 -4
  7. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/_internal/tool_extractors.json +5 -4
  8. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/audit_remote.py +26 -0
  9. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/main.py +52 -16
  10. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/antigravity.yaml +35 -13
  11. {controlzero-1.9.7 → controlzero-1.9.9}/pyproject.toml +9 -4
  12. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_antigravity_hook_check.py +8 -5
  13. controlzero-1.9.9/tests/test_antigravity_tool_vocab_1303.py +323 -0
  14. controlzero-1.9.9/tests/test_config_format_parity_1303.py +145 -0
  15. controlzero-1.9.9/tests/test_failopen_1303.py +96 -0
  16. {controlzero-1.9.7 → controlzero-1.9.9}/.gitignore +0 -0
  17. {controlzero-1.9.7 → controlzero-1.9.9}/Dockerfile.test +0 -0
  18. {controlzero-1.9.7 → controlzero-1.9.9}/LICENSE +0 -0
  19. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/_internal/__init__.py +0 -0
  20. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/_internal/action_aliases.py +0 -0
  21. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/_internal/action_validator.py +0 -0
  22. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/_internal/credential_hook.py +0 -0
  23. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/_internal/credential_scanner.py +0 -0
  24. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/_internal/credentials_data/__init__.py +0 -0
  25. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/_internal/credentials_data/built_in.yaml +0 -0
  26. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/_internal/dlp_scanner.py +0 -0
  27. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/_internal/enforcer.py +0 -0
  28. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/_internal/types.py +0 -0
  29. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/audit_local.py +0 -0
  30. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/canonical.py +0 -0
  31. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/__init__.py +0 -0
  32. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/__main__.py +0 -0
  33. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/_secrets.py +0 -0
  34. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/console.py +0 -0
  35. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/debug_bundle.py +0 -0
  36. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/doctor.py +0 -0
  37. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/hosts/__init__.py +0 -0
  38. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/hosts/antigravity.py +0 -0
  39. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/hosts/base.py +0 -0
  40. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/hosts/claude_code.py +0 -0
  41. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/hosts/codex_cli.py +0 -0
  42. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/hosts/gemini_cli.py +0 -0
  43. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/hosts/kiro.py +0 -0
  44. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/hosts/unknown.py +0 -0
  45. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/kiro_adapter.py +0 -0
  46. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/migrate.py +0 -0
  47. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/spool_cmd.py +0 -0
  48. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/telemetry_consent.py +0 -0
  49. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/antigravity/hooks.json +0 -0
  50. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/autogen.yaml +0 -0
  51. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/claude-code.yaml +0 -0
  52. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/codex-cli.yaml +0 -0
  53. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/cost-cap.yaml +0 -0
  54. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/crewai.yaml +0 -0
  55. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/cursor.yaml +0 -0
  56. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/gemini-cli.yaml +0 -0
  57. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/generic.yaml +0 -0
  58. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook +0 -0
  59. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/kiro/ide-pre-tool-use.kiro.hook +0 -0
  60. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/kiro/ide-prompt-submit.kiro.hook +0 -0
  61. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/kiro/kiro.yaml +0 -0
  62. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/langchain.yaml +0 -0
  63. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/mcp.yaml +0 -0
  64. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/rag.yaml +0 -0
  65. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/client.py +0 -0
  66. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/device.py +0 -0
  67. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/enrollment.py +0 -0
  68. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/error_codes.py +0 -0
  69. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/error_codes.yaml +0 -0
  70. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/errors.py +0 -0
  71. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/hitl/__init__.py +0 -0
  72. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/hitl/grant_protocol.py +0 -0
  73. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/hitl/mock.py +0 -0
  74. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/hitl/pending_approval.py +0 -0
  75. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/hitl/secret_leak_guard.py +0 -0
  76. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/hitl/status.py +0 -0
  77. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/hooks/__init__.py +0 -0
  78. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/hooks/tool_output_handler.py +0 -0
  79. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/hosted_policy.py +0 -0
  80. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/__init__.py +0 -0
  81. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/anthropic.py +0 -0
  82. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/autogen.py +0 -0
  83. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/braintrust.py +0 -0
  84. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/crewai/__init__.py +0 -0
  85. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/crewai/agent.py +0 -0
  86. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/crewai/crew.py +0 -0
  87. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/crewai/task.py +0 -0
  88. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/crewai/tool.py +0 -0
  89. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/google.py +0 -0
  90. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/google_adk/__init__.py +0 -0
  91. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/google_adk/agent.py +0 -0
  92. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/google_adk/tool.py +0 -0
  93. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/langchain/__init__.py +0 -0
  94. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/langchain/agent.py +0 -0
  95. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/langchain/callbacks.py +0 -0
  96. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/langchain/chain.py +0 -0
  97. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/langchain/graph.py +0 -0
  98. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/langchain/modern.py +0 -0
  99. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/langchain/tool.py +0 -0
  100. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/langfuse.py +0 -0
  101. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/litellm.py +0 -0
  102. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/openai.py +0 -0
  103. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/pydantic_ai.py +0 -0
  104. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/vercel_ai.py +0 -0
  105. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/layout_migration.py +0 -0
  106. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/policy_loader.py +0 -0
  107. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/spool/__init__.py +0 -0
  108. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/spool/_compress.py +0 -0
  109. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/spool/_constants.py +0 -0
  110. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/spool/_crc32c.py +0 -0
  111. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/spool/_crypto.py +0 -0
  112. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/spool/_frame.py +0 -0
  113. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/spool/_keyring.py +0 -0
  114. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/spool/_metrics.py +0 -0
  115. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/spool/_spool.py +0 -0
  116. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/spool/_state.py +0 -0
  117. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/spool/_uploader.py +0 -0
  118. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/spool/cz-audit-v1.dict +0 -0
  119. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/tamper.py +0 -0
  120. {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/tracecontext.py +0 -0
  121. {controlzero-1.9.7 → controlzero-1.9.9}/examples/hello_world.py +0 -0
  122. {controlzero-1.9.7 → controlzero-1.9.9}/tests/_fixtures/jcs_args_hash_vectors.json +0 -0
  123. {controlzero-1.9.7 → controlzero-1.9.9}/tests/conftest.py +0 -0
  124. {controlzero-1.9.7 → controlzero-1.9.9}/tests/integrations/__init__.py +0 -0
  125. {controlzero-1.9.7 → controlzero-1.9.9}/tests/integrations/test_google.py +0 -0
  126. {controlzero-1.9.7 → controlzero-1.9.9}/tests/parity/action_aliases.json +0 -0
  127. {controlzero-1.9.7 → controlzero-1.9.9}/tests/spool/__init__.py +0 -0
  128. {controlzero-1.9.7 → controlzero-1.9.9}/tests/spool/conftest.py +0 -0
  129. {controlzero-1.9.7 → controlzero-1.9.9}/tests/spool/test_spool_cli.py +0 -0
  130. {controlzero-1.9.7 → controlzero-1.9.9}/tests/spool/test_spool_concurrency.py +0 -0
  131. {controlzero-1.9.7 → controlzero-1.9.9}/tests/spool/test_spool_conformance.py +0 -0
  132. {controlzero-1.9.7 → controlzero-1.9.9}/tests/spool/test_spool_core.py +0 -0
  133. {controlzero-1.9.7 → controlzero-1.9.9}/tests/spool/test_spool_crash.py +0 -0
  134. {controlzero-1.9.7 → controlzero-1.9.9}/tests/spool/test_spool_diskfull.py +0 -0
  135. {controlzero-1.9.7 → controlzero-1.9.9}/tests/spool/test_spool_durable_default_tamper.py +0 -0
  136. {controlzero-1.9.7 → controlzero-1.9.9}/tests/spool/test_spool_keychain_dek.py +0 -0
  137. {controlzero-1.9.7 → controlzero-1.9.9}/tests/spool/test_spool_sink_wiring.py +0 -0
  138. {controlzero-1.9.7 → controlzero-1.9.9}/tests/spool/test_spool_transcript_localack.py +0 -0
  139. {controlzero-1.9.7 → controlzero-1.9.9}/tests/spool/test_spool_uploader.py +0 -0
  140. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_action_aliases.py +0 -0
  141. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_action_canonicalization.py +0 -0
  142. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_action_validator_t86.py +0 -0
  143. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_agent_name_env.py +0 -0
  144. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_antigravity_adapter.py +0 -0
  145. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_antigravity_ga_blockers_1248.py +0 -0
  146. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_antigravity_install.py +0 -0
  147. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_api_key_mask.py +0 -0
  148. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_audit_remote.py +0 -0
  149. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_audit_remote_sdk_version.py +0 -0
  150. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_audit_sink_isolation.py +0 -0
  151. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_bundle_parser.py +0 -0
  152. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_bundle_translate.py +0 -0
  153. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_canonical_phase1a.py +0 -0
  154. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_cli_carve_out.py +0 -0
  155. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_cli_debug_bundle.py +0 -0
  156. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_cli_extractor_integration.py +0 -0
  157. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_cli_hook.py +0 -0
  158. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_cli_hosted_refresh.py +0 -0
  159. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_cli_init.py +0 -0
  160. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_cli_init_templates.py +0 -0
  161. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_cli_tail.py +0 -0
  162. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_cli_test.py +0 -0
  163. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_cli_validate.py +0 -0
  164. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_coding_agent_hooks.py +0 -0
  165. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_conditions.py +0 -0
  166. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_conformance.py +0 -0
  167. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_console.py +0 -0
  168. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_credential_hook.py +0 -0
  169. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_default_action.py +0 -0
  170. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_device.py +0 -0
  171. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_dlp_scanner.py +0 -0
  172. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_doctor.py +0 -0
  173. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_engine_version_consistency.py +0 -0
  174. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_enrollment.py +0 -0
  175. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_env_dump_438.py +0 -0
  176. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_epic_1247_bryan_acceptance.py +0 -0
  177. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_error_codes.py +0 -0
  178. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_errors_e_codes.py +0 -0
  179. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_fail_closed_eval.py +0 -0
  180. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_glob_matching.py +0 -0
  181. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hitl_5d_email_install.py +0 -0
  182. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hitl_6a_cli_flag.py +0 -0
  183. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hitl_6a_exceptions.py +0 -0
  184. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hitl_6a_get_secret_hitl.py +0 -0
  185. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hitl_6a_mock_backend.py +0 -0
  186. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hitl_6a_pending_approval.py +0 -0
  187. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hitl_6a_request_approval.py +0 -0
  188. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hitl_6a_secret_leak_guard.py +0 -0
  189. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hitl_6a_wait.py +0 -0
  190. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hitl_conformance.py +0 -0
  191. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hitl_phase2b_protocol.py +0 -0
  192. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hitl_reason_codes.py +0 -0
  193. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hitl_validator_keys.py +0 -0
  194. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hook_extractors.py +0 -0
  195. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hosted_local_audit_1247.py +0 -0
  196. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hosted_policy_e2e.py +0 -0
  197. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hosts_adapter.py +0 -0
  198. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hybrid_mode_strict.py +0 -0
  199. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hybrid_mode_warn.py +0 -0
  200. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_install_hook_command.py +0 -0
  201. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_install_hooks.py +0 -0
  202. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_kiro_adapter.py +0 -0
  203. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_kiro_cli_e2e.py +0 -0
  204. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_kiro_hook_templates.py +0 -0
  205. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_kiro_install.py +0 -0
  206. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_layout_migration_t101.py +0 -0
  207. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_layout_parity_t102.py +0 -0
  208. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_local_mode_dict.py +0 -0
  209. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_local_mode_file_json.py +0 -0
  210. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_local_mode_file_yaml.py +0 -0
  211. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_log_fallback_stderr.py +0 -0
  212. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_log_options_ignored_hosted.py +0 -0
  213. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_log_rotation.py +0 -0
  214. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_migrate.py +0 -0
  215. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_min_sdk_version_gate.py +0 -0
  216. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_multi_client_per_project_175.py +0 -0
  217. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_no_policy_no_key.py +0 -0
  218. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_observe_mode_1247.py +0 -0
  219. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_package_rename_shim.py +0 -0
  220. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_policy_engine_version_phase1b.py +0 -0
  221. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_policy_freshness.py +0 -0
  222. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_policy_settings.py +0 -0
  223. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_policy_source_audit.py +0 -0
  224. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_quarantine.py +0 -0
  225. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_reason_code.py +0 -0
  226. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_refresh.py +0 -0
  227. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_secrets.py +0 -0
  228. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_sql_semantic_class.py +0 -0
  229. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_synthetic_policy_id_t79.py +0 -0
  230. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_t103_precedence.py +0 -0
  231. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_t104_cache_gc.py +0 -0
  232. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_t108_local_override_audit.py +0 -0
  233. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_t96_single_audit_log.py +0 -0
  234. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_t99_install_prefetch_bundle.py +0 -0
  235. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_tamper.py +0 -0
  236. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_tamper_behavior.py +0 -0
  237. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_tamper_hook.py +0 -0
  238. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_telemetry_consent.py +0 -0
  239. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_tracecontext.py +0 -0
  240. {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_unsafe_int_boundary.py +0 -0
  241. {controlzero-1.9.7 → controlzero-1.9.9}/tools/cz-kiro-adapter +0 -0
@@ -1,5 +1,66 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.9 -- 2026-06-17 (Antigravity tool vocab, JSON+YAML config parity, spool reliability)
4
+
5
+ Follow-ups to the gh#1303 fail-open work plus two correctness fixes.
6
+
7
+ ### Fixed
8
+
9
+ - **Antigravity enforcement (gh#1303 part 2).** Real Antigravity emits
10
+ `run_command` (args under `CommandLine`) / `ListDir` / `view_file` /
11
+ `write_to_file` / `replace_file_content`, but the inbound alias table and the
12
+ dangerous-shell extractor assumed `run_command`/`read_file` with
13
+ `args.command`. Two compounding fail-opens against allow-by-default: a
14
+ canonical `deny: Bash` never matched Antigravity's `run_command`, and
15
+ `rm -rf` inside `CommandLine` was invisible to the argument-level scanner.
16
+ Added inbound aliases and made `Bash` read either `command` or `CommandLine`
17
+ (first non-empty wins, fail-closed fallback). Inbound-only -- the policy
18
+ vocabulary the four working hosts match is unchanged.
19
+ - **CLI hook path now auto-discovers JSON/YAML policy files (gh#67).** All
20
+ three SDKs already parse `.yaml`/`.yml`/`.json` and the in-process `Client`
21
+ auto-discovers all three in the working directory, but the CLI `hook-check`
22
+ resolver searched only `controlzero.yaml`. A project authored as
23
+ `controlzero.json` (no `.yaml`) was enforced by the `Client` yet INVISIBLE to
24
+ the enforcement hook -- it fell through to the global policy / BUNDLE_MISSING.
25
+ `hook-check` now searches `controlzero.{yaml,yml,json}` in the working
26
+ directory and `policy.{yaml,yml,json}` globally (first existing,
27
+ `.yaml` > `.yml` > `.json`), matching the `Client`.
28
+ - **Offline audit spool reliability on Python 3.13 (gh#1315).** The encrypted
29
+ spool needs `zstandard` and `cryptography`. `zstandard` 0.22.0 has no cp313
30
+ wheel, so on Python 3.13 the import could fail, the error was swallowed in
31
+ `BearerAuditSink._init_spool`, and the spool silently never formed -- hosted
32
+ audit fell back to a non-durable in-memory buffer. The `zstandard` floor is
33
+ raised to `>=0.23.0` (first cp313 wheels), the import failure is now logged
34
+ loudly and actionably (naming the missing dependency), and a
35
+ `_spool_unavailable_reason` is recorded for diagnostics.
36
+
37
+ ## 1.9.8 -- 2026-06-17 (P0 enforcement fail-open: degraded/empty bundle, gh#1303)
38
+
39
+ Closes a P0 SECURITY fail-open found via a customer report: a hosted-policy
40
+ customer's destructive tool calls (e.g. `rm -rf`) were intermittently ALLOWED
41
+ across agent surfaces.
42
+
43
+ ### Fixed
44
+
45
+ - **Degraded/partial/stale bundle no longer fails OPEN (gh#1303).** The shared
46
+ decision core (`translate_to_local_policy`) synthesized an allow-all OBSERVE
47
+ rule whenever the translated rule set was empty -- but that fired not only for
48
+ a genuinely-empty project (intended observe, gh#1247) but also for a degraded
49
+ bundle (policies attached yet zero translatable rules) or a missing/malformed
50
+ `policies` key (truncated/stale). A customer who HAS a policy could thus get
51
+ allow-all. The translator now distinguishes a genuinely-empty project (the
52
+ backend ships an explicit empty list) -- which still OBSERVES (gh#1247
53
+ preserved) -- from any other zero-rule outcome, which now FAILS CLOSED (deny)
54
+ via `default_on_missing`. This lives in the shared core, so all five host
55
+ surfaces (Claude Code, Gemini CLI, Codex CLI, Antigravity, Kiro) inherit it.
56
+ - **Unrecognized rule effect now fails closed.** A validly-signed rule carrying
57
+ an unknown/future/typo effect was coerced to `allow` (allow-* for its
58
+ pattern); it now defaults to `deny`.
59
+
60
+ Residual stale-empty-cache replay window (backend bundle provenance + cache
61
+ freshness) and the Antigravity tool-vocabulary gap are tracked in gh#1303 and
62
+ land in follow-up releases.
63
+
3
64
  ## 1.9.7 -- 2026-06-16 (hosted audit delivery for short-lived processes, gh#1292)
4
65
 
5
66
  Fixes a P0 found via a customer report: hosted-mode audit rows were written to
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: controlzero
3
- Version: 1.9.7
3
+ Version: 1.9.9
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
@@ -30,7 +30,7 @@ Requires-Dist: pydantic>=2.0.0
30
30
  Requires-Dist: pyyaml>=6.0
31
31
  Requires-Dist: rfc8785<0.2,>=0.1.4
32
32
  Requires-Dist: rich>=13.0.0
33
- Requires-Dist: zstandard>=0.22.0
33
+ Requires-Dist: zstandard>=0.23.0
34
34
  Provides-Extra: anthropic
35
35
  Requires-Dist: anthropic>=0.25.0; extra == 'anthropic'
36
36
  Provides-Extra: dev
@@ -41,13 +41,13 @@ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
41
41
  Requires-Dist: pytest>=7.0.0; extra == 'dev'
42
42
  Requires-Dist: pyyaml>=6.0; extra == 'dev'
43
43
  Requires-Dist: respx>=0.20.0; extra == 'dev'
44
- Requires-Dist: zstandard>=0.22.0; extra == 'dev'
44
+ Requires-Dist: zstandard>=0.23.0; extra == 'dev'
45
45
  Provides-Extra: google
46
46
  Requires-Dist: google-genai>=0.3.0; extra == 'google'
47
47
  Provides-Extra: hosted
48
48
  Requires-Dist: cryptography>=41.0.0; extra == 'hosted'
49
49
  Requires-Dist: httpx>=0.25.0; extra == 'hosted'
50
- Requires-Dist: zstandard>=0.22.0; extra == 'hosted'
50
+ Requires-Dist: zstandard>=0.23.0; extra == 'hosted'
51
51
  Provides-Extra: openai
52
52
  Requires-Dist: openai>=1.0.0; extra == 'openai'
53
53
  Description-Content-Type: text/markdown
@@ -93,7 +93,7 @@ Your AI agents call tools. Some of those tools should never be called by an
93
93
  agent without a human in the loop. `controlzero` is the policy layer between
94
94
  the model's output and the tool execution. Decisions are fail-closed by default.
95
95
 
96
- You can use it offline with a local YAML file or Python dict. When you want to
96
+ You can use it offline with a local YAML or JSON file or Python dict. When you want to
97
97
  share policies across a team or get a hosted audit dashboard, sign up at
98
98
  [controlzero.ai](https://controlzero.ai) and set `CONTROLZERO_API_KEY`.
99
99
 
@@ -155,8 +155,10 @@ cz = Client(policy_file="./controlzero.yaml")
155
155
  cz = Client()
156
156
  ```
157
157
 
158
- If `./controlzero.yaml` exists in the current directory, it is picked up
159
- automatically. No environment variable needed.
158
+ If a policy file exists in the current directory it is picked up
159
+ automatically -- `controlzero.yaml`, `controlzero.yml`, or `controlzero.json`
160
+ are auto-detected in that order (first existing wins). No environment
161
+ variable needed. The file may be YAML or JSON; both use the identical schema.
160
162
 
161
163
  ## Policy schema
162
164
 
@@ -39,7 +39,7 @@ Your AI agents call tools. Some of those tools should never be called by an
39
39
  agent without a human in the loop. `controlzero` is the policy layer between
40
40
  the model's output and the tool execution. Decisions are fail-closed by default.
41
41
 
42
- You can use it offline with a local YAML file or Python dict. When you want to
42
+ You can use it offline with a local YAML or JSON file or Python dict. When you want to
43
43
  share policies across a team or get a hosted audit dashboard, sign up at
44
44
  [controlzero.ai](https://controlzero.ai) and set `CONTROLZERO_API_KEY`.
45
45
 
@@ -101,8 +101,10 @@ cz = Client(policy_file="./controlzero.yaml")
101
101
  cz = Client()
102
102
  ```
103
103
 
104
- If `./controlzero.yaml` exists in the current directory, it is picked up
105
- automatically. No environment variable needed.
104
+ If a policy file exists in the current directory it is picked up
105
+ automatically -- `controlzero.yaml`, `controlzero.yml`, or `controlzero.json`
106
+ are auto-detected in that order (first existing wins). No environment
107
+ variable needed. The file may be YAML or JSON; both use the identical schema.
106
108
 
107
109
  ## Policy schema
108
110
 
@@ -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.7"
43
+ __version__ = "1.9.9"
44
44
 
45
45
  __all__ = [
46
46
  "Client",
@@ -527,7 +527,13 @@ def translate_to_local_policy(payload: dict) -> dict:
527
527
  if default_on_tamper not in VALID_DEFAULT_ON_TAMPER:
528
528
  default_on_tamper = DEFAULT_BUNDLE_ON_TAMPER
529
529
 
530
- policies = payload.get("policies") or []
530
+ # #1303: keep the RAW policies value to distinguish a genuinely-empty
531
+ # project (the backend ships an explicit empty list `policies: []`) from a
532
+ # DEGRADED/partial bundle (policies attached but zero translatable rules, or
533
+ # a missing/malformed `policies` key). The former is observe (#1247); the
534
+ # latter must fail CLOSED, never observe-allow.
535
+ raw_policies = payload.get("policies")
536
+ policies = raw_policies or []
531
537
  policies = sorted(
532
538
  [p for p in policies if isinstance(p, dict)],
533
539
  key=lambda p: int(p.get("priority", 100)),
@@ -550,6 +556,40 @@ def translate_to_local_policy(payload: dict) -> dict:
550
556
  if translated is not None:
551
557
  flat.append(translated)
552
558
 
559
+ if not flat and not (isinstance(raw_policies, list) and len(raw_policies) == 0):
560
+ # #1303 FAIL-OPEN FIX (the empty-vs-degraded boundary). We have zero
561
+ # translatable rules, but this is NOT a genuinely-empty project: the
562
+ # payload carried attached policies that produced no enforceable rules,
563
+ # OR a missing / non-list `policies` key (truncated / malformed / stale
564
+ # bundle). Treating that as observe would ALLOW EVERY tool call for a
565
+ # customer who HAS a policy -- the reproduced rm-rf fail-open. Fail
566
+ # CLOSED here via default_on_missing (canonical deny), with a distinct
567
+ # reason_code so it is not confused with a genuine empty project. The
568
+ # genuinely-empty observe path (#1247) below is reached ONLY when the
569
+ # backend shipped an explicit empty list (`policies: []`).
570
+ #
571
+ # NOTE: a stale CACHED bundle that legitimately held `policies: []` from
572
+ # when the project WAS empty is NOT caught here (it looks genuinely
573
+ # empty); that residual replay window is closed by bundle provenance +
574
+ # freshness (#1303 part 3 / backend active_policy_count).
575
+ # Reuse the registered BUNDLE_MISSING reason_code / synthetic id (both
576
+ # already in VALID_REASON_CODES / VALID_SYNTHETIC_POLICY_IDS) so no new
577
+ # error-catalog entry is introduced; the human-readable reason names the
578
+ # degraded/partial/stale cause. effect honors default_on_missing (deny).
579
+ flat.append({
580
+ "effect": default_on_missing,
581
+ "action": "*",
582
+ "id": "synthetic:BUNDLE_MISSING",
583
+ "reason": (
584
+ "Your project has attached policies but the resolved bundle "
585
+ "produced zero enforceable rules (a degraded, partial, or stale "
586
+ "bundle). Control Zero is failing CLOSED (deny) rather than "
587
+ "allowing every tool call. Regenerate the policy bundle in the "
588
+ "Control Zero dashboard; contact support if this persists."
589
+ ),
590
+ "reason_code": "BUNDLE_MISSING",
591
+ })
592
+
553
593
  if not flat:
554
594
  # Empty policy set (RESOLVED SUCCESSFULLY, zero translatable
555
595
  # rules): synthetic catch-all rule whose posture is driven by
@@ -711,9 +751,12 @@ def _translate_rule(rule: dict, policy_id: str) -> Optional[dict]:
711
751
  singular (``tool`` / ``pattern`` / ``action`` / ``match.tool``) forms
712
752
  are now supported; plural wins when both are present.
713
753
  """
714
- # Accept several spellings of "effect". Policy engine has four
715
- # canonical effects; anything else is treated as ``allow`` to stay
716
- # fail-safe on forward-compat.
754
+ # Accept several spellings of "effect". Policy engine has four canonical
755
+ # effects; an UNRECOGNIZED effect on a validly-signed rule (typo, future
756
+ # effect, corruption) must fail CLOSED. Coercing it to "allow" (the old
757
+ # behavior) turned an unknown rule into allow-*-for-its-pattern -- a
758
+ # fail-open for a security gate (#1303). A deny here over-denies that one
759
+ # pattern at worst, which is the safe direction.
717
760
  effect_raw = rule.get("effect")
718
761
  if not effect_raw:
719
762
  # Only fall back to rule["action"] for effect if it looks like
@@ -722,7 +765,7 @@ def _translate_rule(rule: dict, policy_id: str) -> Optional[dict]:
722
765
  fallback = rule.get("action")
723
766
  if fallback in ("allow", "deny", "warn", "audit"):
724
767
  effect_raw = fallback
725
- effect = effect_raw if effect_raw in ("allow", "deny", "warn", "audit") else "allow"
768
+ effect = effect_raw if effect_raw in ("allow", "deny", "warn", "audit") else "deny"
726
769
 
727
770
  # Tool pattern resolution order:
728
771
  # 1. plural ``actions`` (list, as emitted by the backend)
@@ -566,7 +566,14 @@ def extract_method(
566
566
  2. If the tool is unknown, method = ``"*"``.
567
567
  3. Read ``args_path`` from the entry. If non-null, ``raw =
568
568
  args[args_path]``; else ``raw = tool_name`` (used by
569
- ``file_read`` / ``file_write``).
569
+ ``file_read`` / ``file_write``). ``args_path`` may be a single
570
+ key (string) or a list of candidate keys, in which case the
571
+ first key that resolves to a non-empty string is used. The list
572
+ form lets one canonical tool read the differently-named command
573
+ argument each host emits -- e.g. the ``Bash`` shell tool reads
574
+ ``command`` (Claude Code / Gemini CLI) or ``CommandLine``
575
+ (Antigravity ``run_command``) so the dangerous-command scanner
576
+ fires regardless of which host sent the call.
570
577
  4. Apply the ``extract`` function. Non-empty result = method.
571
578
  5. Empty result -> ``fallback_method``.
572
579
 
@@ -593,11 +600,24 @@ def extract_method(
593
600
  else:
594
601
  if not isinstance(args, dict):
595
602
  return canonical, fallback
596
- raw = args.get(args_path)
603
+ # ``args_path`` is either a single key (string) or a list of
604
+ # candidate keys. The list form lets one canonical tool read
605
+ # the differently-named command argument each host emits --
606
+ # e.g. ``Bash`` reads ``command`` (Claude Code / Gemini CLI)
607
+ # or ``CommandLine`` (Antigravity ``run_command``). The first
608
+ # key that resolves to a non-empty string wins; if none do,
609
+ # fall back so the no-arg path stays fail-closed.
610
+ candidates = (
611
+ args_path if isinstance(args_path, list) else [args_path]
612
+ )
613
+ raw = None
614
+ for key in candidates:
615
+ value = args.get(key)
616
+ if isinstance(value, str) and value != "":
617
+ raw = value
618
+ break
597
619
  if raw is None:
598
620
  return canonical, fallback
599
- if not isinstance(raw, str):
600
- return canonical, fallback
601
621
 
602
622
  func = _EXTRACTORS.get(extract_name)
603
623
  if func is None:
@@ -9,7 +9,7 @@
9
9
  "aliases": ["sql", "Database", "PostgreSQL", "MySQL", "postgres", "sqlite"]
10
10
  },
11
11
  "Bash": {
12
- "args_path": "command",
12
+ "args_path": ["command", "CommandLine"],
13
13
  "extract": "most_dangerous_shell_command",
14
14
  "fallback_method": "*",
15
15
  "aliases": [
@@ -17,6 +17,7 @@
17
17
  "shell",
18
18
  "ShellTool",
19
19
  "run_shell_command",
20
+ "run_command",
20
21
  "PowerShell",
21
22
  "powershell",
22
23
  "Shell"
@@ -44,19 +45,19 @@
44
45
  "args_path": null,
45
46
  "extract": "tool_name_as_method",
46
47
  "fallback_method": "read",
47
- "aliases": ["read_file", "Read", "ReadFile", "read_many_files"]
48
+ "aliases": ["read_file", "Read", "ReadFile", "read_many_files", "view_file"]
48
49
  },
49
50
  "file_write": {
50
51
  "args_path": null,
51
52
  "extract": "tool_name_as_method",
52
53
  "fallback_method": "write",
53
- "aliases": ["write_file", "Write", "WriteFile", "edit_file", "Edit", "replace", "apply_patch"]
54
+ "aliases": ["write_file", "Write", "WriteFile", "edit_file", "Edit", "replace", "apply_patch", "write_to_file", "replace_file_content"]
54
55
  },
55
56
  "file_search": {
56
57
  "args_path": null,
57
58
  "extract": "tool_name_as_method",
58
59
  "fallback_method": "search",
59
- "aliases": ["Grep", "grep_search", "Glob", "glob"]
60
+ "aliases": ["Grep", "grep_search", "Glob", "glob", "ListDir"]
60
61
  },
61
62
  "task": {
62
63
  "args_path": null,
@@ -301,6 +301,11 @@ class _SpoolWiringMixin:
301
301
  self._detached_drain_spawned = False
302
302
  self._spool = None
303
303
  self._spool_mode = "off"
304
+ # #1315: when the durable spool cannot form (e.g. zstandard/cryptography
305
+ # missing or binary-incompatible on this interpreter), record WHY so
306
+ # diagnostics (`controlzero doctor`) can surface it instead of the user
307
+ # silently running on the ephemeral in-memory buffer. None == healthy.
308
+ self._spool_unavailable_reason = None
304
309
  # Fast path: flag off/unset, NO hosted default, AND no spool
305
310
  # directory on disk -- skip the spool import entirely so the
306
311
  # legacy default path stays byte-identical. When default_mode is
@@ -342,8 +347,29 @@ class _SpoolWiringMixin:
342
347
  if (self._spool is not None
343
348
  and getattr(self._spool, "in_memory_mode", False)):
344
349
  self._spool_init_degraded("memory_mode")
350
+ except ImportError as exc:
351
+ # #1315: the durable spool needs `zstandard` (compression) and
352
+ # `cryptography` (AES-GCM). A missing or binary-incompatible wheel
353
+ # (classically zstandard 0.22.0 on Python 3.13 -- no cp313 wheel)
354
+ # raises here BEFORE the spool dir is created. Name the cause loudly
355
+ # so the operator can fix the dependency instead of unknowingly
356
+ # running on the non-durable in-memory buffer. The dependency floor
357
+ # was raised (zstandard>=0.23.0) to prevent the common case.
358
+ self._spool_init_degraded("import_error")
359
+ self._spool_unavailable_reason = f"import_error: {exc}"
360
+ logger.warning(
361
+ "controlzero: audit spool dependencies unavailable (%s); "
362
+ "the encrypted offline spool is DISABLED and audit falls back "
363
+ "to in-memory buffering (durability reduced). Ensure "
364
+ "'zstandard>=0.23.0' and 'cryptography' are installed for this "
365
+ "Python (pip install -U controlzero).",
366
+ exc,
367
+ )
368
+ self._spool = None
369
+ self._spool_mode = "off"
345
370
  except Exception as exc: # noqa: BLE001
346
371
  self._spool_init_degraded("exception")
372
+ self._spool_unavailable_reason = f"exception: {exc}"
347
373
  logger.warning(
348
374
  "controlzero: audit spool unavailable (%s); "
349
375
  "falling back to in-memory buffering",
@@ -50,11 +50,47 @@ DEFAULT_TEMPLATES = [
50
50
  ]
51
51
 
52
52
  # Where the global policy lives when controlzero is used as a Claude Code hook.
53
- # Per-project ./controlzero.yaml takes precedence (the SDK already does this).
53
+ # Per-project ./controlzero.{yaml,yml,json} takes precedence (the SDK already
54
+ # does this).
54
55
  GLOBAL_POLICY_DIR = Path.home() / ".controlzero"
55
56
  GLOBAL_POLICY_PATH = GLOBAL_POLICY_DIR / "policy.yaml"
56
57
  GLOBAL_POLICY_SIG_PATH = GLOBAL_POLICY_DIR / "policy.yaml.sig"
57
58
  GLOBAL_TAMPER_KEY_PATH = GLOBAL_POLICY_DIR / "tamper.key"
59
+
60
+ # Policy-file extensions auto-discovered in the hook resolution order, in
61
+ # precedence order. The local policy loader accepts all three (YAML and JSON
62
+ # -- see ``policy_loader._load_from_file``) and the in-process ``Client``
63
+ # already auto-discovers all three in cwd; the CLI hook path must search the
64
+ # same set so a project authored as ``controlzero.json`` is enforced by the
65
+ # hook exactly like ``controlzero.yaml`` (else it falls through to the global
66
+ # policy / BUNDLE_MISSING -- a JSON-config user would otherwise be invisible
67
+ # to enforcement). Keep this list in sync with ``client._resolve_local_source``.
68
+ _POLICY_FILE_EXTS = (".yaml", ".yml", ".json")
69
+
70
+
71
+ def _first_existing_policy(directory: Path, basename: str) -> Optional[Path]:
72
+ """Return the first existing ``<directory>/<basename><ext>`` for ``ext`` in
73
+ :data:`_POLICY_FILE_EXTS` (``.yaml`` -> ``.yml`` -> ``.json``), else None."""
74
+ for ext in _POLICY_FILE_EXTS:
75
+ candidate = directory / f"{basename}{ext}"
76
+ if candidate.exists():
77
+ return candidate
78
+ return None
79
+
80
+
81
+ def _first_existing_variant(base: Path) -> Optional[Path]:
82
+ """Return the first existing ``base`` with each policy extension swapped in
83
+ (``.yaml`` -> ``.yml`` -> ``.json``), else None.
84
+
85
+ Anchored on ``base`` (not its directory) so the global lookup honors a
86
+ monkeypatched ``GLOBAL_POLICY_PATH`` -- the established test contract
87
+ (e.g. ``test_install_hooks`` sets ``GLOBAL_POLICY_PATH`` to a non-existent
88
+ file to assert the missing-policy path)."""
89
+ for ext in _POLICY_FILE_EXTS:
90
+ candidate = base.with_suffix(ext)
91
+ if candidate.exists():
92
+ return candidate
93
+ return None
58
94
  GLOBAL_AUDIT_PATH = GLOBAL_POLICY_DIR / "audit.log"
59
95
  # Where the hook-check CLI logs surface-level events that don't flow
60
96
  # through the policy evaluator -- today just the unenrolled first-run
@@ -1349,20 +1385,17 @@ def sign_policy(policy: Optional[str], verify_only: bool):
1349
1385
 
1350
1386
 
1351
1387
  def _has_resolvable_policy_file() -> bool:
1352
- """True when a ``./controlzero.yaml`` or ``~/.controlzero/policy.yaml``
1353
- exists and looks loadable.
1388
+ """True when a ``./controlzero.{yaml,yml,json}`` or
1389
+ ``~/.controlzero/policy.{yaml,yml,json}`` exists and looks loadable.
1354
1390
 
1355
1391
  Scoped to the carve-out logic: "do we already have a policy in
1356
1392
  the standard search paths?" If yes, the user is past first-run
1357
1393
  and the carve-out banner is noise. Mirrors the happy-path search
1358
1394
  order in :func:`_resolve_hook_policy` so the two stay in sync.
1359
1395
  """
1360
- cwd_policy = Path.cwd() / "controlzero.yaml"
1361
- if cwd_policy.exists():
1362
- return True
1363
- if GLOBAL_POLICY_PATH.exists():
1396
+ if _first_existing_policy(Path.cwd(), "controlzero") is not None:
1364
1397
  return True
1365
- return False
1398
+ return _first_existing_variant(GLOBAL_POLICY_PATH) is not None
1366
1399
 
1367
1400
 
1368
1401
  def _is_unenrolled_first_run() -> bool:
@@ -1783,19 +1816,22 @@ def _maybe_refresh_policy(
1783
1816
  def _resolve_hook_policy(explicit: Optional[str]) -> Optional[Path]:
1784
1817
  """Find a policy file using the hook-time resolution order:
1785
1818
 
1786
- 1. Explicit --policy argument
1787
- 2. ./controlzero.yaml (per-project)
1788
- 3. ~/.controlzero/policy.yaml (global)
1819
+ 1. Explicit --policy argument (any extension; the loader dispatches on it)
1820
+ 2. ./controlzero.{yaml,yml,json} (per-project, first existing)
1821
+ 3. ~/.controlzero/policy.{yaml,yml,json} (global, first existing)
1822
+
1823
+ The per-project and global searches probe all three extensions so a
1824
+ project authored as ``controlzero.json`` (or ``.yml``) is enforced by the
1825
+ hook identically to ``controlzero.yaml`` -- matching the in-process
1826
+ ``Client`` (:func:`client._resolve_local_source`).
1789
1827
  """
1790
1828
  if explicit:
1791
1829
  p = Path(explicit)
1792
1830
  return p if p.exists() else None
1793
- cwd = Path.cwd() / "controlzero.yaml"
1794
- if cwd.exists():
1831
+ cwd = _first_existing_policy(Path.cwd(), "controlzero")
1832
+ if cwd is not None:
1795
1833
  return cwd
1796
- if GLOBAL_POLICY_PATH.exists():
1797
- return GLOBAL_POLICY_PATH
1798
- return None
1834
+ return _first_existing_variant(GLOBAL_POLICY_PATH)
1799
1835
 
1800
1836
 
1801
1837
  def _policy_has_deny_rules(policy_path: Path) -> bool:
@@ -22,13 +22,27 @@
22
22
  # To see what got blocked (and what was allowed):
23
23
  # tail -f ~/.controlzero/audit.log
24
24
  #
25
- # Antigravity common tools (as of 2026-06):
26
- # run_command shell execution (args: CommandLine, Cwd)
27
- # write_to_file file write (args: TargetFile, CodeContent)
28
- # replace_file_content file edit (args: TargetFile, ReplacementChunks)
29
- # read_file file read
30
- # browser_* browser navigation / interaction
31
- # mcp_* MCP server tools (tool-specific args)
25
+ # Antigravity (agy) real tool names (as of 2026-06) and the canonical
26
+ # Control Zero tool each one normalizes to. Write your deny rules
27
+ # against the CANONICAL name on the right -- the SDK resolves every
28
+ # agy tool name to its canonical equivalent before the policy engine
29
+ # evaluates anything, so one rule (e.g. `Bash`) covers agy + Claude
30
+ # Code + Gemini CLI + Codex CLI:
31
+ #
32
+ # agy tool name args canonical tool
33
+ # -------------------- ------------------------ --------------
34
+ # run_command CommandLine, Cwd Bash
35
+ # write_to_file TargetFile, CodeContent file_write
36
+ # replace_file_content TargetFile, ...Chunks file_write
37
+ # read_file ... file_read
38
+ # view_file ... file_read
39
+ # ListDir ... file_search
40
+ # browser_* ... (literal / glob)
41
+ # mcp_* tool-specific args (literal / glob)
42
+ #
43
+ # The dangerous-command scanner reads agy's `CommandLine` argument the
44
+ # same way it reads `command` from other hosts, so `Bash:rm` matches a
45
+ # `run_command` whose `CommandLine` is `rm -rf /`.
32
46
  #
33
47
  # Antigravity packs the call under a top-level `toolCall{name,args}`
34
48
  # envelope; the host adapter flattens it to tool_name / tool_input
@@ -56,17 +70,25 @@ rules:
56
70
  # DENY: destructive patterns at the tool level
57
71
  # ============================================================
58
72
  #
59
- # v0.1 matches on TOOL NAME, not argument content. For
60
- # argument-level checks (e.g., block `rm -rf /` specifically),
61
- # use the SDK inside your own code. Until then, you can deny
62
- # whole tool classes.
73
+ # Write rules against the CANONICAL tool name (see the table above).
74
+ # `Bash` covers agy's `run_command`; `file_write` covers
75
+ # `write_to_file` + `replace_file_content`; `file_read` covers
76
+ # `read_file` + `view_file`; `file_search` covers `ListDir`.
77
+ #
78
+ # Argument-level rules also work: the SDK extracts the dangerous
79
+ # command from agy's `CommandLine` argument, so `Bash:rm` blocks an
80
+ # `rm` while leaving the rest of `Bash` allowed.
81
+ #
82
+ # - id: deny-rm
83
+ # deny: 'Bash:rm'
84
+ # reason: 'rm blocked in this workspace'
63
85
  #
64
86
  # - id: deny-file-writes
65
- # deny: 'write_to_file'
87
+ # deny: 'file_write'
66
88
  # reason: 'No unattended file writes in this project'
67
89
  #
68
90
  # - id: deny-shell-entirely
69
- # deny: 'run_command'
91
+ # deny: 'Bash'
70
92
  # reason: 'This workspace does not allow shell execution'
71
93
 
72
94
  # ============================================================
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "controlzero"
7
- version = "1.9.7"
7
+ version = "1.9.9"
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"}
@@ -37,7 +37,12 @@ dependencies = [
37
37
  # (cryptography is the bulk), acceptable for the UX win.
38
38
  "httpx>=0.25.0",
39
39
  "cryptography>=41.0.0",
40
- "zstandard>=0.22.0",
40
+ # zstandard floor is 0.23.0: it is the first release to ship cp313
41
+ # wheels. With the old >=0.22.0 floor a Python 3.13 environment could
42
+ # resolve 0.22.0 (no cp313 wheel) and fail to import, which silently
43
+ # disabled the encrypted audit spool (the import error was swallowed in
44
+ # BearerAuditSink._init_spool). See #1315.
45
+ "zstandard>=0.23.0",
41
46
  # RFC 8785 JSON Canonicalization Scheme. Single source of truth for
42
47
  # cross-SDK audit hashing (args_hash field). Same bytes from Python,
43
48
  # Node, and Go SDKs for the same input. ~12 KB pure-Python wheel.
@@ -60,7 +65,7 @@ dependencies = [
60
65
  hosted = [
61
66
  "httpx>=0.25.0",
62
67
  "cryptography>=41.0.0",
63
- "zstandard>=0.22.0",
68
+ "zstandard>=0.23.0", # cp313 wheels (#1315)
64
69
  ]
65
70
  google = [
66
71
  # New Gemini SDK. The deprecated google-generativeai package is no
@@ -80,7 +85,7 @@ dev = [
80
85
  "pyyaml>=6.0",
81
86
  "httpx>=0.25.0",
82
87
  "cryptography>=41.0.0",
83
- "zstandard>=0.22.0",
88
+ "zstandard>=0.23.0", # cp313 wheels (#1315)
84
89
  "respx>=0.20.0",
85
90
  ]
86
91
 
@@ -69,8 +69,10 @@ def test_allow_emits_explicit_allow_decision(tmp_path):
69
69
 
70
70
 
71
71
  def test_deny_emits_deny_decision_from_toolcall_and_exits_zero(tmp_path):
72
- # A deny rule on the FLATTENED tool name proves toolCall normalization
73
- # reached the engine through the real command path.
72
+ # A deny rule on the CANONICAL tool name proves toolCall normalization
73
+ # reached the engine through the real command path. Since #1303 Part 2
74
+ # agy's `run_command` resolves to canonical `Bash`, so the portable rule
75
+ # is `deny: Bash` (one rule covers agy + Claude Code + Gemini + Codex).
74
76
  #
75
77
  # CRITICAL: Antigravity decides from the stdout JSON, NOT the exit code. A
76
78
  # non-zero exit is read by agy as a fail-closed deny that would OVERRIDE
@@ -79,7 +81,7 @@ def test_deny_emits_deny_decision_from_toolcall_and_exits_zero(tmp_path):
79
81
  p = _write_policy(
80
82
  tmp_path,
81
83
  [
82
- {"deny": "run_command", "reason": "shell blocked in this workspace"},
84
+ {"deny": "Bash", "reason": "shell blocked in this workspace"},
83
85
  {"allow": "*"},
84
86
  ],
85
87
  )
@@ -126,10 +128,11 @@ def test_decision_is_never_empty_object(tmp_path):
126
128
  def test_exit_code_convention_is_host_aware(tmp_path):
127
129
  # REGRESSION: the host-aware exit code must not change Claude Code's
128
130
  # convention. The SAME deny rule exits 0 for Antigravity (decision lives
129
- # in the JSON) but exits 2 for Claude Code (non-zero = block).
131
+ # in the JSON) but exits 2 for Claude Code (non-zero = block). The rule
132
+ # targets canonical `Bash` (run_command resolves to it since #1303 Part 2).
130
133
  p = _write_policy(
131
134
  tmp_path,
132
- [{"deny": "run_command", "reason": "blocked"}, {"allow": "*"}],
135
+ [{"deny": "Bash", "reason": "blocked"}, {"allow": "*"}],
133
136
  )
134
137
  runner = CliRunner()
135
138