controlzero 1.8.1__tar.gz → 1.9.0__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 (230) hide show
  1. {controlzero-1.8.1 → controlzero-1.9.0}/CHANGELOG.md +108 -0
  2. {controlzero-1.8.1 → controlzero-1.9.0}/PKG-INFO +2 -1
  3. {controlzero-1.8.1 → controlzero-1.9.0}/README.md +1 -0
  4. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/__init__.py +1 -1
  5. controlzero-1.9.0/controlzero/cli/hosts/antigravity.py +161 -0
  6. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/cli/hosts/base.py +9 -0
  7. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/cli/hosts/claude_code.py +14 -0
  8. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/cli/kiro_adapter.py +42 -2
  9. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/cli/main.py +422 -7
  10. controlzero-1.9.0/controlzero/cli/templates/antigravity/hooks.json +28 -0
  11. controlzero-1.9.0/controlzero/cli/templates/antigravity.yaml +93 -0
  12. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook +2 -2
  13. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/cli/templates/kiro/ide-pre-tool-use.kiro.hook +2 -2
  14. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/cli/templates/kiro/ide-prompt-submit.kiro.hook +3 -3
  15. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/device.py +24 -3
  16. {controlzero-1.8.1 → controlzero-1.9.0}/pyproject.toml +8 -1
  17. controlzero-1.9.0/tests/test_antigravity_adapter.py +275 -0
  18. controlzero-1.9.0/tests/test_antigravity_hook_check.py +160 -0
  19. controlzero-1.9.0/tests/test_antigravity_install.py +383 -0
  20. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_device.py +54 -1
  21. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_hosts_adapter.py +6 -3
  22. controlzero-1.9.0/tests/test_kiro_hook_templates.py +324 -0
  23. controlzero-1.8.1/controlzero/cli/hosts/antigravity.py +0 -76
  24. {controlzero-1.8.1 → controlzero-1.9.0}/.gitignore +0 -0
  25. {controlzero-1.8.1 → controlzero-1.9.0}/Dockerfile.test +0 -0
  26. {controlzero-1.8.1 → controlzero-1.9.0}/LICENSE +0 -0
  27. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/_internal/__init__.py +0 -0
  28. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/_internal/action_aliases.py +0 -0
  29. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/_internal/action_validator.py +0 -0
  30. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/_internal/bundle.py +0 -0
  31. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/_internal/credential_hook.py +0 -0
  32. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/_internal/credential_scanner.py +0 -0
  33. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/_internal/credentials_data/__init__.py +0 -0
  34. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/_internal/credentials_data/built_in.yaml +0 -0
  35. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/_internal/dlp_scanner.py +0 -0
  36. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/_internal/enforcer.py +0 -0
  37. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/_internal/hook_extractors.py +0 -0
  38. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/_internal/tool_extractors.json +0 -0
  39. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/_internal/types.py +0 -0
  40. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/audit_local.py +0 -0
  41. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/audit_remote.py +0 -0
  42. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/canonical.py +0 -0
  43. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/cli/__init__.py +0 -0
  44. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/cli/_secrets.py +0 -0
  45. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/cli/console.py +0 -0
  46. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/cli/debug_bundle.py +0 -0
  47. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/cli/doctor.py +0 -0
  48. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/cli/hosts/__init__.py +0 -0
  49. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/cli/hosts/codex_cli.py +0 -0
  50. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/cli/hosts/gemini_cli.py +0 -0
  51. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/cli/hosts/kiro.py +0 -0
  52. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/cli/hosts/unknown.py +0 -0
  53. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/cli/migrate.py +0 -0
  54. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/cli/spool_cmd.py +0 -0
  55. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/cli/telemetry_consent.py +0 -0
  56. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/cli/templates/autogen.yaml +0 -0
  57. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/cli/templates/claude-code.yaml +0 -0
  58. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/cli/templates/codex-cli.yaml +0 -0
  59. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/cli/templates/cost-cap.yaml +0 -0
  60. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/cli/templates/crewai.yaml +0 -0
  61. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/cli/templates/cursor.yaml +0 -0
  62. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/cli/templates/gemini-cli.yaml +0 -0
  63. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/cli/templates/generic.yaml +0 -0
  64. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/cli/templates/kiro/kiro.yaml +0 -0
  65. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/cli/templates/langchain.yaml +0 -0
  66. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/cli/templates/mcp.yaml +0 -0
  67. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/cli/templates/rag.yaml +0 -0
  68. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/client.py +0 -0
  69. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/enrollment.py +0 -0
  70. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/error_codes.py +0 -0
  71. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/error_codes.yaml +0 -0
  72. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/errors.py +0 -0
  73. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/hitl/__init__.py +0 -0
  74. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/hitl/grant_protocol.py +0 -0
  75. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/hitl/mock.py +0 -0
  76. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/hitl/pending_approval.py +0 -0
  77. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/hitl/secret_leak_guard.py +0 -0
  78. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/hitl/status.py +0 -0
  79. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/hooks/__init__.py +0 -0
  80. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/hooks/tool_output_handler.py +0 -0
  81. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/hosted_policy.py +0 -0
  82. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/integrations/__init__.py +0 -0
  83. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/integrations/anthropic.py +0 -0
  84. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/integrations/autogen.py +0 -0
  85. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/integrations/braintrust.py +0 -0
  86. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/integrations/crewai/__init__.py +0 -0
  87. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/integrations/crewai/agent.py +0 -0
  88. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/integrations/crewai/crew.py +0 -0
  89. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/integrations/crewai/task.py +0 -0
  90. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/integrations/crewai/tool.py +0 -0
  91. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/integrations/google.py +0 -0
  92. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/integrations/google_adk/__init__.py +0 -0
  93. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/integrations/google_adk/agent.py +0 -0
  94. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/integrations/google_adk/tool.py +0 -0
  95. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/integrations/langchain/__init__.py +0 -0
  96. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/integrations/langchain/agent.py +0 -0
  97. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/integrations/langchain/callbacks.py +0 -0
  98. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/integrations/langchain/chain.py +0 -0
  99. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/integrations/langchain/graph.py +0 -0
  100. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/integrations/langchain/modern.py +0 -0
  101. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/integrations/langchain/tool.py +0 -0
  102. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/integrations/langfuse.py +0 -0
  103. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/integrations/litellm.py +0 -0
  104. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/integrations/openai.py +0 -0
  105. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/integrations/pydantic_ai.py +0 -0
  106. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/integrations/vercel_ai.py +0 -0
  107. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/layout_migration.py +0 -0
  108. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/policy_loader.py +0 -0
  109. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/spool/__init__.py +0 -0
  110. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/spool/_compress.py +0 -0
  111. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/spool/_constants.py +0 -0
  112. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/spool/_crc32c.py +0 -0
  113. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/spool/_crypto.py +0 -0
  114. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/spool/_frame.py +0 -0
  115. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/spool/_metrics.py +0 -0
  116. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/spool/_spool.py +0 -0
  117. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/spool/_state.py +0 -0
  118. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/spool/_uploader.py +0 -0
  119. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/spool/cz-audit-v1.dict +0 -0
  120. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/tamper.py +0 -0
  121. {controlzero-1.8.1 → controlzero-1.9.0}/controlzero/tracecontext.py +0 -0
  122. {controlzero-1.8.1 → controlzero-1.9.0}/examples/hello_world.py +0 -0
  123. {controlzero-1.8.1 → controlzero-1.9.0}/tests/_fixtures/jcs_args_hash_vectors.json +0 -0
  124. {controlzero-1.8.1 → controlzero-1.9.0}/tests/conftest.py +0 -0
  125. {controlzero-1.8.1 → controlzero-1.9.0}/tests/integrations/__init__.py +0 -0
  126. {controlzero-1.8.1 → controlzero-1.9.0}/tests/integrations/test_google.py +0 -0
  127. {controlzero-1.8.1 → controlzero-1.9.0}/tests/parity/action_aliases.json +0 -0
  128. {controlzero-1.8.1 → controlzero-1.9.0}/tests/spool/__init__.py +0 -0
  129. {controlzero-1.8.1 → controlzero-1.9.0}/tests/spool/conftest.py +0 -0
  130. {controlzero-1.8.1 → controlzero-1.9.0}/tests/spool/test_spool_cli.py +0 -0
  131. {controlzero-1.8.1 → controlzero-1.9.0}/tests/spool/test_spool_concurrency.py +0 -0
  132. {controlzero-1.8.1 → controlzero-1.9.0}/tests/spool/test_spool_conformance.py +0 -0
  133. {controlzero-1.8.1 → controlzero-1.9.0}/tests/spool/test_spool_core.py +0 -0
  134. {controlzero-1.8.1 → controlzero-1.9.0}/tests/spool/test_spool_crash.py +0 -0
  135. {controlzero-1.8.1 → controlzero-1.9.0}/tests/spool/test_spool_diskfull.py +0 -0
  136. {controlzero-1.8.1 → controlzero-1.9.0}/tests/spool/test_spool_sink_wiring.py +0 -0
  137. {controlzero-1.8.1 → controlzero-1.9.0}/tests/spool/test_spool_transcript_localack.py +0 -0
  138. {controlzero-1.8.1 → controlzero-1.9.0}/tests/spool/test_spool_uploader.py +0 -0
  139. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_action_aliases.py +0 -0
  140. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_action_canonicalization.py +0 -0
  141. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_action_validator_t86.py +0 -0
  142. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_agent_name_env.py +0 -0
  143. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_api_key_mask.py +0 -0
  144. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_audit_remote.py +0 -0
  145. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_audit_remote_sdk_version.py +0 -0
  146. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_audit_sink_isolation.py +0 -0
  147. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_bundle_parser.py +0 -0
  148. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_bundle_translate.py +0 -0
  149. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_canonical_phase1a.py +0 -0
  150. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_cli_carve_out.py +0 -0
  151. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_cli_debug_bundle.py +0 -0
  152. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_cli_extractor_integration.py +0 -0
  153. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_cli_hook.py +0 -0
  154. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_cli_hosted_refresh.py +0 -0
  155. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_cli_init.py +0 -0
  156. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_cli_init_templates.py +0 -0
  157. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_cli_tail.py +0 -0
  158. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_cli_test.py +0 -0
  159. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_cli_validate.py +0 -0
  160. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_coding_agent_hooks.py +0 -0
  161. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_conditions.py +0 -0
  162. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_conformance.py +0 -0
  163. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_console.py +0 -0
  164. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_credential_hook.py +0 -0
  165. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_default_action.py +0 -0
  166. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_dlp_scanner.py +0 -0
  167. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_doctor.py +0 -0
  168. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_engine_version_consistency.py +0 -0
  169. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_enrollment.py +0 -0
  170. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_env_dump_438.py +0 -0
  171. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_error_codes.py +0 -0
  172. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_errors_e_codes.py +0 -0
  173. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_fail_closed_eval.py +0 -0
  174. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_glob_matching.py +0 -0
  175. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_hitl_5d_email_install.py +0 -0
  176. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_hitl_6a_cli_flag.py +0 -0
  177. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_hitl_6a_exceptions.py +0 -0
  178. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_hitl_6a_get_secret_hitl.py +0 -0
  179. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_hitl_6a_mock_backend.py +0 -0
  180. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_hitl_6a_pending_approval.py +0 -0
  181. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_hitl_6a_request_approval.py +0 -0
  182. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_hitl_6a_secret_leak_guard.py +0 -0
  183. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_hitl_6a_wait.py +0 -0
  184. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_hitl_conformance.py +0 -0
  185. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_hitl_phase2b_protocol.py +0 -0
  186. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_hitl_reason_codes.py +0 -0
  187. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_hitl_validator_keys.py +0 -0
  188. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_hook_extractors.py +0 -0
  189. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_hosted_policy_e2e.py +0 -0
  190. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_hybrid_mode_strict.py +0 -0
  191. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_hybrid_mode_warn.py +0 -0
  192. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_install_hook_command.py +0 -0
  193. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_install_hooks.py +0 -0
  194. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_kiro_adapter.py +0 -0
  195. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_kiro_install.py +0 -0
  196. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_layout_migration_t101.py +0 -0
  197. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_layout_parity_t102.py +0 -0
  198. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_local_mode_dict.py +0 -0
  199. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_local_mode_file_json.py +0 -0
  200. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_local_mode_file_yaml.py +0 -0
  201. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_log_fallback_stderr.py +0 -0
  202. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_log_options_ignored_hosted.py +0 -0
  203. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_log_rotation.py +0 -0
  204. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_migrate.py +0 -0
  205. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_min_sdk_version_gate.py +0 -0
  206. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_multi_client_per_project_175.py +0 -0
  207. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_no_policy_no_key.py +0 -0
  208. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_package_rename_shim.py +0 -0
  209. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_policy_engine_version_phase1b.py +0 -0
  210. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_policy_freshness.py +0 -0
  211. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_policy_settings.py +0 -0
  212. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_policy_source_audit.py +0 -0
  213. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_quarantine.py +0 -0
  214. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_reason_code.py +0 -0
  215. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_refresh.py +0 -0
  216. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_secrets.py +0 -0
  217. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_sql_semantic_class.py +0 -0
  218. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_synthetic_policy_id_t79.py +0 -0
  219. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_t103_precedence.py +0 -0
  220. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_t104_cache_gc.py +0 -0
  221. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_t108_local_override_audit.py +0 -0
  222. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_t96_single_audit_log.py +0 -0
  223. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_t99_install_prefetch_bundle.py +0 -0
  224. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_tamper.py +0 -0
  225. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_tamper_behavior.py +0 -0
  226. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_tamper_hook.py +0 -0
  227. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_telemetry_consent.py +0 -0
  228. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_tracecontext.py +0 -0
  229. {controlzero-1.8.1 → controlzero-1.9.0}/tests/test_unsafe_int_boundary.py +0 -0
  230. {controlzero-1.8.1 → controlzero-1.9.0}/tools/cz-kiro-adapter +0 -0
