controlzero 1.9.6__tar.gz → 1.9.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. {controlzero-1.9.6 → controlzero-1.9.7}/CHANGELOG.md +22 -0
  2. {controlzero-1.9.6 → controlzero-1.9.7}/PKG-INFO +1 -1
  3. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/__init__.py +1 -1
  4. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/audit_remote.py +104 -4
  5. {controlzero-1.9.6 → controlzero-1.9.7}/pyproject.toml +1 -1
  6. {controlzero-1.9.6 → controlzero-1.9.7}/tests/spool/test_spool_sink_wiring.py +127 -3
  7. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_hosted_policy_e2e.py +7 -5
  8. {controlzero-1.9.6 → controlzero-1.9.7}/.gitignore +0 -0
  9. {controlzero-1.9.6 → controlzero-1.9.7}/Dockerfile.test +0 -0
  10. {controlzero-1.9.6 → controlzero-1.9.7}/LICENSE +0 -0
  11. {controlzero-1.9.6 → controlzero-1.9.7}/README.md +0 -0
  12. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/_internal/__init__.py +0 -0
  13. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/_internal/action_aliases.py +0 -0
  14. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/_internal/action_validator.py +0 -0
  15. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/_internal/bundle.py +0 -0
  16. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/_internal/credential_hook.py +0 -0
  17. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/_internal/credential_scanner.py +0 -0
  18. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/_internal/credentials_data/__init__.py +0 -0
  19. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/_internal/credentials_data/built_in.yaml +0 -0
  20. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/_internal/dlp_scanner.py +0 -0
  21. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/_internal/enforcer.py +0 -0
  22. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/_internal/hook_extractors.py +0 -0
  23. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/_internal/tool_extractors.json +0 -0
  24. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/_internal/types.py +0 -0
  25. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/audit_local.py +0 -0
  26. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/canonical.py +0 -0
  27. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/__init__.py +0 -0
  28. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/__main__.py +0 -0
  29. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/_secrets.py +0 -0
  30. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/console.py +0 -0
  31. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/debug_bundle.py +0 -0
  32. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/doctor.py +0 -0
  33. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/hosts/__init__.py +0 -0
  34. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/hosts/antigravity.py +0 -0
  35. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/hosts/base.py +0 -0
  36. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/hosts/claude_code.py +0 -0
  37. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/hosts/codex_cli.py +0 -0
  38. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/hosts/gemini_cli.py +0 -0
  39. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/hosts/kiro.py +0 -0
  40. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/hosts/unknown.py +0 -0
  41. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/kiro_adapter.py +0 -0
  42. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/main.py +0 -0
  43. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/migrate.py +0 -0
  44. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/spool_cmd.py +0 -0
  45. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/telemetry_consent.py +0 -0
  46. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/templates/antigravity/hooks.json +0 -0
  47. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/templates/antigravity.yaml +0 -0
  48. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/templates/autogen.yaml +0 -0
  49. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/templates/claude-code.yaml +0 -0
  50. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/templates/codex-cli.yaml +0 -0
  51. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/templates/cost-cap.yaml +0 -0
  52. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/templates/crewai.yaml +0 -0
  53. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/templates/cursor.yaml +0 -0
  54. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/templates/gemini-cli.yaml +0 -0
  55. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/templates/generic.yaml +0 -0
  56. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook +0 -0
  57. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/templates/kiro/ide-pre-tool-use.kiro.hook +0 -0
  58. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/templates/kiro/ide-prompt-submit.kiro.hook +0 -0
  59. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/templates/kiro/kiro.yaml +0 -0
  60. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/templates/langchain.yaml +0 -0
  61. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/templates/mcp.yaml +0 -0
  62. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/cli/templates/rag.yaml +0 -0
  63. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/client.py +0 -0
  64. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/device.py +0 -0
  65. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/enrollment.py +0 -0
  66. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/error_codes.py +0 -0
  67. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/error_codes.yaml +0 -0
  68. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/errors.py +0 -0
  69. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/hitl/__init__.py +0 -0
  70. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/hitl/grant_protocol.py +0 -0
  71. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/hitl/mock.py +0 -0
  72. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/hitl/pending_approval.py +0 -0
  73. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/hitl/secret_leak_guard.py +0 -0
  74. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/hitl/status.py +0 -0
  75. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/hooks/__init__.py +0 -0
  76. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/hooks/tool_output_handler.py +0 -0
  77. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/hosted_policy.py +0 -0
  78. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/integrations/__init__.py +0 -0
  79. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/integrations/anthropic.py +0 -0
  80. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/integrations/autogen.py +0 -0
  81. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/integrations/braintrust.py +0 -0
  82. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/integrations/crewai/__init__.py +0 -0
  83. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/integrations/crewai/agent.py +0 -0
  84. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/integrations/crewai/crew.py +0 -0
  85. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/integrations/crewai/task.py +0 -0
  86. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/integrations/crewai/tool.py +0 -0
  87. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/integrations/google.py +0 -0
  88. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/integrations/google_adk/__init__.py +0 -0
  89. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/integrations/google_adk/agent.py +0 -0
  90. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/integrations/google_adk/tool.py +0 -0
  91. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/integrations/langchain/__init__.py +0 -0
  92. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/integrations/langchain/agent.py +0 -0
  93. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/integrations/langchain/callbacks.py +0 -0
  94. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/integrations/langchain/chain.py +0 -0
  95. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/integrations/langchain/graph.py +0 -0
  96. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/integrations/langchain/modern.py +0 -0
  97. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/integrations/langchain/tool.py +0 -0
  98. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/integrations/langfuse.py +0 -0
  99. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/integrations/litellm.py +0 -0
  100. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/integrations/openai.py +0 -0
  101. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/integrations/pydantic_ai.py +0 -0
  102. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/integrations/vercel_ai.py +0 -0
  103. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/layout_migration.py +0 -0
  104. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/policy_loader.py +0 -0
  105. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/spool/__init__.py +0 -0
  106. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/spool/_compress.py +0 -0
  107. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/spool/_constants.py +0 -0
  108. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/spool/_crc32c.py +0 -0
  109. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/spool/_crypto.py +0 -0
  110. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/spool/_frame.py +0 -0
  111. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/spool/_keyring.py +0 -0
  112. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/spool/_metrics.py +0 -0
  113. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/spool/_spool.py +0 -0
  114. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/spool/_state.py +0 -0
  115. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/spool/_uploader.py +0 -0
  116. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/spool/cz-audit-v1.dict +0 -0
  117. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/tamper.py +0 -0
  118. {controlzero-1.9.6 → controlzero-1.9.7}/controlzero/tracecontext.py +0 -0
  119. {controlzero-1.9.6 → controlzero-1.9.7}/examples/hello_world.py +0 -0
  120. {controlzero-1.9.6 → controlzero-1.9.7}/tests/_fixtures/jcs_args_hash_vectors.json +0 -0
  121. {controlzero-1.9.6 → controlzero-1.9.7}/tests/conftest.py +0 -0
  122. {controlzero-1.9.6 → controlzero-1.9.7}/tests/integrations/__init__.py +0 -0
  123. {controlzero-1.9.6 → controlzero-1.9.7}/tests/integrations/test_google.py +0 -0
  124. {controlzero-1.9.6 → controlzero-1.9.7}/tests/parity/action_aliases.json +0 -0
  125. {controlzero-1.9.6 → controlzero-1.9.7}/tests/spool/__init__.py +0 -0
  126. {controlzero-1.9.6 → controlzero-1.9.7}/tests/spool/conftest.py +0 -0
  127. {controlzero-1.9.6 → controlzero-1.9.7}/tests/spool/test_spool_cli.py +0 -0
  128. {controlzero-1.9.6 → controlzero-1.9.7}/tests/spool/test_spool_concurrency.py +0 -0
  129. {controlzero-1.9.6 → controlzero-1.9.7}/tests/spool/test_spool_conformance.py +0 -0
  130. {controlzero-1.9.6 → controlzero-1.9.7}/tests/spool/test_spool_core.py +0 -0
  131. {controlzero-1.9.6 → controlzero-1.9.7}/tests/spool/test_spool_crash.py +0 -0
  132. {controlzero-1.9.6 → controlzero-1.9.7}/tests/spool/test_spool_diskfull.py +0 -0
  133. {controlzero-1.9.6 → controlzero-1.9.7}/tests/spool/test_spool_durable_default_tamper.py +0 -0
  134. {controlzero-1.9.6 → controlzero-1.9.7}/tests/spool/test_spool_keychain_dek.py +0 -0
  135. {controlzero-1.9.6 → controlzero-1.9.7}/tests/spool/test_spool_transcript_localack.py +0 -0
  136. {controlzero-1.9.6 → controlzero-1.9.7}/tests/spool/test_spool_uploader.py +0 -0
  137. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_action_aliases.py +0 -0
  138. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_action_canonicalization.py +0 -0
  139. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_action_validator_t86.py +0 -0
  140. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_agent_name_env.py +0 -0
  141. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_antigravity_adapter.py +0 -0
  142. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_antigravity_ga_blockers_1248.py +0 -0
  143. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_antigravity_hook_check.py +0 -0
  144. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_antigravity_install.py +0 -0
  145. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_api_key_mask.py +0 -0
  146. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_audit_remote.py +0 -0
  147. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_audit_remote_sdk_version.py +0 -0
  148. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_audit_sink_isolation.py +0 -0
  149. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_bundle_parser.py +0 -0
  150. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_bundle_translate.py +0 -0
  151. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_canonical_phase1a.py +0 -0
  152. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_cli_carve_out.py +0 -0
  153. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_cli_debug_bundle.py +0 -0
  154. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_cli_extractor_integration.py +0 -0
  155. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_cli_hook.py +0 -0
  156. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_cli_hosted_refresh.py +0 -0
  157. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_cli_init.py +0 -0
  158. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_cli_init_templates.py +0 -0
  159. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_cli_tail.py +0 -0
  160. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_cli_test.py +0 -0
  161. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_cli_validate.py +0 -0
  162. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_coding_agent_hooks.py +0 -0
  163. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_conditions.py +0 -0
  164. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_conformance.py +0 -0
  165. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_console.py +0 -0
  166. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_credential_hook.py +0 -0
  167. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_default_action.py +0 -0
  168. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_device.py +0 -0
  169. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_dlp_scanner.py +0 -0
  170. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_doctor.py +0 -0
  171. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_engine_version_consistency.py +0 -0
  172. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_enrollment.py +0 -0
  173. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_env_dump_438.py +0 -0
  174. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_epic_1247_bryan_acceptance.py +0 -0
  175. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_error_codes.py +0 -0
  176. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_errors_e_codes.py +0 -0
  177. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_fail_closed_eval.py +0 -0
  178. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_glob_matching.py +0 -0
  179. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_hitl_5d_email_install.py +0 -0
  180. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_hitl_6a_cli_flag.py +0 -0
  181. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_hitl_6a_exceptions.py +0 -0
  182. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_hitl_6a_get_secret_hitl.py +0 -0
  183. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_hitl_6a_mock_backend.py +0 -0
  184. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_hitl_6a_pending_approval.py +0 -0
  185. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_hitl_6a_request_approval.py +0 -0
  186. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_hitl_6a_secret_leak_guard.py +0 -0
  187. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_hitl_6a_wait.py +0 -0
  188. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_hitl_conformance.py +0 -0
  189. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_hitl_phase2b_protocol.py +0 -0
  190. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_hitl_reason_codes.py +0 -0
  191. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_hitl_validator_keys.py +0 -0
  192. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_hook_extractors.py +0 -0
  193. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_hosted_local_audit_1247.py +0 -0
  194. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_hosts_adapter.py +0 -0
  195. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_hybrid_mode_strict.py +0 -0
  196. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_hybrid_mode_warn.py +0 -0
  197. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_install_hook_command.py +0 -0
  198. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_install_hooks.py +0 -0
  199. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_kiro_adapter.py +0 -0
  200. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_kiro_cli_e2e.py +0 -0
  201. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_kiro_hook_templates.py +0 -0
  202. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_kiro_install.py +0 -0
  203. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_layout_migration_t101.py +0 -0
  204. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_layout_parity_t102.py +0 -0
  205. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_local_mode_dict.py +0 -0
  206. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_local_mode_file_json.py +0 -0
  207. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_local_mode_file_yaml.py +0 -0
  208. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_log_fallback_stderr.py +0 -0
  209. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_log_options_ignored_hosted.py +0 -0
  210. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_log_rotation.py +0 -0
  211. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_migrate.py +0 -0
  212. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_min_sdk_version_gate.py +0 -0
  213. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_multi_client_per_project_175.py +0 -0
  214. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_no_policy_no_key.py +0 -0
  215. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_observe_mode_1247.py +0 -0
  216. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_package_rename_shim.py +0 -0
  217. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_policy_engine_version_phase1b.py +0 -0
  218. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_policy_freshness.py +0 -0
  219. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_policy_settings.py +0 -0
  220. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_policy_source_audit.py +0 -0
  221. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_quarantine.py +0 -0
  222. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_reason_code.py +0 -0
  223. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_refresh.py +0 -0
  224. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_secrets.py +0 -0
  225. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_sql_semantic_class.py +0 -0
  226. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_synthetic_policy_id_t79.py +0 -0
  227. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_t103_precedence.py +0 -0
  228. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_t104_cache_gc.py +0 -0
  229. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_t108_local_override_audit.py +0 -0
  230. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_t96_single_audit_log.py +0 -0
  231. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_t99_install_prefetch_bundle.py +0 -0
  232. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_tamper.py +0 -0
  233. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_tamper_behavior.py +0 -0
  234. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_tamper_hook.py +0 -0
  235. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_telemetry_consent.py +0 -0
  236. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_tracecontext.py +0 -0
  237. {controlzero-1.9.6 → controlzero-1.9.7}/tests/test_unsafe_int_boundary.py +0 -0
  238. {controlzero-1.9.6 → controlzero-1.9.7}/tools/cz-kiro-adapter +0 -0
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.7 -- 2026-06-16 (hosted audit delivery for short-lived processes, gh#1292)
4
+
5
+ Fixes a P0 found via a customer report: hosted-mode audit rows were written to
6
+ the local `~/.controlzero/audit.log` but never reached the dashboard for
7
+ short-lived processes (PreToolUse hooks, CLI one-shots).
8
+
9
+ ### Fixed
10
+
11
+ - **Remote audit never delivered for short-lived processes (gh#1292).** Since
12
+ 1.9.2 the hosted (`BearerAuditSink`) and enrolled (`RemoteAuditSink`) sinks
13
+ default to the durable encrypted spool, where `log()` WALs the row and an
14
+ opportunistic `daemon=True` thread drains it. A short-lived process exits
15
+ before that thread can finish its HTTPS POST, and `close()` was a fsync-only
16
+ boundary, so the row stayed on local disk and never shipped. The hosted sink
17
+ now hands the drain to a **detached `controlzero spool flush` child** that
18
+ outlives the process (no latency on the agent hot path; the keystore and
19
+ network work happen in the child, never on the close path). The api key
20
+ travels via the child's environment, never on its argv. The enrolled sink
21
+ (opt-in, off the hosted hot path) does a bounded, fail-open in-process drain
22
+ on close instead. Both paths are fail-open: any error leaves the durable WAL
23
+ intact for a later drain. The durable WAL guarantee is unchanged.
24
+
3
25
  ## 1.9.6 -- 2026-06-16 (Antigravity GA-blocker hardening, gh#1248 / epic gh#925)
4
26
 
5
27
  Closes 3 of the prod-readiness GA blockers for the Antigravity (`agy`)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: controlzero
3
- Version: 1.9.6
3
+ Version: 1.9.7
4
4
  Summary: AI agent governance: policies, audit, and observability for tool calls. Works locally with no signup.
5
5
  Project-URL: Homepage, https://controlzero.ai
6
6
  Project-URL: Documentation, https://docs.controlzero.ai
@@ -40,7 +40,7 @@ from controlzero.hitl.grant_protocol import (
40
40
  )
41
41
  from controlzero.policy_loader import load_policy
42
42
 
43
- __version__ = "1.9.6"
43
+ __version__ = "1.9.7"
44
44
 
45
45
  __all__ = [
46
46
  "Client",
@@ -24,6 +24,7 @@ import json
24
24
  import logging
25
25
  import os
26
26
  import platform
27
+ import sys
27
28
  import threading
28
29
  import uuid
29
30
  from datetime import datetime, timezone
@@ -110,6 +111,15 @@ _SPOOL_DEFAULT_DIR = "~/.controlzero/spool"
110
111
  # processes.
111
112
  _SPOOL_HOOK_BUDGET_S = 2.0
112
113
  _SPOOL_TIMER_BUDGET_S = 25.0
114
+ # close() handoff budgets (#1292). A short-lived process (PreToolUse hook /
115
+ # CLI one-shot) exits before the daemon drain ships its spooled rows, and
116
+ # close() was a fsync-only boundary -- so the rows never reached the backend.
117
+ # The hosted (Bearer) sink now hands the drain to a DETACHED uploader child
118
+ # that outlives this process (no latency on the agent hot path, keystore +
119
+ # network work happen off the close path). The enrolled sink -- opt-in, off
120
+ # the hosted hot path -- does a bounded, fail-open in-process drain instead.
121
+ _DETACHED_DRAIN_BUDGET_S = 30.0
122
+ _CLOSE_DRAIN_BUDGET_S = 2.0
113
123
 
114
124
 
115
125
  def _build_wire_entry(
@@ -284,6 +294,11 @@ class _SpoolWiringMixin:
284
294
  self._drain_inflight = False
285
295
  self._drain_again = False
286
296
  self._drain_auth_blocked = False
297
+ # #1292: whether this process WAL-logged at least one entry (gates the
298
+ # close-time handoff so a pure-read process never spawns a drainer) and
299
+ # whether the detached drainer was already spawned (coalesce to one).
300
+ self._spool_logged = False
301
+ self._detached_drain_spawned = False
287
302
  self._spool = None
288
303
  self._spool_mode = "off"
289
304
  # Fast path: flag off/unset, NO hosted default, AND no spool
@@ -365,6 +380,10 @@ class _SpoolWiringMixin:
365
380
  except Exception as exc: # noqa: BLE001
366
381
  logger.warning("controlzero: spool append failed (%s)", exc)
367
382
  return False
383
+ # This process has durably WAL-logged at least one entry; close() may
384
+ # need to hand its drain to a detached uploader so a short-lived
385
+ # process does not strand it (#1292).
386
+ self._spool_logged = True
368
387
  if self._spool_mode == _SPOOL_MODE_DURABLE:
369
388
  self._drain_async(budget_s=_SPOOL_HOOK_BUDGET_S)
370
389
  return True
@@ -377,6 +396,73 @@ class _SpoolWiringMixin:
377
396
  def _spool_drain_orphans(self) -> bool:
378
397
  return False
379
398
 
399
+ def _spawn_detached_drain(self) -> None:
400
+ """Hand the spool drain to a DETACHED child that outlives this
401
+ process, then return immediately (#1292).
402
+
403
+ A short-lived hosted process (PreToolUse hook / CLI one-shot) exits
404
+ before the in-process daemon drain can finish its HTTPS POST, so
405
+ durably-spooled rows never reach the backend -- close() was a
406
+ fsync-only boundary that stranded them. Spawning ``controlzero spool
407
+ flush`` in a new session lets the upload complete after this process
408
+ is gone: no latency on the agent hot path, and the keystore + network
409
+ work happen in the child, never on this close path (the founder
410
+ non-blocking-hook and no-keystore-on-close constraints).
411
+
412
+ Only the Bearer (hosted api-key) sink can be drained this way --
413
+ ``cz spool flush`` authenticates with the api key, which it reads from
414
+ the environment (never argv, so the key never lands in ``ps``). The
415
+ enrolled sink has no such entrypoint and uses an in-process close
416
+ drain instead.
417
+
418
+ Fail-open: any error leaves the durable WAL intact for a later drain
419
+ (next long-lived process, the timer, or an explicit ``cz spool
420
+ flush``)."""
421
+ if getattr(self, "_detached_drain_spawned", False):
422
+ return
423
+ if self._spool is None or not getattr(self, "_spool_logged", False):
424
+ return
425
+ api_key = getattr(self, "_api_key", None)
426
+ if not api_key:
427
+ return
428
+ try:
429
+ import subprocess
430
+
431
+ env = dict(os.environ)
432
+ env["CONTROLZERO_API_KEY"] = api_key
433
+ api_url = getattr(self, "_api_url", None)
434
+ if api_url:
435
+ env["CONTROLZERO_API_URL"] = api_url
436
+ popen_kwargs = dict(
437
+ stdin=subprocess.DEVNULL,
438
+ stdout=subprocess.DEVNULL,
439
+ stderr=subprocess.DEVNULL,
440
+ env=env,
441
+ close_fds=True,
442
+ )
443
+ if os.name == "nt": # detach on Windows
444
+ popen_kwargs["creationflags"] = (
445
+ getattr(subprocess, "DETACHED_PROCESS", 0)
446
+ | getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
447
+ )
448
+ else: # POSIX: escape the process group so it outlives the parent
449
+ popen_kwargs["start_new_session"] = True
450
+ subprocess.Popen( # noqa: S603 -- fixed argv, no shell, no user input
451
+ [
452
+ sys.executable, "-m", "controlzero.cli.main",
453
+ "spool", "flush",
454
+ "--timeout", str(_DETACHED_DRAIN_BUDGET_S),
455
+ ],
456
+ **popen_kwargs,
457
+ )
458
+ self._detached_drain_spawned = True
459
+ except Exception as exc: # noqa: BLE001 -- never block/crash the host
460
+ logger.warning(
461
+ "controlzero: detached spool drain spawn failed (%s); audit "
462
+ "rows remain durable in the spool for a later drain",
463
+ exc,
464
+ )
465
+
380
466
  def _drain_async(self, budget_s: float = _SPOOL_HOOK_BUDGET_S) -> None:
381
467
  """One opportunistic non-blocking drain in a daemon thread.
382
468
  Coalesced: at most one drain in flight per sink instance; an
@@ -506,8 +592,17 @@ class RemoteAuditSink(_SpoolWiringMixin):
506
592
  self._closed = True
507
593
  self._cancel_flush_timer()
508
594
  if self._spool_wal:
509
- # Spool mode: every entry is already durable on disk; close
510
- # is a final fsync boundary, NEVER a blocking network flush.
595
+ # DURABLE mode (enrolled, opt-in, off the hosted hot path) ships
596
+ # synchronously with a bounded, fail-open budget before the final
597
+ # fsync so a short-lived process does not strand its rows (#1292).
598
+ # spool_only is replay-only and must NEVER live-send on close.
599
+ if (self._spool_mode == _SPOOL_MODE_DURABLE
600
+ and getattr(self, "_spool_logged", False)):
601
+ try:
602
+ self._drain_once(blocking=True,
603
+ budget_s=_CLOSE_DRAIN_BUDGET_S)
604
+ except Exception: # noqa: BLE001 -- fail-open; WAL preserved
605
+ pass
511
606
  self._spool_close()
512
607
  return
513
608
  # Synchronous flush -- we are shutting down
@@ -748,8 +843,13 @@ class BearerAuditSink(_SpoolWiringMixin):
748
843
  self._closed = True
749
844
  self._cancel_flush_timer()
750
845
  if self._spool_wal:
751
- # Spool mode: entries are already durable; close is a final
752
- # fsync boundary, NEVER a blocking network flush.
846
+ # DURABLE mode hands the drain to a DETACHED child that outlives
847
+ # this (possibly short-lived) process so its rows are delivered;
848
+ # close() makes NO in-process network call -- the child does the
849
+ # keystore + network work off this hot path (#1292). spool_only is
850
+ # replay-only and must NEVER live-send on close -- only fsync.
851
+ if self._spool_mode == _SPOOL_MODE_DURABLE:
852
+ self._spawn_detached_drain()
753
853
  self._spool_close()
754
854
  return
755
855
  self._flush_sync()
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "controlzero"
7
- version = "1.9.6"
7
+ version = "1.9.7"
8
8
  description = "AI agent governance: policies, audit, and observability for tool calls. Works locally with no signup."
9
9
  readme = "README.md"
10
10
  license = {text = "Apache-2.0"}
@@ -17,6 +17,8 @@ from __future__ import annotations
17
17
 
18
18
  import collections
19
19
  import json
20
+ import os
21
+ import sys
20
22
  import threading
21
23
  import time
22
24
  from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
@@ -211,6 +213,44 @@ def test_enrolled_sink_still_off_by_default(server, spool_env, monkeypatch):
211
213
  sink.close()
212
214
 
213
215
 
216
+ def test_enrolled_durable_close_drains_in_process(server, spool_env,
217
+ monkeypatch):
218
+ # #1292: the enrolled sink (opt-in, off the hosted hook hot path) cannot
219
+ # use `cz spool flush` (no api key), so on close it does a bounded,
220
+ # fail-open IN-PROCESS drain (blocking=True) rather than stranding a
221
+ # short-lived process's rows. Reverting close() to a bare fsync makes this
222
+ # test fail (no blocking drain observed).
223
+ monkeypatch.setenv("CONTROLZERO_SPOOL", "durable")
224
+
225
+ class FakeState:
226
+ machine_id = "11111111-2222-4333-8444-555555555555"
227
+
228
+ import controlzero.enrollment as enrollment
229
+ monkeypatch.setattr(enrollment, "load_state", lambda: FakeState())
230
+ monkeypatch.setattr(
231
+ enrollment, "sign_request",
232
+ lambda *_a, **_k: {"X-CZ-Machine-ID": FakeState.machine_id,
233
+ "X-CZ-Signature": "sig"})
234
+ sink = RemoteAuditSink(
235
+ api_url=server.url, machine_token="", org_id="org-1",
236
+ machine_id=FakeState.machine_id)
237
+ assert sink._spool is not None
238
+
239
+ blocking_calls = []
240
+ real_drain_once = sink._drain_once
241
+
242
+ def recording_drain_once(*a, **kw):
243
+ blocking_calls.append(bool(kw.get("blocking", False)))
244
+ return real_drain_once(*a, **kw)
245
+
246
+ monkeypatch.setattr(sink, "_drain_once", recording_drain_once)
247
+ sink.log(_entry(1))
248
+ sink.close()
249
+ # close() performed a blocking in-process drain (the daemon drain uses
250
+ # blocking=False, so a True here can only come from close()).
251
+ assert any(blocking_calls)
252
+
253
+
214
254
  def test_flag_off_401_still_clears_buffer_legacy(server, spool_env,
215
255
  monkeypatch):
216
256
  """The legacy (non-spool) 401 contract is unchanged: sink disables
@@ -304,7 +344,12 @@ def test_durable_wal_survives_network_down(server, spool_env, monkeypatch):
304
344
  sink.close()
305
345
 
306
346
 
307
- def test_durable_close_is_not_a_network_flush(server, spool_env, monkeypatch):
347
+ def test_durable_close_spawns_detached_drain(server, spool_env, monkeypatch):
348
+ # #1292: a short-lived hosted process must not strand its spooled rows.
349
+ # close() makes NO in-process network call (the agent hot path stays
350
+ # fast), but it MUST hand the drain to a DETACHED `controlzero spool
351
+ # flush` child that outlives this process so the rows are delivered.
352
+ # Reverting close() to a bare fsync makes this test fail (spawned == 0).
308
353
  monkeypatch.setenv("CONTROLZERO_SPOOL", "durable")
309
354
  sink = BearerAuditSink(api_url=server.url, api_key=API_KEY)
310
355
  import httpx
@@ -316,8 +361,18 @@ def test_durable_close_is_not_a_network_flush(server, spool_env, monkeypatch):
316
361
  raise httpx.ConnectError("down")
317
362
 
318
363
  monkeypatch.setattr(httpx, "post", counting_post)
364
+
365
+ spawned = []
366
+
367
+ class _FakePopen:
368
+ def __init__(self, argv, **kwargs):
369
+ spawned.append((argv, kwargs))
370
+
371
+ import subprocess
372
+ monkeypatch.setattr(subprocess, "Popen", _FakePopen)
373
+
319
374
  sink.log(_entry(1))
320
- # let the opportunistic drain fail
375
+ # let the opportunistic daemon drain settle (it fails against the dead net)
321
376
  deadline = time.monotonic() + 5
322
377
  while time.monotonic() < deadline:
323
378
  with sink._drain_state_lock:
@@ -327,9 +382,78 @@ def test_durable_close_is_not_a_network_flush(server, spool_env, monkeypatch):
327
382
  n_before = len(posted)
328
383
  t0 = time.monotonic()
329
384
  sink.close()
330
- # close() returned promptly and made NO network attempt of its own.
385
+ # close() returned promptly and made NO network attempt of its own ...
331
386
  assert time.monotonic() - t0 < 1.0
332
387
  assert len(posted) == n_before
388
+ # ... but it DID hand the drain to a detached `controlzero spool flush`.
389
+ assert len(spawned) == 1
390
+ argv, kwargs = spawned[0]
391
+ assert argv[0] == sys.executable
392
+ assert "spool" in argv and "flush" in argv
393
+ # the api key travels via env, NEVER on argv (no leak in `ps`).
394
+ assert API_KEY not in argv
395
+ assert kwargs["env"]["CONTROLZERO_API_KEY"] == API_KEY
396
+ # detached so it outlives this (possibly short-lived) process.
397
+ if os.name == "nt":
398
+ assert kwargs.get("creationflags", 0) != 0
399
+ else:
400
+ assert kwargs.get("start_new_session") is True
401
+
402
+
403
+ def test_durable_close_spawn_failure_is_fail_open(server, spool_env,
404
+ monkeypatch):
405
+ # If spawning the detached drainer fails, close() must NOT raise -- the
406
+ # rows stay durable in the WAL for a later drain.
407
+ monkeypatch.setenv("CONTROLZERO_SPOOL", "durable")
408
+ sink = BearerAuditSink(api_url=server.url, api_key=API_KEY)
409
+
410
+ import subprocess
411
+
412
+ def boom(*a, **kw):
413
+ raise OSError("cannot fork")
414
+
415
+ monkeypatch.setattr(subprocess, "Popen", boom)
416
+ sink.log(_entry(1))
417
+ sink.close() # must not raise
418
+
419
+
420
+ def test_durable_close_no_spawn_when_nothing_logged(server, spool_env,
421
+ monkeypatch):
422
+ # A pure-read process that never WAL-logged must not spawn a drainer.
423
+ monkeypatch.setenv("CONTROLZERO_SPOOL", "durable")
424
+ sink = BearerAuditSink(api_url=server.url, api_key=API_KEY)
425
+
426
+ spawned = []
427
+
428
+ class _FakePopen:
429
+ def __init__(self, argv, **kwargs):
430
+ spawned.append(argv)
431
+
432
+ import subprocess
433
+ monkeypatch.setattr(subprocess, "Popen", _FakePopen)
434
+ sink.close()
435
+ assert spawned == []
436
+
437
+
438
+ def test_spool_only_close_does_not_spawn_drain(server, spool_env, monkeypatch):
439
+ # spool_only is replay-only ("never live-send"): close() must NOT spawn a
440
+ # detached uploader even after logging -- only durable mode does (#1292).
441
+ # The close-drain is gated on _spool_mode == durable, NOT on _spool_wal
442
+ # (which is also true for spool_only); reverting that gate fails this test.
443
+ monkeypatch.setenv("CONTROLZERO_SPOOL", "spool_only")
444
+ sink = BearerAuditSink(api_url=server.url, api_key=API_KEY)
445
+
446
+ spawned = []
447
+
448
+ class _FakePopen:
449
+ def __init__(self, argv, **kwargs):
450
+ spawned.append(argv)
451
+
452
+ import subprocess
453
+ monkeypatch.setattr(subprocess, "Popen", _FakePopen)
454
+ sink.log(_entry(1))
455
+ sink.close()
456
+ assert spawned == []
333
457
 
334
458
 
335
459
  def test_durable_401_preserves_spool_and_keeps_logging(server, spool_env,
@@ -184,11 +184,13 @@ def test_hosted_mode_audit_pushed(mock_backend):
184
184
  client = Client(api_key="cz_live_e2e_test")
185
185
  client.guard("web_search", args={})
186
186
  client.guard("send_email", args={})
187
- # Hosted mode now defaults to the DURABLE encrypted spool (gh#1247):
188
- # each decision is WAL'd to encrypted disk and delivered by the
189
- # opportunistic async drain, so close() is a final fsync boundary,
190
- # NOT a blocking network flush. Delivery is guaranteed (WAL) but
191
- # asynchronous; poll for it instead of assuming a synchronous close.
187
+ # Hosted mode defaults to the DURABLE encrypted spool (gh#1247): each
188
+ # decision is WAL'd to encrypted disk and delivered by the opportunistic
189
+ # async drain. close() makes NO in-process network call; instead it hands
190
+ # the drain to a DETACHED `controlzero spool flush` child so a short-lived
191
+ # process does not strand its rows (gh#1292). Delivery is guaranteed (WAL)
192
+ # but asynchronous via the daemon drain and/or the detached child; poll for
193
+ # it instead of assuming a synchronous close.
192
194
  client.close()
193
195
 
194
196
  deadline = time.monotonic() + 8.0
File without changes
File without changes
File without changes
File without changes