controlzero 1.9.5__tar.gz → 1.9.7__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.7}/CHANGELOG.md +62 -0
  2. {controlzero-1.9.5 → controlzero-1.9.7}/PKG-INFO +1 -1
  3. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/__init__.py +1 -1
  4. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/audit_remote.py +104 -4
  5. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/doctor.py +225 -2
  6. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/hosts/antigravity.py +58 -15
  7. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/main.py +125 -11
  8. {controlzero-1.9.5 → controlzero-1.9.7}/pyproject.toml +1 -1
  9. {controlzero-1.9.5 → controlzero-1.9.7}/tests/spool/test_spool_sink_wiring.py +127 -3
  10. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_antigravity_adapter.py +18 -12
  11. controlzero-1.9.7/tests/test_antigravity_ga_blockers_1248.py +554 -0
  12. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_hosted_policy_e2e.py +7 -5
  13. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_hosts_adapter.py +6 -5
  14. {controlzero-1.9.5 → controlzero-1.9.7}/.gitignore +0 -0
  15. {controlzero-1.9.5 → controlzero-1.9.7}/Dockerfile.test +0 -0
  16. {controlzero-1.9.5 → controlzero-1.9.7}/LICENSE +0 -0
  17. {controlzero-1.9.5 → controlzero-1.9.7}/README.md +0 -0
  18. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/_internal/__init__.py +0 -0
  19. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/_internal/action_aliases.py +0 -0
  20. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/_internal/action_validator.py +0 -0
  21. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/_internal/bundle.py +0 -0
  22. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/_internal/credential_hook.py +0 -0
  23. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/_internal/credential_scanner.py +0 -0
  24. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/_internal/credentials_data/__init__.py +0 -0
  25. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/_internal/credentials_data/built_in.yaml +0 -0
  26. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/_internal/dlp_scanner.py +0 -0
  27. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/_internal/enforcer.py +0 -0
  28. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/_internal/hook_extractors.py +0 -0
  29. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/_internal/tool_extractors.json +0 -0
  30. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/_internal/types.py +0 -0
  31. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/audit_local.py +0 -0
  32. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/canonical.py +0 -0
  33. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/__init__.py +0 -0
  34. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/__main__.py +0 -0
  35. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/_secrets.py +0 -0
  36. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/console.py +0 -0
  37. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/debug_bundle.py +0 -0
  38. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/hosts/__init__.py +0 -0
  39. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/hosts/base.py +0 -0
  40. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/hosts/claude_code.py +0 -0
  41. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/hosts/codex_cli.py +0 -0
  42. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/hosts/gemini_cli.py +0 -0
  43. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/hosts/kiro.py +0 -0
  44. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/hosts/unknown.py +0 -0
  45. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/kiro_adapter.py +0 -0
  46. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/migrate.py +0 -0
  47. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/spool_cmd.py +0 -0
  48. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/telemetry_consent.py +0 -0
  49. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/templates/antigravity/hooks.json +0 -0
  50. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/templates/antigravity.yaml +0 -0
  51. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/templates/autogen.yaml +0 -0
  52. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/templates/claude-code.yaml +0 -0
  53. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/templates/codex-cli.yaml +0 -0
  54. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/templates/cost-cap.yaml +0 -0
  55. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/templates/crewai.yaml +0 -0
  56. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/templates/cursor.yaml +0 -0
  57. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/templates/gemini-cli.yaml +0 -0
  58. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/templates/generic.yaml +0 -0
  59. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook +0 -0
  60. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/templates/kiro/ide-pre-tool-use.kiro.hook +0 -0
  61. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/templates/kiro/ide-prompt-submit.kiro.hook +0 -0
  62. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/templates/kiro/kiro.yaml +0 -0
  63. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/templates/langchain.yaml +0 -0
  64. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/templates/mcp.yaml +0 -0
  65. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/cli/templates/rag.yaml +0 -0
  66. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/client.py +0 -0
  67. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/device.py +0 -0
  68. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/enrollment.py +0 -0
  69. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/error_codes.py +0 -0
  70. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/error_codes.yaml +0 -0
  71. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/errors.py +0 -0
  72. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/hitl/__init__.py +0 -0
  73. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/hitl/grant_protocol.py +0 -0
  74. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/hitl/mock.py +0 -0
  75. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/hitl/pending_approval.py +0 -0
  76. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/hitl/secret_leak_guard.py +0 -0
  77. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/hitl/status.py +0 -0
  78. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/hooks/__init__.py +0 -0
  79. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/hooks/tool_output_handler.py +0 -0
  80. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/hosted_policy.py +0 -0
  81. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/integrations/__init__.py +0 -0
  82. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/integrations/anthropic.py +0 -0
  83. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/integrations/autogen.py +0 -0
  84. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/integrations/braintrust.py +0 -0
  85. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/integrations/crewai/__init__.py +0 -0
  86. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/integrations/crewai/agent.py +0 -0
  87. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/integrations/crewai/crew.py +0 -0
  88. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/integrations/crewai/task.py +0 -0
  89. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/integrations/crewai/tool.py +0 -0
  90. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/integrations/google.py +0 -0
  91. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/integrations/google_adk/__init__.py +0 -0
  92. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/integrations/google_adk/agent.py +0 -0
  93. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/integrations/google_adk/tool.py +0 -0
  94. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/integrations/langchain/__init__.py +0 -0
  95. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/integrations/langchain/agent.py +0 -0
  96. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/integrations/langchain/callbacks.py +0 -0
  97. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/integrations/langchain/chain.py +0 -0
  98. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/integrations/langchain/graph.py +0 -0
  99. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/integrations/langchain/modern.py +0 -0
  100. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/integrations/langchain/tool.py +0 -0
  101. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/integrations/langfuse.py +0 -0
  102. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/integrations/litellm.py +0 -0
  103. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/integrations/openai.py +0 -0
  104. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/integrations/pydantic_ai.py +0 -0
  105. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/integrations/vercel_ai.py +0 -0
  106. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/layout_migration.py +0 -0
  107. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/policy_loader.py +0 -0
  108. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/spool/__init__.py +0 -0
  109. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/spool/_compress.py +0 -0
  110. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/spool/_constants.py +0 -0
  111. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/spool/_crc32c.py +0 -0
  112. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/spool/_crypto.py +0 -0
  113. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/spool/_frame.py +0 -0
  114. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/spool/_keyring.py +0 -0
  115. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/spool/_metrics.py +0 -0
  116. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/spool/_spool.py +0 -0
  117. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/spool/_state.py +0 -0
  118. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/spool/_uploader.py +0 -0
  119. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/spool/cz-audit-v1.dict +0 -0
  120. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/tamper.py +0 -0
  121. {controlzero-1.9.5 → controlzero-1.9.7}/controlzero/tracecontext.py +0 -0
  122. {controlzero-1.9.5 → controlzero-1.9.7}/examples/hello_world.py +0 -0
  123. {controlzero-1.9.5 → controlzero-1.9.7}/tests/_fixtures/jcs_args_hash_vectors.json +0 -0
  124. {controlzero-1.9.5 → controlzero-1.9.7}/tests/conftest.py +0 -0
  125. {controlzero-1.9.5 → controlzero-1.9.7}/tests/integrations/__init__.py +0 -0
  126. {controlzero-1.9.5 → controlzero-1.9.7}/tests/integrations/test_google.py +0 -0
  127. {controlzero-1.9.5 → controlzero-1.9.7}/tests/parity/action_aliases.json +0 -0
  128. {controlzero-1.9.5 → controlzero-1.9.7}/tests/spool/__init__.py +0 -0
  129. {controlzero-1.9.5 → controlzero-1.9.7}/tests/spool/conftest.py +0 -0
  130. {controlzero-1.9.5 → controlzero-1.9.7}/tests/spool/test_spool_cli.py +0 -0
  131. {controlzero-1.9.5 → controlzero-1.9.7}/tests/spool/test_spool_concurrency.py +0 -0
  132. {controlzero-1.9.5 → controlzero-1.9.7}/tests/spool/test_spool_conformance.py +0 -0
  133. {controlzero-1.9.5 → controlzero-1.9.7}/tests/spool/test_spool_core.py +0 -0
  134. {controlzero-1.9.5 → controlzero-1.9.7}/tests/spool/test_spool_crash.py +0 -0
  135. {controlzero-1.9.5 → controlzero-1.9.7}/tests/spool/test_spool_diskfull.py +0 -0
  136. {controlzero-1.9.5 → controlzero-1.9.7}/tests/spool/test_spool_durable_default_tamper.py +0 -0
  137. {controlzero-1.9.5 → controlzero-1.9.7}/tests/spool/test_spool_keychain_dek.py +0 -0
  138. {controlzero-1.9.5 → controlzero-1.9.7}/tests/spool/test_spool_transcript_localack.py +0 -0
  139. {controlzero-1.9.5 → controlzero-1.9.7}/tests/spool/test_spool_uploader.py +0 -0
  140. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_action_aliases.py +0 -0
  141. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_action_canonicalization.py +0 -0
  142. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_action_validator_t86.py +0 -0
  143. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_agent_name_env.py +0 -0
  144. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_antigravity_hook_check.py +0 -0
  145. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_antigravity_install.py +0 -0
  146. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_api_key_mask.py +0 -0
  147. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_audit_remote.py +0 -0
  148. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_audit_remote_sdk_version.py +0 -0
  149. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_audit_sink_isolation.py +0 -0
  150. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_bundle_parser.py +0 -0
  151. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_bundle_translate.py +0 -0
  152. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_canonical_phase1a.py +0 -0
  153. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_cli_carve_out.py +0 -0
  154. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_cli_debug_bundle.py +0 -0
  155. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_cli_extractor_integration.py +0 -0
  156. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_cli_hook.py +0 -0
  157. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_cli_hosted_refresh.py +0 -0
  158. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_cli_init.py +0 -0
  159. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_cli_init_templates.py +0 -0
  160. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_cli_tail.py +0 -0
  161. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_cli_test.py +0 -0
  162. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_cli_validate.py +0 -0
  163. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_coding_agent_hooks.py +0 -0
  164. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_conditions.py +0 -0
  165. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_conformance.py +0 -0
  166. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_console.py +0 -0
  167. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_credential_hook.py +0 -0
  168. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_default_action.py +0 -0
  169. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_device.py +0 -0
  170. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_dlp_scanner.py +0 -0
  171. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_doctor.py +0 -0
  172. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_engine_version_consistency.py +0 -0
  173. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_enrollment.py +0 -0
  174. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_env_dump_438.py +0 -0
  175. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_epic_1247_bryan_acceptance.py +0 -0
  176. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_error_codes.py +0 -0
  177. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_errors_e_codes.py +0 -0
  178. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_fail_closed_eval.py +0 -0
  179. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_glob_matching.py +0 -0
  180. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_hitl_5d_email_install.py +0 -0
  181. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_hitl_6a_cli_flag.py +0 -0
  182. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_hitl_6a_exceptions.py +0 -0
  183. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_hitl_6a_get_secret_hitl.py +0 -0
  184. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_hitl_6a_mock_backend.py +0 -0
  185. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_hitl_6a_pending_approval.py +0 -0
  186. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_hitl_6a_request_approval.py +0 -0
  187. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_hitl_6a_secret_leak_guard.py +0 -0
  188. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_hitl_6a_wait.py +0 -0
  189. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_hitl_conformance.py +0 -0
  190. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_hitl_phase2b_protocol.py +0 -0
  191. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_hitl_reason_codes.py +0 -0
  192. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_hitl_validator_keys.py +0 -0
  193. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_hook_extractors.py +0 -0
  194. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_hosted_local_audit_1247.py +0 -0
  195. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_hybrid_mode_strict.py +0 -0
  196. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_hybrid_mode_warn.py +0 -0
  197. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_install_hook_command.py +0 -0
  198. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_install_hooks.py +0 -0
  199. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_kiro_adapter.py +0 -0
  200. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_kiro_cli_e2e.py +0 -0
  201. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_kiro_hook_templates.py +0 -0
  202. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_kiro_install.py +0 -0
  203. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_layout_migration_t101.py +0 -0
  204. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_layout_parity_t102.py +0 -0
  205. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_local_mode_dict.py +0 -0
  206. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_local_mode_file_json.py +0 -0
  207. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_local_mode_file_yaml.py +0 -0
  208. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_log_fallback_stderr.py +0 -0
  209. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_log_options_ignored_hosted.py +0 -0
  210. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_log_rotation.py +0 -0
  211. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_migrate.py +0 -0
  212. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_min_sdk_version_gate.py +0 -0
  213. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_multi_client_per_project_175.py +0 -0
  214. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_no_policy_no_key.py +0 -0
  215. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_observe_mode_1247.py +0 -0
  216. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_package_rename_shim.py +0 -0
  217. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_policy_engine_version_phase1b.py +0 -0
  218. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_policy_freshness.py +0 -0
  219. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_policy_settings.py +0 -0
  220. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_policy_source_audit.py +0 -0
  221. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_quarantine.py +0 -0
  222. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_reason_code.py +0 -0
  223. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_refresh.py +0 -0
  224. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_secrets.py +0 -0
  225. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_sql_semantic_class.py +0 -0
  226. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_synthetic_policy_id_t79.py +0 -0
  227. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_t103_precedence.py +0 -0
  228. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_t104_cache_gc.py +0 -0
  229. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_t108_local_override_audit.py +0 -0
  230. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_t96_single_audit_log.py +0 -0
  231. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_t99_install_prefetch_bundle.py +0 -0
  232. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_tamper.py +0 -0
  233. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_tamper_behavior.py +0 -0
  234. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_tamper_hook.py +0 -0
  235. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_telemetry_consent.py +0 -0
  236. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_tracecontext.py +0 -0
  237. {controlzero-1.9.5 → controlzero-1.9.7}/tests/test_unsafe_int_boundary.py +0 -0
  238. {controlzero-1.9.5 → controlzero-1.9.7}/tools/cz-kiro-adapter +0 -0
