controlzero 1.11.9__tar.gz → 1.12.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (286) hide show
  1. {controlzero-1.11.9 → controlzero-1.12.0}/CHANGELOG.md +32 -0
  2. {controlzero-1.11.9 → controlzero-1.12.0}/PKG-INFO +1 -1
  3. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/__init__.py +1 -1
  4. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/agent_config.py +50 -0
  5. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/debug_bundle.py +66 -13
  6. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/main.py +139 -20
  7. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/hosted_policy.py +133 -14
  8. {controlzero-1.11.9 → controlzero-1.12.0}/pyproject.toml +1 -1
  9. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_cli_debug_bundle.py +2 -1
  10. controlzero-1.12.0/tests/test_per_client_isolation_1011.py +251 -0
  11. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_pr4_status.py +21 -0
  12. {controlzero-1.11.9 → controlzero-1.12.0}/.gitignore +0 -0
  13. {controlzero-1.11.9 → controlzero-1.12.0}/Dockerfile.test +0 -0
  14. {controlzero-1.11.9 → controlzero-1.12.0}/LICENSE +0 -0
  15. {controlzero-1.11.9 → controlzero-1.12.0}/README.md +0 -0
  16. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/__main__.py +0 -0
  17. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/_internal/__init__.py +0 -0
  18. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/_internal/_messages.py +0 -0
  19. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/_internal/action_aliases.py +0 -0
  20. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/_internal/action_validator.py +0 -0
  21. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/_internal/bundle.py +0 -0
  22. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/_internal/credential_hook.py +0 -0
  23. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/_internal/credential_scanner.py +0 -0
  24. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/_internal/credentials_data/__init__.py +0 -0
  25. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/_internal/credentials_data/built_in.yaml +0 -0
  26. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/_internal/dlp_scanner.py +0 -0
  27. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/_internal/enforcer.py +0 -0
  28. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/_internal/hook_extractors.py +0 -0
  29. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/_internal/io_capture_producer.py +0 -0
  30. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/_internal/source_labels.py +0 -0
  31. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/_internal/tool_extractors.json +0 -0
  32. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/_internal/types.py +0 -0
  33. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/audit_local.py +0 -0
  34. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/audit_remote.py +0 -0
  35. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/canonical.py +0 -0
  36. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/__init__.py +0 -0
  37. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/__main__.py +0 -0
  38. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/_secrets.py +0 -0
  39. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/console.py +0 -0
  40. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/doctor.py +0 -0
  41. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/exec_cmd.py +0 -0
  42. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/hook_entry.py +0 -0
  43. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/hosts/__init__.py +0 -0
  44. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/hosts/antigravity.py +0 -0
  45. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/hosts/base.py +0 -0
  46. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/hosts/claude_code.py +0 -0
  47. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/hosts/codex_cli.py +0 -0
  48. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/hosts/cursor.py +0 -0
  49. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/hosts/cursor_cli.py +0 -0
  50. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/hosts/gemini_cli.py +0 -0
  51. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/hosts/kiro.py +0 -0
  52. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/hosts/unknown.py +0 -0
  53. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/kiro_adapter.py +0 -0
  54. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/migrate.py +0 -0
  55. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/output.py +0 -0
  56. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/spool_cmd.py +0 -0
  57. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/telemetry_consent.py +0 -0
  58. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/antigravity/hooks.json +0 -0
  59. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/antigravity.yaml +0 -0
  60. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/autogen.yaml +0 -0
  61. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/claude-code.yaml +0 -0
  62. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/codex-cli.yaml +0 -0
  63. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/cost-cap.yaml +0 -0
  64. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/crewai.yaml +0 -0
  65. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/cursor.yaml +0 -0
  66. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/gemini-cli.yaml +0 -0
  67. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/generic.yaml +0 -0
  68. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook +0 -0
  69. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/kiro/ide-pre-tool-use.kiro.hook +0 -0
  70. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/kiro/ide-prompt-submit.kiro.hook +0 -0
  71. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/kiro/kiro.yaml +0 -0
  72. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/langchain.yaml +0 -0
  73. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/mcp.yaml +0 -0
  74. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/rag.yaml +0 -0
  75. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/update_cmd.py +0 -0
  76. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/client.py +0 -0
  77. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/device.py +0 -0
  78. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/enrollment.py +0 -0
  79. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/error_codes.py +0 -0
  80. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/error_codes.yaml +0 -0
  81. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/errors.py +0 -0
  82. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/hitl/__init__.py +0 -0
  83. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/hitl/grant_protocol.py +0 -0
  84. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/hitl/mock.py +0 -0
  85. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/hitl/pending_approval.py +0 -0
  86. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/hitl/secret_leak_guard.py +0 -0
  87. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/hitl/status.py +0 -0
  88. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/hook_integrity.py +0 -0
  89. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/hooks/__init__.py +0 -0
  90. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/hooks/tool_output_handler.py +0 -0
  91. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/__init__.py +0 -0
  92. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/anthropic.py +0 -0
  93. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/autogen.py +0 -0
  94. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/braintrust.py +0 -0
  95. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/crewai/__init__.py +0 -0
  96. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/crewai/agent.py +0 -0
  97. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/crewai/crew.py +0 -0
  98. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/crewai/task.py +0 -0
  99. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/crewai/tool.py +0 -0
  100. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/google.py +0 -0
  101. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/google_adk/__init__.py +0 -0
  102. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/google_adk/agent.py +0 -0
  103. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/google_adk/tool.py +0 -0
  104. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/langchain/__init__.py +0 -0
  105. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/langchain/agent.py +0 -0
  106. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/langchain/callbacks.py +0 -0
  107. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/langchain/chain.py +0 -0
  108. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/langchain/graph.py +0 -0
  109. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/langchain/modern.py +0 -0
  110. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/langchain/tool.py +0 -0
  111. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/langfuse.py +0 -0
  112. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/litellm.py +0 -0
  113. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/openai.py +0 -0
  114. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/pydantic_ai.py +0 -0
  115. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/vercel_ai.py +0 -0
  116. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/layout_migration.py +0 -0
  117. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/policy_loader.py +0 -0
  118. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/secrets/__init__.py +0 -0
  119. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/secrets/reference.py +0 -0
  120. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/secrets/resolver.py +0 -0
  121. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/secrets/secretstr.py +0 -0
  122. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/spool/__init__.py +0 -0
  123. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/spool/_compress.py +0 -0
  124. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/spool/_constants.py +0 -0
  125. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/spool/_crc32c.py +0 -0
  126. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/spool/_crypto.py +0 -0
  127. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/spool/_frame.py +0 -0
  128. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/spool/_keyring.py +0 -0
  129. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/spool/_metrics.py +0 -0
  130. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/spool/_spool.py +0 -0
  131. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/spool/_state.py +0 -0
  132. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/spool/_uploader.py +0 -0
  133. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/spool/cz-audit-v1.dict +0 -0
  134. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/tamper.py +0 -0
  135. {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/tracecontext.py +0 -0
  136. {controlzero-1.11.9 → controlzero-1.12.0}/examples/hello_world.py +0 -0
  137. {controlzero-1.11.9 → controlzero-1.12.0}/tests/_fixtures/antigravity-envelope-expected.json +0 -0
  138. {controlzero-1.11.9 → controlzero-1.12.0}/tests/_fixtures/jcs_args_hash_vectors.json +0 -0
  139. {controlzero-1.11.9 → controlzero-1.12.0}/tests/conftest.py +0 -0
  140. {controlzero-1.11.9 → controlzero-1.12.0}/tests/integrations/__init__.py +0 -0
  141. {controlzero-1.11.9 → controlzero-1.12.0}/tests/integrations/test_google.py +0 -0
  142. {controlzero-1.11.9 → controlzero-1.12.0}/tests/parity/action_aliases.json +0 -0
  143. {controlzero-1.11.9 → controlzero-1.12.0}/tests/spool/__init__.py +0 -0
  144. {controlzero-1.11.9 → controlzero-1.12.0}/tests/spool/conftest.py +0 -0
  145. {controlzero-1.11.9 → controlzero-1.12.0}/tests/spool/test_spool_cli.py +0 -0
  146. {controlzero-1.11.9 → controlzero-1.12.0}/tests/spool/test_spool_concurrency.py +0 -0
  147. {controlzero-1.11.9 → controlzero-1.12.0}/tests/spool/test_spool_conformance.py +0 -0
  148. {controlzero-1.11.9 → controlzero-1.12.0}/tests/spool/test_spool_core.py +0 -0
  149. {controlzero-1.11.9 → controlzero-1.12.0}/tests/spool/test_spool_crash.py +0 -0
  150. {controlzero-1.11.9 → controlzero-1.12.0}/tests/spool/test_spool_diskfull.py +0 -0
  151. {controlzero-1.11.9 → controlzero-1.12.0}/tests/spool/test_spool_durable_default_tamper.py +0 -0
  152. {controlzero-1.11.9 → controlzero-1.12.0}/tests/spool/test_spool_keychain_dek.py +0 -0
  153. {controlzero-1.11.9 → controlzero-1.12.0}/tests/spool/test_spool_sink_wiring.py +0 -0
  154. {controlzero-1.11.9 → controlzero-1.12.0}/tests/spool/test_spool_transcript_localack.py +0 -0
  155. {controlzero-1.11.9 → controlzero-1.12.0}/tests/spool/test_spool_uploader.py +0 -0
  156. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_action_aliases.py +0 -0
  157. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_action_canonicalization.py +0 -0
  158. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_action_validator_t86.py +0 -0
  159. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_adapter_interface.py +0 -0
  160. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_agent_name_env.py +0 -0
  161. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_agy_docs_no_exit2_1373.py +0 -0
  162. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_antigravity_adapter.py +0 -0
  163. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_antigravity_envelope_parity.py +0 -0
  164. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_antigravity_ga_blockers_1248.py +0 -0
  165. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_antigravity_hook_check.py +0 -0
  166. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_antigravity_install.py +0 -0
  167. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_antigravity_posttooluse_observe_58.py +0 -0
  168. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_antigravity_tool_vocab_1303.py +0 -0
  169. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_api_key_mask.py +0 -0
  170. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_audit_project_id_attribution_pr2.py +0 -0
  171. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_audit_remote.py +0 -0
  172. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_audit_remote_io_capture.py +0 -0
  173. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_audit_remote_sdk_version.py +0 -0
  174. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_audit_sink_isolation.py +0 -0
  175. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_bundle_cache_freshness_1303.py +0 -0
  176. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_bundle_parser.py +0 -0
  177. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_bundle_translate.py +0 -0
  178. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_canonical_phase1a.py +0 -0
  179. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_capability_matrix_doc.py +0 -0
  180. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_cli_carve_out.py +0 -0
  181. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_cli_extractor_integration.py +0 -0
  182. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_cli_hook.py +0 -0
  183. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_cli_hook_entry.py +0 -0
  184. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_cli_hosted_refresh.py +0 -0
  185. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_cli_init.py +0 -0
  186. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_cli_init_templates.py +0 -0
  187. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_cli_tail.py +0 -0
  188. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_cli_test.py +0 -0
  189. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_cli_validate.py +0 -0
  190. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_coding_agent_hooks.py +0 -0
  191. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_conditions.py +0 -0
  192. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_config_format_parity_1303.py +0 -0
  193. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_conformance.py +0 -0
  194. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_console.py +0 -0
  195. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_credential_hook.py +0 -0
  196. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_cursor_adapter.py +0 -0
  197. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_default_action.py +0 -0
  198. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_device.py +0 -0
  199. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_dlp_mask.py +0 -0
  200. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_dlp_scanner.py +0 -0
  201. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_doctor.py +0 -0
  202. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_engine_version_consistency.py +0 -0
  203. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_enrollment.py +0 -0
  204. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_env_dump_438.py +0 -0
  205. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_epic_1247_bryan_acceptance.py +0 -0
  206. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_error_codes.py +0 -0
  207. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_errors_e_codes.py +0 -0
  208. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_fail_closed_eval.py +0 -0
  209. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_failopen_1303.py +0 -0
  210. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_glob_matching.py +0 -0
  211. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hitl_5d_email_install.py +0 -0
  212. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hitl_6a_cli_flag.py +0 -0
  213. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hitl_6a_exceptions.py +0 -0
  214. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hitl_6a_get_secret_hitl.py +0 -0
  215. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hitl_6a_mock_backend.py +0 -0
  216. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hitl_6a_pending_approval.py +0 -0
  217. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hitl_6a_request_approval.py +0 -0
  218. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hitl_6a_secret_leak_guard.py +0 -0
  219. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hitl_6a_wait.py +0 -0
  220. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hitl_conformance.py +0 -0
  221. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hitl_phase2b_protocol.py +0 -0
  222. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hitl_reason_codes.py +0 -0
  223. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hitl_validator_keys.py +0 -0
  224. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hook_extractors.py +0 -0
  225. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hook_integrity.py +0 -0
  226. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hook_mask_failclosed.py +0 -0
  227. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hosted_local_audit_1247.py +0 -0
  228. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hosted_local_shadow_warn_1265.py +0 -0
  229. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hosted_policy_e2e.py +0 -0
  230. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hosts_adapter.py +0 -0
  231. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hybrid_mode_strict.py +0 -0
  232. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hybrid_mode_warn.py +0 -0
  233. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_install_hook_command.py +0 -0
  234. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_install_hooks.py +0 -0
  235. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_io_capture_producer.py +0 -0
  236. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_kiro_adapter.py +0 -0
  237. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_kiro_cli_e2e.py +0 -0
  238. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_kiro_cli_hook_pin_1265.py +0 -0
  239. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_kiro_hook_templates.py +0 -0
  240. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_kiro_install.py +0 -0
  241. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_layout_migration_t101.py +0 -0
  242. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_layout_parity_t102.py +0 -0
  243. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_local_mode_dict.py +0 -0
  244. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_local_mode_file_json.py +0 -0
  245. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_local_mode_file_yaml.py +0 -0
  246. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_localized_reason.py +0 -0
  247. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_log_fallback_stderr.py +0 -0
  248. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_log_options_ignored_hosted.py +0 -0
  249. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_log_rotation.py +0 -0
  250. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_migrate.py +0 -0
  251. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_min_sdk_version_gate.py +0 -0
  252. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_multi_client_per_project_175.py +0 -0
  253. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_no_policy_no_key.py +0 -0
  254. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_observe_mode_1247.py +0 -0
  255. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_package_rename_shim.py +0 -0
  256. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_part3_active_policy_count_1303.py +0 -0
  257. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_policy_engine_version_phase1b.py +0 -0
  258. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_policy_freshness.py +0 -0
  259. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_policy_load_failclosed_f3.py +0 -0
  260. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_policy_settings.py +0 -0
  261. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_policy_source_audit.py +0 -0
  262. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_pr4_install_messaging.py +0 -0
  263. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_pr4_output.py +0 -0
  264. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_probe_audit_isolation.py +0 -0
  265. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_quarantine.py +0 -0
  266. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_reason_code.py +0 -0
  267. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_refresh.py +0 -0
  268. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_secrets.py +0 -0
  269. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_secrets_reference_resolver.py +0 -0
  270. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_source_labels.py +0 -0
  271. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_sql_semantic_class.py +0 -0
  272. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_synthetic_policy_id_t79.py +0 -0
  273. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_t103_precedence.py +0 -0
  274. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_t104_cache_gc.py +0 -0
  275. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_t108_local_override_audit.py +0 -0
  276. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_t96_single_audit_log.py +0 -0
  277. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_t99_install_prefetch_bundle.py +0 -0
  278. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_tamper.py +0 -0
  279. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_tamper_behavior.py +0 -0
  280. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_tamper_hook.py +0 -0
  281. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_telemetry_consent.py +0 -0
  282. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_tracecontext.py +0 -0
  283. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_unsafe_int_boundary.py +0 -0
  284. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_update_cmd.py +0 -0
  285. {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_upgrade_nudge.py +0 -0
  286. {controlzero-1.11.9 → controlzero-1.12.0}/tools/cz-kiro-adapter +0 -0
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.12.0 -- 2026-06-25 (per-agent key authority + per-agent bundle cache + whoami)
4
+
5
+ ### Added
6
+
7
+ - **`controlzero whoami`.** Prints which API key the SDK will actually use for a
8
+ surface (per-agent / env / global), its prefix only (never the full key), and
9
+ the project + org it resolves to. Diagnoses the most common "my audit logs are
10
+ missing" cause: a `CONTROLZERO_API_KEY` exported in the shell that maps to a
11
+ different project than the integration you installed, so events land in the
12
+ wrong tenant and the dashboard shows nothing.
13
+ - **Per-agent bundle cache.** Each governed surface now caches its policy bundle
14
+ under `~/.controlzero/integrations/<agent>/cache/` (next to the per-agent
15
+ `config.yaml`), so two surfaces on one machine each bound to their own project
16
+ no longer share a bundle. A read fallback keeps existing installs working on
17
+ upgrade: an empty per-agent cache transparently reads the legacy shared cache
18
+ until the first fetch re-caches it (no fail-closed on upgrade).
19
+
20
+ ### Changed
21
+
22
+ - **Per-agent key is now AUTHORITATIVE in the enforcement hook.** Hook-check
23
+ precedence becomes per-agent (for the active surface) > env > global. A key
24
+ installed via `install <agent> --api-key` / `kiro init --api-key` now wins over
25
+ a stale ambient `CONTROLZERO_API_KEY`, with a loud one-line warning when they
26
+ differ. This closes the silent wrong-tenant bug where a hook spooled + uploaded
27
+ + ingested correctly but attributed the events to whatever key was exported in
28
+ the shell, so the dashboard for the installed project showed "no audit entries".
29
+ Non-hook resolution (`status`, install-time display) is unchanged
30
+ (env > per-agent > global).
31
+ - **Cache bundle extension is now `.czpolicy`** (was `.bin`), matching the UI
32
+ policy export (both are the same signed bundle format). Legacy `.bin` caches
33
+ are still read and garbage-collected, so nothing breaks on upgrade.
34
+
3
35
  ## 1.11.9 -- 2026-06-25 (policy-posture remediation: honest hosted/local CLI, per-agent keys, key-validity diagnostics)
4
36
 
5
37
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: controlzero
3
- Version: 1.11.9
3
+ Version: 1.12.0
4
4
  Summary: AI agent governance: policies, audit, and observability for tool calls. Works locally with no signup.
5
5
  Project-URL: Homepage, https://controlzero.ai
6
6
  Project-URL: Documentation, https://docs.controlzero.ai
@@ -41,7 +41,7 @@ from controlzero.hitl.grant_protocol import (
41
41
  from controlzero.policy_loader import load_policy
42
42
  from controlzero.secrets import SecretRef, SecretStr, secret_ref
43
43
 
44
- __version__ = "1.11.9"
44
+ __version__ = "1.12.0"
45
45
 
46
46
  __all__ = [
47
47
  "Client",
@@ -169,3 +169,53 @@ def resolve_api_key(
169
169
  return global_key, "global"
170
170
 
171
171
  return None, "none"
172
+
173
+
174
+ def per_agent_api_key(
175
+ state_dir: Path, canonical_source: Optional[str]
176
+ ) -> Optional[str]:
177
+ """The api_key installed for THIS agent surface only -- no global fallback.
178
+
179
+ Unlike :func:`resolve_api_key` (env > per-agent > global), this returns
180
+ ONLY the per-agent ``integrations/<canonical_source>/config.yaml`` key, or
181
+ None. hook-check uses it to make a surface's installed key AUTHORITATIVE
182
+ over a stale ambient ``CONTROLZERO_API_KEY`` (#1011 follow-up): a surface
183
+ wired by ``install <agent> --api-key`` / ``kiro init --api-key`` must
184
+ attribute its audit to the project it was installed for, never to whatever
185
+ key happens to be exported in the shell. Never raises.
186
+ """
187
+ if not canonical_source:
188
+ return None
189
+ return _read_api_key_from_yaml(
190
+ per_agent_config_path(state_dir, canonical_source)
191
+ )
192
+
193
+
194
+ def resolve_hook_api_key(
195
+ state_dir: Path,
196
+ canonical_source: Optional[str],
197
+ *,
198
+ env: Optional[dict] = None,
199
+ ) -> tuple[Optional[str], str, bool]:
200
+ """Resolve the hook-check api_key with PER-AGENT-AUTHORITATIVE precedence.
201
+
202
+ Unlike :func:`resolve_api_key` (env > per-agent > global), the enforcement
203
+ hook makes a surface's installed key win over a stale ambient env var
204
+ (#1011 follow-up): per-agent > env > global. This is what fixes the silent
205
+ wrong-tenant bug. Returns ``(api_key_or_None, source_label, overrode_env)``
206
+ where ``source_label`` is ``"per_agent"`` / ``"env"`` / ``"global"`` /
207
+ ``"none"`` and ``overrode_env`` is True when a per-agent key replaced a
208
+ DIFFERENT ambient ``CONTROLZERO_API_KEY`` (the case the hook warns about).
209
+ Never raises.
210
+ """
211
+ env = env if env is not None else os.environ
212
+ ambient = env.get("CONTROLZERO_API_KEY")
213
+ per_agent = per_agent_api_key(state_dir, canonical_source)
214
+ if per_agent:
215
+ return per_agent, "per_agent", bool(ambient and ambient != per_agent)
216
+ if ambient:
217
+ return ambient, "env", False
218
+ global_key = _read_api_key_from_yaml(state_dir / "config.yaml")
219
+ if global_key:
220
+ return global_key, "global", False
221
+ return None, "none", False
@@ -10,7 +10,8 @@ What it does
10
10
 
11
11
  1. Reads ``~/.controlzero/cache/bootstrap-<prefix>.json`` to get the
12
12
  project encryption key + signing public key.
13
- 2. Reads ``~/.controlzero/cache/bundle-<prefix>.bin`` for the raw blob.
13
+ 2. Reads ``bundle-<prefix>.czpolicy`` (legacy ``.bin``) for the raw blob,
14
+ from the shared cache or a per-client ``integrations/<slug>/cache/`` dir.
14
15
  3. Calls :func:`controlzero._internal.bundle.parse_bundle` to verify
15
16
  the signature, decrypt the AES-GCM payload, and decode the JSON.
16
17
  4. Prints a human-readable summary (bundle id / created_at / expires_at,
@@ -133,30 +134,79 @@ def _api_key_prefix(api_key: str) -> str:
133
134
  return api_key[:_KEY_SCOPE_LEN].replace("/", "_").replace(".", "_")
134
135
 
135
136
 
137
+ def _all_cache_dirs() -> list[Path]:
138
+ """Per-client integration cache dirs FIRST, then the shared cache dir.
139
+
140
+ #1011 follow-up: a surface's bundle now lives under
141
+ ~/.controlzero/integrations/<slug>/cache/. Integration dirs are listed
142
+ before the legacy shared ~/.controlzero/cache/ so the debug tool prefers
143
+ the bundle the hook actually enforces over a stale shared-cache entry.
144
+ """
145
+ dirs: list[Path] = []
146
+ integ = Path.home() / ".controlzero" / "integrations"
147
+ if integ.exists():
148
+ for sub in sorted(integ.iterdir()):
149
+ c = sub / "cache"
150
+ if c.is_dir():
151
+ dirs.append(c)
152
+ dirs.append(CACHE_DIR)
153
+ return dirs
154
+
155
+
136
156
  def _resolve_cache_for_prefix(prefix: str) -> CacheLocation:
157
+ # The bundle for a prefix may live in the shared cache or under a per-client
158
+ # integration cache. Prefer a dir that actually holds the BUNDLE (the policy
159
+ # the hook enforces), integration caches before the shared cache, and the
160
+ # current .czpolicy over a legacy .bin. A migrated install can have a stale
161
+ # bootstrap/old bundle in the shared cache plus the live bundle under
162
+ # integrations/<slug>/cache; preferring the bundle avoids inspecting the
163
+ # stale shared copy.
164
+ dirs = _all_cache_dirs()
165
+ for d in dirs:
166
+ czp = d / f"bundle-{prefix}.czpolicy"
167
+ legacy = d / f"bundle-{prefix}.bin"
168
+ if czp.exists() or legacy.exists():
169
+ bundle_path = czp if czp.exists() else legacy
170
+ return CacheLocation(
171
+ prefix=prefix,
172
+ bootstrap_path=d / f"bootstrap-{prefix}.json",
173
+ bundle_path=bundle_path,
174
+ )
175
+ # No bundle anywhere -- fall back to a dir that at least has the bootstrap
176
+ # (so the not-found message points at a real bootstrap), else the shared
177
+ # cache with the current extension.
178
+ for d in dirs:
179
+ boot = d / f"bootstrap-{prefix}.json"
180
+ if boot.exists():
181
+ return CacheLocation(
182
+ prefix=prefix,
183
+ bootstrap_path=boot,
184
+ bundle_path=d / f"bundle-{prefix}.czpolicy",
185
+ )
137
186
  return CacheLocation(
138
187
  prefix=prefix,
139
188
  bootstrap_path=CACHE_DIR / f"bootstrap-{prefix}.json",
140
- bundle_path=CACHE_DIR / f"bundle-{prefix}.bin",
189
+ bundle_path=CACHE_DIR / f"bundle-{prefix}.czpolicy",
141
190
  )
142
191
 
143
192
 
144
193
  def _autodetect_cache() -> Optional[CacheLocation]:
145
- """Pick a single bootstrap+bundle pair under ``CACHE_DIR``.
194
+ """Pick a single bootstrap+bundle pair across all cache dirs.
146
195
 
147
196
  Used when the operator did not specify ``--api-key-prefix`` and we
148
197
  cannot find an API key in env / config. Returns the unique pair
149
- when there is exactly one; returns None when there are zero or
150
- several (caller surfaces a helpful error in either case).
198
+ when there is exactly one (across the shared + per-client caches);
199
+ returns None when there are zero or several (caller surfaces a
200
+ helpful error in either case).
151
201
  """
152
- if not CACHE_DIR.exists():
202
+ prefixes: list[str] = []
203
+ for d in _all_cache_dirs():
204
+ for b in sorted(d.glob("bootstrap-*.json")):
205
+ prefixes.append(b.name[len("bootstrap-") : -len(".json")])
206
+ prefixes = sorted(set(prefixes))
207
+ if len(prefixes) != 1:
153
208
  return None
154
- bootstraps = sorted(CACHE_DIR.glob("bootstrap-*.json"))
155
- if len(bootstraps) != 1:
156
- return None
157
- name = bootstraps[0].name # bootstrap-<prefix>.json
158
- prefix = name[len("bootstrap-") : -len(".json")]
159
- return _resolve_cache_for_prefix(prefix)
209
+ return _resolve_cache_for_prefix(prefixes[0])
160
210
 
161
211
 
162
212
  # --- Bundle load ----------------------------------------------------------
@@ -478,7 +528,10 @@ def debug_bundle_cmd(api_key_prefix: Optional[str], simulate: Optional[str]):
478
528
  " Set CONTROLZERO_API_KEY=cz_live_... or pass --api-key-prefix.",
479
529
  err=True,
480
530
  )
481
- click.echo(f" Looked under: {CACHE_DIR}", err=True)
531
+ click.echo(
532
+ f" Looked under: {', '.join(str(d) for d in _all_cache_dirs())}",
533
+ err=True,
534
+ )
482
535
  sys.exit(1)
483
536
 
484
537
  # 2. Load bootstrap (keys + safe metadata).
@@ -541,6 +541,94 @@ def heartbeat_cmd():
541
541
  click.echo(" tamper : none (hooks intact)")
542
542
 
543
543
 
544
+ @cli.command("whoami")
545
+ @click.option(
546
+ "--client",
547
+ default=None,
548
+ help="Integration surface to inspect (e.g. kiro_cli, kiro_ide). "
549
+ "Defaults to CONTROLZERO_CLIENT.",
550
+ )
551
+ def whoami_cmd(client: Optional[str]) -> None:
552
+ """Show which API key the SDK will use and the project/org it maps to.
553
+
554
+ Diagnoses the most common "my audit logs are missing" cause: a
555
+ CONTROLZERO_API_KEY exported in the shell that maps to a DIFFERENT project
556
+ than the integration you installed, so events land in the wrong tenant and
557
+ the dashboard shows nothing. The key is never printed in full -- only its
558
+ prefix, its source, and the project/org it resolves to.
559
+ """
560
+ from controlzero._internal.source_labels import canonicalize_source
561
+
562
+ client = client or os.environ.get("CONTROLZERO_CLIENT")
563
+ canon = canonicalize_source(client) if client else None
564
+ canon = canon if canon and canon != "unknown" else None
565
+
566
+ # Use the SAME precedence the hook uses (per-agent authoritative > env >
567
+ # global) so whoami reports exactly what enforcement will do.
568
+ env_key = os.environ.get("CONTROLZERO_API_KEY")
569
+ effective, label, _overrode = agent_config.resolve_hook_api_key(
570
+ GLOBAL_POLICY_DIR, canon
571
+ )
572
+ if not effective:
573
+ click.echo(
574
+ "No Control Zero API key configured (no integration config, no "
575
+ "CONTROLZERO_API_KEY, no ~/.controlzero/config.yaml). Local-file "
576
+ "policy mode."
577
+ )
578
+ click.echo(
579
+ "Run `controlzero install <agent> --api-key cz_...` or "
580
+ "`controlzero kiro init --api-key cz_...` to enable hosted "
581
+ "policy + audit sync."
582
+ )
583
+ return
584
+ source = {
585
+ "per_agent": f"integration config (integrations/{canon}/config.yaml)",
586
+ "env": "CONTROLZERO_API_KEY environment variable",
587
+ "global": "~/.controlzero/config.yaml",
588
+ }.get(label, label)
589
+
590
+ click.echo(f"Client surface: {canon or '(none)'}")
591
+ click.echo(f"Effective API key: {effective[:12]}... (source: {source})")
592
+ if env_key and effective != env_key:
593
+ click.echo(
594
+ "WARNING: a DIFFERENT CONTROLZERO_API_KEY is set in your shell. The "
595
+ "key above takes precedence for this surface; the shell key would "
596
+ "attribute audit to its own project. Unset it to avoid confusion."
597
+ )
598
+
599
+ # Resolve project/org via the per-surface cache, then a live call.
600
+ _prev = os.environ.get("CONTROLZERO_CLIENT")
601
+ if canon:
602
+ os.environ["CONTROLZERO_CLIENT"] = canon
603
+ try:
604
+ from controlzero.hosted_policy import (
605
+ get_or_fetch_bootstrap,
606
+ load_cached_bootstrap,
607
+ )
608
+ keys = load_cached_bootstrap(effective)
609
+ origin = "cache"
610
+ if keys is None:
611
+ keys = get_or_fetch_bootstrap(effective)
612
+ origin = "backend"
613
+ click.echo(f"Project: {keys.project_id}")
614
+ click.echo(f"Org: {keys.org_id or '(unknown)'} (from {origin})")
615
+ click.echo(
616
+ "Audit events from this surface are attributed to that project."
617
+ )
618
+ except Exception as exc: # noqa: BLE001
619
+ click.echo(f"Could not resolve the project/org for this key: {exc}")
620
+ click.echo(
621
+ "Check the key validity and network reachability to "
622
+ "https://api.controlzero.ai."
623
+ )
624
+ finally:
625
+ if canon:
626
+ if _prev is None:
627
+ os.environ.pop("CONTROLZERO_CLIENT", None)
628
+ else:
629
+ os.environ["CONTROLZERO_CLIENT"] = _prev
630
+
631
+
544
632
  @cli.command("policy-pull")
545
633
  def policy_pull_cmd():
546
634
  """Pull the latest policy bundle from the backend.
@@ -930,10 +1018,27 @@ def status_cmd(check_key: bool):
930
1018
  hosted = bool(resolved_key)
931
1019
  unenrolled_first_run = not (api_key_env or api_key_config or enrolled)
932
1020
 
1021
+ # If the env/global default is local but individual agents are bound to
1022
+ # their own hosted keys, the machine is in a MIXED posture. Report that
1023
+ # instead of a flat LOCAL, which would understate that some agents are
1024
+ # actually enforcing a hosted bundle (the bindings table below names them).
1025
+ any_per_agent_key = any(
1026
+ agent_config._read_api_key_from_yaml(
1027
+ agent_config.per_agent_config_path(GLOBAL_POLICY_DIR, slug)
1028
+ )
1029
+ for slug in _KNOWN_AGENT_SLUGS
1030
+ )
1031
+ if hosted:
1032
+ mode = "HOSTED"
1033
+ elif any_per_agent_key:
1034
+ mode = "MIXED (local default; some agents hosted -- see bindings)"
1035
+ else:
1036
+ mode = "LOCAL"
1037
+
933
1038
  cli_output.emit(Severity.INFO, "Control Zero status")
934
1039
  facts = {
935
1040
  "state dir": str(GLOBAL_POLICY_DIR),
936
- "mode": "HOSTED" if hosted else "LOCAL",
1041
+ "mode": mode,
937
1042
  "config source": resolved_source,
938
1043
  "key (masked, env/global default)": _status_mask_key(resolved_key),
939
1044
  "enrolled": enrolled,
@@ -1567,18 +1672,35 @@ def hook_check(policy: Optional[str], strict_flag: Optional[bool]):
1567
1672
  # key in config.yaml but not in env fell through to the local-file
1568
1673
  # path on every hook-check.
1569
1674
  #
1570
- # PR4-F precedence (highest first): explicit env var > per-agent
1571
- # integrations/<canonical_source>/config.yaml > global config.yaml. The
1572
- # slug helper is SHARED with the install-write path (agent_config), so a
1573
- # key written by `install <agent> --api-key` is found here under the same
1574
- # adapter. A malformed per-agent file degrades to global (never crashes).
1575
- if not os.environ.get("CONTROLZERO_API_KEY"):
1576
- _resolved_key, _ = agent_config.resolve_api_key(
1577
- GLOBAL_POLICY_DIR,
1578
- getattr(_adapter, "canonical_source", None),
1675
+ # Precedence (#1011 follow-up, per-client isolation): the key installed
1676
+ # FOR THIS agent surface (integrations/<canonical_source>/config.yaml) is
1677
+ # AUTHORITATIVE -- it wins over a stale ambient CONTROLZERO_API_KEY so the
1678
+ # surface's audit lands in the project it was installed for, not whatever
1679
+ # key happens to be exported in the shell. That stale-env-hijack was the
1680
+ # silent wrong-tenant bug: a Kiro hook spooled + uploaded + ingested fine,
1681
+ # but the events were attributed to the env key's org, so the dashboard for
1682
+ # the installed project showed "no audit entries". Only when there is NO
1683
+ # per-agent key do we keep the env > global precedence (the PR4-F default).
1684
+ # The slug helper is SHARED with the install-write path (agent_config), so
1685
+ # a key written by `install <agent> --api-key` is found here under the same
1686
+ # adapter. A malformed per-agent file degrades to env/global (never crashes).
1687
+ _canon_source = getattr(_adapter, "canonical_source", None)
1688
+ _eff_key, _eff_src, _overrode_env = agent_config.resolve_hook_api_key(
1689
+ GLOBAL_POLICY_DIR, _canon_source
1690
+ )
1691
+ if _overrode_env:
1692
+ # Make the override LOUD (mirrors the hosted/local shadow warning): a
1693
+ # stale shell key silently stealing a surface's audit is exactly what
1694
+ # we are closing. Never print the key itself.
1695
+ click.echo(
1696
+ "[Control Zero] Using the API key installed for this integration; "
1697
+ "ignoring a different CONTROLZERO_API_KEY found in the environment "
1698
+ "(audit is attributed to the installed key's project). Run "
1699
+ "`controlzero whoami` to confirm.",
1700
+ err=True,
1579
1701
  )
1580
- if _resolved_key:
1581
- os.environ["CONTROLZERO_API_KEY"] = _resolved_key
1702
+ if _eff_key:
1703
+ os.environ["CONTROLZERO_API_KEY"] = _eff_key
1582
1704
 
1583
1705
  # T103 review (B1, B2): the CLI hook-check is a non-interactive
1584
1706
  # subprocess that Claude Code / Gemini CLI / Codex CLI spawn fresh
@@ -2836,17 +2958,14 @@ def _warn_no_deny_rules(agent_name: str, template_path: Path) -> None:
2836
2958
 
2837
2959
  Also shows what deny rules the template WOULD have added.
2838
2960
  """
2839
- click.secho(
2840
- f"WARNING: Your existing policy at {GLOBAL_POLICY_PATH} has NO deny rules.",
2841
- fg="yellow",
2842
- bold=True,
2843
- err=True,
2961
+ cli_output.emit(
2962
+ Severity.WARNING,
2963
+ f"Your existing policy at {GLOBAL_POLICY_PATH} has NO deny rules.",
2844
2964
  )
2845
- click.secho(
2965
+ cli_output.emit(
2966
+ Severity.WARNING,
2846
2967
  f"The {agent_name} hook will ALLOW ALL tool calls. "
2847
2968
  "This may not be what you want.",
2848
- fg="yellow",
2849
- err=True,
2850
2969
  )
2851
2970
  click.echo("", err=True)
2852
2971
  click.echo(
@@ -138,13 +138,71 @@ def get_api_url() -> str:
138
138
  return os.environ.get("CONTROLZERO_API_URL", DEFAULT_API_URL).rstrip("/")
139
139
 
140
140
 
141
- def _cache_dir() -> Path:
142
- """Resolve the cache directory, creating it if missing."""
143
- p = Path.home() / ".controlzero" / "cache"
141
+ def _integration_slug(client: Optional[str] = None) -> Optional[str]:
142
+ """Canonical integration slug for per-client cache isolation, or None.
143
+
144
+ Per-client isolation (#1011 follow-up): when a hook-check runs for a
145
+ specific client surface, ``cli.main`` pins ``CONTROLZERO_CLIENT`` to the
146
+ adapter's canonical source BEFORE the policy load, so each surface's cache
147
+ lives under ``~/.controlzero/integrations/<slug>/cache/`` instead of one
148
+ shared directory. Two surfaces on one machine (each installed with its own
149
+ project's key) then never share a bundle cache. Returns ``None`` for a
150
+ library caller with no client -- the legacy shared ``~/.controlzero/cache/``
151
+ layout.
152
+ """
153
+ raw = client if client is not None else os.environ.get("CONTROLZERO_CLIENT")
154
+ if not raw:
155
+ return None
156
+ # Lazy import: source_labels is pure data, but importing it lazily keeps
157
+ # the hosted_policy import graph flat (same pattern as __version__ below).
158
+ from controlzero._internal.source_labels import canonicalize_source
159
+ slug = canonicalize_source(raw)
160
+ return slug if slug != "unknown" else None
161
+
162
+
163
+ def _cache_dir(client: Optional[str] = None) -> Path:
164
+ """Resolve the cache directory, creating it if missing.
165
+
166
+ When a client integration is active (see :func:`_integration_slug`) the
167
+ cache is namespaced under ``~/.controlzero/integrations/<slug>/cache/`` so
168
+ each governed surface keeps its own bundle; otherwise the shared
169
+ ``~/.controlzero/cache/`` layout is used.
170
+ """
171
+ slug = _integration_slug(client)
172
+ if slug:
173
+ p = Path.home() / ".controlzero" / "integrations" / slug / "cache"
174
+ else:
175
+ p = Path.home() / ".controlzero" / "cache"
144
176
  p.mkdir(parents=True, exist_ok=True)
145
177
  return p
146
178
 
147
179
 
180
+ def _cache_read_dirs(client: Optional[str] = None) -> list[Path]:
181
+ """Cache dirs to READ from, in priority order.
182
+
183
+ Writes always go to the active (per-client when set) dir via
184
+ :func:`_cache_dir`. Reads additionally fall back to the legacy shared
185
+ ~/.controlzero/cache/ so an EXISTING install keeps serving its
186
+ last-known-good bundle on first run after upgrading to per-client caches
187
+ (#1011 follow-up) -- it would otherwise see an empty per-client cache and,
188
+ if the backend were briefly unreachable, fail closed. The bundle is still
189
+ signature-verified on parse, so the fallback never weakens enforcement.
190
+
191
+ The legacy dir is derived from the active dir's <root>/integrations/<slug>/
192
+ cache shape, NOT from Path.home() independently, so a test that points
193
+ _cache_dir at an arbitrary throwaway dir does not leak onto the real home.
194
+ """
195
+ # Call _cache_dir() with no positional arg on the common (env-driven) path
196
+ # so a test that monkeypatches _cache_dir with a zero-arg stub still works.
197
+ primary = _cache_dir() if client is None else _cache_dir(client)
198
+ parts = primary.parts
199
+ if len(parts) >= 4 and parts[-1] == "cache" and parts[-3] == "integrations":
200
+ legacy = primary.parent.parent.parent / "cache"
201
+ if legacy != primary:
202
+ return [primary, legacy]
203
+ return [primary]
204
+
205
+
148
206
  def _key_scope(api_key: str) -> str:
149
207
  """Return a filename-safe scope identifier for the given API key."""
150
208
  if not api_key:
@@ -171,9 +229,29 @@ def _bootstrap_cache_path(api_key: str) -> Path:
171
229
 
172
230
 
173
231
  def load_cached_bootstrap(api_key: str) -> Optional[BootstrapKeys]:
174
- path = _bootstrap_cache_path(api_key)
175
- if not path.exists():
176
- return None
232
+ # Read from the active (per-client) dir first, then the legacy shared dir
233
+ # as a migration fallback (#1011 follow-up). When served from the fallback,
234
+ # copy into the active dir so the per-agent cache stops depending on the
235
+ # shared one (see load_cached_bundle for the rationale).
236
+ scope = _key_scope(api_key)
237
+ read_dirs = _cache_read_dirs()
238
+ primary = read_dirs[0]
239
+ for d in read_dirs:
240
+ path = d / f"bootstrap-{scope}.json"
241
+ if not path.exists():
242
+ continue
243
+ keys = _read_bootstrap_file(path)
244
+ if keys is not None:
245
+ if d != primary:
246
+ try:
247
+ save_cached_bootstrap(api_key, keys)
248
+ except OSError:
249
+ pass
250
+ return keys
251
+ return None
252
+
253
+
254
+ def _read_bootstrap_file(path: Path) -> Optional[BootstrapKeys]:
177
255
  try:
178
256
  data = json.loads(path.read_text(encoding="utf-8"))
179
257
  return BootstrapKeys(
@@ -252,11 +330,14 @@ def _extract_scope_from_cache_filename(name: str) -> Optional[str]:
252
330
 
253
331
  Matches:
254
332
  bootstrap-<scope>.json
255
- bundle-<scope>.bin
333
+ bundle-<scope>.czpolicy (current)
334
+ bundle-<scope>.bin (legacy -- still recognised so a key
335
+ rotation GCs pre-.czpolicy caches)
256
336
  bundle-<scope>.meta
257
337
  """
258
338
  for prefix, suffix in (
259
339
  ("bootstrap-", ".json"),
340
+ ("bundle-", ".czpolicy"),
260
341
  ("bundle-", ".bin"),
261
342
  ("bundle-", ".meta"),
262
343
  ):
@@ -363,20 +444,58 @@ class CachedBundle:
363
444
 
364
445
  def _bundle_cache_paths(api_key: str) -> tuple[Path, Path]:
365
446
  scope = _key_scope(api_key)
366
- bin_path = _cache_dir() / f"bundle-{scope}.bin"
367
- meta_path = _cache_dir() / f"bundle-{scope}.meta"
368
- return bin_path, meta_path
447
+ d = _cache_dir()
448
+ # #1011 follow-up: the cached bundle uses the .czpolicy extension to match
449
+ # the UI policy export, so the auto-downloaded and hand-exported bundles
450
+ # share one recognizable suffix (both are the same signed CZ01 format).
451
+ bundle_path = d / f"bundle-{scope}.czpolicy"
452
+ meta_path = d / f"bundle-{scope}.meta"
453
+ return bundle_path, meta_path
369
454
 
370
455
 
371
456
  def load_cached_bundle(api_key: str) -> Optional[CachedBundle]:
372
- bin_path, meta_path = _bundle_cache_paths(api_key)
373
- if not bin_path.exists() or not meta_path.exists():
374
- return None
457
+ # Read from the active (per-client) dir first, then the legacy shared dir
458
+ # (#1011 follow-up); within each dir prefer the current .czpolicy bundle and
459
+ # fall back to a legacy .bin so a pre-#1011 cache stays readable until the
460
+ # next successful fetch re-caches it under the new name/dir.
461
+ scope = _key_scope(api_key)
462
+ read_dirs = _cache_read_dirs()
463
+ primary = read_dirs[0]
464
+ for d in read_dirs:
465
+ meta_path = d / f"bundle-{scope}.meta"
466
+ if not meta_path.exists():
467
+ continue
468
+ bin_path = d / f"bundle-{scope}.czpolicy"
469
+ if not bin_path.exists():
470
+ bin_path = d / f"bundle-{scope}.bin"
471
+ if not bin_path.exists():
472
+ continue
473
+ loaded = _read_cached_bundle_pair(bin_path, meta_path)
474
+ if loaded is not None:
475
+ if d != primary:
476
+ # Migrate-on-read: copy the last-known-good bundle from the
477
+ # legacy shared cache into the active per-agent cache so the
478
+ # surface stops depending on the shared dir. Without this, the
479
+ # common upgrade path (next pull is a 304, so load_hosted_policy
480
+ # serves this cached bundle and never calls save_cached_bundle)
481
+ # would leave the per-agent cache empty indefinitely -- and if
482
+ # the shared cache were later GC'd, the surface would lose its
483
+ # last-known-good. Best-effort: a failed copy still serves the
484
+ # read. The bundle is re-written under the .czpolicy name.
485
+ try:
486
+ save_cached_bundle(api_key, loaded.bytes_, loaded.etag)
487
+ except OSError:
488
+ pass
489
+ return loaded
490
+ return None
491
+
492
+
493
+ def _read_cached_bundle_pair(bin_path: Path, meta_path: Path) -> Optional[CachedBundle]:
375
494
  try:
376
495
  blob = bin_path.read_bytes()
377
496
  meta = json.loads(meta_path.read_text(encoding="utf-8"))
378
497
  stored_checksum = str(meta.get("checksum", ""))
379
- # #1303 residual B (cache integrity): verify the .bin blob still
498
+ # #1303 residual B (cache integrity): verify the cached blob still
380
499
  # matches the sha256 checksum recorded in .meta. A mismatch means the
381
500
  # cached bundle on disk was tampered with, truncated, or corrupted
382
501
  # since it was written. We NEVER silently trust such a blob -- a
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "controlzero"
7
- version = "1.11.9"
7
+ version = "1.12.0"
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"}
@@ -345,7 +345,8 @@ def test_missing_bundle_file_emits_helpful_error(tmp_path, monkeypatch):
345
345
  )
346
346
  assert result.exit_code == 2, result.output
347
347
  assert "bundle file not found" in result.output
348
- assert f"bundle-{prefix}.bin" in result.output
348
+ # #1011 follow-up: the default cache bundle extension is now .czpolicy.
349
+ assert f"bundle-{prefix}.czpolicy" in result.output
349
350
 
350
351
 
351
352
  def test_output_does_not_leak_encryption_key_or_signing_pubkey(cache_with_bundle):