controlzero 1.11.9__tar.gz → 1.13.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 (289) hide show
  1. {controlzero-1.11.9 → controlzero-1.13.0}/CHANGELOG.md +68 -0
  2. {controlzero-1.11.9 → controlzero-1.13.0}/PKG-INFO +1 -1
  3. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/__init__.py +1 -1
  4. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/_internal/bundle.py +17 -0
  5. controlzero-1.13.0/controlzero/_internal/condition_ops.py +226 -0
  6. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/_internal/enforcer.py +127 -3
  7. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/_internal/io_capture_producer.py +16 -9
  8. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/agent_config.py +50 -0
  9. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/debug_bundle.py +66 -13
  10. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/main.py +139 -20
  11. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/hosted_policy.py +133 -14
  12. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/policy_loader.py +53 -1
  13. {controlzero-1.11.9 → controlzero-1.13.0}/pyproject.toml +1 -1
  14. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_cli_debug_bundle.py +2 -1
  15. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_coding_agent_hooks.py +11 -8
  16. controlzero-1.13.0/tests/test_condition_operators_1495.py +192 -0
  17. controlzero-1.13.0/tests/test_deny_rule_reconciliation_1011.py +247 -0
  18. controlzero-1.13.0/tests/test_per_client_isolation_1011.py +251 -0
  19. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_pr4_status.py +21 -0
  20. {controlzero-1.11.9 → controlzero-1.13.0}/.gitignore +0 -0
  21. {controlzero-1.11.9 → controlzero-1.13.0}/Dockerfile.test +0 -0
  22. {controlzero-1.11.9 → controlzero-1.13.0}/LICENSE +0 -0
  23. {controlzero-1.11.9 → controlzero-1.13.0}/README.md +0 -0
  24. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/__main__.py +0 -0
  25. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/_internal/__init__.py +0 -0
  26. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/_internal/_messages.py +0 -0
  27. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/_internal/action_aliases.py +0 -0
  28. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/_internal/action_validator.py +0 -0
  29. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/_internal/credential_hook.py +0 -0
  30. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/_internal/credential_scanner.py +0 -0
  31. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/_internal/credentials_data/__init__.py +0 -0
  32. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/_internal/credentials_data/built_in.yaml +0 -0
  33. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/_internal/dlp_scanner.py +0 -0
  34. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/_internal/hook_extractors.py +0 -0
  35. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/_internal/source_labels.py +0 -0
  36. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/_internal/tool_extractors.json +0 -0
  37. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/_internal/types.py +0 -0
  38. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/audit_local.py +0 -0
  39. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/audit_remote.py +0 -0
  40. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/canonical.py +0 -0
  41. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/__init__.py +0 -0
  42. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/__main__.py +0 -0
  43. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/_secrets.py +0 -0
  44. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/console.py +0 -0
  45. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/doctor.py +0 -0
  46. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/exec_cmd.py +0 -0
  47. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/hook_entry.py +0 -0
  48. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/hosts/__init__.py +0 -0
  49. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/hosts/antigravity.py +0 -0
  50. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/hosts/base.py +0 -0
  51. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/hosts/claude_code.py +0 -0
  52. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/hosts/codex_cli.py +0 -0
  53. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/hosts/cursor.py +0 -0
  54. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/hosts/cursor_cli.py +0 -0
  55. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/hosts/gemini_cli.py +0 -0
  56. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/hosts/kiro.py +0 -0
  57. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/hosts/unknown.py +0 -0
  58. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/kiro_adapter.py +0 -0
  59. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/migrate.py +0 -0
  60. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/output.py +0 -0
  61. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/spool_cmd.py +0 -0
  62. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/telemetry_consent.py +0 -0
  63. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/templates/antigravity/hooks.json +0 -0
  64. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/templates/antigravity.yaml +0 -0
  65. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/templates/autogen.yaml +0 -0
  66. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/templates/claude-code.yaml +0 -0
  67. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/templates/codex-cli.yaml +0 -0
  68. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/templates/cost-cap.yaml +0 -0
  69. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/templates/crewai.yaml +0 -0
  70. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/templates/cursor.yaml +0 -0
  71. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/templates/gemini-cli.yaml +0 -0
  72. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/templates/generic.yaml +0 -0
  73. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook +0 -0
  74. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/templates/kiro/ide-pre-tool-use.kiro.hook +0 -0
  75. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/templates/kiro/ide-prompt-submit.kiro.hook +0 -0
  76. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/templates/kiro/kiro.yaml +0 -0
  77. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/templates/langchain.yaml +0 -0
  78. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/templates/mcp.yaml +0 -0
  79. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/templates/rag.yaml +0 -0
  80. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/cli/update_cmd.py +0 -0
  81. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/client.py +0 -0
  82. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/device.py +0 -0
  83. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/enrollment.py +0 -0
  84. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/error_codes.py +0 -0
  85. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/error_codes.yaml +0 -0
  86. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/errors.py +0 -0
  87. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/hitl/__init__.py +0 -0
  88. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/hitl/grant_protocol.py +0 -0
  89. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/hitl/mock.py +0 -0
  90. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/hitl/pending_approval.py +0 -0
  91. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/hitl/secret_leak_guard.py +0 -0
  92. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/hitl/status.py +0 -0
  93. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/hook_integrity.py +0 -0
  94. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/hooks/__init__.py +0 -0
  95. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/hooks/tool_output_handler.py +0 -0
  96. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/integrations/__init__.py +0 -0
  97. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/integrations/anthropic.py +0 -0
  98. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/integrations/autogen.py +0 -0
  99. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/integrations/braintrust.py +0 -0
  100. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/integrations/crewai/__init__.py +0 -0
  101. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/integrations/crewai/agent.py +0 -0
  102. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/integrations/crewai/crew.py +0 -0
  103. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/integrations/crewai/task.py +0 -0
  104. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/integrations/crewai/tool.py +0 -0
  105. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/integrations/google.py +0 -0
  106. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/integrations/google_adk/__init__.py +0 -0
  107. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/integrations/google_adk/agent.py +0 -0
  108. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/integrations/google_adk/tool.py +0 -0
  109. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/integrations/langchain/__init__.py +0 -0
  110. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/integrations/langchain/agent.py +0 -0
  111. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/integrations/langchain/callbacks.py +0 -0
  112. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/integrations/langchain/chain.py +0 -0
  113. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/integrations/langchain/graph.py +0 -0
  114. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/integrations/langchain/modern.py +0 -0
  115. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/integrations/langchain/tool.py +0 -0
  116. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/integrations/langfuse.py +0 -0
  117. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/integrations/litellm.py +0 -0
  118. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/integrations/openai.py +0 -0
  119. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/integrations/pydantic_ai.py +0 -0
  120. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/integrations/vercel_ai.py +0 -0
  121. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/layout_migration.py +0 -0
  122. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/secrets/__init__.py +0 -0
  123. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/secrets/reference.py +0 -0
  124. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/secrets/resolver.py +0 -0
  125. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/secrets/secretstr.py +0 -0
  126. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/spool/__init__.py +0 -0
  127. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/spool/_compress.py +0 -0
  128. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/spool/_constants.py +0 -0
  129. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/spool/_crc32c.py +0 -0
  130. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/spool/_crypto.py +0 -0
  131. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/spool/_frame.py +0 -0
  132. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/spool/_keyring.py +0 -0
  133. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/spool/_metrics.py +0 -0
  134. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/spool/_spool.py +0 -0
  135. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/spool/_state.py +0 -0
  136. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/spool/_uploader.py +0 -0
  137. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/spool/cz-audit-v1.dict +0 -0
  138. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/tamper.py +0 -0
  139. {controlzero-1.11.9 → controlzero-1.13.0}/controlzero/tracecontext.py +0 -0
  140. {controlzero-1.11.9 → controlzero-1.13.0}/examples/hello_world.py +0 -0
  141. {controlzero-1.11.9 → controlzero-1.13.0}/tests/_fixtures/antigravity-envelope-expected.json +0 -0
  142. {controlzero-1.11.9 → controlzero-1.13.0}/tests/_fixtures/jcs_args_hash_vectors.json +0 -0
  143. {controlzero-1.11.9 → controlzero-1.13.0}/tests/conftest.py +0 -0
  144. {controlzero-1.11.9 → controlzero-1.13.0}/tests/integrations/__init__.py +0 -0
  145. {controlzero-1.11.9 → controlzero-1.13.0}/tests/integrations/test_google.py +0 -0
  146. {controlzero-1.11.9 → controlzero-1.13.0}/tests/parity/action_aliases.json +0 -0
  147. {controlzero-1.11.9 → controlzero-1.13.0}/tests/spool/__init__.py +0 -0
  148. {controlzero-1.11.9 → controlzero-1.13.0}/tests/spool/conftest.py +0 -0
  149. {controlzero-1.11.9 → controlzero-1.13.0}/tests/spool/test_spool_cli.py +0 -0
  150. {controlzero-1.11.9 → controlzero-1.13.0}/tests/spool/test_spool_concurrency.py +0 -0
  151. {controlzero-1.11.9 → controlzero-1.13.0}/tests/spool/test_spool_conformance.py +0 -0
  152. {controlzero-1.11.9 → controlzero-1.13.0}/tests/spool/test_spool_core.py +0 -0
  153. {controlzero-1.11.9 → controlzero-1.13.0}/tests/spool/test_spool_crash.py +0 -0
  154. {controlzero-1.11.9 → controlzero-1.13.0}/tests/spool/test_spool_diskfull.py +0 -0
  155. {controlzero-1.11.9 → controlzero-1.13.0}/tests/spool/test_spool_durable_default_tamper.py +0 -0
  156. {controlzero-1.11.9 → controlzero-1.13.0}/tests/spool/test_spool_keychain_dek.py +0 -0
  157. {controlzero-1.11.9 → controlzero-1.13.0}/tests/spool/test_spool_sink_wiring.py +0 -0
  158. {controlzero-1.11.9 → controlzero-1.13.0}/tests/spool/test_spool_transcript_localack.py +0 -0
  159. {controlzero-1.11.9 → controlzero-1.13.0}/tests/spool/test_spool_uploader.py +0 -0
  160. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_action_aliases.py +0 -0
  161. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_action_canonicalization.py +0 -0
  162. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_action_validator_t86.py +0 -0
  163. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_adapter_interface.py +0 -0
  164. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_agent_name_env.py +0 -0
  165. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_agy_docs_no_exit2_1373.py +0 -0
  166. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_antigravity_adapter.py +0 -0
  167. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_antigravity_envelope_parity.py +0 -0
  168. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_antigravity_ga_blockers_1248.py +0 -0
  169. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_antigravity_hook_check.py +0 -0
  170. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_antigravity_install.py +0 -0
  171. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_antigravity_posttooluse_observe_58.py +0 -0
  172. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_antigravity_tool_vocab_1303.py +0 -0
  173. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_api_key_mask.py +0 -0
  174. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_audit_project_id_attribution_pr2.py +0 -0
  175. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_audit_remote.py +0 -0
  176. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_audit_remote_io_capture.py +0 -0
  177. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_audit_remote_sdk_version.py +0 -0
  178. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_audit_sink_isolation.py +0 -0
  179. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_bundle_cache_freshness_1303.py +0 -0
  180. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_bundle_parser.py +0 -0
  181. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_bundle_translate.py +0 -0
  182. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_canonical_phase1a.py +0 -0
  183. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_capability_matrix_doc.py +0 -0
  184. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_cli_carve_out.py +0 -0
  185. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_cli_extractor_integration.py +0 -0
  186. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_cli_hook.py +0 -0
  187. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_cli_hook_entry.py +0 -0
  188. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_cli_hosted_refresh.py +0 -0
  189. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_cli_init.py +0 -0
  190. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_cli_init_templates.py +0 -0
  191. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_cli_tail.py +0 -0
  192. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_cli_test.py +0 -0
  193. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_cli_validate.py +0 -0
  194. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_conditions.py +0 -0
  195. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_config_format_parity_1303.py +0 -0
  196. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_conformance.py +0 -0
  197. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_console.py +0 -0
  198. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_credential_hook.py +0 -0
  199. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_cursor_adapter.py +0 -0
  200. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_default_action.py +0 -0
  201. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_device.py +0 -0
  202. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_dlp_mask.py +0 -0
  203. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_dlp_scanner.py +0 -0
  204. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_doctor.py +0 -0
  205. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_engine_version_consistency.py +0 -0
  206. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_enrollment.py +0 -0
  207. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_env_dump_438.py +0 -0
  208. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_epic_1247_bryan_acceptance.py +0 -0
  209. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_error_codes.py +0 -0
  210. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_errors_e_codes.py +0 -0
  211. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_fail_closed_eval.py +0 -0
  212. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_failopen_1303.py +0 -0
  213. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_glob_matching.py +0 -0
  214. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_hitl_5d_email_install.py +0 -0
  215. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_hitl_6a_cli_flag.py +0 -0
  216. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_hitl_6a_exceptions.py +0 -0
  217. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_hitl_6a_get_secret_hitl.py +0 -0
  218. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_hitl_6a_mock_backend.py +0 -0
  219. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_hitl_6a_pending_approval.py +0 -0
  220. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_hitl_6a_request_approval.py +0 -0
  221. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_hitl_6a_secret_leak_guard.py +0 -0
  222. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_hitl_6a_wait.py +0 -0
  223. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_hitl_conformance.py +0 -0
  224. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_hitl_phase2b_protocol.py +0 -0
  225. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_hitl_reason_codes.py +0 -0
  226. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_hitl_validator_keys.py +0 -0
  227. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_hook_extractors.py +0 -0
  228. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_hook_integrity.py +0 -0
  229. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_hook_mask_failclosed.py +0 -0
  230. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_hosted_local_audit_1247.py +0 -0
  231. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_hosted_local_shadow_warn_1265.py +0 -0
  232. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_hosted_policy_e2e.py +0 -0
  233. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_hosts_adapter.py +0 -0
  234. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_hybrid_mode_strict.py +0 -0
  235. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_hybrid_mode_warn.py +0 -0
  236. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_install_hook_command.py +0 -0
  237. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_install_hooks.py +0 -0
  238. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_io_capture_producer.py +0 -0
  239. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_kiro_adapter.py +0 -0
  240. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_kiro_cli_e2e.py +0 -0
  241. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_kiro_cli_hook_pin_1265.py +0 -0
  242. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_kiro_hook_templates.py +0 -0
  243. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_kiro_install.py +0 -0
  244. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_layout_migration_t101.py +0 -0
  245. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_layout_parity_t102.py +0 -0
  246. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_local_mode_dict.py +0 -0
  247. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_local_mode_file_json.py +0 -0
  248. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_local_mode_file_yaml.py +0 -0
  249. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_localized_reason.py +0 -0
  250. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_log_fallback_stderr.py +0 -0
  251. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_log_options_ignored_hosted.py +0 -0
  252. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_log_rotation.py +0 -0
  253. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_migrate.py +0 -0
  254. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_min_sdk_version_gate.py +0 -0
  255. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_multi_client_per_project_175.py +0 -0
  256. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_no_policy_no_key.py +0 -0
  257. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_observe_mode_1247.py +0 -0
  258. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_package_rename_shim.py +0 -0
  259. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_part3_active_policy_count_1303.py +0 -0
  260. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_policy_engine_version_phase1b.py +0 -0
  261. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_policy_freshness.py +0 -0
  262. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_policy_load_failclosed_f3.py +0 -0
  263. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_policy_settings.py +0 -0
  264. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_policy_source_audit.py +0 -0
  265. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_pr4_install_messaging.py +0 -0
  266. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_pr4_output.py +0 -0
  267. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_probe_audit_isolation.py +0 -0
  268. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_quarantine.py +0 -0
  269. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_reason_code.py +0 -0
  270. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_refresh.py +0 -0
  271. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_secrets.py +0 -0
  272. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_secrets_reference_resolver.py +0 -0
  273. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_source_labels.py +0 -0
  274. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_sql_semantic_class.py +0 -0
  275. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_synthetic_policy_id_t79.py +0 -0
  276. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_t103_precedence.py +0 -0
  277. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_t104_cache_gc.py +0 -0
  278. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_t108_local_override_audit.py +0 -0
  279. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_t96_single_audit_log.py +0 -0
  280. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_t99_install_prefetch_bundle.py +0 -0
  281. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_tamper.py +0 -0
  282. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_tamper_behavior.py +0 -0
  283. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_tamper_hook.py +0 -0
  284. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_telemetry_consent.py +0 -0
  285. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_tracecontext.py +0 -0
  286. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_unsafe_int_boundary.py +0 -0
  287. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_update_cmd.py +0 -0
  288. {controlzero-1.11.9 → controlzero-1.13.0}/tests/test_upgrade_nudge.py +0 -0
  289. {controlzero-1.11.9 → controlzero-1.13.0}/tools/cz-kiro-adapter +0 -0
