controlzero 1.9.5__tar.gz → 1.9.6__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 (238) hide show
  1. {controlzero-1.9.5 → controlzero-1.9.6}/CHANGELOG.md +40 -0
  2. {controlzero-1.9.5 → controlzero-1.9.6}/PKG-INFO +1 -1
  3. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/__init__.py +1 -1
  4. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/doctor.py +225 -2
  5. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/hosts/antigravity.py +58 -15
  6. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/main.py +125 -11
  7. {controlzero-1.9.5 → controlzero-1.9.6}/pyproject.toml +1 -1
  8. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_antigravity_adapter.py +18 -12
  9. controlzero-1.9.6/tests/test_antigravity_ga_blockers_1248.py +554 -0
  10. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hosts_adapter.py +6 -5
  11. {controlzero-1.9.5 → controlzero-1.9.6}/.gitignore +0 -0
  12. {controlzero-1.9.5 → controlzero-1.9.6}/Dockerfile.test +0 -0
  13. {controlzero-1.9.5 → controlzero-1.9.6}/LICENSE +0 -0
  14. {controlzero-1.9.5 → controlzero-1.9.6}/README.md +0 -0
  15. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/_internal/__init__.py +0 -0
  16. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/_internal/action_aliases.py +0 -0
  17. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/_internal/action_validator.py +0 -0
  18. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/_internal/bundle.py +0 -0
  19. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/_internal/credential_hook.py +0 -0
  20. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/_internal/credential_scanner.py +0 -0
  21. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/_internal/credentials_data/__init__.py +0 -0
  22. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/_internal/credentials_data/built_in.yaml +0 -0
  23. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/_internal/dlp_scanner.py +0 -0
  24. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/_internal/enforcer.py +0 -0
  25. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/_internal/hook_extractors.py +0 -0
  26. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/_internal/tool_extractors.json +0 -0
  27. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/_internal/types.py +0 -0
  28. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/audit_local.py +0 -0
  29. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/audit_remote.py +0 -0
  30. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/canonical.py +0 -0
  31. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/__init__.py +0 -0
  32. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/__main__.py +0 -0
  33. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/_secrets.py +0 -0
  34. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/console.py +0 -0
  35. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/debug_bundle.py +0 -0
  36. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/hosts/__init__.py +0 -0
  37. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/hosts/base.py +0 -0
  38. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/hosts/claude_code.py +0 -0
  39. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/hosts/codex_cli.py +0 -0
  40. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/hosts/gemini_cli.py +0 -0
  41. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/hosts/kiro.py +0 -0
  42. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/hosts/unknown.py +0 -0
  43. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/kiro_adapter.py +0 -0
  44. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/migrate.py +0 -0
  45. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/spool_cmd.py +0 -0
  46. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/telemetry_consent.py +0 -0
  47. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/antigravity/hooks.json +0 -0
  48. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/antigravity.yaml +0 -0
  49. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/autogen.yaml +0 -0
  50. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/claude-code.yaml +0 -0
  51. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/codex-cli.yaml +0 -0
  52. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/cost-cap.yaml +0 -0
  53. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/crewai.yaml +0 -0
  54. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/cursor.yaml +0 -0
  55. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/gemini-cli.yaml +0 -0
  56. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/generic.yaml +0 -0
  57. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook +0 -0
  58. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/kiro/ide-pre-tool-use.kiro.hook +0 -0
  59. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/kiro/ide-prompt-submit.kiro.hook +0 -0
  60. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/kiro/kiro.yaml +0 -0
  61. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/langchain.yaml +0 -0
  62. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/mcp.yaml +0 -0
  63. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/rag.yaml +0 -0
  64. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/client.py +0 -0
  65. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/device.py +0 -0
  66. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/enrollment.py +0 -0
  67. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/error_codes.py +0 -0
  68. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/error_codes.yaml +0 -0
  69. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/errors.py +0 -0
  70. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/hitl/__init__.py +0 -0
  71. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/hitl/grant_protocol.py +0 -0
  72. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/hitl/mock.py +0 -0
  73. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/hitl/pending_approval.py +0 -0
  74. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/hitl/secret_leak_guard.py +0 -0
  75. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/hitl/status.py +0 -0
  76. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/hooks/__init__.py +0 -0
  77. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/hooks/tool_output_handler.py +0 -0
  78. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/hosted_policy.py +0 -0
  79. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/__init__.py +0 -0
  80. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/anthropic.py +0 -0
  81. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/autogen.py +0 -0
  82. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/braintrust.py +0 -0
  83. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/crewai/__init__.py +0 -0
  84. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/crewai/agent.py +0 -0
  85. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/crewai/crew.py +0 -0
  86. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/crewai/task.py +0 -0
  87. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/crewai/tool.py +0 -0
  88. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/google.py +0 -0
  89. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/google_adk/__init__.py +0 -0
  90. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/google_adk/agent.py +0 -0
  91. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/google_adk/tool.py +0 -0
  92. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/langchain/__init__.py +0 -0
  93. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/langchain/agent.py +0 -0
  94. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/langchain/callbacks.py +0 -0
  95. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/langchain/chain.py +0 -0
  96. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/langchain/graph.py +0 -0
  97. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/langchain/modern.py +0 -0
  98. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/langchain/tool.py +0 -0
  99. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/langfuse.py +0 -0
  100. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/litellm.py +0 -0
  101. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/openai.py +0 -0
  102. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/pydantic_ai.py +0 -0
  103. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/vercel_ai.py +0 -0
  104. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/layout_migration.py +0 -0
  105. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/policy_loader.py +0 -0
  106. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/spool/__init__.py +0 -0
  107. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/spool/_compress.py +0 -0
  108. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/spool/_constants.py +0 -0
  109. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/spool/_crc32c.py +0 -0
  110. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/spool/_crypto.py +0 -0
  111. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/spool/_frame.py +0 -0
  112. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/spool/_keyring.py +0 -0
  113. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/spool/_metrics.py +0 -0
  114. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/spool/_spool.py +0 -0
  115. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/spool/_state.py +0 -0
  116. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/spool/_uploader.py +0 -0
  117. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/spool/cz-audit-v1.dict +0 -0
  118. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/tamper.py +0 -0
  119. {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/tracecontext.py +0 -0
  120. {controlzero-1.9.5 → controlzero-1.9.6}/examples/hello_world.py +0 -0
  121. {controlzero-1.9.5 → controlzero-1.9.6}/tests/_fixtures/jcs_args_hash_vectors.json +0 -0
  122. {controlzero-1.9.5 → controlzero-1.9.6}/tests/conftest.py +0 -0
  123. {controlzero-1.9.5 → controlzero-1.9.6}/tests/integrations/__init__.py +0 -0
  124. {controlzero-1.9.5 → controlzero-1.9.6}/tests/integrations/test_google.py +0 -0
  125. {controlzero-1.9.5 → controlzero-1.9.6}/tests/parity/action_aliases.json +0 -0
  126. {controlzero-1.9.5 → controlzero-1.9.6}/tests/spool/__init__.py +0 -0
  127. {controlzero-1.9.5 → controlzero-1.9.6}/tests/spool/conftest.py +0 -0
  128. {controlzero-1.9.5 → controlzero-1.9.6}/tests/spool/test_spool_cli.py +0 -0
  129. {controlzero-1.9.5 → controlzero-1.9.6}/tests/spool/test_spool_concurrency.py +0 -0
  130. {controlzero-1.9.5 → controlzero-1.9.6}/tests/spool/test_spool_conformance.py +0 -0
  131. {controlzero-1.9.5 → controlzero-1.9.6}/tests/spool/test_spool_core.py +0 -0
  132. {controlzero-1.9.5 → controlzero-1.9.6}/tests/spool/test_spool_crash.py +0 -0
  133. {controlzero-1.9.5 → controlzero-1.9.6}/tests/spool/test_spool_diskfull.py +0 -0
  134. {controlzero-1.9.5 → controlzero-1.9.6}/tests/spool/test_spool_durable_default_tamper.py +0 -0
  135. {controlzero-1.9.5 → controlzero-1.9.6}/tests/spool/test_spool_keychain_dek.py +0 -0
  136. {controlzero-1.9.5 → controlzero-1.9.6}/tests/spool/test_spool_sink_wiring.py +0 -0
  137. {controlzero-1.9.5 → controlzero-1.9.6}/tests/spool/test_spool_transcript_localack.py +0 -0
  138. {controlzero-1.9.5 → controlzero-1.9.6}/tests/spool/test_spool_uploader.py +0 -0
  139. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_action_aliases.py +0 -0
  140. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_action_canonicalization.py +0 -0
  141. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_action_validator_t86.py +0 -0
  142. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_agent_name_env.py +0 -0
  143. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_antigravity_hook_check.py +0 -0
  144. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_antigravity_install.py +0 -0
  145. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_api_key_mask.py +0 -0
  146. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_audit_remote.py +0 -0
  147. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_audit_remote_sdk_version.py +0 -0
  148. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_audit_sink_isolation.py +0 -0
  149. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_bundle_parser.py +0 -0
  150. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_bundle_translate.py +0 -0
  151. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_canonical_phase1a.py +0 -0
  152. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_cli_carve_out.py +0 -0
  153. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_cli_debug_bundle.py +0 -0
  154. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_cli_extractor_integration.py +0 -0
  155. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_cli_hook.py +0 -0
  156. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_cli_hosted_refresh.py +0 -0
  157. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_cli_init.py +0 -0
  158. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_cli_init_templates.py +0 -0
  159. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_cli_tail.py +0 -0
  160. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_cli_test.py +0 -0
  161. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_cli_validate.py +0 -0
  162. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_coding_agent_hooks.py +0 -0
  163. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_conditions.py +0 -0
  164. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_conformance.py +0 -0
  165. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_console.py +0 -0
  166. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_credential_hook.py +0 -0
  167. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_default_action.py +0 -0
  168. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_device.py +0 -0
  169. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_dlp_scanner.py +0 -0
  170. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_doctor.py +0 -0
  171. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_engine_version_consistency.py +0 -0
  172. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_enrollment.py +0 -0
  173. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_env_dump_438.py +0 -0
  174. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_epic_1247_bryan_acceptance.py +0 -0
  175. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_error_codes.py +0 -0
  176. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_errors_e_codes.py +0 -0
  177. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_fail_closed_eval.py +0 -0
  178. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_glob_matching.py +0 -0
  179. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hitl_5d_email_install.py +0 -0
  180. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hitl_6a_cli_flag.py +0 -0
  181. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hitl_6a_exceptions.py +0 -0
  182. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hitl_6a_get_secret_hitl.py +0 -0
  183. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hitl_6a_mock_backend.py +0 -0
  184. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hitl_6a_pending_approval.py +0 -0
  185. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hitl_6a_request_approval.py +0 -0
  186. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hitl_6a_secret_leak_guard.py +0 -0
  187. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hitl_6a_wait.py +0 -0
  188. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hitl_conformance.py +0 -0
  189. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hitl_phase2b_protocol.py +0 -0
  190. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hitl_reason_codes.py +0 -0
  191. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hitl_validator_keys.py +0 -0
  192. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hook_extractors.py +0 -0
  193. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hosted_local_audit_1247.py +0 -0
  194. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hosted_policy_e2e.py +0 -0
  195. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hybrid_mode_strict.py +0 -0
  196. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hybrid_mode_warn.py +0 -0
  197. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_install_hook_command.py +0 -0
  198. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_install_hooks.py +0 -0
  199. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_kiro_adapter.py +0 -0
  200. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_kiro_cli_e2e.py +0 -0
  201. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_kiro_hook_templates.py +0 -0
  202. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_kiro_install.py +0 -0
  203. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_layout_migration_t101.py +0 -0
  204. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_layout_parity_t102.py +0 -0
  205. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_local_mode_dict.py +0 -0
  206. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_local_mode_file_json.py +0 -0
  207. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_local_mode_file_yaml.py +0 -0
  208. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_log_fallback_stderr.py +0 -0
  209. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_log_options_ignored_hosted.py +0 -0
  210. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_log_rotation.py +0 -0
  211. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_migrate.py +0 -0
  212. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_min_sdk_version_gate.py +0 -0
  213. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_multi_client_per_project_175.py +0 -0
  214. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_no_policy_no_key.py +0 -0
  215. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_observe_mode_1247.py +0 -0
  216. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_package_rename_shim.py +0 -0
  217. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_policy_engine_version_phase1b.py +0 -0
  218. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_policy_freshness.py +0 -0
  219. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_policy_settings.py +0 -0
  220. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_policy_source_audit.py +0 -0
  221. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_quarantine.py +0 -0
  222. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_reason_code.py +0 -0
  223. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_refresh.py +0 -0
  224. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_secrets.py +0 -0
  225. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_sql_semantic_class.py +0 -0
  226. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_synthetic_policy_id_t79.py +0 -0
  227. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_t103_precedence.py +0 -0
  228. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_t104_cache_gc.py +0 -0
  229. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_t108_local_override_audit.py +0 -0
  230. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_t96_single_audit_log.py +0 -0
  231. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_t99_install_prefetch_bundle.py +0 -0
  232. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_tamper.py +0 -0
  233. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_tamper_behavior.py +0 -0
  234. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_tamper_hook.py +0 -0
  235. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_telemetry_consent.py +0 -0
  236. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_tracecontext.py +0 -0
  237. {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_unsafe_int_boundary.py +0 -0
  238. {controlzero-1.9.5 → controlzero-1.9.6}/tools/cz-kiro-adapter +0 -0
@@ -1,5 +1,45 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.6 -- 2026-06-16 (Antigravity GA-blocker hardening, gh#1248 / epic gh#925)
4
+
5
+ Closes 3 of the prod-readiness GA blockers for the Antigravity (`agy`)
6
+ integration. Antigravity stays **BETA** -- GA still requires a manual check
7
+ against a real pinned agy build (agy cannot run headless in CI), which these
8
+ changes do not perform.
9
+
10
+ ### Fixed
11
+
12
+ - **Empty-stdout fail-closed on the Antigravity surface (blocker #1).** Every
13
+ `controlzero hook-check` error exit -- stdin read error, empty stdin,
14
+ malformed JSON, missing `tool_name`, and an invalid/unreadable policy --
15
+ previously emitted EMPTY stdout and exited 0. Agy decides from stdout JSON,
16
+ and empty stdout is undocumented (agy *currently* reads it as
17
+ `invalid_args` -> deny, but that is unverified across builds). On the
18
+ Antigravity surface those paths now emit an explicit
19
+ `{"decision":"deny", "reason":...}` so a parse/policy error DENIES
20
+ deterministically. Pass-through hosts (Claude Code / Gemini / Codex) keep
21
+ the documented empty-stdout = "no opinion, proceed" behavior unchanged.
22
+
23
+ - **HITL approval gate no longer silently auto-approvable (blocker #2).** The
24
+ HITL decision token now defaults to `force_ask` (a MANDATORY prompt that
25
+ ignores agy's "Always Allow" cache) instead of `ask` (which a cached
26
+ approval can silently satisfy -- fail-OPEN on an approval-gated destructive
27
+ action). `CZ_ANTIGRAVITY_HITL_DECISION` selects the token; an empty or
28
+ unrecognized value falls back to `force_ask`, NEVER to `ask`. If a pinned
29
+ agy build rejects `force_ask`, set `CZ_ANTIGRAVITY_HITL_DECISION=deny` for a
30
+ hard block-with-reason. `ask`/`allow` remain available only as an explicit,
31
+ knowing operator downgrade.
32
+
33
+ - **`doctor` checks the REAL Antigravity paths and verifies the hook is live
34
+ (blocker #3).** `controlzero doctor` previously checked
35
+ `~/.antigravity/config.json` -- a path the installer never writes. It now
36
+ scans the executed `~/.gemini/config/hooks.json` and the agy TUI mirror
37
+ `~/.gemini/antigravity-cli/hooks.json` (plus the cwd-relative project-scope
38
+ `.agents/hooks.json`), confirms a live `controlzero hook-check` `PreToolUse`
39
+ entry is present (`E1006` when missing), and runs a dry-run hook-check
40
+ round-trip to confirm the adapter renders an explicit decision rather than
41
+ empty stdout (`E1007` when it does not).
42
+
3
43
  ## 1.9.5 -- 2026-06-16 (Kiro CLI -> GA, epic gh#877)
4
44
 
5
45
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: controlzero
3
- Version: 1.9.5
3
+ Version: 1.9.6
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.5"
43
+ __version__ = "1.9.6"
44
44
 
45
45
  __all__ = [
46
46
  "Client",
@@ -82,8 +82,18 @@ def _agent_paths() -> List[_AgentPath]:
82
82
  "VS Code user settings"),
83
83
  _AgentPath("cline", home / "Library" / "Application Support" / "Code" / "User" / "globalStorage" / "saoudrizwan.claude-dev" / "settings" / "cline_mcp_settings.json",
84
84
  "Cline MCP settings (macOS)"),
85
- _AgentPath("antigravity", home / ".antigravity" / "config.json",
86
- "Antigravity config"),
85
+ # Antigravity (agy): the install writes the EXECUTED hook config to
86
+ # ~/.gemini/config/hooks.json and mirrors it to
87
+ # ~/.gemini/antigravity-cli/hooks.json for the agy TUI /hooks display
88
+ # (antigravity-cli#49). The pre-#1248 doctor checked
89
+ # ~/.antigravity/config.json -- a path the installer NEVER writes -- so
90
+ # it could neither find a leaked key there nor confirm the hook was
91
+ # live. Scan the real executed + mirror paths now. Project scope
92
+ # (<cwd>/.agents/hooks.json) is covered by the cwd-aware live check.
93
+ _AgentPath("antigravity", home / ".gemini" / "config" / "hooks.json",
94
+ "Antigravity PreToolUse hook command"),
95
+ _AgentPath("antigravity", home / ".gemini" / "antigravity-cli" / "hooks.json",
96
+ "Antigravity agy TUI /hooks mirror"),
87
97
  _AgentPath("adal", home / ".adal" / "config.json",
88
98
  "Adal config"),
89
99
  _AgentPath("jetbrains", home / ".config" / "JetBrains" / "mcp.json",
@@ -243,6 +253,208 @@ def _check_global_environment() -> List[Finding]:
243
253
  return findings
244
254
 
245
255
 
256
+ # -----------------------------------------------------------------------------
257
+ # Antigravity hook liveness (GA blocker #3, issue #1248).
258
+ #
259
+ # The pre-#1248 doctor only scanned files for leaked keys. For Antigravity it
260
+ # also checked the WRONG path (~/.antigravity/config.json), so it gave a user
261
+ # zero signal about whether their governance hook was actually installed and
262
+ # firing. These checks confirm the hook entry is present at the REAL executed
263
+ # path AND that a dry-run hook-check round-trips a valid decision -- the two
264
+ # things that have to be true for the gate to govern a single tool call.
265
+ # -----------------------------------------------------------------------------
266
+
267
+ # The substring that marks a Control Zero hook entry, matching the installer's
268
+ # own self-detection (`_antigravity_hook_is_cz`). Kept in one place so a future
269
+ # command-string change only edits here.
270
+ _CZ_HOOK_MARKER = "controlzero hook-check"
271
+
272
+
273
+ def _antigravity_executed_hook_paths() -> List[Path]:
274
+ """The hooks.json paths agy actually EXECUTES, in precedence order.
275
+
276
+ Project scope wins for a workspace, so it is checked first; the global
277
+ user file is the fallback. The TUI mirror (~/.gemini/antigravity-cli/...)
278
+ is a display surface, not an executed path, so it is intentionally NOT in
279
+ this list -- a hook present only in the mirror does not fire.
280
+ """
281
+ home = Path.home()
282
+ return [
283
+ Path.cwd() / ".agents" / "hooks.json", # project scope (<cwd>)
284
+ home / ".gemini" / "config" / "hooks.json", # global user (executed)
285
+ ]
286
+
287
+
288
+ def _antigravity_mirror_path() -> Path:
289
+ """The agy TUI /hooks DISPLAY mirror (antigravity-cli#49).
290
+
291
+ A hook present ONLY here is shown in the agy `/hooks` UI but is NOT
292
+ executed, so it does not fire. Used purely as a presence signal so doctor
293
+ can warn about that display-but-not-live state.
294
+ """
295
+ return Path.home() / ".gemini" / "antigravity-cli" / "hooks.json"
296
+
297
+
298
+ def _antigravity_hook_command_present(hooks_path: Path) -> bool:
299
+ """True iff hooks_path has a live PreToolUse Control Zero hook entry.
300
+
301
+ Mirrors the installer's written shape: top-level ``hooks`` ->
302
+ ``PreToolUse`` -> list of blocks each with a ``hooks`` list of
303
+ ``{type, command, ...}`` entries. A CZ entry is any whose ``command``
304
+ contains ``controlzero hook-check``. Tolerant of hand-edits / partial
305
+ shapes -- anything it cannot parse counts as "not present".
306
+ """
307
+ import json
308
+
309
+ try:
310
+ data = json.loads(hooks_path.read_text(encoding="utf-8"))
311
+ except (OSError, ValueError):
312
+ return False
313
+ if not isinstance(data, dict):
314
+ return False
315
+ hooks = data.get("hooks")
316
+ if not isinstance(hooks, dict):
317
+ return False
318
+ pre = hooks.get("PreToolUse")
319
+ if not isinstance(pre, list):
320
+ return False
321
+ for block in pre:
322
+ if not isinstance(block, dict):
323
+ continue
324
+ block_hooks = block.get("hooks")
325
+ if not isinstance(block_hooks, list):
326
+ # Tolerate a malformed-but-valid-JSON shape (e.g. "hooks": 3);
327
+ # iterating a non-list would raise TypeError and crash doctor.
328
+ continue
329
+ for entry in block_hooks:
330
+ if not isinstance(entry, dict):
331
+ continue
332
+ cmd = entry.get("command")
333
+ if isinstance(cmd, str) and _CZ_HOOK_MARKER in cmd:
334
+ return True
335
+ return False
336
+
337
+
338
+ def _antigravity_dry_run_roundtrips() -> bool:
339
+ """True iff a dry-run hook-check produces a valid Antigravity decision.
340
+
341
+ This is the "does the hook actually fire" half of the check. We do NOT
342
+ shell out to ``controlzero hook-check`` (slow, and a misconfigured PATH
343
+ would make a working install look broken). Instead we exercise the same
344
+ code path the hook runs: select the Antigravity adapter and render a
345
+ decision, asserting it emits an explicit ``decision`` token (never the
346
+ empty-stdout ``invalid_args`` trap). If the adapter import or render
347
+ raises, the round-trip has failed.
348
+ """
349
+ try:
350
+ from controlzero.cli.hosts import CZDecision, select_adapter
351
+
352
+ # Force Antigravity selection via the env signal its claim() honors.
353
+ adapter = select_adapter({}, {"CONTROLZERO_CLIENT": "antigravity"})
354
+ if getattr(adapter, "name", "") != "antigravity":
355
+ return False
356
+ out = adapter.render(
357
+ CZDecision(effect="allow", reason="doctor dry-run", reason_code="RULE_MATCH")
358
+ )
359
+ return isinstance(out, dict) and out.get("decision") == "allow"
360
+ except Exception: # noqa: BLE001
361
+ return False
362
+
363
+
364
+ def _check_antigravity_hook_live() -> List[Finding]:
365
+ """Confirm the Antigravity governance hook is installed AND can fire.
366
+
367
+ Respects agy's file precedence: the highest-precedence EXISTING executed
368
+ file wins (project scope <cwd>/.agents/hooks.json shadows the global
369
+ ~/.gemini/config/hooks.json), so that file alone determines liveness.
370
+
371
+ Emits at most one finding:
372
+
373
+ * If the authoritative executed file carries a CZ entry and the dry-run
374
+ round-trips -> no finding (the gate is live).
375
+ * If the authoritative executed file (or only the display mirror) lacks a
376
+ CZ entry -> WARN installed-but-not-live (or never installed). WARN, not
377
+ ERROR: a box that never installed the agy hook is a valid state, and
378
+ doctor exit 1 is reserved for active security problems (leaked keys,
379
+ bad perms).
380
+ * If the CZ entry is present but the dry-run does NOT round-trip -> ERROR:
381
+ the entry exists but the adapter cannot render a decision, so the gate
382
+ would emit empty stdout -> agy invalid_args / undefined behavior.
383
+ """
384
+ findings: List[Finding] = []
385
+ executed = _antigravity_executed_hook_paths()
386
+
387
+ # agy executes the HIGHEST-PRECEDENCE existing file only (project scope
388
+ # <cwd>/.agents/hooks.json shadows the global ~/.gemini/config/hooks.json).
389
+ # So the FIRST existing executed file is authoritative: if it carries a CZ
390
+ # entry the gate is live; if it does NOT, the workspace is ungoverned even
391
+ # when a lower-precedence file happens to have one. We must NOT fall through
392
+ # to that lower file, or doctor would falsely report "live" (codex P1).
393
+ authoritative = next((p for p in executed if p.exists()), None)
394
+ live_path = (
395
+ authoritative
396
+ if authoritative is not None and _antigravity_hook_command_present(authoritative)
397
+ else None
398
+ )
399
+
400
+ if live_path is None:
401
+ # Surface a not-live WARN when ANY agy hooks.json exists -- the
402
+ # authoritative executed file without a CZ entry, or only the
403
+ # display-only TUI mirror. A hook present only in the mirror (or absent
404
+ # from the executed file that wins) is shown in the agy `/hooks` UI yet
405
+ # does NOT govern tool calls -- exactly the misleading state to flag.
406
+ # Stay silent when no agy file exists at all so a non-Antigravity user
407
+ # gets no spurious warning.
408
+ mirror = _antigravity_mirror_path()
409
+ present = [p for p in executed if p.exists()]
410
+ if mirror.exists():
411
+ present.append(mirror)
412
+ if present:
413
+ mirror_only = all(p == mirror for p in present)
414
+ findings.append(Finding(
415
+ path=str(present[0]),
416
+ line=1,
417
+ col=1,
418
+ severity="WARN",
419
+ code="E1006",
420
+ message=(
421
+ (
422
+ "Antigravity hook appears only in the agy /hooks DISPLAY "
423
+ "mirror (~/.gemini/antigravity-cli/hooks.json) -- that path "
424
+ "is NOT executed, so the governance gate is NOT firing."
425
+ )
426
+ if mirror_only else
427
+ (
428
+ "Antigravity hooks.json exists but has no live "
429
+ f"'{_CZ_HOOK_MARKER}' PreToolUse entry -- the governance "
430
+ "gate is NOT firing for tool calls."
431
+ )
432
+ ),
433
+ fix_hint="run `controlzero install antigravity` to (re)install the hook",
434
+ ))
435
+ return findings
436
+
437
+ # Hook entry is present -- verify it can actually render a decision.
438
+ if not _antigravity_dry_run_roundtrips():
439
+ findings.append(Finding(
440
+ path=str(live_path),
441
+ line=1,
442
+ col=1,
443
+ severity="ERROR",
444
+ code="E1007",
445
+ message=(
446
+ "Antigravity hook entry is installed but a dry-run hook-check "
447
+ "does NOT round-trip a decision -- the gate would emit empty "
448
+ "stdout (agy reads that as invalid_args / undefined)."
449
+ ),
450
+ fix_hint=(
451
+ "reinstall the SDK (`pip install -U control-zero`) and rerun "
452
+ "`controlzero install antigravity`; if it persists, file a bug"
453
+ ),
454
+ ))
455
+ return findings
456
+
457
+
246
458
  # -----------------------------------------------------------------------------
247
459
  # Public entrypoint. Wired into the CLI as `controlzero doctor`.
248
460
  # -----------------------------------------------------------------------------
@@ -257,6 +469,7 @@ def run_doctor(verbose: bool = False) -> int:
257
469
  findings.extend(_check_agent_key_leaks(_agent_paths()))
258
470
  findings.extend(_check_config_permissions())
259
471
  findings.extend(_check_global_environment())
472
+ findings.extend(_check_antigravity_hook_live())
260
473
 
261
474
  errors = [f for f in findings if f.severity == "ERROR"]
262
475
  warns = [f for f in findings if f.severity == "WARN"]
@@ -272,6 +485,16 @@ def run_doctor(verbose: bool = False) -> int:
272
485
  for ap in _agent_paths():
273
486
  status = "exists" if ap.path.exists() else "absent"
274
487
  cz_console.info(f" {ap.agent:14s} {status:7s} {ap.path}")
488
+ _live = next(
489
+ (p for p in _antigravity_executed_hook_paths()
490
+ if p.exists() and _antigravity_hook_command_present(p)),
491
+ None,
492
+ )
493
+ if _live is not None:
494
+ rt = "round-trips" if _antigravity_dry_run_roundtrips() else "BROKEN"
495
+ cz_console.info(f" {'antigravity':14s} {'live':7s} {_live} (dry-run: {rt})")
496
+ else:
497
+ cz_console.info(f" {'antigravity':14s} {'not-live':7s} (no executed PreToolUse hook entry)")
275
498
  return 0
276
499
 
277
500
  for f in findings:
@@ -28,14 +28,21 @@ danicat.dev):
28
28
  We therefore ALWAYS emit an explicit ``{"decision": ...}`` -- the allow
29
29
  path is ``{"decision":"allow"}``, never silence-that-still-prints-{}.
30
30
 
31
- HITL mapping: a Control Zero approval gate maps to ``ask`` by default. A
32
- plain ``ask`` CAN be auto-satisfied by a cached "Always Allow" (so it is not
33
- a guaranteed prompt); some agy builds also accept ``force_ask`` (always
34
- prompts). Because ``force_ask`` is not confirmed across all builds and an
35
- unrecognized decision risks the same ``invalid_args`` -> deny trap, the HITL
36
- decision token is configurable via ``CZ_ANTIGRAVITY_HITL_DECISION`` (default
37
- ``ask``; set to ``force_ask`` once verified against your pinned agy, or
38
- ``deny`` for a hard block-with-reason).
31
+ HITL mapping (GA blocker #2, issue #1248): a Control Zero approval gate maps to
32
+ ``force_ask`` by DEFAULT -- a MANDATORY prompt that ignores agy's "Always Allow"
33
+ cache. A plain ``ask`` CAN be silently auto-satisfied by a cached "Always Allow"
34
+ (fail-OPEN on an approval-gated destructive action), so it is NOT the default
35
+ and is NEVER reached by the unrecognized-value fallback. The token is
36
+ configurable via ``CZ_ANTIGRAVITY_HITL_DECISION``:
37
+
38
+ * unset / unrecognized -> ``force_ask`` (fail-closed default).
39
+ * ``force_ask`` -> guaranteed prompt.
40
+ * ``deny`` -> hard block-with-reason. Use this as the fallback when a pinned agy
41
+ build is known to reject ``force_ask``; the gate then blocks rather than
42
+ silently auto-approving.
43
+ * ``ask`` -> the legacy cache-bypassable behavior. Permitted only as an
44
+ explicit, knowing downgrade (fail-open).
45
+ * ``allow`` -> defeats the gate; explicit override only.
39
46
 
40
47
  Subagent tool calls fire the parent ``PreToolUse`` hook, so one adapter
41
48
  governs parent + subagents.
@@ -68,21 +75,57 @@ _ENV_PREFIXES = ("ANTIGRAVITY_", "AGY_")
68
75
 
69
76
  # Source-confirmed decision tokens for PreToolUse output.
70
77
  _VALID_DECISIONS = frozenset({"allow", "deny", "ask", "force_ask"})
71
- _DEFAULT_HITL_DECISION = "ask"
78
+
79
+ # HITL approval-gate token. GA blocker #2 (issue #1248): a plain ``ask`` CAN be
80
+ # silently auto-satisfied by a cached "Always Allow" -- which is fail-OPEN on an
81
+ # approval-gated destructive action. For a governance gate that is unacceptable,
82
+ # so the safe default for a HITL gate is ``force_ask`` (a MANDATORY prompt that
83
+ # ignores the cache). If a particular agy build rejects ``force_ask`` (older
84
+ # builds may not implement it), the operator must fall back to ``deny`` -- a hard
85
+ # block-with-reason -- NOT to ``ask``. ``ask`` is therefore no longer the default
86
+ # and is never reached by the unrecognized-value fallback; an operator who wants
87
+ # the cache-bypassable behavior must opt into it EXPLICITLY and knowingly.
88
+ _DEFAULT_HITL_DECISION = "force_ask"
89
+ # The fail-closed fallback for a HITL gate when the configured token is empty or
90
+ # unrecognized. Must be a guaranteed-prompt / hard-block token, never ``ask``.
91
+ _HITL_FALLBACK_DECISION = "force_ask"
92
+ # Tokens that are SAFE for a HITL approval gate (cannot be silently
93
+ # auto-approved from a cache). ``ask`` is deliberately excluded -- it is
94
+ # cache-bypassable -- and ``allow`` is excluded because allowing outright defeats
95
+ # the gate. Only an explicit operator override may select a non-safe token.
96
+ _HITL_SAFE_DECISIONS = frozenset({"force_ask", "deny"})
72
97
 
73
98
 
74
99
  def _resolve_hitl_decision(env: Mapping[str, str] | None = None) -> str:
75
100
  """Resolve the decision token used for a HITL approval gate.
76
101
 
77
- Default ``ask`` (source-confirmed valid on every agy build). Operators
78
- can opt into ``force_ask`` (guaranteed prompt) or ``deny`` once verified
79
- against their pinned agy via ``CZ_ANTIGRAVITY_HITL_DECISION``. An
80
- unrecognized value falls back to ``ask`` so we never emit a token that
81
- could trip the ``invalid_args`` -> deny trap.
102
+ GA blocker #2 (issue #1248): a HITL gate must never silently auto-approve.
103
+ A plain ``ask`` can be satisfied by agy's cached "Always Allow" (fail-OPEN),
104
+ so it is NOT a safe HITL token. The resolution rules are:
105
+
106
+ * Default (env unset): ``force_ask`` -- a MANDATORY prompt that ignores the
107
+ cache. This is the safe, fail-closed choice.
108
+ * Empty / unrecognized value: falls back to ``force_ask`` (NEVER ``ask``),
109
+ so a typo or an unset shell can never silently downgrade the gate to a
110
+ cache-bypassable token.
111
+ * Explicit operator override via ``CZ_ANTIGRAVITY_HITL_DECISION``:
112
+ - ``force_ask`` -- guaranteed prompt (the default).
113
+ - ``deny`` -- hard block-with-reason. This is the correct fallback when
114
+ a pinned agy build is known to reject ``force_ask`` (which would
115
+ otherwise trip the ``invalid_args`` -> deny trap anyway, but ``deny``
116
+ makes the block explicit and carries the reason).
117
+ - ``ask`` -- the LEGACY cache-bypassable behavior. Permitted only as an
118
+ explicit, knowing downgrade; it is fail-open and must not be used for
119
+ an approval-gated destructive action.
120
+ - ``allow`` -- defeats the gate entirely; permitted only as an explicit
121
+ override (e.g. a soak/observe deployment).
82
122
  """
83
123
  src = env if env is not None else os.environ
84
124
  raw = (src.get("CZ_ANTIGRAVITY_HITL_DECISION") or "").strip().lower()
85
- return raw if raw in _VALID_DECISIONS else _DEFAULT_HITL_DECISION
125
+ if raw in _VALID_DECISIONS:
126
+ return raw
127
+ # Empty or unrecognized -> fail closed to a guaranteed-prompt token.
128
+ return _HITL_FALLBACK_DECISION
86
129
 
87
130
 
88
131
  class AntigravityAdapter(HostAdapter):
@@ -699,15 +699,83 @@ def hook_check(policy: Optional[str]):
699
699
  # the hosted/tamper paths, which already fail closed.
700
700
  _strict = _hook_check_strict()
701
701
 
702
+ # GA blocker #1 (issue #1248): empty-stdout fail-closed for Antigravity.
703
+ #
704
+ # Several hook-check early exits below (stdin read error, empty stdin,
705
+ # malformed JSON, missing tool_name, invalid policy) historically printed
706
+ # NOTHING to stdout and exited 0. On Claude Code / Gemini / Codex an empty
707
+ # stdout + exit 0 is the documented "no opinion, proceed" signal, so that is
708
+ # the right behavior there -- bricking those agents on a malformed payload
709
+ # would be a regression.
710
+ #
711
+ # Antigravity is different: it decides from the stdout JSON, and an empty /
712
+ # decision-less stdout is read by agy as ``invalid_args`` and DENIES the tool
713
+ # (cmux#5358). That happens to fail closed, but it is UNDOCUMENTED and
714
+ # UNVERIFIED across builds -- a future agy could read empty stdout as
715
+ # fail-open. For a governance gate the deny must be DETERMINISTIC, so on the
716
+ # Antigravity surface EVERY error exit emits an explicit
717
+ # ``{"decision":"deny", "reason":...}`` rather than relying on the empty-
718
+ # stdout heuristic.
719
+ #
720
+ # These three early exits run BEFORE the payload parses, so the per-payload
721
+ # ``toolCall`` signature is unavailable. Antigravity is still detectable from
722
+ # the environment alone (``CONTROLZERO_CLIENT=antigravity|agy|antigravity-cli``
723
+ # or any ``ANTIGRAVITY_*`` / ``AGY_*`` var), which is exactly what the
724
+ # adapter's ``claim()`` checks. We pass an empty payload so only the env
725
+ # signal can claim -- a non-Antigravity host falls through to the historical
726
+ # pass-through.
727
+ def _failclosed_if_antigravity(reason: str) -> None:
728
+ """Emit an explicit Antigravity deny iff the host is Antigravity.
729
+
730
+ No-op for every other host (preserves the pass-through contract). The
731
+ deny is rendered through the adapter so the on-the-wire shape is the
732
+ canonical ``{"decision":"deny", "reason":..., "controlzero":{...}}``.
733
+ Antigravity decides from stdout JSON, not the exit code, so the caller
734
+ still exits 0 after this returns. Wrapped in a broad guard: this runs on
735
+ an already-degraded error path, so a failure to import / select / render
736
+ must never raise out of here -- it would turn a clean pass-through exit
737
+ into a traceback. On failure it is a no-op and the caller's exit stands.
738
+ """
739
+ try:
740
+ from controlzero.cli.hosts import (
741
+ CZDecision as _CZDecision,
742
+ select_adapter as _select_adapter,
743
+ )
744
+
745
+ _early_adapter = _select_adapter({}, os.environ)
746
+ if getattr(_early_adapter, "name", "") != "antigravity":
747
+ return
748
+ _decision = _CZDecision(
749
+ effect="deny",
750
+ reason=f"[Control Zero] {reason}",
751
+ reason_code=REASON_CODE_BUNDLE_MISSING,
752
+ )
753
+ click.echo(json.dumps(_early_adapter.render(_decision)))
754
+ except Exception: # noqa: BLE001
755
+ # Never raise from an error-path fail-closed helper. The caller
756
+ # still exits; on Antigravity a missing decision line falls back to
757
+ # agy's own empty-stdout handling (currently a deny), which is no
758
+ # worse than the pre-fix behavior.
759
+ return
760
+
702
761
  # Read stdin -- Claude Code passes the tool payload here
703
762
  try:
704
763
  raw = sys.stdin.read()
705
764
  except (KeyboardInterrupt, EOFError):
706
- # No payload: pass through (do not break the agent)
707
- sys.exit(2 if _strict else 0)
765
+ # No payload. Kiro CLI strict mode fails CLOSED (exit 2); every other
766
+ # host passes through (exit 0). On Antigravity, emit an explicit deny
767
+ # first -- a missing payload must not silently allow on the governance
768
+ # surface.
769
+ if _strict:
770
+ sys.exit(2)
771
+ _failclosed_if_antigravity(
772
+ "Hook received no payload on stdin; denying (fail-closed)."
773
+ )
774
+ sys.exit(0)
708
775
 
709
776
  if not raw.strip():
710
- # Empty stdin.
777
+ # Empty stdin. Kiro CLI strict mode fails CLOSED with a specific
778
+ # diagnostic; other hosts pass through, denying explicitly on Antigravity.
711
779
  if _strict:
712
780
  click.echo(
713
781
  "controlzero: empty hook payload on stdin; blocking "
@@ -716,12 +784,18 @@ def hook_check(policy: Optional[str]):
716
784
  err=True,
717
785
  )
718
786
  sys.exit(2)
787
+ _failclosed_if_antigravity(
788
+ "Hook received empty stdin; denying (fail-closed)."
789
+ )
719
790
  sys.exit(0)
720
791
 
721
792
  try:
722
793
  payload = json.loads(raw)
723
794
  except json.JSONDecodeError as e:
724
- # Malformed payload.
795
+ # Malformed payload. Kiro CLI strict mode fails CLOSED on a parse error;
796
+ # other hosts log to stderr and pass through. On Antigravity, a parse
797
+ # error must DENY (a corrupted payload could otherwise smuggle a tool
798
+ # call past the gate).
725
799
  if _strict:
726
800
  click.echo(
727
801
  f"controlzero: hook payload is not JSON ({e}); blocking "
@@ -730,8 +804,25 @@ def hook_check(policy: Optional[str]):
730
804
  err=True,
731
805
  )
732
806
  sys.exit(2)
733
- # Default: log to stderr and pass through. Never brick the agent.
734
807
  click.echo(f"controlzero: hook payload is not JSON ({e}); allowing", err=True)
808
+ _failclosed_if_antigravity(
809
+ f"Hook payload is not valid JSON ({e}); denying (fail-closed)."
810
+ )
811
+ sys.exit(0)
812
+
813
+ if not isinstance(payload, dict):
814
+ # Valid JSON but not an object (a bare list / string / number / null).
815
+ # ``payload.get(...)`` below would AttributeError on a non-dict, and the
816
+ # adapter cannot extract a tool from it. Treat it exactly like a
817
+ # malformed payload: pass-through hosts allow, Antigravity denies
818
+ # explicitly (GA blocker #1, #1248). Done BEFORE select_adapter so a
819
+ # non-dict never reaches normalize_payload / .get().
820
+ click.echo(
821
+ "controlzero: hook payload is not a JSON object; allowing", err=True
822
+ )
823
+ _failclosed_if_antigravity(
824
+ "Hook payload is not a JSON object; denying (fail-closed)."
825
+ )
735
826
  sys.exit(0)
736
827
 
737
828
  # 1.5.3+ adapter base: pick the right host-agent adapter BEFORE
@@ -757,6 +848,14 @@ def hook_check(policy: Optional[str]):
757
848
  tool_name = payload.get("tool_name") or payload.get("toolName") or ""
758
849
  tool_args = payload.get("tool_input") or payload.get("toolInput") or {}
759
850
  if not tool_name:
851
+ # Payload parsed but carries no tool to evaluate. Kiro CLI strict mode
852
+ # fails CLOSED (a security hook that cannot see which tool is running
853
+ # must not fail open). Pass-through hosts treat empty stdout / exit 0
854
+ # as "no opinion". On Antigravity that is the undocumented invalid_args
855
+ # trap, so emit an explicit deny instead (GA blocker #1, #1248) -- a
856
+ # tool call whose name we cannot read must not slip past the gate.
857
+ # _adapter is already the correctly selected host adapter here
858
+ # (toolCall- or env-detected for Antigravity).
760
859
  if _strict:
761
860
  click.echo(
762
861
  "controlzero: hook payload missing tool_name; blocking "
@@ -766,6 +865,12 @@ def hook_check(policy: Optional[str]):
766
865
  )
767
866
  sys.exit(2)
768
867
  click.echo("controlzero: hook payload missing tool_name; allowing", err=True)
868
+ if getattr(_adapter, "name", "") == "antigravity":
869
+ click.echo(json.dumps(_adapter.render(CZDecision(
870
+ effect="deny",
871
+ reason="[Control Zero] Hook payload missing tool_name; denying (fail-closed).",
872
+ reason_code=REASON_CODE_BUNDLE_MISSING,
873
+ ))))
769
874
  sys.exit(0)
770
875
 
771
876
  def _emit_decision(
@@ -1031,12 +1136,14 @@ def hook_check(policy: Optional[str]):
1031
1136
  log_path=str(GLOBAL_AUDIT_PATH),
1032
1137
  )
1033
1138
  except (PolicyLoadError, PolicyValidationError, PermissionError, OSError) as e:
1034
- # Bad/unreadable policy file. Default: log + allow (do not silently
1035
- # break the agent). Strict mode: fail closed with a diagnostic that
1036
- # distinguishes an unreadable/parse-broken policy from "no policy
1037
- # installed" (which is the BUNDLE_MISSING path above and is governed
1038
- # by default_on_missing). An installed-but-unreadable policy is an
1039
- # operator error a security hook should not silently allow through.
1139
+ # Bad/unreadable policy file. Kiro CLI strict mode fails CLOSED with a
1140
+ # diagnostic that distinguishes an unreadable/parse-broken policy from
1141
+ # "no policy installed" (the BUNDLE_MISSING path above, governed by
1142
+ # default_on_missing); an installed-but-unreadable policy is an operator
1143
+ # error a security hook should not silently allow through. Other
1144
+ # pass-through hosts log + allow (do not break the agent on a local-file
1145
+ # glitch). On Antigravity an invalid policy must DENY (GA blocker #1,
1146
+ # #1248): a governance gate whose policy will not parse must fail closed.
1040
1147
  if _strict:
1041
1148
  click.echo(
1042
1149
  f"controlzero: local policy is present but unreadable/invalid "
@@ -1051,6 +1158,13 @@ def hook_check(policy: Optional[str]):
1051
1158
  )
1052
1159
  sys.exit(2)
1053
1160
  click.echo(f"controlzero: policy file invalid ({e}); allowing", err=True)
1161
+ if getattr(_adapter, "name", "") == "antigravity":
1162
+ _emit_decision(
1163
+ effect="deny",
1164
+ reason=f"[Control Zero] Policy file invalid ({e}); denying (fail-closed).",
1165
+ reason_code=REASON_CODE_BUNDLE_MISSING,
1166
+ )
1167
+ sys.exit(2 if _adapter.decision_via_exit_code else 0)
1054
1168
  sys.exit(0)
1055
1169
  except Exception as e: # noqa: BLE001
1056
1170
  # Hosted-pull failure (HostedAuthError, HostedBootstrapError, etc).
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "controlzero"
7
- version = "1.9.5"
7
+ version = "1.9.6"
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"}