controlzero 1.10.1__tar.gz → 1.11.5__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 (268) hide show
  1. {controlzero-1.10.1 → controlzero-1.11.5}/CHANGELOG.md +157 -1
  2. {controlzero-1.10.1 → controlzero-1.11.5}/PKG-INFO +2 -2
  3. {controlzero-1.10.1 → controlzero-1.11.5}/README.md +1 -1
  4. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/__init__.py +5 -1
  5. controlzero-1.11.5/controlzero/__main__.py +12 -0
  6. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/_internal/bundle.py +99 -6
  7. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/doctor.py +3 -1
  8. controlzero-1.11.5/controlzero/cli/exec_cmd.py +90 -0
  9. controlzero-1.11.5/controlzero/cli/hook_entry.py +118 -0
  10. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/hosts/__init__.py +18 -1
  11. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/hosts/antigravity.py +92 -4
  12. controlzero-1.11.5/controlzero/cli/hosts/base.py +532 -0
  13. controlzero-1.11.5/controlzero/cli/hosts/claude_code.py +191 -0
  14. controlzero-1.11.5/controlzero/cli/hosts/codex_cli.py +101 -0
  15. controlzero-1.11.5/controlzero/cli/hosts/cursor.py +322 -0
  16. controlzero-1.11.5/controlzero/cli/hosts/cursor_cli.py +223 -0
  17. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/hosts/gemini_cli.py +27 -7
  18. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/hosts/kiro.py +53 -3
  19. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/hosts/unknown.py +18 -2
  20. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/main.py +746 -47
  21. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/client.py +51 -0
  22. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/error_codes.yaml +22 -3
  23. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/hosted_policy.py +84 -2
  24. controlzero-1.11.5/controlzero/secrets/__init__.py +54 -0
  25. controlzero-1.11.5/controlzero/secrets/reference.py +127 -0
  26. controlzero-1.11.5/controlzero/secrets/resolver.py +156 -0
  27. controlzero-1.11.5/controlzero/secrets/secretstr.py +72 -0
  28. {controlzero-1.10.1 → controlzero-1.11.5}/pyproject.toml +8 -2
  29. controlzero-1.11.5/tests/_fixtures/antigravity-envelope-expected.json +146 -0
  30. controlzero-1.11.5/tests/test_adapter_interface.py +384 -0
  31. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_antigravity_adapter.py +70 -11
  32. controlzero-1.11.5/tests/test_antigravity_envelope_parity.py +96 -0
  33. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_antigravity_ga_blockers_1248.py +3 -3
  34. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_antigravity_install.py +48 -0
  35. controlzero-1.11.5/tests/test_antigravity_posttooluse_observe_58.py +295 -0
  36. controlzero-1.11.5/tests/test_bundle_cache_freshness_1303.py +303 -0
  37. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_cli_hook.py +15 -3
  38. controlzero-1.11.5/tests/test_cli_hook_entry.py +81 -0
  39. controlzero-1.11.5/tests/test_cursor_adapter.py +362 -0
  40. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_device.py +25 -38
  41. controlzero-1.11.5/tests/test_hosted_local_shadow_warn_1265.py +265 -0
  42. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hosts_adapter.py +207 -33
  43. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_install_hooks.py +24 -11
  44. controlzero-1.11.5/tests/test_kiro_cli_hook_pin_1265.py +121 -0
  45. controlzero-1.11.5/tests/test_part3_active_policy_count_1303.py +142 -0
  46. controlzero-1.11.5/tests/test_policy_load_failclosed_f3.py +279 -0
  47. controlzero-1.11.5/tests/test_secrets_reference_resolver.py +192 -0
  48. controlzero-1.10.1/controlzero/cli/hosts/base.py +0 -171
  49. controlzero-1.10.1/controlzero/cli/hosts/claude_code.py +0 -135
  50. controlzero-1.10.1/controlzero/cli/hosts/codex_cli.py +0 -54
  51. {controlzero-1.10.1 → controlzero-1.11.5}/.gitignore +0 -0
  52. {controlzero-1.10.1 → controlzero-1.11.5}/Dockerfile.test +0 -0
  53. {controlzero-1.10.1 → controlzero-1.11.5}/LICENSE +0 -0
  54. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/_internal/__init__.py +0 -0
  55. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/_internal/action_aliases.py +0 -0
  56. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/_internal/action_validator.py +0 -0
  57. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/_internal/credential_hook.py +0 -0
  58. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/_internal/credential_scanner.py +0 -0
  59. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/_internal/credentials_data/__init__.py +0 -0
  60. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/_internal/credentials_data/built_in.yaml +0 -0
  61. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/_internal/dlp_scanner.py +0 -0
  62. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/_internal/enforcer.py +0 -0
  63. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/_internal/hook_extractors.py +0 -0
  64. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/_internal/tool_extractors.json +0 -0
  65. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/_internal/types.py +0 -0
  66. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/audit_local.py +0 -0
  67. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/audit_remote.py +0 -0
  68. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/canonical.py +0 -0
  69. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/__init__.py +0 -0
  70. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/__main__.py +0 -0
  71. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/_secrets.py +0 -0
  72. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/console.py +0 -0
  73. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/debug_bundle.py +0 -0
  74. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/kiro_adapter.py +0 -0
  75. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/migrate.py +0 -0
  76. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/spool_cmd.py +0 -0
  77. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/telemetry_consent.py +0 -0
  78. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/antigravity/hooks.json +0 -0
  79. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/antigravity.yaml +0 -0
  80. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/autogen.yaml +0 -0
  81. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/claude-code.yaml +0 -0
  82. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/codex-cli.yaml +0 -0
  83. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/cost-cap.yaml +0 -0
  84. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/crewai.yaml +0 -0
  85. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/cursor.yaml +0 -0
  86. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/gemini-cli.yaml +0 -0
  87. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/generic.yaml +0 -0
  88. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook +0 -0
  89. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/kiro/ide-pre-tool-use.kiro.hook +0 -0
  90. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/kiro/ide-prompt-submit.kiro.hook +0 -0
  91. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/kiro/kiro.yaml +0 -0
  92. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/langchain.yaml +0 -0
  93. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/mcp.yaml +0 -0
  94. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/rag.yaml +0 -0
  95. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/update_cmd.py +0 -0
  96. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/device.py +0 -0
  97. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/enrollment.py +0 -0
  98. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/error_codes.py +0 -0
  99. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/errors.py +0 -0
  100. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/hitl/__init__.py +0 -0
  101. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/hitl/grant_protocol.py +0 -0
  102. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/hitl/mock.py +0 -0
  103. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/hitl/pending_approval.py +0 -0
  104. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/hitl/secret_leak_guard.py +0 -0
  105. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/hitl/status.py +0 -0
  106. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/hooks/__init__.py +0 -0
  107. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/hooks/tool_output_handler.py +0 -0
  108. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/__init__.py +0 -0
  109. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/anthropic.py +0 -0
  110. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/autogen.py +0 -0
  111. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/braintrust.py +0 -0
  112. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/crewai/__init__.py +0 -0
  113. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/crewai/agent.py +0 -0
  114. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/crewai/crew.py +0 -0
  115. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/crewai/task.py +0 -0
  116. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/crewai/tool.py +0 -0
  117. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/google.py +0 -0
  118. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/google_adk/__init__.py +0 -0
  119. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/google_adk/agent.py +0 -0
  120. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/google_adk/tool.py +0 -0
  121. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/langchain/__init__.py +0 -0
  122. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/langchain/agent.py +0 -0
  123. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/langchain/callbacks.py +0 -0
  124. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/langchain/chain.py +0 -0
  125. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/langchain/graph.py +0 -0
  126. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/langchain/modern.py +0 -0
  127. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/langchain/tool.py +0 -0
  128. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/langfuse.py +0 -0
  129. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/litellm.py +0 -0
  130. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/openai.py +0 -0
  131. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/pydantic_ai.py +0 -0
  132. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/vercel_ai.py +0 -0
  133. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/layout_migration.py +0 -0
  134. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/policy_loader.py +0 -0
  135. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/spool/__init__.py +0 -0
  136. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/spool/_compress.py +0 -0
  137. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/spool/_constants.py +0 -0
  138. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/spool/_crc32c.py +0 -0
  139. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/spool/_crypto.py +0 -0
  140. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/spool/_frame.py +0 -0
  141. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/spool/_keyring.py +0 -0
  142. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/spool/_metrics.py +0 -0
  143. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/spool/_spool.py +0 -0
  144. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/spool/_state.py +0 -0
  145. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/spool/_uploader.py +0 -0
  146. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/spool/cz-audit-v1.dict +0 -0
  147. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/tamper.py +0 -0
  148. {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/tracecontext.py +0 -0
  149. {controlzero-1.10.1 → controlzero-1.11.5}/examples/hello_world.py +0 -0
  150. {controlzero-1.10.1 → controlzero-1.11.5}/tests/_fixtures/jcs_args_hash_vectors.json +0 -0
  151. {controlzero-1.10.1 → controlzero-1.11.5}/tests/conftest.py +0 -0
  152. {controlzero-1.10.1 → controlzero-1.11.5}/tests/integrations/__init__.py +0 -0
  153. {controlzero-1.10.1 → controlzero-1.11.5}/tests/integrations/test_google.py +0 -0
  154. {controlzero-1.10.1 → controlzero-1.11.5}/tests/parity/action_aliases.json +0 -0
  155. {controlzero-1.10.1 → controlzero-1.11.5}/tests/spool/__init__.py +0 -0
  156. {controlzero-1.10.1 → controlzero-1.11.5}/tests/spool/conftest.py +0 -0
  157. {controlzero-1.10.1 → controlzero-1.11.5}/tests/spool/test_spool_cli.py +0 -0
  158. {controlzero-1.10.1 → controlzero-1.11.5}/tests/spool/test_spool_concurrency.py +0 -0
  159. {controlzero-1.10.1 → controlzero-1.11.5}/tests/spool/test_spool_conformance.py +0 -0
  160. {controlzero-1.10.1 → controlzero-1.11.5}/tests/spool/test_spool_core.py +0 -0
  161. {controlzero-1.10.1 → controlzero-1.11.5}/tests/spool/test_spool_crash.py +0 -0
  162. {controlzero-1.10.1 → controlzero-1.11.5}/tests/spool/test_spool_diskfull.py +0 -0
  163. {controlzero-1.10.1 → controlzero-1.11.5}/tests/spool/test_spool_durable_default_tamper.py +0 -0
  164. {controlzero-1.10.1 → controlzero-1.11.5}/tests/spool/test_spool_keychain_dek.py +0 -0
  165. {controlzero-1.10.1 → controlzero-1.11.5}/tests/spool/test_spool_sink_wiring.py +0 -0
  166. {controlzero-1.10.1 → controlzero-1.11.5}/tests/spool/test_spool_transcript_localack.py +0 -0
  167. {controlzero-1.10.1 → controlzero-1.11.5}/tests/spool/test_spool_uploader.py +0 -0
  168. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_action_aliases.py +0 -0
  169. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_action_canonicalization.py +0 -0
  170. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_action_validator_t86.py +0 -0
  171. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_agent_name_env.py +0 -0
  172. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_antigravity_hook_check.py +0 -0
  173. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_antigravity_tool_vocab_1303.py +0 -0
  174. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_api_key_mask.py +0 -0
  175. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_audit_remote.py +0 -0
  176. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_audit_remote_sdk_version.py +0 -0
  177. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_audit_sink_isolation.py +0 -0
  178. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_bundle_parser.py +0 -0
  179. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_bundle_translate.py +0 -0
  180. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_canonical_phase1a.py +0 -0
  181. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_cli_carve_out.py +0 -0
  182. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_cli_debug_bundle.py +0 -0
  183. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_cli_extractor_integration.py +0 -0
  184. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_cli_hosted_refresh.py +0 -0
  185. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_cli_init.py +0 -0
  186. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_cli_init_templates.py +0 -0
  187. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_cli_tail.py +0 -0
  188. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_cli_test.py +0 -0
  189. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_cli_validate.py +0 -0
  190. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_coding_agent_hooks.py +0 -0
  191. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_conditions.py +0 -0
  192. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_config_format_parity_1303.py +0 -0
  193. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_conformance.py +0 -0
  194. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_console.py +0 -0
  195. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_credential_hook.py +0 -0
  196. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_default_action.py +0 -0
  197. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_dlp_scanner.py +0 -0
  198. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_doctor.py +0 -0
  199. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_engine_version_consistency.py +0 -0
  200. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_enrollment.py +0 -0
  201. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_env_dump_438.py +0 -0
  202. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_epic_1247_bryan_acceptance.py +0 -0
  203. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_error_codes.py +0 -0
  204. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_errors_e_codes.py +0 -0
  205. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_fail_closed_eval.py +0 -0
  206. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_failopen_1303.py +0 -0
  207. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_glob_matching.py +0 -0
  208. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hitl_5d_email_install.py +0 -0
  209. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hitl_6a_cli_flag.py +0 -0
  210. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hitl_6a_exceptions.py +0 -0
  211. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hitl_6a_get_secret_hitl.py +0 -0
  212. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hitl_6a_mock_backend.py +0 -0
  213. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hitl_6a_pending_approval.py +0 -0
  214. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hitl_6a_request_approval.py +0 -0
  215. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hitl_6a_secret_leak_guard.py +0 -0
  216. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hitl_6a_wait.py +0 -0
  217. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hitl_conformance.py +0 -0
  218. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hitl_phase2b_protocol.py +0 -0
  219. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hitl_reason_codes.py +0 -0
  220. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hitl_validator_keys.py +0 -0
  221. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hook_extractors.py +0 -0
  222. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hosted_local_audit_1247.py +0 -0
  223. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hosted_policy_e2e.py +0 -0
  224. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hybrid_mode_strict.py +0 -0
  225. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hybrid_mode_warn.py +0 -0
  226. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_install_hook_command.py +0 -0
  227. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_kiro_adapter.py +0 -0
  228. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_kiro_cli_e2e.py +0 -0
  229. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_kiro_hook_templates.py +0 -0
  230. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_kiro_install.py +0 -0
  231. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_layout_migration_t101.py +0 -0
  232. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_layout_parity_t102.py +0 -0
  233. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_local_mode_dict.py +0 -0
  234. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_local_mode_file_json.py +0 -0
  235. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_local_mode_file_yaml.py +0 -0
  236. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_log_fallback_stderr.py +0 -0
  237. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_log_options_ignored_hosted.py +0 -0
  238. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_log_rotation.py +0 -0
  239. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_migrate.py +0 -0
  240. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_min_sdk_version_gate.py +0 -0
  241. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_multi_client_per_project_175.py +0 -0
  242. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_no_policy_no_key.py +0 -0
  243. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_observe_mode_1247.py +0 -0
  244. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_package_rename_shim.py +0 -0
  245. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_policy_engine_version_phase1b.py +0 -0
  246. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_policy_freshness.py +0 -0
  247. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_policy_settings.py +0 -0
  248. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_policy_source_audit.py +0 -0
  249. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_quarantine.py +0 -0
  250. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_reason_code.py +0 -0
  251. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_refresh.py +0 -0
  252. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_secrets.py +0 -0
  253. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_sql_semantic_class.py +0 -0
  254. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_synthetic_policy_id_t79.py +0 -0
  255. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_t103_precedence.py +0 -0
  256. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_t104_cache_gc.py +0 -0
  257. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_t108_local_override_audit.py +0 -0
  258. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_t96_single_audit_log.py +0 -0
  259. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_t99_install_prefetch_bundle.py +0 -0
  260. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_tamper.py +0 -0
  261. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_tamper_behavior.py +0 -0
  262. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_tamper_hook.py +0 -0
  263. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_telemetry_consent.py +0 -0
  264. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_tracecontext.py +0 -0
  265. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_unsafe_int_boundary.py +0 -0
  266. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_update_cmd.py +0 -0
  267. {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_upgrade_nudge.py +0 -0
  268. {controlzero-1.10.1 → controlzero-1.11.5}/tools/cz-kiro-adapter +0 -0
@@ -1,5 +1,161 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.11.5 -- 2026-06-19 (cross-surface CONTROLZERO_CLIENT foreign-pin fail-open)
4
+
5
+ Closes a routing fail-open where an explicit `CONTROLZERO_CLIENT` pin -- set by
6
+ `controlzero install <host>` so a kiro/codex/antigravity/cursor hook routes to
7
+ the right adapter -- was NOT authoritative. The `ClaudeCodeAdapter` is first in
8
+ the registry and falls through to ambient Anthropic env hints (`CLAUDECODE` /
9
+ `CLAUDE_CODE` / ...), so a correctly installed foreign-host hook that ALSO
10
+ carried an ambient `CLAUDECODE` (e.g. launched from a Claude Code-spawned shell)
11
+ was hijacked to the Claude adapter, which renders the wrong wire shape and reads
12
+ as allow on a destructive tool call.
13
+
14
+ ### Fixed
15
+
16
+ - **Foreign `CONTROLZERO_CLIENT` pin now defers (cross-surface fail-open).**
17
+ Every host adapter's `claim()` defers when `CONTROLZERO_CLIENT` names a
18
+ DIFFERENT client, via the shared `HostAdapter._pinned_elsewhere` helper, so an
19
+ installer-pinned hook can never be hijacked by an ambient `CLAUDECODE`;
20
+ `UnknownHostAdapter` stays the always-claim backstop. A 7-surface guard matrix
21
+ (`tests/test_hosts_adapter.py`) pins it fail-if-reverted. Mirrors the Node SDK
22
+ fix. gh#81.
23
+
24
+ ## 1.11.4 -- 2026-06-17 (close the count==0 degraded fail-open + stale-empty-cache replay + cached-bundle integrity, gh#1303)
25
+
26
+ Closes a degraded-bundle fail-open in the count-provenance discriminator plus
27
+ the two residuals tracked on gh#1303 after the 1.9.8 degraded/empty fail-open
28
+ fix. All live in the shared decision core / hosted cache, so all five host
29
+ surfaces (Claude Code, Gemini CLI, Codex CLI, Antigravity, Kiro) inherit them.
30
+
31
+ ### Fixed
32
+
33
+ - **`active_policy_count == 0` no longer treats a degraded bundle as genuinely
34
+ empty (gh#1303 fail-open).** Part 3 made the stamped count authoritative for
35
+ genuine-empty, but `genuinely_empty = (count == 0)` did NOT also require the
36
+ backend's explicit `policies: []`. A bundle with `active_policy_count == 0`
37
+ whose `policies` key was MISSING or non-list (truncated / malformed /
38
+ degraded) therefore read as a genuinely-empty project and fell through to
39
+ OBSERVE -> allow -- the rm-rf fail-open class. Genuine-empty now requires
40
+ `count == 0` AND an explicit `policies: []` (the backend always ships that
41
+ for an empty project -- `bundle_handler.go` builds `Policies` as
42
+ `make([]bundlePolicy, 0)` with no `omitempty`), so a `count == 0` paired with
43
+ a missing/malformed `policies` key now FAILS CLOSED (`BUNDLE_MISSING`). The
44
+ gh#1247 genuine-empty observe posture (count == 0 + explicit `[]`) is
45
+ preserved. The Node and Go SDKs are aligned to the same rule for cross-SDK
46
+ decision parity.
47
+ - **Stale-empty-cache replay window (gh#1303 residual A).** A cached bundle
48
+ whose stamped `metadata.active_policy_count` was a legitimate `0` (the
49
+ project was empty when the bundle was minted) still read as genuinely-empty
50
+ AFTER a policy was later attached -- because a count cannot see time --
51
+ so replaying a stale cached empty bundle could silently OBSERVE-allow a
52
+ project that now HAS a policy. The shared core `translate_to_local_policy`
53
+ now takes a `genuine_empty_is_authoritative` flag; the hosted orchestrator
54
+ drops it to `False` once a cached bundle ages past
55
+ `BUNDLE_CACHE_GENUINE_EMPTY_TTL_S` (the bundle's signed `created_at` vs now).
56
+ A stale empty bundle then FAILS CLOSED (deny / re-fetch, `BUNDLE_MISSING`)
57
+ instead of replaying the gh#1247 observe. A FRESH empty bundle keeps the
58
+ gh#1247 genuine-empty observe posture, and a cached bundle WITH real rules
59
+ is still enforced as the last-known-good fallback at any age.
60
+ - **Cached-bundle integrity verification (gh#1303 residual B).**
61
+ `load_cached_bundle` now verifies `sha256(blob) == the stored .meta
62
+ checksum`. On a mismatch (tampered / corrupt / truncated cache) the cache is
63
+ ignored and the caller re-fetches -- and, if the backend is unreachable,
64
+ fails closed -- rather than silently trusting a blob that no longer matches
65
+ its recorded checksum.
66
+
67
+ ## 1.11.3 -- 2026-06-17 (hosted mode no longer silently ignores a local policy file, #1265)
68
+
69
+ ### Fixed
70
+
71
+ - **Hosted mode now warns when a local policy file is being ignored (#1265
72
+ follow-up).** With an API key set, `controlzero hook-check` enforces the
73
+ HOSTED dashboard bundle and intentionally ignores any local
74
+ `./controlzero.{yaml,yml,json}` or `~/.controlzero/policy.{yaml,yml,json}`
75
+ (T103, so a stale local file cannot silently shadow the dashboard). That
76
+ suppression was SILENT: a customer who dropped a project-local
77
+ `controlzero.yaml` with a deny rule, expecting it to enforce, got no signal
78
+ that hosted mode was winning and the file was inert. The hook now emits a
79
+ loud warning to **stderr** that names the ignored file and explains both
80
+ remedies -- set `CONTROLZERO_LOCAL_OVERRIDE=1` to use the local file, or edit
81
+ the policy in the dashboard. The warning is deduped once per process AND once
82
+ per machine per shadowed path (via a small marker under `~/.controlzero/`), so
83
+ it surfaces the signal without spamming stderr on every tool call in the
84
+ per-call hook-subprocess context. Enforcement behavior is UNCHANGED (hosted
85
+ still wins); the warning never touches stdout, so the exit-0/exit-2 hook
86
+ decision contract that Claude Code / Kiro / Codex read is unaffected.
87
+
88
+ ## 1.11.2 -- 2026-06-17 (Antigravity PostToolUse observe-only contract, #58)
89
+
90
+ ### Fixed
91
+
92
+ - **PostToolUse is now observe-only on the Antigravity surface (#58).**
93
+ The installer wires `controlzero hook-check` to BOTH the `PreToolUse`
94
+ deciding gate and the `PostToolUse` observe event, but `hook-check` never
95
+ read the event name -- it ran the full deciding evaluator on every payload.
96
+ A `PostToolUse` event whose tool matched a deny rule therefore rendered a
97
+ post-execution `{"decision":"deny"}` (or `force_ask` for a HITL gate): a
98
+ contract-incorrect verdict on an observe-only event (the tool has already
99
+ run and cannot be un-run), and on agy's strict stdout schema a deny it can
100
+ neither honor nor cleanly interpret. `hook-check` now recognizes the
101
+ post-execution events (`PostToolUse` / `PostInvocation`, plus Kiro's
102
+ `postToolUse`) and emits the observe no-op **in the form each host expects**:
103
+ Antigravity (the empty-stdout-is-deny host, `decision_via_exit_code=False`)
104
+ gets an explicit `{"decision":"allow"}`; the exit-code hosts (Claude Code /
105
+ Gemini / Codex / Kiro) get **empty stdout + exit 0**, their documented "no
106
+ opinion, proceed" signal. `PostToolUse` is also a Claude Code event name, so
107
+ this path is reached on Claude / Gemini post payloads too -- emitting a
108
+ decision JSON (Claude's `approve`, ...) there would have changed their
109
+ post-hook stdout contract. The DECIDING gate (`PreToolUse`, and any payload
110
+ without a recognized post-event name) keeps its fail-closed deciding
111
+ semantics unchanged. The fix lives in the shared `hook-check` core, so all
112
+ five governed surfaces inherit it.
113
+
114
+
115
+ ## 1.11.1 -- 2026-06-17 (kiro-cli hook fail-open fix, #1265)
116
+
117
+ ### Fixed
118
+
119
+ - **Kiro CLI hook no longer silently fails open (#1265).**
120
+ `controlzero kiro init` wired a BARE `controlzero hook-check` into
121
+ `~/.kiro/settings.json`. On a venv install (or a machine with a broken /
122
+ shadowing `controlzero` on PATH) kiro-cli's hook subshell resolved the wrong
123
+ interpreter, the hook crashed, and kiro-cli FAILED OPEN -- tools ran with no
124
+ enforcement, silently. The hook command is now **interpreter-pinned**:
125
+ `"<python>" -m controlzero hook-check --strict`, so kiro-cli runs the exact
126
+ Python that installed it regardless of PATH/venv. Strict mode moved from the
127
+ Windows-unparseable `CZ_KIRO_CLI_STRICT=1` bash env-prefix to a portable
128
+ `--strict` flag (the env var is still honored for back-compat). New
129
+ `python -m controlzero` entrypoint. `kiro init` now runs a fail-LOUD
130
+ self-check that WARNS if the installed hook does not block a synthetic deny.
131
+
132
+ ## 1.11.0 -- 2026-06-17 (secret references: plaintext out of the agent context)
133
+
134
+ ### Added
135
+
136
+ - **Secret references (`czsec://`).** `controlzero.secret_ref(name)` returns an
137
+ opaque, value-free reference you can hold in the model context, put in tool
138
+ arguments, and pass around freely. It performs no fetch and carries no value.
139
+ - **Egress-time resolution.** `controlzero exec -- CMD` and
140
+ `controlzero.secrets.run(cmd, env=...)` resolve any `czsec://` references in
141
+ the argv / environment to plaintext ONLY at the moment a child process is
142
+ spawned, injecting the value into the child's argv / environment. The
143
+ plaintext never re-enters the parent (agent) process. Resolution reuses
144
+ `Client.get_secret`, so it is still policy-gated, HITL-aware, and audited; the
145
+ audit line records the reference + a non-reversible fingerprint, never the
146
+ value.
147
+ - **`SecretStr` taint wrapper.** When a value must live briefly in-process, it is
148
+ wrapped so `repr` / `str` / `format` redact and iteration is refused; the only
149
+ way to get the bytes is `.reveal()`, called at the egress call site.
150
+
151
+ ### Changed
152
+
153
+ - **`Client.get_secret` now warns** (once per process) that it returns plaintext
154
+ into the agent context, pointing at `secret_ref` + `controlzero exec`. Set
155
+ `CONTROLZERO_SECRETS_PROTECTED=1` to BLOCK plaintext reads entirely
156
+ (`get_secret` raises before any fetch). Default behaviour is unchanged
157
+ (warn-only); existing callers keep working.
158
+
3
159
  ## 1.10.1 -- 2026-06-17 (upgrade nudge)
4
160
 
5
161
  ### Added
@@ -580,7 +736,7 @@ secret-value-leak guard that gates every outbound HITL payload.
580
736
  POSTs a HITL approval request, builds the body from the
581
737
  `PolicyDecision`, threads in `X-CZ-Requestor-Email` from
582
738
  `~/.controlzero/config.yaml`, sends a per-call `Idempotency-Key`, and
583
- returns a `PendingApproval`. Maps backend error codes E1305-E1308 onto
739
+ returns a `PendingApproval`. Maps backend error codes E1705-E1708 onto
584
740
  the matching SDK exceptions. (#577)
585
741
 
586
742
  ### Conformance
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: controlzero
3
- Version: 1.10.1
3
+ Version: 1.11.5
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
@@ -333,7 +333,7 @@ Basic flow:
333
333
  ```python
334
334
  from controlzero import Client
335
335
 
336
- cz = Client(api_key="cz_live_...") # approvals require hosted mode
336
+ cz = Client(api_key="cz_live_...") # approvals run on any Postgres-backed deployment: hosted (SaaS), self-managed, or air-gapped
337
337
 
338
338
  decision = cz.guard("delete_file", {"path": "/etc/passwd"})
339
339
  if decision.decision == "deny" and getattr(decision, "hitl_eligible", False):
@@ -279,7 +279,7 @@ Basic flow:
279
279
  ```python
280
280
  from controlzero import Client
281
281
 
282
- cz = Client(api_key="cz_live_...") # approvals require hosted mode
282
+ cz = Client(api_key="cz_live_...") # approvals run on any Postgres-backed deployment: hosted (SaaS), self-managed, or air-gapped
283
283
 
284
284
  decision = cz.guard("delete_file", {"path": "/etc/passwd"})
285
285
  if decision.decision == "deny" and getattr(decision, "hitl_eligible", False):
@@ -39,12 +39,16 @@ from controlzero.hitl.grant_protocol import (
39
39
  REASON_HITL_RETRY_LOOP,
40
40
  )
41
41
  from controlzero.policy_loader import load_policy
42
+ from controlzero.secrets import SecretRef, SecretStr, secret_ref
42
43
 
43
- __version__ = "1.10.1"
44
+ __version__ = "1.11.5"
44
45
 
45
46
  __all__ = [
46
47
  "Client",
47
48
  "PendingApproval",
49
+ "SecretRef",
50
+ "SecretStr",
51
+ "secret_ref",
48
52
  "REASON_HITL_BACKEND_UNREACHABLE",
49
53
  "REASON_HITL_GRANT_APPROVED",
50
54
  "REASON_HITL_GRANT_CANCELED",
@@ -0,0 +1,12 @@
1
+ """Enable ``python -m controlzero`` to run the CLI.
2
+
3
+ The Kiro CLI hook command pins the interpreter as
4
+ ``"<sys.executable>" -m controlzero hook-check --strict`` (#1265) so kiro-cli
5
+ invokes the exact, importable controlzero the user installed, never a bare
6
+ ``controlzero`` that PATH might resolve to a broken or shadowing install.
7
+ """
8
+
9
+ from controlzero.cli.main import cli
10
+
11
+ if __name__ == "__main__":
12
+ cli()
@@ -464,9 +464,55 @@ def check_min_sdk_version(payload: dict, sdk_version: str) -> None:
464
464
  # --- Schema translation ----------------------------------------------------
465
465
 
466
466
 
467
- def translate_to_local_policy(payload: dict) -> dict:
467
+ def _bundle_active_policy_count(payload: dict) -> Optional[int]:
468
+ """Return ``metadata.active_policy_count`` -- the backend's LIVE count of
469
+ active policy attachments at bundle-build time (#1303 part 3) -- or
470
+ ``None`` when the field is absent / malformed (an older backend that
471
+ predates the field).
472
+
473
+ This is the AUTHORITATIVE empty-vs-degraded discriminator. ``0`` means a
474
+ genuinely-empty project (observe, #1247); ``>0`` means policies ARE
475
+ attached, so an empty translated rule set is a degraded / stripped /
476
+ stale bundle and must fail closed. ``None`` makes the caller fall back to
477
+ the older ``policies: []`` shape heuristic. Never raises -- a malformed
478
+ value is treated as absent (fall back), never as a genuine 0.
479
+ """
480
+ metadata = payload.get("metadata")
481
+ if not isinstance(metadata, dict):
482
+ return None
483
+ count = metadata.get("active_policy_count")
484
+ # bool is an int subclass -- reject it so a stray `true` is not read as 1.
485
+ if isinstance(count, bool) or not isinstance(count, int):
486
+ return None
487
+ if count < 0:
488
+ return None
489
+ return count
490
+
491
+
492
+ def translate_to_local_policy(
493
+ payload: dict,
494
+ *,
495
+ genuine_empty_is_authoritative: bool = True,
496
+ ) -> dict:
468
497
  """Translate a decrypted bundle payload to the local policy dict shape.
469
498
 
499
+ Args:
500
+ payload: the decrypted bundle payload dict.
501
+ genuine_empty_is_authoritative: whether a stamped genuinely-empty
502
+ project (``metadata.active_policy_count == 0``, or an explicit
503
+ empty ``policies: []`` on older backends) may be TRUSTED as the
504
+ #1247 observe posture. Defaults to ``True`` -- a freshly-pulled
505
+ bundle is authoritative. The hosted orchestrator passes ``False``
506
+ when serving a STALE CACHED bundle (#1303 residual A: the
507
+ stale-empty-cache replay window). A cached genuine-empty signal
508
+ cannot see time -- a count that was a legitimate 0 when the project
509
+ WAS empty still reads as genuinely empty after policies are later
510
+ attached -- so once the cached bundle ages past the freshness
511
+ bound the empty signal is no longer trustworthy. With this flag
512
+ ``False`` an empty translated rule set fails CLOSED (re-fetch /
513
+ deny) instead of replaying a stale observe-allow. A FRESH empty
514
+ bundle keeps the #1247 genuine-empty observe path intact.
515
+
470
516
  The bundle's `policies` list comes from the backend in the shape
471
517
  produced by :func:`BundleHandler.SDKPull` (Go: bundlePolicy). The
472
518
  local :class:`PolicyEvaluator` expects the input format from
@@ -556,7 +602,49 @@ def translate_to_local_policy(payload: dict) -> dict:
556
602
  if translated is not None:
557
603
  flat.append(translated)
558
604
 
559
- if not flat and not (isinstance(raw_policies, list) and len(raw_policies) == 0):
605
+ # #1303 part 3: the backend stamps metadata.active_policy_count -- the LIVE
606
+ # attachment count at bundle-build time. It is the AUTHORITATIVE
607
+ # discriminator for the DEGRADED direction: >0 means policies ARE attached,
608
+ # so an empty translated rule set is a degraded/stripped bundle -> fail
609
+ # closed (catches a stripped bundle that still ships an explicit
610
+ # `policies: []` while the project HAS attachments, which the shape check
611
+ # alone read as genuinely empty).
612
+ #
613
+ # For the GENUINELY-EMPTY direction the count is NECESSARY BUT NOT
614
+ # SUFFICIENT: a genuine empty project requires count == 0 AND the backend's
615
+ # explicit `policies: []`. The backend ALWAYS ships that explicit empty list
616
+ # for an empty project (bundle_handler.go builds Policies as
617
+ # `make([]bundlePolicy, 0)` and the JSON field has no `omitempty`), so
618
+ # requiring it costs a legitimate empty project nothing. A count == 0 paired
619
+ # with a MISSING or non-list `policies` key is a truncated / malformed /
620
+ # degraded bundle, NOT a genuine empty project -- trusting the count alone
621
+ # there would OBSERVE -> allow-all a degraded bundle (the rm-rf fail-open
622
+ # class, #1303), so it must fail closed below. Count ABSENT (older backend
623
+ # that predates the field) -> fall back to the explicit `policies: []` shape
624
+ # check.
625
+ active_policy_count = _bundle_active_policy_count(payload)
626
+ explicit_empty_policies = isinstance(raw_policies, list) and len(raw_policies) == 0
627
+ if active_policy_count is not None:
628
+ genuinely_empty = active_policy_count == 0 and explicit_empty_policies
629
+ else:
630
+ genuinely_empty = explicit_empty_policies
631
+
632
+ # #1303 residual A (stale-empty-cache replay): the genuine-empty signal
633
+ # (count==0 / explicit `policies: []`) cannot see TIME. A count that was a
634
+ # legitimate 0 from when the project WAS empty still reads as genuinely
635
+ # empty after policies are later attached, so replaying a STALE cached
636
+ # empty bundle would observe-allow a project that now HAS a policy. The
637
+ # hosted orchestrator knows the bundle age (header.created_at) and passes
638
+ # genuine_empty_is_authoritative=False once a cached bundle is past its
639
+ # freshness bound; in that case we DROP the genuine-empty trust so the
640
+ # zero-rule outcome falls through to the BUNDLE_MISSING fail-closed branch
641
+ # below (re-fetch / deny) instead of replaying a stale observe. A FRESH
642
+ # empty bundle (the default, and every newly-pulled bundle) keeps the
643
+ # #1247 genuine-empty observe path.
644
+ if not genuine_empty_is_authoritative:
645
+ genuinely_empty = False
646
+
647
+ if not flat and not genuinely_empty:
560
648
  # #1303 FAIL-OPEN FIX (the empty-vs-degraded boundary). We have zero
561
649
  # translatable rules, but this is NOT a genuinely-empty project: the
562
650
  # payload carried attached policies that produced no enforceable rules,
@@ -568,10 +656,15 @@ def translate_to_local_policy(payload: dict) -> dict:
568
656
  # genuinely-empty observe path (#1247) below is reached ONLY when the
569
657
  # backend shipped an explicit empty list (`policies: []`).
570
658
  #
571
- # NOTE: a stale CACHED bundle that legitimately held `policies: []` from
572
- # when the project WAS empty is NOT caught here (it looks genuinely
573
- # empty); that residual replay window is closed by bundle provenance +
574
- # freshness (#1303 part 3 / backend active_policy_count).
659
+ # NOTE: with active_policy_count (part 3) a stripped bundle that ships
660
+ # `policies: []` while the project HAS attachments now fails closed
661
+ # above (count > 0). The remaining purely-time-based residual -- a
662
+ # stale CACHED bundle whose stamped count was a legitimate 0 from when
663
+ # the project WAS empty -- is now ALSO routed here: the hosted
664
+ # orchestrator drops genuine_empty_is_authoritative once the cached
665
+ # bundle ages past its freshness bound (#1303 residual A), which clears
666
+ # genuinely_empty above so this fail-closed branch is taken instead of
667
+ # the stale observe replay.
575
668
  # Reuse the registered BUNDLE_MISSING reason_code / synthetic id (both
576
669
  # already in VALID_REASON_CODES / VALID_SYNTHETIC_POLICY_IDS) so no new
577
670
  # error-catalog entry is introduced; the human-readable reason names the
@@ -353,7 +353,9 @@ def _antigravity_dry_run_roundtrips() -> bool:
353
353
  adapter = select_adapter({}, {"CONTROLZERO_CLIENT": "antigravity"})
354
354
  if getattr(adapter, "name", "") != "antigravity":
355
355
  return False
356
- out = adapter.render(
356
+ # render_envelope is the per-host wire dict; render() folds it into a
357
+ # RenderResult (exit code + stdout). The dry-run inspects the envelope.
358
+ out = adapter.render_envelope(
357
359
  CZDecision(effect="allow", reason="doctor dry-run", reason_code="RULE_MATCH")
358
360
  )
359
361
  return isinstance(out, dict) and out.get("decision") == "allow"
@@ -0,0 +1,90 @@
1
+ """``controlzero exec`` -- run a command with ``czsec://`` references resolved
2
+ to plaintext at the spawn boundary, injected into the child ONLY.
3
+
4
+ Usage::
5
+
6
+ controlzero exec --env OPENAI_API_KEY=czsec://prod/openai-key -- \
7
+ python my_agent.py
8
+
9
+ controlzero exec -- curl -H "Authorization: Bearer czsec://prod/token" https://api
10
+
11
+ Any ``czsec://`` reference in ``--env`` values or in the command's arguments is
12
+ resolved (policy-gated + audited, via ``Client.get_secret``) and placed into the
13
+ child process's environment / argv. The reference -- not the secret -- is all
14
+ that ever lived in the agent / model context; the plaintext appears only in the
15
+ child. This process never prints the resolved value.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ import sys
22
+
23
+ import click
24
+
25
+ from controlzero.secrets.reference import find_refs
26
+ from controlzero.secrets.resolver import resolve_env, resolve_refs
27
+
28
+
29
+ @click.command(
30
+ "exec",
31
+ context_settings={"ignore_unknown_options": True, "allow_interspersed_args": False},
32
+ )
33
+ @click.option(
34
+ "--env",
35
+ "env_overrides",
36
+ multiple=True,
37
+ metavar="NAME=VALUE",
38
+ help="Set a child env var; VALUE may be a czsec:// reference. Repeatable.",
39
+ )
40
+ @click.argument("command", nargs=-1, type=click.UNPROCESSED)
41
+ def exec_cmd(env_overrides, command) -> None:
42
+ """Resolve czsec:// references at spawn and exec COMMAND with them."""
43
+ if not command:
44
+ raise click.UsageError("no command given (usage: controlzero exec -- CMD ARGS)")
45
+
46
+ # Parse --env NAME=VALUE overrides.
47
+ overrides = {}
48
+ for item in env_overrides:
49
+ if "=" not in item:
50
+ raise click.UsageError(f"--env expects NAME=VALUE, got {item!r}")
51
+ name, value = item.split("=", 1)
52
+ if not name:
53
+ raise click.UsageError(f"--env name is empty in {item!r}")
54
+ overrides[name] = value
55
+
56
+ # Build a Client only if there is actually something to resolve -- a plain
57
+ # exec with no references should not require an API key.
58
+ has_refs = any(find_refs(v) for v in overrides.values()) or any(
59
+ find_refs(tok) for tok in command
60
+ )
61
+ client = None
62
+ if has_refs:
63
+ from controlzero import Client
64
+
65
+ client = Client()
66
+ if getattr(client, "_api_key", None) is None:
67
+ raise click.ClickException(
68
+ "resolving czsec:// references requires CONTROLZERO_API_KEY "
69
+ "(the backend owns the secret store)"
70
+ )
71
+
72
+ # Resolve at the boundary. These are plaintext, destined for the child.
73
+ try:
74
+ resolved_argv = list(resolve_refs(command, client=client))
75
+ child_env = dict(os.environ)
76
+ if overrides:
77
+ child_env.update(resolve_env(overrides, client=client))
78
+ except Exception as exc: # surface a clean error, never a half-resolved spawn
79
+ raise click.ClickException(f"secret resolution failed: {exc}") from exc
80
+
81
+ # execvpe replaces this interpreter, so no resolved plaintext lingers in a
82
+ # Python frame after the child starts.
83
+ try:
84
+ os.execvpe(resolved_argv[0], resolved_argv, child_env)
85
+ except FileNotFoundError:
86
+ raise click.ClickException(f"command not found: {resolved_argv[0]!r}")
87
+ except OSError as exc:
88
+ raise click.ClickException(f"failed to exec {resolved_argv[0]!r}: {exc}")
89
+ # Unreachable on success (process is replaced).
90
+ sys.exit(127)
@@ -0,0 +1,118 @@
1
+ """Crash-proof console-script entry point for the ``controlzero`` CLI (#1189).
2
+
3
+ The ``controlzero`` console script declared in ``pyproject.toml`` MUST NOT be
4
+ pointed directly at ``controlzero.cli.main:cli``. The pip-generated wrapper
5
+ imports the target at module load with no guard, so a top-level import crash in
6
+ ``controlzero/cli/main.py`` (a missing transitive dependency, a packaging
7
+ defect, a syntax error in any eagerly-imported submodule) raises before
8
+ ``cli()`` runs. The wrapper then exits nonzero with a traceback, and a host that
9
+ treats nonzero-with-no-decision as "proceed" (Claude Code's PreToolUse falls
10
+ back to allow on a hook it cannot evaluate) silently lets the tool call
11
+ through. That is a fail-OPEN on the governance hot path -- the exact defect
12
+ QA #1189 flagged.
13
+
14
+ This shim makes the entry point crash-proof:
15
+
16
+ * The heavy import of ``controlzero.cli.main`` happens inside a try/except
17
+ with a ``sys.path`` source-checkout fallback, mirroring
18
+ ``tools/cz-kiro-adapter`` so a dev checkout that has not been pip-installed
19
+ still resolves the package.
20
+ * If the import or evaluation of the CLI cannot complete AT ALL and this is a
21
+ hook invocation (``hook-check`` on argv -- the governance hot path), the
22
+ shim fails SAFE: it writes a hard, human-readable error to stderr and exits
23
+ with the host's DENY/block code (Claude Code PreToolUse = exit 2), never a
24
+ silent allow.
25
+ * For a non-hook invocation (interactive ``controlzero ...`` usage) a broken
26
+ install should surface its traceback so the user can fix it -- there is no
27
+ governance decision at stake -- so the shim re-raises in that case.
28
+
29
+ What this shim does NOT change: the runtime fail-OPEN contract that lives
30
+ inside ``hook_check`` (E1501 bad-JSON, E1502 timeout, empty stdin, missing
31
+ tool_name) is intentional and is preserved. Those paths are reached only after
32
+ the CLI imports successfully and represent a *decision* to pass through, not an
33
+ import crash. This shim only converts the previously-silent import-crash
34
+ fall-through into an explicit fail-safe deny.
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import os
40
+ import sys
41
+
42
+ # Host block/deny exit codes for the hook hot path. Claude Code's PreToolUse
43
+ # protocol treats exit 2 as "block"; Kiro reuses the same convention. We do not
44
+ # have the resolved host adapter available before the import succeeds, so we use
45
+ # the lowest-common-denominator block code (2), which every supported host
46
+ # interprets as deny rather than allow.
47
+ _HOOK_DENY_EXIT_CODE = 2
48
+
49
+ _HOOK_SUBCOMMANDS = frozenset({"hook-check"})
50
+
51
+
52
+ def _is_hook_invocation(argv: list[str]) -> bool:
53
+ """True when the process was launched on the governance hot path.
54
+
55
+ The ``hook-check`` subcommand is what hosts (Claude Code, Kiro, Codex,
56
+ Gemini, Antigravity) wire into their PreToolUse hooks. Detecting it on argv
57
+ lets the shim choose fail-SAFE (deny) on an import crash for hook calls
58
+ while leaving interactive CLI usage to surface its traceback normally.
59
+ """
60
+ return any(arg in _HOOK_SUBCOMMANDS for arg in argv[1:])
61
+
62
+
63
+ def cli() -> None:
64
+ """Crash-proof wrapper around ``controlzero.cli.main:cli``.
65
+
66
+ Defers the heavy import so an import-time crash cannot escape as a bare
67
+ nonzero traceback that a host would mistake for "allow".
68
+ """
69
+ try:
70
+ from controlzero.cli.main import cli as _real_cli
71
+ except ModuleNotFoundError:
72
+ # Possibly a source checkout where the package is not pip-installed.
73
+ # Add the SDK root to sys.path and retry once, mirroring
74
+ # tools/cz-kiro-adapter. controlzero/cli/hook_entry.py -> SDK root is
75
+ # three levels up (controlzero/cli/ -> controlzero/ -> SDK root).
76
+ _sdk_root = os.path.dirname(
77
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
78
+ )
79
+ if _sdk_root not in sys.path:
80
+ sys.path.insert(0, _sdk_root)
81
+ try:
82
+ from controlzero.cli.main import cli as _real_cli
83
+ except BaseException: # noqa: BLE001 -- crash-proof boundary
84
+ _fail_safe_or_reraise()
85
+ return
86
+ except BaseException: # noqa: BLE001 -- crash-proof boundary
87
+ # Any other top-level import failure (ImportError on a transitive dep,
88
+ # a syntax error in an eagerly-imported submodule, etc).
89
+ _fail_safe_or_reraise()
90
+ return
91
+
92
+ _real_cli()
93
+
94
+
95
+ def _fail_safe_or_reraise() -> None:
96
+ """On import failure: fail-safe DENY for hook calls, re-raise otherwise.
97
+
98
+ Reads the live exception from ``sys.exc_info`` so it can re-raise the
99
+ original traceback for interactive usage.
100
+ """
101
+ if _is_hook_invocation(sys.argv):
102
+ exc = sys.exc_info()[1]
103
+ # Hard, loud error to stderr -- never a silent allow. The host sees a
104
+ # nonzero block exit code and denies the tool call.
105
+ sys.stderr.write(
106
+ "[Control Zero] hook entry point could not load the policy engine "
107
+ f"({type(exc).__name__}: {exc}); DENYING this tool call (fail-safe). "
108
+ "Reinstall the controlzero package to restore policy enforcement.\n"
109
+ )
110
+ sys.stderr.flush()
111
+ sys.exit(_HOOK_DENY_EXIT_CODE)
112
+ # Not a hook invocation: surface the real traceback so the user can fix
113
+ # their install. No governance decision is at stake here.
114
+ raise
115
+
116
+
117
+ if __name__ == "__main__":
118
+ cli()
@@ -55,9 +55,18 @@ known host treats no-decision as "proceed") and the universal
55
55
  from __future__ import annotations
56
56
 
57
57
  from controlzero.cli.hosts.antigravity import AntigravityAdapter
58
- from controlzero.cli.hosts.base import CZDecision, HostAdapter
58
+ from controlzero.cli.hosts.base import (
59
+ AdapterError,
60
+ CoverageDecl,
61
+ CZDecision,
62
+ HostAdapter,
63
+ RenderResult,
64
+ ToolCall,
65
+ )
59
66
  from controlzero.cli.hosts.claude_code import ClaudeCodeAdapter
60
67
  from controlzero.cli.hosts.codex_cli import CodexCLIAdapter
68
+ from controlzero.cli.hosts.cursor import CursorIDEAdapter
69
+ from controlzero.cli.hosts.cursor_cli import CursorCLIAdapter
61
70
  from controlzero.cli.hosts.gemini_cli import GeminiCLIAdapter
62
71
  from controlzero.cli.hosts.kiro import KiroCLIAdapter, KiroIDEAdapter
63
72
  from controlzero.cli.hosts.unknown import UnknownHostAdapter
@@ -77,8 +86,10 @@ REGISTRY: list[HostAdapter] = [
77
86
  CodexCLIAdapter(),
78
87
  GeminiCLIAdapter(),
79
88
  AntigravityAdapter(), # distinct toolCall / PreToolCall signature
89
+ CursorCLIAdapter(), # beforeShellExecution + top-level `command`, no tool_name (before KiroCLI)
80
90
  KiroCLIAdapter(), # camelCase hook_event_name (disjoint from Claude PascalCase)
81
91
  KiroIDEAdapter(), # CZ_KIRO_SURFACE=ide / TERM_PROGRAM=kiro (shim-set)
92
+ CursorIDEAdapter(), # cursor_version / generation_id payload marker or CURSOR_* env (after KiroIDE)
82
93
  UnknownHostAdapter(), # always claims; must stay at the tail
83
94
  ]
84
95
 
@@ -105,9 +116,15 @@ def select_adapter(payload: dict, env) -> HostAdapter:
105
116
 
106
117
  __all__ = [
107
118
  "CZDecision",
119
+ "ToolCall",
120
+ "RenderResult",
121
+ "CoverageDecl",
122
+ "AdapterError",
108
123
  "HostAdapter",
109
124
  "ClaudeCodeAdapter",
110
125
  "CodexCLIAdapter",
126
+ "CursorCLIAdapter",
127
+ "CursorIDEAdapter",
111
128
  "GeminiCLIAdapter",
112
129
  "AntigravityAdapter",
113
130
  "KiroCLIAdapter",