@@ -1,5 +1,73 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.13.0 (2026-06-26): policy-condition operator vocab
4
+
5
+ ### Added
6
+
7
+ - **Condition operators.** A rule condition value may now be a tagged envelope
8
+ `{op, value}` honoring a closed operator set: `equals`, `contains`, `glob`,
9
+ `matches` (regex, BETA), `gt`/`lt`/`gte`/`lte`, `in`/`not_in`,
10
+ `in_range`/`outside_range`, `exists`/`not_exists`. Read-only aliases
11
+ `matches_pattern`/`greater_than`/`less_than` are canonicalized on load, so
12
+ existing dashboard templates work with zero migration. A legacy scalar value
13
+ (a plain string or number) keeps its prior glob/equals behavior exactly, so
14
+ existing policies and signed bundles evaluate unchanged.
15
+ - **File-path conditions.** `file_path` / `path` / `notebook_path` now feed the
16
+ `input_text` condition value, so a condition like "input_text contains .env"
17
+ fires on Read/Write/Edit tool calls.
18
+
19
+ ### Notes
20
+
21
+ - Fail-closed: a missing field is no-match for every operator except
22
+ `exists`/`not_exists`; an unknown operator is rejected at load; a malformed
23
+ condition fails closed. `matches` is BETA and length-capped to bound regex
24
+ backtracking. Secret detection stays on the DLP scanner, not a regex condition.
25
+
26
+ ## 1.12.1 (2026-06-26): deny rules actually deny
27
+
28
+ ### Fixed
29
+
30
+ - **Deny rules now actually deny (#1011).** A policy DENY rule with a condition
31
+ (e.g. "block Bash when the command contains rm -rf") could silently fail to
32
+ match, so the action ran anyway. Reconciled the rule-authoring vocabulary with
33
+ the engine: tool names match case-insensitively (a rule for `bash:*` catches
34
+ the canonical `Bash:rm`), the command text is matched as `input_text` by
35
+ CONTAINS, and conditions are no longer dropped when a hosted policy is
36
+ translated for local enforcement. A present-but-malformed condition now fails
37
+ closed at load instead of silently becoming an unconditional allow.
38
+
39
+ ## 1.12.0 -- 2026-06-25 (per-agent key authority + per-agent bundle cache + whoami)
40
+
41
+ ### Added
42
+
43
+ - **`controlzero whoami`.** Prints which API key the SDK will actually use for a
44
+ surface (per-agent / env / global), its prefix only (never the full key), and
45
+ the project + org it resolves to. Diagnoses the most common "my audit logs are
46
+ missing" cause: a `CONTROLZERO_API_KEY` exported in the shell that maps to a
47
+ different project than the integration you installed, so events land in the
48
+ wrong tenant and the dashboard shows nothing.
49
+ - **Per-agent bundle cache.** Each governed surface now caches its policy bundle
50
+ under `~/.controlzero/integrations/<agent>/cache/` (next to the per-agent
51
+ `config.yaml`), so two surfaces on one machine each bound to their own project
52
+ no longer share a bundle. A read fallback keeps existing installs working on
53
+ upgrade: an empty per-agent cache transparently reads the legacy shared cache
54
+ until the first fetch re-caches it (no fail-closed on upgrade).
55
+
56
+ ### Changed
57
+
58
+ - **Per-agent key is now AUTHORITATIVE in the enforcement hook.** Hook-check
59
+ precedence becomes per-agent (for the active surface) > env > global. A key
60
+ installed via `install <agent> --api-key` / `kiro init --api-key` now wins over
61
+ a stale ambient `CONTROLZERO_API_KEY`, with a loud one-line warning when they
62
+ differ. This closes the silent wrong-tenant bug where a hook spooled + uploaded
63
+ + ingested correctly but attributed the events to whatever key was exported in
64
+ the shell, so the dashboard for the installed project showed "no audit entries".
65
+ Non-hook resolution (`status`, install-time display) is unchanged
66
+ (env > per-agent > global).
67
+ - **Cache bundle extension is now `.czpolicy`** (was `.bin`), matching the UI
68
+ policy export (both are the same signed bundle format). Legacy `.bin` caches
69
+ are still read and garbage-collected, so nothing breaks on upgrade.
70
+
3
71
  ## 1.11.9 -- 2026-06-25 (policy-posture remediation: honest hosted/local CLI, per-agent keys, key-validity diagnostics)
4
72
 
5
73
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: controlzero
3
- Version: 1.11.9
3
+ Version: 1.13.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.13.0"
45
45
 
46
46
  __all__ = [
47
47
  "Client",
@@ -958,4 +958,21 @@ def _translate_rule(rule: dict, policy_id: str) -> Optional[dict]:
958
958
  if isinstance(k, str) and isinstance(v, str)
959
959
  }
960
960
 
961
+ # #1011 follow-up: the SAME allowlist-strip trap. A hosted rule's
962
+ # `conditions` (e.g. {input_text: "rm -rf"}) was being dropped here, so a
963
+ # condition authored in the dashboard NEVER reached the local enforcer, so
964
+ # the rule silently matched on actions only (or, with a tool-case mismatch,
965
+ # not at all). Forward BOTH alias keys verbatim whenever present (incl. a
966
+ # malformed/falsy shape: [] / "" / 0 / False), and let the loader apply ONE
967
+ # consistent precedence + shape validation (truthy-first; reject any
968
+ # non-dict; a genuine empty dict {} is "no conditions"). Collapsing to a
969
+ # single key HERE re-introduces a divergent precedence where a present `{}`
970
+ # could shadow a real alias (codex re-review), so the bundle stays dumb and
971
+ # the loader is the single source of truth. (The backend wire struct carries
972
+ # Conditions; see bundle_handler.go PolicyRule.)
973
+ if "conditions" in rule:
974
+ translated["conditions"] = rule["conditions"]
975
+ if "when" in rule:
976
+ translated["when"] = rule["when"]
977
+
961
978
  return translated
@@ -0,0 +1,226 @@
1
+ """Policy-condition operator table (single source of truth) for #1011 / #1495.
2
+
3
+ A rule condition value is either a LEGACY SCALAR (string -> glob, number/bool ->
4
+ equals; evaluated in enforcer._conditions_match, byte-identical to pre-#1495) or
5
+ a TAGGED ENVELOPE ``{"op": <name>, "value": <scalar|array>, "case_insensitive"?:
6
+ bool}``. This module evaluates the envelope operators ONLY; the scalar path stays
7
+ in the enforcer so existing policies and signed bundles evaluate unchanged.
8
+
9
+ This is the Python reference; the Node twin (conditionOps.ts) MUST agree
10
+ byte-for-byte (cross-SDK parity is proven by tests/parity/decisions.json). Go is
11
+ paused and not part of this contract; the Go BACKEND engine evaluates only the
12
+ reserved server-side keys (time_range/ip_whitelist/tags) and defers tool-level
13
+ conditions to the SDK (it has no tool args).
14
+
15
+ FAIL-CLOSED LAW: a MISSING field (value is None) is NO-MATCH for EVERY operator
16
+ except ``exists``/``not_exists``. A regex that will not compile, or a numeric op
17
+ on a non-numeric field, is NO-MATCH (never an error that fails open).
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import fnmatch
23
+ import re
24
+ from typing import Any
25
+
26
+ # Read-only aliases canonicalised on evaluation so the shipped templates
27
+ # (matches_pattern / greater_than / less_than) load and evaluate with ZERO data
28
+ # migration. The dashboard going forward emits the canonical names.
29
+ _OP_ALIASES = {
30
+ "matches_pattern": "matches",
31
+ "greater_than": "gt",
32
+ "less_than": "lt",
33
+ "greater_than_or_equal": "gte",
34
+ "less_than_or_equal": "lte",
35
+ }
36
+
37
+ # The closed operator set the engine honors. The Go publish-time validator (and
38
+ # the frontend) reject any op not in here, so an inert/typo'd operator can never
39
+ # ship.
40
+ KNOWN_OPS = frozenset(
41
+ {
42
+ "glob",
43
+ "equals",
44
+ "contains",
45
+ "matches",
46
+ "gt",
47
+ "lt",
48
+ "gte",
49
+ "lte",
50
+ "in",
51
+ "not_in",
52
+ "outside_range",
53
+ "in_range",
54
+ "exists",
55
+ "not_exists",
56
+ }
57
+ )
58
+
59
+
60
+ # Reserved structured condition keys are matched by KEY NAME (the Go backend's
61
+ # server-side matchers) and are NEVER interpreted as a {op,value} envelope, even
62
+ # when their value nests a sub-key literally named `op` (e.g. a tag named "op").
63
+ # This closes the tags:{op:...} collision hole. The caller checks the KEY against
64
+ # this set before treating a value as an envelope.
65
+ RESERVED_STRUCTURED_KEYS = frozenset({"time_range", "ip_whitelist", "tags"})
66
+
67
+
68
+ def canonicalize_op(op: str) -> str:
69
+ """Map a read alias to its canonical operator name; pass through otherwise."""
70
+ return _OP_ALIASES.get(op, op)
71
+
72
+
73
+ def is_envelope(value: Any) -> bool:
74
+ """True iff ``value`` is a tagged operator envelope: a dict with a string
75
+ ``op`` key. Reserved structured keys are disambiguated by KEY NAME upstream,
76
+ never here, and a runtime args/context value is never treated as an envelope.
77
+ """
78
+ return isinstance(value, dict) and isinstance(value.get("op"), str)
79
+
80
+
81
+ def _as_float(x: Any):
82
+ """Coerce to float for numeric ops; return None if not numeric (-> NO-MATCH)."""
83
+ if isinstance(x, bool): # bool is an int subclass; never treat as a number here
84
+ return None
85
+ if isinstance(x, (int, float)):
86
+ return float(x)
87
+ if isinstance(x, str):
88
+ try:
89
+ return float(x.strip())
90
+ except ValueError:
91
+ return None
92
+ return None
93
+
94
+
95
+ def _as_list(operand: Any) -> list:
96
+ """Operand for in/not_in: a JSON array, or a legacy comma-separated string
97
+ ('Saturday,Sunday') split by this compat shim so the shipped templates work
98
+ unmodified. A None / empty operand yields an EMPTY list (the caller treats an
99
+ empty operand as NO-MATCH, fail-closed (codex P2: a missing `value` must not
100
+ turn `not_in` into match-everything))."""
101
+ if operand is None:
102
+ return []
103
+ if isinstance(operand, (list, tuple)):
104
+ return [x for x in operand if x is not None]
105
+ if isinstance(operand, str):
106
+ return [p.strip() for p in operand.split(",") if p.strip()]
107
+ return [operand]
108
+
109
+
110
+ # matches (regex) is BETA: Python `re` has no per-match timeout, so a pathological
111
+ # author-supplied pattern can backtrack catastrophically against adversarial field
112
+ # input (codex P1 ReDoS). Until a timeout / RE2 binding lands, bound BOTH the
113
+ # scanned input and the pattern length so practical backtracking is capped; the
114
+ # publish-time validator (#1498) also caps the pattern. A regex error stays
115
+ # fail-closed (NO-MATCH).
116
+ _MATCHES_INPUT_CAP = 8192
117
+ _MATCHES_PATTERN_CAP = 1024
118
+
119
+
120
+ def _range_bounds(operand: Any):
121
+ """[lo, hi] for in_range/outside_range: a 2-element array, or a legacy
122
+ 'lo-hi' / 'HH:MM-HH:MM' string. Returns (lo_str, hi_str) or None if malformed.
123
+ Comparison is lexical for HH:MM (24h zero-padded) and numeric otherwise; both
124
+ sides are compared as strings of the same kind, matching the Go matchTimeRange
125
+ lexical compare for time."""
126
+ if isinstance(operand, (list, tuple)) and len(operand) == 2:
127
+ return str(operand[0]), str(operand[1])
128
+ if isinstance(operand, str) and "-" in operand:
129
+ lo, _, hi = operand.partition("-")
130
+ lo, hi = lo.strip(), hi.strip()
131
+ if lo and hi:
132
+ return lo, hi
133
+ return None
134
+
135
+
136
+ def _in_range(value: Any, lo: str, hi: str) -> bool:
137
+ """Inclusive lo<=value<=hi. Numeric when all three parse as numbers, else a
138
+ lexical string compare (so 'HH:MM' 24h times order correctly)."""
139
+ fv, flo, fhi = _as_float(value), _as_float(lo), _as_float(hi)
140
+ if fv is not None and flo is not None and fhi is not None:
141
+ return flo <= fv <= fhi
142
+ sv = str(value)
143
+ return lo <= sv <= hi
144
+
145
+
146
+ def match_operator(
147
+ op: str, value: Any, operand: Any, case_insensitive: bool = False
148
+ ) -> bool:
149
+ """Evaluate one field-level envelope operator. ``value`` is the field value
150
+ from the merged context+args dict (None if the field is absent); ``operand``
151
+ is the envelope's ``value``. Returns True iff the condition is satisfied.
152
+ Unknown op -> False (fail-closed; the validator rejects it at publish too)."""
153
+ op = canonicalize_op(op)
154
+
155
+ # Presence ops are the ONLY ones where a missing field can be a match.
156
+ if op == "exists":
157
+ return value is not None
158
+ if op == "not_exists":
159
+ return value is None
160
+
161
+ # Every other op: a missing field is NO-MATCH (uniform, fail-closed).
162
+ if value is None:
163
+ return False
164
+
165
+ if op in ("gt", "lt", "gte", "lte"):
166
+ fv, fo = _as_float(value), _as_float(operand)
167
+ if fv is None or fo is None:
168
+ return False # non-numeric operand/field -> NO-MATCH
169
+ if op == "gt":
170
+ return fv > fo
171
+ if op == "lt":
172
+ return fv < fo
173
+ if op == "gte":
174
+ return fv >= fo
175
+ return fv <= fo
176
+
177
+ if op in ("in", "not_in"):
178
+ items = [str(x) for x in _as_list(operand)]
179
+ if not items:
180
+ # Malformed / empty operand -> NO-MATCH for BOTH in and not_in
181
+ # (fail-closed; a deny does not over-fire, an allow does not
182
+ # over-allow). The validator rejects an empty operand at publish.
183
+ return False
184
+ if case_insensitive:
185
+ present = str(value).casefold() in {i.casefold() for i in items}
186
+ else:
187
+ present = str(value) in items
188
+ return present if op == "in" else not present
189
+
190
+ if op in ("in_range", "outside_range"):
191
+ bounds = _range_bounds(operand)
192
+ if bounds is None:
193
+ return False
194
+ inside = _in_range(value, bounds[0], bounds[1])
195
+ return inside if op == "in_range" else not inside
196
+
197
+ # String ops below.
198
+ sval, soperand = str(value), str(operand)
199
+ if case_insensitive:
200
+ sval, soperand = sval.casefold(), soperand.casefold()
201
+
202
+ if op == "equals":
203
+ # Exact equality; wildcards are LITERAL (unlike glob).
204
+ return sval == soperand
205
+
206
+ if op == "contains":
207
+ # Substring; the operand's own glob metachars are literal (not wildcards).
208
+ return soperand in sval
209
+
210
+ if op == "glob":
211
+ return fnmatch.fnmatchcase(sval, soperand)
212
+
213
+ if op == "matches":
214
+ # Regex (re.search). Author owns anchoring. BETA: bound input + pattern
215
+ # length to cap ReDoS backtracking (codex P1); uncompilable / runtime
216
+ # error -> NO-MATCH (fail-closed), never an exception that fails open.
217
+ if len(soperand) > _MATCHES_PATTERN_CAP:
218
+ return False
219
+ flags = re.IGNORECASE if case_insensitive else 0
220
+ try:
221
+ return re.search(soperand, sval[:_MATCHES_INPUT_CAP], flags) is not None
222
+ except re.error:
223
+ return False
224
+
225
+ # Unknown operator: fail-closed (the publish-time validator also rejects it).
226
+ return False
@@ -516,7 +516,9 @@ class PolicyEvaluator:
516
516
 
517
517
  for rule in self._rules:
518
518
  evaluated += 1
519
- if not any(_glob_any(rule.actions, a) for a in candidate_actions):
519
+ # Action gate: tool-case-insensitive so `bash:*` matches `Bash:rm`
520
+ # (#1011). Resource/client/project gates below stay case-sensitive.
521
+ if not any(_action_glob_any(rule.actions, a) for a in candidate_actions):
520
522
  continue
521
523
  # Client + project gates (gh#175). A rule whose selector
522
524
  # excludes this request does NOT apply to this surface, so
@@ -676,10 +678,22 @@ class PolicyEvaluator:
676
678
  All condition keys must match. Missing key = no match. Context keys
677
679
  override args on collision. Nested ``context['tags']`` dict is flattened.
678
680
  """
679
- if not rule_conditions:
681
+ # #1011 follow-up (codex P1): the non-dict guard MUST run before the
682
+ # falsy short-circuit, otherwise a falsy-but-malformed conditions ([],
683
+ # "", 0, False) would be read as "no conditions" and the rule would match
684
+ # unconditionally (an allow rule -> unconditional allow, fail-open). A
685
+ # corrupt/tampered or future-shaped signed rule is unverifiable, so the
686
+ # rule does NOT match: an allow rule then does not fire and the default
687
+ # posture governs (fail-closed); a deny rule simply does not contribute.
688
+ # `None` is the genuine "no conditions" case and matches.
689
+ if not isinstance(rule_conditions, dict):
690
+ return rule_conditions is None
691
+ if not rule_conditions: # empty dict -> no conditions to satisfy
680
692
  return True
681
693
  import fnmatch as _fn
682
694
 
695
+ from .io_capture_producer import _INPUT_TEXT_KEYS
696
+
683
697
  merged: dict = {}
684
698
  if args:
685
699
  merged.update(args)
@@ -688,11 +702,71 @@ class PolicyEvaluator:
688
702
  tags = context.get("tags") or {}
689
703
  if isinstance(tags, dict):
690
704
  merged.update(tags)
705
+ # #1011 follow-up: derive `input_text` (the key the dashboard/templates
706
+ # author, "command/input contains X") DETERMINISTICALLY from the payload
707
+ # rather than trusting a caller-supplied label. The raw command/content
708
+ # is the GROUND TRUTH of what runs, so it wins first: neither a benign
709
+ # `input_text` nor a benign pre-extracted `input_payload` can shadow a
710
+ # dangerous `command` (codex P1: a payload setting input_text="ls" or
711
+ # input_payload="ls" while command="rm -rf /" would otherwise bypass the
712
+ # deny under default allow). Precedence: the canonical _INPUT_TEXT_KEYS
713
+ # scan (the SAME order audit extraction uses, so enforcement and audit
714
+ # never drift), then the host's pre-extracted input_payload, then any
715
+ # existing input_text, each only as a fallback when nothing richer is
716
+ # present.
717
+ _derived = None
718
+ for _k in _INPUT_TEXT_KEYS:
719
+ _v = merged.get(_k)
720
+ if _v:
721
+ _derived = _v
722
+ break
723
+ if not _derived:
724
+ _derived = merged.get("input_payload")
725
+ if not _derived:
726
+ _derived = merged.get("input_text")
727
+ if _derived:
728
+ merged["input_text"] = _derived
729
+ from .condition_ops import (
730
+ RESERVED_STRUCTURED_KEYS,
731
+ is_envelope,
732
+ match_operator,
733
+ )
734
+
691
735
  for key, pattern in rule_conditions.items():
736
+ # #1495: a TAGGED ENVELOPE {op, value} dispatches to the shared
737
+ # operator table (equals/contains/matches/gt/lt/in/not_in/range/
738
+ # exists/...). Everything else is a LEGACY SCALAR and keeps the exact
739
+ # pre-#1495 glob/CONTAINS path below, so existing policies and signed
740
+ # bundles evaluate byte-identically. exists/not_exists are the only
741
+ # ops where a missing field can match, so the envelope branch runs
742
+ # BEFORE the missing-key short-circuit. A RESERVED structured key is
743
+ # matched by key name, never as an envelope (the tags:{op:...} hole).
744
+ if key not in RESERVED_STRUCTURED_KEYS and is_envelope(pattern):
745
+ if not match_operator(
746
+ pattern["op"],
747
+ merged.get(key),
748
+ pattern.get("value"),
749
+ bool(pattern.get("case_insensitive")),
750
+ ):
751
+ return False
752
+ continue
692
753
  value = merged.get(key)
693
754
  if value is None:
694
755
  return False
695
- if not _fn.fnmatchcase(str(value), str(pattern)):
756
+ sval, spat = str(value), str(pattern)
757
+ # Only `input_text` (the dashboard's authored text-content key) uses
758
+ # CONTAINS for a plain pattern, so `input_text: "rm -rf"` catches
759
+ # `rm -rf /tmp/foo`. A pattern with glob metacharacters still uses
760
+ # fnmatch; EVERY other key (including the raw `command`/`prompt`
761
+ # sources) keeps exact-glob semantics, so an existing exact-match
762
+ # allowlist is never silently broadened into a substring match
763
+ # (codex P1 fail-open regression).
764
+ if key.casefold() in _TEXT_CONTENT_CONDITION_KEYS and not any(
765
+ c in spat for c in "*?["
766
+ ):
767
+ if spat not in sval:
768
+ return False
769
+ elif not _fn.fnmatchcase(sval, spat):
696
770
  return False
697
771
  return True
698
772
 
@@ -836,8 +910,58 @@ def _glob_any(patterns: list[str], value: str) -> bool:
836
910
 
837
911
  fnmatchcase is case-sensitive (matches policy semantics) and supports the
838
912
  same syntax users learn from .gitignore: *, ?, [seq], [!seq].
913
+
914
+ NOTE: this stays case-sensitive and is used for the resource / client /
915
+ project selector gates. Action matching uses :func:`_action_glob_any`,
916
+ which case-folds the tool segment (see below).
839
917
  """
840
918
  for p in patterns:
841
919
  if fnmatch.fnmatchcase(value, p):
842
920
  return True
843
921
  return False
922
+
923
+
924
+ def _norm_action_tool(s: str) -> str:
925
+ """Case-fold ONLY the tool segment of an action token (the part before the
926
+ first ``:``); leave the method/arg glob untouched.
927
+
928
+ The canonical action the SDK emits is capital-tool (e.g. ``Bash:rm``, the
929
+ canonical TOOL NAME from the extractor spec), but a user or a policy
930
+ template may author the tool in any case (``bash:*``). The tool name is a
931
+ fixed, known vocabulary, so case is never meaningful for it; folding it lets
932
+ a user-authored rule reconcile with the call the agent actually makes. The
933
+ METHOD stays case-sensitive so method-cased semantics (e.g. ``database:DROP``
934
+ vs ``database:drop``) are preserved. A token with no ``:`` (e.g. the
935
+ universal ``*``) is folded whole, a no-op for glob metacharacters.
936
+ """
937
+ tool, sep, method = s.partition(":")
938
+ return (tool.casefold() + sep + method) if sep else s.casefold()
939
+
940
+
941
+ def _action_glob_any(patterns: list[str], action: str) -> bool:
942
+ """Action-gate matcher: like :func:`_glob_any` but case-INSENSITIVE on the
943
+ tool segment of both the rule pattern and the action (see
944
+ :func:`_norm_action_tool`).
945
+
946
+ This is what makes a deny rule authored as ``bash:*`` actually match the
947
+ canonical ``Bash:rm`` (#1011 follow-up: a customer's deny rule silently
948
+ never fired because the matcher compared the tool case-sensitively).
949
+ """
950
+ a = _norm_action_tool(action)
951
+ for p in patterns:
952
+ if fnmatch.fnmatchcase(a, _norm_action_tool(p)):
953
+ return True
954
+ return False
955
+
956
+
957
+ # The ONE condition key whose plain (metachar-free) pattern is matched by
958
+ # CONTAINS (substring): `input_text`. It is the key the dashboard/templates
959
+ # author for "the command/input contains X", and a user typing `rm -rf` means
960
+ # "contains rm -rf", not "IS exactly rm -rf". Patterns WITH glob metachars still
961
+ # use fnmatch. This set is deliberately NARROW: the raw payload keys (command,
962
+ # input_payload, prompt, ...) are seeding SOURCES for input_text, not author-
963
+ # facing condition keys. Briefly including them here broadened any existing
964
+ # exact-match allowlist keyed on `command` into a substring match, a fail-open
965
+ # regression (codex P1). Every key except `input_text` keeps exact-glob
966
+ # semantics. (#1011 follow-up.)
967
+ _TEXT_CONTENT_CONDITION_KEYS = frozenset({"input_text"})
@@ -69,15 +69,22 @@ _OUTPUT_EVENT_NAMES = frozenset(
69
69
  # in extraction priority order. The whole tool_input dict is the fallback when
70
70
  # none match (rendered as canonical JSON so the capture is still faithful).
71
71
  _INPUT_TEXT_KEYS = (
72
- "command", # Bash / run_command
73
- "CommandLine", # Antigravity run_command
74
- "sql", # database
75
- "query", # search / http
76
- "prompt", # prompt-style tools
77
- "content", # Write / file content
78
- "new_string", # Edit
79
- "text", # generic text tools
80
- "url", # http / browser
72
+ "command", # Bash / run_command
73
+ "CommandLine", # Antigravity run_command
74
+ "sql", # database
75
+ "query", # search / http
76
+ "prompt", # prompt-style tools
77
+ "content", # Write / file content
78
+ "new_string", # Edit
79
+ "text", # generic text tools
80
+ "url", # http / browser
81
+ # File-target paths (#1011): so a condition like `input_text contains .env`
82
+ # on a Read/Write/Edit fires on the PATH being touched. Lower priority than
83
+ # the command/content keys above (a tool rarely carries both; if it does the
84
+ # executed text wins). These also enrich the audit input_payload.
85
+ "file_path", # Read / Write / Edit
86
+ "path", # Glob / Grep / LS
87
+ "notebook_path", # NotebookEdit
81
88
  )
82
89
 
83
90
  # tool_response / tool_output field shapes a host may deliver on a PostToolUse
@@ -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