@@ -1,5 +1,67 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.7 -- 2026-06-16 (hosted audit delivery for short-lived processes, gh#1292)
4
+
5
+ Fixes a P0 found via a customer report: hosted-mode audit rows were written to
6
+ the local `~/.controlzero/audit.log` but never reached the dashboard for
7
+ short-lived processes (PreToolUse hooks, CLI one-shots).
8
+
9
+ ### Fixed
10
+
11
+ - **Remote audit never delivered for short-lived processes (gh#1292).** Since
12
+ 1.9.2 the hosted (`BearerAuditSink`) and enrolled (`RemoteAuditSink`) sinks
13
+ default to the durable encrypted spool, where `log()` WALs the row and an
14
+ opportunistic `daemon=True` thread drains it. A short-lived process exits
15
+ before that thread can finish its HTTPS POST, and `close()` was a fsync-only
16
+ boundary, so the row stayed on local disk and never shipped. The hosted sink
17
+ now hands the drain to a **detached `controlzero spool flush` child** that
18
+ outlives the process (no latency on the agent hot path; the keystore and
19
+ network work happen in the child, never on the close path). The api key
20
+ travels via the child's environment, never on its argv. The enrolled sink
21
+ (opt-in, off the hosted hot path) does a bounded, fail-open in-process drain
22
+ on close instead. Both paths are fail-open: any error leaves the durable WAL
23
+ intact for a later drain. The durable WAL guarantee is unchanged.
24
+
25
+ ## 1.9.6 -- 2026-06-16 (Antigravity GA-blocker hardening, gh#1248 / epic gh#925)
26
+
27
+ Closes 3 of the prod-readiness GA blockers for the Antigravity (`agy`)
28
+ integration. Antigravity stays **BETA** -- GA still requires a manual check
29
+ against a real pinned agy build (agy cannot run headless in CI), which these
30
+ changes do not perform.
31
+
32
+ ### Fixed
33
+
34
+ - **Empty-stdout fail-closed on the Antigravity surface (blocker #1).** Every
35
+ `controlzero hook-check` error exit -- stdin read error, empty stdin,
36
+ malformed JSON, missing `tool_name`, and an invalid/unreadable policy --
37
+ previously emitted EMPTY stdout and exited 0. Agy decides from stdout JSON,
38
+ and empty stdout is undocumented (agy *currently* reads it as
39
+ `invalid_args` -> deny, but that is unverified across builds). On the
40
+ Antigravity surface those paths now emit an explicit
41
+ `{"decision":"deny", "reason":...}` so a parse/policy error DENIES
42
+ deterministically. Pass-through hosts (Claude Code / Gemini / Codex) keep
43
+ the documented empty-stdout = "no opinion, proceed" behavior unchanged.
44
+
45
+ - **HITL approval gate no longer silently auto-approvable (blocker #2).** The
46
+ HITL decision token now defaults to `force_ask` (a MANDATORY prompt that
47
+ ignores agy's "Always Allow" cache) instead of `ask` (which a cached
48
+ approval can silently satisfy -- fail-OPEN on an approval-gated destructive
49
+ action). `CZ_ANTIGRAVITY_HITL_DECISION` selects the token; an empty or
50
+ unrecognized value falls back to `force_ask`, NEVER to `ask`. If a pinned
51
+ agy build rejects `force_ask`, set `CZ_ANTIGRAVITY_HITL_DECISION=deny` for a
52
+ hard block-with-reason. `ask`/`allow` remain available only as an explicit,
53
+ knowing operator downgrade.
54
+
55
+ - **`doctor` checks the REAL Antigravity paths and verifies the hook is live
56
+ (blocker #3).** `controlzero doctor` previously checked
57
+ `~/.antigravity/config.json` -- a path the installer never writes. It now
58
+ scans the executed `~/.gemini/config/hooks.json` and the agy TUI mirror
59
+ `~/.gemini/antigravity-cli/hooks.json` (plus the cwd-relative project-scope
60
+ `.agents/hooks.json`), confirms a live `controlzero hook-check` `PreToolUse`
61
+ entry is present (`E1006` when missing), and runs a dry-run hook-check
62
+ round-trip to confirm the adapter renders an explicit decision rather than
63
+ empty stdout (`E1007` when it does not).
64
+
3
65
  ## 1.9.5 -- 2026-06-16 (Kiro CLI -> GA, epic gh#877)
4
66
 
5
67
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: controlzero
3
- Version: 1.9.5
3
+ Version: 1.9.7
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.7"
44
44
 
45
45
  __all__ = [
46
46
  "Client",
@@ -24,6 +24,7 @@ import json
24
24
  import logging
25
25
  import os
26
26
  import platform
27
+ import sys
27
28
  import threading
28
29
  import uuid
29
30
  from datetime import datetime, timezone
@@ -110,6 +111,15 @@ _SPOOL_DEFAULT_DIR = "~/.controlzero/spool"
110
111
  # processes.
111
112
  _SPOOL_HOOK_BUDGET_S = 2.0
112
113
  _SPOOL_TIMER_BUDGET_S = 25.0
114
+ # close() handoff budgets (#1292). A short-lived process (PreToolUse hook /
115
+ # CLI one-shot) exits before the daemon drain ships its spooled rows, and
116
+ # close() was a fsync-only boundary -- so the rows never reached the backend.
117
+ # The hosted (Bearer) sink now hands the drain to a DETACHED uploader child
118
+ # that outlives this process (no latency on the agent hot path, keystore +
119
+ # network work happen off the close path). The enrolled sink -- opt-in, off
120
+ # the hosted hot path -- does a bounded, fail-open in-process drain instead.
121
+ _DETACHED_DRAIN_BUDGET_S = 30.0
122
+ _CLOSE_DRAIN_BUDGET_S = 2.0
113
123
 
114
124
 
115
125
  def _build_wire_entry(
@@ -284,6 +294,11 @@ class _SpoolWiringMixin:
284
294
  self._drain_inflight = False
285
295
  self._drain_again = False
286
296
  self._drain_auth_blocked = False
297
+ # #1292: whether this process WAL-logged at least one entry (gates the
298
+ # close-time handoff so a pure-read process never spawns a drainer) and
299
+ # whether the detached drainer was already spawned (coalesce to one).
300
+ self._spool_logged = False
301
+ self._detached_drain_spawned = False
287
302
  self._spool = None
288
303
  self._spool_mode = "off"
289
304
  # Fast path: flag off/unset, NO hosted default, AND no spool
@@ -365,6 +380,10 @@ class _SpoolWiringMixin:
365
380
  except Exception as exc: # noqa: BLE001
366
381
  logger.warning("controlzero: spool append failed (%s)", exc)
367
382
  return False
383
+ # This process has durably WAL-logged at least one entry; close() may
384
+ # need to hand its drain to a detached uploader so a short-lived
385
+ # process does not strand it (#1292).
386
+ self._spool_logged = True
368
387
  if self._spool_mode == _SPOOL_MODE_DURABLE:
369
388
  self._drain_async(budget_s=_SPOOL_HOOK_BUDGET_S)
370
389
  return True
@@ -377,6 +396,73 @@ class _SpoolWiringMixin:
377
396
  def _spool_drain_orphans(self) -> bool:
378
397
  return False
379
398
 
399
+ def _spawn_detached_drain(self) -> None:
400
+ """Hand the spool drain to a DETACHED child that outlives this
401
+ process, then return immediately (#1292).
402
+
403
+ A short-lived hosted process (PreToolUse hook / CLI one-shot) exits
404
+ before the in-process daemon drain can finish its HTTPS POST, so
405
+ durably-spooled rows never reach the backend -- close() was a
406
+ fsync-only boundary that stranded them. Spawning ``controlzero spool
407
+ flush`` in a new session lets the upload complete after this process
408
+ is gone: no latency on the agent hot path, and the keystore + network
409
+ work happen in the child, never on this close path (the founder
410
+ non-blocking-hook and no-keystore-on-close constraints).
411
+
412
+ Only the Bearer (hosted api-key) sink can be drained this way --
413
+ ``cz spool flush`` authenticates with the api key, which it reads from
414
+ the environment (never argv, so the key never lands in ``ps``). The
415
+ enrolled sink has no such entrypoint and uses an in-process close
416
+ drain instead.
417
+
418
+ Fail-open: any error leaves the durable WAL intact for a later drain
419
+ (next long-lived process, the timer, or an explicit ``cz spool
420
+ flush``)."""
421
+ if getattr(self, "_detached_drain_spawned", False):
422
+ return
423
+ if self._spool is None or not getattr(self, "_spool_logged", False):
424
+ return
425
+ api_key = getattr(self, "_api_key", None)
426
+ if not api_key:
427
+ return
428
+ try:
429
+ import subprocess
430
+
431
+ env = dict(os.environ)
432
+ env["CONTROLZERO_API_KEY"] = api_key
433
+ api_url = getattr(self, "_api_url", None)
434
+ if api_url:
435
+ env["CONTROLZERO_API_URL"] = api_url
436
+ popen_kwargs = dict(
437
+ stdin=subprocess.DEVNULL,
438
+ stdout=subprocess.DEVNULL,
439
+ stderr=subprocess.DEVNULL,
440
+ env=env,
441
+ close_fds=True,
442
+ )
443
+ if os.name == "nt": # detach on Windows
444
+ popen_kwargs["creationflags"] = (
445
+ getattr(subprocess, "DETACHED_PROCESS", 0)
446
+ | getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
447
+ )
448
+ else: # POSIX: escape the process group so it outlives the parent
449
+ popen_kwargs["start_new_session"] = True
450
+ subprocess.Popen( # noqa: S603 -- fixed argv, no shell, no user input
451
+ [
452
+ sys.executable, "-m", "controlzero.cli.main",
453
+ "spool", "flush",
454
+ "--timeout", str(_DETACHED_DRAIN_BUDGET_S),
455
+ ],
456
+ **popen_kwargs,
457
+ )
458
+ self._detached_drain_spawned = True
459
+ except Exception as exc: # noqa: BLE001 -- never block/crash the host
460
+ logger.warning(
461
+ "controlzero: detached spool drain spawn failed (%s); audit "
462
+ "rows remain durable in the spool for a later drain",
463
+ exc,
464
+ )
465
+
380
466
  def _drain_async(self, budget_s: float = _SPOOL_HOOK_BUDGET_S) -> None:
381
467
  """One opportunistic non-blocking drain in a daemon thread.
382
468
  Coalesced: at most one drain in flight per sink instance; an
@@ -506,8 +592,17 @@ class RemoteAuditSink(_SpoolWiringMixin):
506
592
  self._closed = True
507
593
  self._cancel_flush_timer()
508
594
  if self._spool_wal:
509
- # Spool mode: every entry is already durable on disk; close
510
- # is a final fsync boundary, NEVER a blocking network flush.
595
+ # DURABLE mode (enrolled, opt-in, off the hosted hot path) ships
596
+ # synchronously with a bounded, fail-open budget before the final
597
+ # fsync so a short-lived process does not strand its rows (#1292).
598
+ # spool_only is replay-only and must NEVER live-send on close.
599
+ if (self._spool_mode == _SPOOL_MODE_DURABLE
600
+ and getattr(self, "_spool_logged", False)):
601
+ try:
602
+ self._drain_once(blocking=True,
603
+ budget_s=_CLOSE_DRAIN_BUDGET_S)
604
+ except Exception: # noqa: BLE001 -- fail-open; WAL preserved
605
+ pass
511
606
  self._spool_close()
512
607
  return
513
608
  # Synchronous flush -- we are shutting down
@@ -748,8 +843,13 @@ class BearerAuditSink(_SpoolWiringMixin):
748
843
  self._closed = True
749
844
  self._cancel_flush_timer()
750
845
  if self._spool_wal:
751
- # Spool mode: entries are already durable; close is a final
752
- # fsync boundary, NEVER a blocking network flush.
846
+ # DURABLE mode hands the drain to a DETACHED child that outlives
847
+ # this (possibly short-lived) process so its rows are delivered;
848
+ # close() makes NO in-process network call -- the child does the
849
+ # keystore + network work off this hot path (#1292). spool_only is
850
+ # replay-only and must NEVER live-send on close -- only fsync.
851
+ if self._spool_mode == _SPOOL_MODE_DURABLE:
852
+ self._spawn_detached_drain()
753
853
  self._spool_close()
754
854
  return
755
855
  self._flush_sync()
@@ -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):