@@ -1,5 +1,113 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.0 -- 2026-06-15 (Antigravity install CLI, epic gh#925)
4
+
5
+ ### Added
6
+
7
+ - **Google Antigravity (`agy`) governance -- BETA.** The `controlzero install
8
+ antigravity` / `uninstall antigravity` commands complete the Antigravity
9
+ integration on top of the host adapter that shipped in 1.8.0 (gh#927). Both
10
+ Antigravity surfaces (the IDE and the `agy` CLI) deliver the hook payload as
11
+ JSON on stdin, so they route directly through the shared
12
+ `controlzero hook-check` -> policy engine -> audit path with no shim, and
13
+ audit rows are attributed to `agent="antigravity"`.
14
+
15
+ - **Real EXTERNAL hooks.json contract.** Wires the `PreToolUse` (deciding
16
+ gate) + `PostToolUse` (observe) file-based events -- stdin JSON in, stdout
17
+ JSON out (camelCase), NOT exit-code based -- which is the contract the agy
18
+ IDE/CLI actually executes. (The in-process Python-SDK hook *class* names
19
+ `PreToolCallDecideHook` / `PostToolCallHook` are a different API and would
20
+ never fire from hooks.json.) The adapter ALWAYS emits an explicit
21
+ `{"decision": ...}`: an empty `{}` is read by agy as `invalid_args` and
22
+ DENIES the tool (cmux#5358), so silence-that-prints-`{}` is impossible.
23
+
24
+ - **`controlzero install antigravity`** (gh#928). Merges a `PreToolUse` +
25
+ `PostToolUse` block (broad `*` matcher) into the Antigravity `hooks.json`.
26
+ Writes the engine-EXECUTED path `~/.gemini/config/hooks.json` by default
27
+ and, for the user scope, also mirrors to the `agy` TUI `/hooks` display
28
+ path `~/.gemini/antigravity-cli/hooks.json` (antigravity-cli#49) so the
29
+ hook both fires AND displays; `<cwd>/.agents/hooks.json` with `--project`,
30
+ or a custom path with `--settings`. Idempotent (re-installs collapse to
31
+ one entry; byte-identical state), preserves pre-existing third-party hooks
32
+ (incl. ones sharing a block) and unrelated keys, and writes the
33
+ `~/.controlzero/policy.yaml` Antigravity template. `uninstall antigravity`
34
+ removes only the Control Zero entries (cleaning both paths) and preserves
35
+ your policy + audit log. Supports `--force` / `--merge` and `--api-key` /
36
+ `--email` for dashboard sync.
37
+
38
+ - **Human-in-the-loop on the wire.** A Control Zero approval gate (a deny
39
+ flagged `requires_approval`) maps to Antigravity's `ask` decision by
40
+ default. A plain `ask` can be auto-satisfied by a cached "Always Allow",
41
+ so set `CZ_ANTIGRAVITY_HITL_DECISION=force_ask` (a guaranteed prompt, once
42
+ verified on your pinned agy build) or `=deny` for a hard block. An
43
+ unrecognized value safely falls back to `ask`.
44
+
45
+ - **Agent fingerprint.** `detect_client_name()` now recognises Antigravity
46
+ via the `ANTIGRAVITY_` / `AGY_` env prefixes (checked BEFORE the Gemini CLI
47
+ branch, since Antigravity shares the `~/.gemini/` config lineage and may
48
+ also carry `GEMINI_*` vars). A stray `GEMINI_API_KEY` is never mislabeled
49
+ as Antigravity.
50
+
51
+ - **Docs.** New `docs/integrations/antigravity.md` install guide alongside
52
+ the existing `antigravity-hook-payloads.md` reference; `antigravity` added
53
+ to the SDK README template list, tagged **BETA**.
54
+
55
+ ## 1.8.2 -- 2026-06-15 (Kiro IDE hook contract hotfix, epic gh#877)
56
+
57
+ ### Fixed
58
+
59
+ The shipped Kiro IDE `.kiro.hook` templates used several **Kiro CLI** schema
60
+ conventions instead of the **Kiro IDE** ones (the two hook systems have
61
+ different event names and field keys). The most visible symptom was a
62
+ customer-reported P0; auditing the templates against Kiro's IDE schema
63
+ surfaced three more latent defects. All four are fixed, and a new regression
64
+ test (`tests/test_kiro_hook_templates.py`) pins each so they cannot ship again.
65
+
66
+ - **(P0) Prompt-submit hook rejected by the IDE.** `ide-prompt-submit.kiro.hook`
67
+ declared `"when": {"type": "userPromptSubmit"}`. `userPromptSubmit` is the
68
+ Kiro **CLI** event name; the IDE event is **`promptSubmit`**. The invalid
69
+ value made the IDE reject the entire hook file ("Hook file has invalid data
70
+ structure"), so the only active data-carrying IDE hook (prompt-submit, while
71
+ preToolUse ships disabled) never loaded and the IDE integration silently did
72
+ nothing. Now `promptSubmit`, with the matching `cz-kiro-adapter promptSubmit`
73
+ command. (The shim already accepts both spellings, so no behavior change.)
74
+
75
+ - **Pre-tool-use tool filter used the wrong key.** `ide-pre-tool-use.kiro.hook`
76
+ filtered tools with `"patterns": ["*"]`; the Kiro IDE tool filter key for
77
+ `preToolUse`/`postToolUse` is **`toolTypes`** (array). Now `"toolTypes":
78
+ ["*"]`, so the hook is schema-correct if/when it is enabled (it still ships
79
+ `enabled: false` pending the upstream empty-payload bug).
80
+
81
+ - **File-save hook passed a tool-event arg + could block saves.**
82
+ `ide-file-save.kiro.hook` ran `cz-kiro-adapter postToolUse`, but a
83
+ `fileEdited` runCommand does not deliver tool context; the adapter would then
84
+ fail closed (block) on the empty payload -- i.e. it could block file saves.
85
+ The command is now `cz-kiro-adapter fileEdited`, and the shim treats file
86
+ events as a post-write **observation**: it synthesizes a `fileSave` tool
87
+ call from whatever `USER_PROMPT` carries, still runs `hook-check` for the
88
+ audit row + secret scan, but **always exits 0** on a file event -- a deny is
89
+ surfaced on stderr as a warning rather than blocking. The file is already
90
+ written, so blocking is pointless; only tool events (preToolUse) gate.
91
+ `when.type` stays the valid `fileEdited` with `patterns`.
92
+
93
+ - **Timeouts were ~83 minutes, not 5 seconds.** All three templates used
94
+ `"timeout": 5000`. Kiro IDE `then.timeout` is in **seconds** (default 60,
95
+ `0` disables), so `5000` meant ~83 minutes. Now `5` (seconds) on all three.
96
+
97
+ The regression test parses every shipped `*.kiro.hook` and asserts: valid
98
+ JSON; `when.type` in Kiro's 11-value IDE event set; tool events use
99
+ `toolTypes` (not `patterns`) and file events use `patterns` (not `toolTypes`);
100
+ `then.timeout` is a small integer (seconds); and the `runCommand` event
101
+ argument is one the `cz-kiro-adapter` shim recognizes.
102
+
103
+ ### Known upstream Kiro limits (not fixed here -- tracked upstream)
104
+
105
+ - IDE `preToolUse` delivers `USER_PROMPT={}` (no tool context): kirodotdev/Kiro
106
+ #7375 / #7408 / #7500. Our pre-tool-use IDE hook stays disabled.
107
+ - IDE `postToolUse` delivers `toolArgs: {}` (kirodotdev/Kiro #6188), so
108
+ argument-level post-tool policy is bounded on the IDE surface regardless of
109
+ schema correctness.
110
+
3
111
  ## 1.8.0 -- 2026-06-15 (Kiro, epic gh#877)
4
112
 
5
113
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: controlzero
3
- Version: 1.8.1
3
+ Version: 1.9.0
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
@@ -130,6 +130,7 @@ Templates available (`controlzero init -t <name>`):
130
130
  - `codex-cli` — Codex CLI hook starter
131
131
  - `gemini-cli` — Gemini CLI hook starter
132
132
  - `kiro` — Kiro (AWS IDE) hook starter (Beta)
133
+ - `antigravity` — Google Antigravity (IDE + `agy` CLI) hook starter (Beta)
133
134
 
134
135
  ## Loading a policy
135
136
 
@@ -76,6 +76,7 @@ Templates available (`controlzero init -t <name>`):
76
76
  - `codex-cli` — Codex CLI hook starter
77
77
  - `gemini-cli` — Gemini CLI hook starter
78
78
  - `kiro` — Kiro (AWS IDE) hook starter (Beta)
79
+ - `antigravity` — Google Antigravity (IDE + `agy` CLI) hook starter (Beta)
79
80
 
80
81
  ## Loading a policy
81
82
 
@@ -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.8.1"
43
+ __version__ = "1.9.0"
44
44
 
45
45
  __all__ = [
46
46
  "Client",
@@ -0,0 +1,161 @@
1
+ """Google Antigravity (agy) external file-based hook adapter.
2
+
3
+ Antigravity's EXTERNAL hook contract (the one wired through ``hooks.json``
4
+ read by the IDE and the ``agy`` CLI) is distinct from the in-process
5
+ Python-SDK hook *classes* (``PreToolCallDecideHook`` / ``PostToolCallHook``,
6
+ registered via ``HookRunner``). Control Zero hooks via the FILE contract, so
7
+ this adapter speaks that protocol -- not the SDK class names.
8
+
9
+ External contract (verified against antigravity.google/docs/hooks and four
10
+ real integrators -- antigravity-cli#49, cmux#5358, munder-difflin#52/#54,
11
+ danicat.dev):
12
+
13
+ * Events: ``PreToolUse`` (the deciding gate -- CAN deny), ``PostToolUse``
14
+ (observe only), plus ``PreInvocation`` / ``PostInvocation`` / ``Stop``
15
+ (model-call lifecycle; ``PreInvocation`` cannot deny). Control Zero's
16
+ governance gate is ``PreToolUse``; the post scan is ``PostToolUse``.
17
+ There is NO prompt-submit / UserPromptSubmit event.
18
+ * Delivery: stdin = JSON in, stdout = JSON out, camelCase. NOT exit-code
19
+ based. (A crashing / non-zero / timed-out hook is read as a deny --
20
+ fail-closed -- so the renderer must never raise.)
21
+ * PreToolUse INPUT: ``{toolCall:{name,args}, stepIdx, conversationId,
22
+ workspacePaths[], transcriptPath}`` (``artifactDirectoryPath`` may also
23
+ appear). ``normalize_payload`` flattens ``toolCall`` -> ``tool_name`` /
24
+ ``tool_input`` so the shared extractor + globs work unchanged.
25
+ * PreToolUse OUTPUT: ``{"decision": "allow"|"deny"|"ask", "reason": "..."}``.
26
+ CRITICAL FAIL-CLOSED LANDMINE (cmux#5358): an empty / decision-less object
27
+ ``{}`` on stdout is read by agy as ``invalid_args`` and DENIES the tool.
28
+ We therefore ALWAYS emit an explicit ``{"decision": ...}`` -- the allow
29
+ path is ``{"decision":"allow"}``, never silence-that-still-prints-{}.
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).
39
+
40
+ Subagent tool calls fire the parent ``PreToolUse`` hook, so one adapter
41
+ governs parent + subagents.
42
+
43
+ Reference (pinned): see docs/integrations/antigravity-hook-payloads.md
44
+ """
45
+
46
+ from __future__ import annotations
47
+
48
+ import os
49
+ from typing import Mapping
50
+
51
+ from controlzero.cli.hosts.base import CZDecision, HostAdapter
52
+
53
+ # File-based EVENT names (NOT the in-process SDK class names). PreToolUse is
54
+ # the deciding gate; PostToolUse is observe-only.
55
+ _ANTIGRAVITY_HOOK_EVENTS = frozenset(
56
+ {"PreToolUse", "PostToolUse", "PreInvocation", "PostInvocation", "Stop"}
57
+ )
58
+ # Events SAFE to claim by name alone. PreToolUse / PostToolUse / Stop collide
59
+ # with Claude Code's PascalCase event names, so a payload carrying ONLY one of
60
+ # those (no toolCall envelope, no env marker) is ambiguous and must fall to
61
+ # Claude Code (which sits ahead in the registry). PreInvocation / PostInvocation
62
+ # are Antigravity-distinct -- no other host emits them -- so they are safe
63
+ # disambiguators. Antigravity's unambiguous signal is the ``toolCall`` envelope,
64
+ # which the claim below keys on first.
65
+ _ANTIGRAVITY_CLAIM_EVENTS = frozenset({"PreInvocation", "PostInvocation"})
66
+ _CLIENT_ALIASES = frozenset({"antigravity", "agy", "antigravity-cli"})
67
+ _ENV_PREFIXES = ("ANTIGRAVITY_", "AGY_")
68
+
69
+ # Source-confirmed decision tokens for PreToolUse output.
70
+ _VALID_DECISIONS = frozenset({"allow", "deny", "ask", "force_ask"})
71
+ _DEFAULT_HITL_DECISION = "ask"
72
+
73
+
74
+ def _resolve_hitl_decision(env: Mapping[str, str] | None = None) -> str:
75
+ """Resolve the decision token used for a HITL approval gate.
76
+
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.
82
+ """
83
+ src = env if env is not None else os.environ
84
+ raw = (src.get("CZ_ANTIGRAVITY_HITL_DECISION") or "").strip().lower()
85
+ return raw if raw in _VALID_DECISIONS else _DEFAULT_HITL_DECISION
86
+
87
+
88
+ class AntigravityAdapter(HostAdapter):
89
+ name = "antigravity"
90
+ canonical_source = "antigravity"
91
+ # Antigravity decides from the stdout JSON, NOT the exit code. A non-zero
92
+ # exit is read as a fail-closed deny that would override an ``ask`` /
93
+ # ``allow`` verdict (turning a HITL prompt into a hard block), so the
94
+ # hook-check command must exit 0 for this host and let the JSON decide.
95
+ decision_via_exit_code = False
96
+
97
+ def claim(self, payload: dict, env: Mapping[str, str]) -> bool:
98
+ if (env.get("CONTROLZERO_CLIENT") or "").strip().lower() in _CLIENT_ALIASES:
99
+ return True
100
+ for key in env:
101
+ if key.upper().startswith(_ENV_PREFIXES):
102
+ return True
103
+ if isinstance(payload, dict):
104
+ # Unambiguous Antigravity signal: the toolCall envelope.
105
+ if "toolCall" in payload:
106
+ return True
107
+ # Only the Antigravity-distinct lifecycle events are safe to claim
108
+ # by name; PreToolUse / PostToolUse / Stop collide with Claude Code
109
+ # and are disambiguated by toolCall / env instead.
110
+ if payload.get("hook_event_name") in _ANTIGRAVITY_CLAIM_EVENTS:
111
+ return True
112
+ return False
113
+
114
+ def normalize_payload(self, payload: dict) -> dict:
115
+ if not isinstance(payload, dict):
116
+ return payload
117
+ tool_call = payload.get("toolCall")
118
+ if not isinstance(tool_call, dict):
119
+ return payload
120
+ normalized = dict(payload) # preserves conversationId / workspacePaths
121
+ name = tool_call.get("name")
122
+ args = tool_call.get("args")
123
+ if name is not None and not normalized.get("tool_name"):
124
+ normalized["tool_name"] = name
125
+ if args is not None and not normalized.get("tool_input"):
126
+ normalized["tool_input"] = args if isinstance(args, dict) else {}
127
+ return normalized
128
+
129
+ def render(self, decision: CZDecision) -> dict:
130
+ """Render to Antigravity's PreToolUse stdout-JSON contract.
131
+
132
+ ALWAYS emits an explicit ``decision`` key. An empty / decision-less
133
+ object would be read by agy as ``invalid_args`` and DENY the tool
134
+ (cmux#5358), so silence-that-still-prints-{} is never produced here.
135
+ camelCase per the external contract; Control Zero diagnostic
136
+ metadata is namespaced under ``controlzero`` so it cannot collide
137
+ with agy's reserved output keys.
138
+ """
139
+ if decision.is_hitl:
140
+ verdict = _resolve_hitl_decision()
141
+ elif decision.is_deny:
142
+ verdict = "deny"
143
+ else:
144
+ verdict = "allow"
145
+
146
+ envelope: dict = {
147
+ "decision": verdict,
148
+ "reason": decision.reason,
149
+ # Diagnostic metadata, namespaced so it never shadows a reserved
150
+ # agy key (decision / reason / permissionOverrides).
151
+ "controlzero": {
152
+ "reasonCode": decision.reason_code,
153
+ "tool": decision.tool,
154
+ "extractedMethod": decision.method,
155
+ "action": decision.extracted_action,
156
+ "actionSemanticClass": decision.semantic_class,
157
+ "tamperDetected": decision.tamper_detected,
158
+ "auditChainBroken": decision.audit_chain_broken,
159
+ },
160
+ }
161
+ return envelope
@@ -127,6 +127,15 @@ class HostAdapter:
127
127
  #: Default is ``"unknown"`` -- subclasses MUST override.
128
128
  canonical_source: str = "unknown"
129
129
 
130
+ #: Whether the host signals a deny via the PROCESS EXIT CODE
131
+ #: (non-zero = block, the Claude Code / Gemini / Codex / Kiro
132
+ #: convention) or purely via the stdout JSON ``decision`` field.
133
+ #: Antigravity is stdout-JSON based: a non-zero exit is read as a
134
+ #: fail-closed deny that would OVERRIDE an ``ask``/``allow`` JSON
135
+ #: verdict, so its hook must exit 0 and let the JSON decide. The
136
+ #: hook-check command consults this to pick the deny/HITL exit code.
137
+ decision_via_exit_code: bool = True
138
+
130
139
  # -- detection --------------------------------------------------
131
140
 
132
141
  def claim(self, payload: dict, env: Mapping[str, str]) -> bool:
@@ -61,6 +61,20 @@ class ClaudeCodeAdapter(HostAdapter):
61
61
  ):
62
62
  return True
63
63
 
64
+ # Disambiguation: Google Antigravity's external hooks use the SAME
65
+ # PascalCase event names (PreToolUse / PostToolUse / Stop) but pack
66
+ # the call under a top-level ``toolCall`` envelope, which Claude Code
67
+ # never sends. If ``toolCall`` is present this is an Antigravity
68
+ # payload -- do NOT claim it (let the AntigravityAdapter claim it).
69
+ # This runs BEFORE the Anthropic env-hint check: an Antigravity hook
70
+ # subprocess can inherit CLAUDECODE / CLAUDE_CODE from a Claude-spawned
71
+ # parent shell, and claiming on that stale env hint would emit Claude's
72
+ # approve/block tokens -- invalid for Antigravity, read as a deny.
73
+ # Only the explicit CONTROLZERO_CLIENT=claude_code override above
74
+ # outranks the toolCall signal.
75
+ if isinstance(payload, dict) and "toolCall" in payload:
76
+ return False
77
+
64
78
  # Env-var hint: any of the Anthropic-family vars present.
65
79
  for hint in self._ENV_HINTS:
66
80
  if env.get(hint):
@@ -7,8 +7,9 @@ on stdin like the Kiro CLI. This shim normalises that into the snake_case JSON
7
7
  shape ``controlzero hook-check`` consumes, marks the surface
8
8
  (``CZ_KIRO_SURFACE=ide`` so KiroIDEAdapter claims it), and pipes it through.
9
9
 
10
- Wire it as the IDE's preToolUse / postToolUse / userPromptSubmit hook command,
11
- e.g.::
10
+ Wire it as the IDE's preToolUse / promptSubmit / fileEdited hook command (the
11
+ IDE `when.type` event names -- note prompt-submit is ``promptSubmit`` on the
12
+ IDE surface, NOT the Kiro CLI's ``userPromptSubmit``), e.g.::
12
13
 
13
14
  cz-kiro-adapter preToolUse
14
15
 
@@ -37,6 +38,12 @@ import subprocess
37
38
  import sys
38
39
 
39
40
 
41
+ # Kiro IDE file events. These are a post-write OBSERVATION surface (scan the
42
+ # saved file / audit it), never a gate, so the adapter must not fail closed on
43
+ # them -- a file save can't be blocked by missing context.
44
+ _FILE_EVENTS = frozenset({"fileEdited", "fileCreated", "fileDeleted"})
45
+
46
+
40
47
  def _event_from_argv(argv: list[str]) -> str:
41
48
  # First non-flag arg is the event name; default to preToolUse.
42
49
  for a in argv[1:]:
@@ -66,6 +73,25 @@ def _normalize(event: str, raw: str) -> dict | None:
66
73
  base["tool_input"] = {"prompt": raw}
67
74
  return base
68
75
 
76
+ if event in _FILE_EVENTS:
77
+ # File events are a POST-WRITE observation, not a gate: a save must
78
+ # never be blocked by missing context, so this branch NEVER returns
79
+ # None (no fail-closed). Kiro IDE does not document a guaranteed
80
+ # structured payload for a file-event runCommand (USER_PROMPT may be a
81
+ # path, a JSON object, or empty), so we synthesize a write-class tool
82
+ # call from whatever arrived and let hook-check scan/audit it.
83
+ base["tool_name"] = "fileSave"
84
+ payload: dict = {}
85
+ text = (raw or "").strip()
86
+ if text:
87
+ try:
88
+ parsed = json.loads(text)
89
+ payload = parsed if isinstance(parsed, dict) else {"raw": text}
90
+ except json.JSONDecodeError:
91
+ payload = {"file_path": text} # most likely a bare path
92
+ base["tool_input"] = payload
93
+ return base
94
+
69
95
  raw = (raw or "").strip()
70
96
  if not raw:
71
97
  return None
@@ -122,6 +148,20 @@ def main(argv: list[str] | None = None) -> int:
122
148
  # controlzero not on PATH: never brick the IDE.
123
149
  print("controlzero(kiro): controlzero CLI not found; allowing", file=sys.stderr)
124
150
  return 0
151
+
152
+ if event in _FILE_EVENTS:
153
+ # OBSERVE-ONLY: a file event is post-write -- the file is already
154
+ # saved, so blocking is pointless and only disrupts the editor. We
155
+ # still run hook-check above for the audit row + secret scan, but a
156
+ # deny is surfaced as a warning and the save is allowed (exit 0).
157
+ if proc.returncode == 2:
158
+ print(
159
+ "controlzero(kiro): file-save policy flagged this write "
160
+ "(post-write, observe-only; not blocking the save)",
161
+ file=sys.stderr,
162
+ )
163
+ return 0
164
+
125
165
  return proc.returncode
126
166
 
127
167