controlzero 1.13.8__tar.gz → 1.13.10__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 (337) hide show
  1. {controlzero-1.13.8 → controlzero-1.13.10}/CHANGELOG.md +112 -2
  2. {controlzero-1.13.8 → controlzero-1.13.10}/PKG-INFO +3 -3
  3. {controlzero-1.13.8 → controlzero-1.13.10}/README.md +2 -2
  4. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/__init__.py +1 -1
  5. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/_internal/_messages.py +45 -0
  6. controlzero-1.13.10/controlzero/_internal/dlp_output_observer.py +95 -0
  7. controlzero-1.13.10/controlzero/_internal/dlp_remediation.py +245 -0
  8. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/_internal/dlp_scanner.py +110 -30
  9. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/_internal/enforcer.py +41 -12
  10. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/audit_remote.py +197 -2
  11. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/enforcement.py +24 -1
  12. controlzero-1.13.10/controlzero/cli/hosts/claude_code.py +541 -0
  13. controlzero-1.13.10/controlzero/cli/hosts/gemini_cli.py +221 -0
  14. controlzero-1.13.10/controlzero/cli/hosts/kiro.py +364 -0
  15. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/kiro_adapter.py +26 -1
  16. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/main.py +753 -70
  17. controlzero-1.13.10/controlzero/cli/maturity.py +72 -0
  18. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/setup_wizard.py +294 -30
  19. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/spool_cmd.py +1 -1
  20. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/client.py +149 -29
  21. controlzero-1.13.10/controlzero/integrations/_export_dlp.py +171 -0
  22. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/integrations/braintrust.py +32 -11
  23. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/integrations/crewai/task.py +20 -0
  24. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/integrations/langfuse.py +68 -22
  25. {controlzero-1.13.8 → controlzero-1.13.10}/pyproject.toml +1 -1
  26. {controlzero-1.13.8 → controlzero-1.13.10}/tests/fixtures/adapter-contracts/claude_code.json +14 -1
  27. {controlzero-1.13.8 → controlzero-1.13.10}/tests/fixtures/adapter-contracts/gemini_cli.json +13 -2
  28. {controlzero-1.13.8 → controlzero-1.13.10}/tests/fixtures/adapter-contracts/kiro.json +4 -2
  29. controlzero-1.13.10/tests/test_cli_coverage.py +62 -0
  30. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_cli_hook.py +84 -0
  31. controlzero-1.13.10/tests/test_dlp_agent_remediation_1938.py +366 -0
  32. controlzero-1.13.10/tests/test_dlp_audit_hashing_1881.py +88 -0
  33. controlzero-1.13.10/tests/test_dlp_egress_conformance_matrix.py +1091 -0
  34. controlzero-1.13.10/tests/test_dlp_path5_audit_egress_1967.py +174 -0
  35. controlzero-1.13.10/tests/test_dlp_paths_8_9_observe_1967.py +170 -0
  36. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_dlp_scanner.py +17 -14
  37. controlzero-1.13.10/tests/test_feature_parity_maturity.py +187 -0
  38. controlzero-1.13.10/tests/test_integration_export_dlp_1967.py +294 -0
  39. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_kiro_adapter.py +196 -1
  40. controlzero-1.13.10/tests/test_kiro_agent_harness_routing.py +196 -0
  41. controlzero-1.13.10/tests/test_kiro_ide_v2_hooks_1944.py +480 -0
  42. controlzero-1.13.10/tests/test_kiro_version_schemes.py +123 -0
  43. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_pr4_install_messaging.py +5 -1
  44. controlzero-1.13.10/tests/test_prompt_submit_dlp_1966.py +421 -0
  45. controlzero-1.13.10/tests/test_setup_wizard.py +504 -0
  46. controlzero-1.13.8/controlzero/cli/hosts/claude_code.py +0 -258
  47. controlzero-1.13.8/controlzero/cli/hosts/gemini_cli.py +0 -114
  48. controlzero-1.13.8/controlzero/cli/hosts/kiro.py +0 -181
  49. controlzero-1.13.8/tests/test_setup_wizard.py +0 -196
  50. {controlzero-1.13.8 → controlzero-1.13.10}/.gitignore +0 -0
  51. {controlzero-1.13.8 → controlzero-1.13.10}/Dockerfile.test +0 -0
  52. {controlzero-1.13.8 → controlzero-1.13.10}/LICENSE +0 -0
  53. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/__main__.py +0 -0
  54. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/_internal/__init__.py +0 -0
  55. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/_internal/action_aliases.py +0 -0
  56. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/_internal/action_validator.py +0 -0
  57. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/_internal/bundle.py +0 -0
  58. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/_internal/condition_ops.py +0 -0
  59. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/_internal/credential_hook.py +0 -0
  60. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/_internal/credential_scanner.py +0 -0
  61. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/_internal/credentials_data/__init__.py +0 -0
  62. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/_internal/credentials_data/built_in.yaml +0 -0
  63. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/_internal/hook_extractors.py +0 -0
  64. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/_internal/io_capture_producer.py +0 -0
  65. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/_internal/source_labels.py +0 -0
  66. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/_internal/tool_extractors.json +0 -0
  67. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/_internal/types.py +0 -0
  68. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/audit_local.py +0 -0
  69. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/canonical.py +0 -0
  70. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/__init__.py +0 -0
  71. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/__main__.py +0 -0
  72. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/_secrets.py +0 -0
  73. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/agent_config.py +0 -0
  74. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/console.py +0 -0
  75. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/debug_bundle.py +0 -0
  76. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/doctor.py +0 -0
  77. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/exec_cmd.py +0 -0
  78. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/hook_entry.py +0 -0
  79. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/hosts/__init__.py +0 -0
  80. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/hosts/antigravity.py +0 -0
  81. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/hosts/base.py +0 -0
  82. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/hosts/codex_cli.py +0 -0
  83. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/hosts/cursor.py +0 -0
  84. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/hosts/cursor_cli.py +0 -0
  85. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/hosts/unknown.py +0 -0
  86. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/migrate.py +0 -0
  87. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/output.py +0 -0
  88. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/telemetry_consent.py +0 -0
  89. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/templates/antigravity/hooks.json +0 -0
  90. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/templates/antigravity.yaml +0 -0
  91. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/templates/autogen.yaml +0 -0
  92. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/templates/claude-code.yaml +0 -0
  93. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/templates/codex-cli.yaml +0 -0
  94. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/templates/cost-cap.yaml +0 -0
  95. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/templates/crewai.yaml +0 -0
  96. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/templates/cursor.yaml +0 -0
  97. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/templates/gemini-cli.yaml +0 -0
  98. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/templates/generic.yaml +0 -0
  99. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook +0 -0
  100. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/templates/kiro/ide-pre-tool-use.kiro.hook +0 -0
  101. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/templates/kiro/ide-prompt-submit.kiro.hook +0 -0
  102. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/templates/kiro/kiro.yaml +0 -0
  103. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/templates/langchain.yaml +0 -0
  104. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/templates/mcp.yaml +0 -0
  105. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/templates/rag.yaml +0 -0
  106. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/cli/update_cmd.py +0 -0
  107. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/device.py +0 -0
  108. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/enrollment.py +0 -0
  109. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/error_codes.py +0 -0
  110. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/error_codes.yaml +0 -0
  111. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/errors.py +0 -0
  112. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/hitl/__init__.py +0 -0
  113. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/hitl/grant_protocol.py +0 -0
  114. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/hitl/mock.py +0 -0
  115. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/hitl/pending_approval.py +0 -0
  116. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/hitl/secret_leak_guard.py +0 -0
  117. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/hitl/status.py +0 -0
  118. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/hook_integrity.py +0 -0
  119. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/hooks/__init__.py +0 -0
  120. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/hooks/tool_output_handler.py +0 -0
  121. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/hosted_policy.py +0 -0
  122. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/integrations/__init__.py +0 -0
  123. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/integrations/anthropic.py +0 -0
  124. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/integrations/autogen.py +0 -0
  125. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/integrations/crewai/__init__.py +0 -0
  126. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/integrations/crewai/agent.py +0 -0
  127. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/integrations/crewai/crew.py +0 -0
  128. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/integrations/crewai/tool.py +0 -0
  129. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/integrations/google.py +0 -0
  130. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/integrations/google_adk/__init__.py +0 -0
  131. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/integrations/google_adk/agent.py +0 -0
  132. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/integrations/google_adk/tool.py +0 -0
  133. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/integrations/langchain/__init__.py +0 -0
  134. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/integrations/langchain/agent.py +0 -0
  135. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/integrations/langchain/callbacks.py +0 -0
  136. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/integrations/langchain/chain.py +0 -0
  137. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/integrations/langchain/graph.py +0 -0
  138. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/integrations/langchain/modern.py +0 -0
  139. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/integrations/langchain/tool.py +0 -0
  140. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/integrations/litellm.py +0 -0
  141. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/integrations/openai.py +0 -0
  142. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/integrations/pydantic_ai.py +0 -0
  143. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/integrations/vercel_ai.py +0 -0
  144. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/layout_migration.py +0 -0
  145. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/policy_loader.py +0 -0
  146. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/secrets/__init__.py +0 -0
  147. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/secrets/reference.py +0 -0
  148. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/secrets/resolver.py +0 -0
  149. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/secrets/secretstr.py +0 -0
  150. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/spool/__init__.py +0 -0
  151. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/spool/_compress.py +0 -0
  152. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/spool/_constants.py +0 -0
  153. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/spool/_crc32c.py +0 -0
  154. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/spool/_crypto.py +0 -0
  155. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/spool/_frame.py +0 -0
  156. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/spool/_keyring.py +0 -0
  157. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/spool/_metrics.py +0 -0
  158. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/spool/_spool.py +0 -0
  159. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/spool/_state.py +0 -0
  160. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/spool/_uploader.py +0 -0
  161. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/spool/cz-audit-v1.dict +0 -0
  162. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/tamper.py +0 -0
  163. {controlzero-1.13.8 → controlzero-1.13.10}/controlzero/tracecontext.py +0 -0
  164. {controlzero-1.13.8 → controlzero-1.13.10}/examples/hello_world.py +0 -0
  165. {controlzero-1.13.8 → controlzero-1.13.10}/ruff.toml +0 -0
  166. {controlzero-1.13.8 → controlzero-1.13.10}/tests/_fixtures/antigravity-envelope-expected.json +0 -0
  167. {controlzero-1.13.8 → controlzero-1.13.10}/tests/_fixtures/jcs_args_hash_vectors.json +0 -0
  168. {controlzero-1.13.8 → controlzero-1.13.10}/tests/conftest.py +0 -0
  169. {controlzero-1.13.8 → controlzero-1.13.10}/tests/fixtures/adapter-contracts/antigravity.json +0 -0
  170. {controlzero-1.13.8 → controlzero-1.13.10}/tests/fixtures/adapter-contracts/codex_cli.json +0 -0
  171. {controlzero-1.13.8 → controlzero-1.13.10}/tests/fixtures/adapter-contracts/cursor.json +0 -0
  172. {controlzero-1.13.8 → controlzero-1.13.10}/tests/integrations/__init__.py +0 -0
  173. {controlzero-1.13.8 → controlzero-1.13.10}/tests/integrations/test_google.py +0 -0
  174. {controlzero-1.13.8 → controlzero-1.13.10}/tests/parity/action_aliases.json +0 -0
  175. {controlzero-1.13.8 → controlzero-1.13.10}/tests/spool/__init__.py +0 -0
  176. {controlzero-1.13.8 → controlzero-1.13.10}/tests/spool/conftest.py +0 -0
  177. {controlzero-1.13.8 → controlzero-1.13.10}/tests/spool/test_spool_cli.py +0 -0
  178. {controlzero-1.13.8 → controlzero-1.13.10}/tests/spool/test_spool_concurrency.py +0 -0
  179. {controlzero-1.13.8 → controlzero-1.13.10}/tests/spool/test_spool_conformance.py +0 -0
  180. {controlzero-1.13.8 → controlzero-1.13.10}/tests/spool/test_spool_core.py +0 -0
  181. {controlzero-1.13.8 → controlzero-1.13.10}/tests/spool/test_spool_crash.py +0 -0
  182. {controlzero-1.13.8 → controlzero-1.13.10}/tests/spool/test_spool_diskfull.py +0 -0
  183. {controlzero-1.13.8 → controlzero-1.13.10}/tests/spool/test_spool_durable_default_tamper.py +0 -0
  184. {controlzero-1.13.8 → controlzero-1.13.10}/tests/spool/test_spool_keychain_dek.py +0 -0
  185. {controlzero-1.13.8 → controlzero-1.13.10}/tests/spool/test_spool_sink_wiring.py +0 -0
  186. {controlzero-1.13.8 → controlzero-1.13.10}/tests/spool/test_spool_transcript_localack.py +0 -0
  187. {controlzero-1.13.8 → controlzero-1.13.10}/tests/spool/test_spool_uploader.py +0 -0
  188. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_action_aliases.py +0 -0
  189. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_action_canonicalization.py +0 -0
  190. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_action_validator_t86.py +0 -0
  191. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_adapter_contracts.py +0 -0
  192. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_adapter_interface.py +0 -0
  193. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_agent_name_env.py +0 -0
  194. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_agy_docs_no_exit2_1373.py +0 -0
  195. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_antigravity_adapter.py +0 -0
  196. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_antigravity_envelope_parity.py +0 -0
  197. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_antigravity_ga_blockers_1248.py +0 -0
  198. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_antigravity_hook_check.py +0 -0
  199. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_antigravity_install.py +0 -0
  200. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_antigravity_posttooluse_observe_58.py +0 -0
  201. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_antigravity_tool_vocab_1303.py +0 -0
  202. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_api_key_mask.py +0 -0
  203. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_api_url_config.py +0 -0
  204. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_audit_project_id_attribution_pr2.py +0 -0
  205. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_audit_remote.py +0 -0
  206. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_audit_remote_io_capture.py +0 -0
  207. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_audit_remote_sdk_version.py +0 -0
  208. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_audit_sink_isolation.py +0 -0
  209. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_bundle_cache_freshness_1303.py +0 -0
  210. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_bundle_parser.py +0 -0
  211. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_bundle_translate.py +0 -0
  212. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_canonical_phase1a.py +0 -0
  213. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_capability_matrix_doc.py +0 -0
  214. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_cli_carve_out.py +0 -0
  215. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_cli_debug_bundle.py +0 -0
  216. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_cli_extractor_integration.py +0 -0
  217. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_cli_hook_entry.py +0 -0
  218. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_cli_hosted_refresh.py +0 -0
  219. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_cli_init.py +0 -0
  220. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_cli_init_templates.py +0 -0
  221. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_cli_tail.py +0 -0
  222. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_cli_test.py +0 -0
  223. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_cli_validate.py +0 -0
  224. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_coding_agent_hooks.py +0 -0
  225. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_condition_operators_1495.py +0 -0
  226. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_conditions.py +0 -0
  227. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_config_format_parity_1303.py +0 -0
  228. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_conformance.py +0 -0
  229. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_console.py +0 -0
  230. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_credential_hook.py +0 -0
  231. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_cross_adapter_shell_enforcement.py +0 -0
  232. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_cursor_adapter.py +0 -0
  233. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_dashboard_shell_template_1904.py +0 -0
  234. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_default_action.py +0 -0
  235. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_deny_rule_reconciliation_1011.py +0 -0
  236. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_deny_wins_default_catchall_1620.py +0 -0
  237. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_device.py +0 -0
  238. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_dlp_mask.py +0 -0
  239. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_dlp_mask_hosts_1938.py +0 -0
  240. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_dlp_origin_contract.py +0 -0
  241. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_dlp_scan_coverage_1927_1931.py +0 -0
  242. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_doctor.py +0 -0
  243. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_engine_version_consistency.py +0 -0
  244. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_enrollment.py +0 -0
  245. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_env_dump_438.py +0 -0
  246. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_epic_1247_bryan_acceptance.py +0 -0
  247. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_error_codes.py +0 -0
  248. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_errors_e_codes.py +0 -0
  249. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_fail_closed_eval.py +0 -0
  250. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_failopen_1303.py +0 -0
  251. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_git_target_1734.py +0 -0
  252. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_glob_matching.py +0 -0
  253. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_hitl_5d_email_install.py +0 -0
  254. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_hitl_6a_cli_flag.py +0 -0
  255. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_hitl_6a_exceptions.py +0 -0
  256. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_hitl_6a_get_secret_hitl.py +0 -0
  257. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_hitl_6a_mock_backend.py +0 -0
  258. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_hitl_6a_pending_approval.py +0 -0
  259. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_hitl_6a_request_approval.py +0 -0
  260. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_hitl_6a_secret_leak_guard.py +0 -0
  261. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_hitl_6a_wait.py +0 -0
  262. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_hitl_conformance.py +0 -0
  263. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_hitl_phase2b_protocol.py +0 -0
  264. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_hitl_reason_codes.py +0 -0
  265. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_hitl_validator_keys.py +0 -0
  266. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_hook_command_pinning_1705.py +0 -0
  267. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_hook_extractors.py +0 -0
  268. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_hook_integrity.py +0 -0
  269. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_hook_mask_failclosed.py +0 -0
  270. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_hook_warn_surfaces_reason_1903.py +0 -0
  271. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_hosted_local_audit_1247.py +0 -0
  272. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_hosted_local_shadow_warn_1265.py +0 -0
  273. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_hosted_policy_e2e.py +0 -0
  274. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_hosts_adapter.py +0 -0
  275. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_hybrid_mode_strict.py +0 -0
  276. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_hybrid_mode_warn.py +0 -0
  277. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_install_api_url.py +0 -0
  278. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_install_hook_command.py +0 -0
  279. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_install_hooks.py +0 -0
  280. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_io_capture_producer.py +0 -0
  281. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_kiro_cli_e2e.py +0 -0
  282. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_kiro_cli_hook_pin_1265.py +0 -0
  283. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_kiro_cli_v210_hooks.py +0 -0
  284. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_kiro_hook_templates.py +0 -0
  285. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_kiro_install.py +0 -0
  286. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_kiro_install_standardisation.py +0 -0
  287. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_layout_migration_t101.py +0 -0
  288. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_layout_parity_t102.py +0 -0
  289. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_local_mode_dict.py +0 -0
  290. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_local_mode_file_json.py +0 -0
  291. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_local_mode_file_yaml.py +0 -0
  292. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_localized_reason.py +0 -0
  293. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_log_fallback_stderr.py +0 -0
  294. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_log_options_ignored_hosted.py +0 -0
  295. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_log_rotation.py +0 -0
  296. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_migrate.py +0 -0
  297. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_min_sdk_version_gate.py +0 -0
  298. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_multi_client_per_project_175.py +0 -0
  299. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_no_policy_no_key.py +0 -0
  300. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_observe_mode_1247.py +0 -0
  301. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_package_rename_shim.py +0 -0
  302. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_part3_active_policy_count_1303.py +0 -0
  303. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_per_client_isolation_1011.py +0 -0
  304. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_policy_engine_version_phase1b.py +0 -0
  305. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_policy_freshness.py +0 -0
  306. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_policy_load_failclosed_f3.py +0 -0
  307. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_policy_settings.py +0 -0
  308. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_policy_source_audit.py +0 -0
  309. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_pr4_output.py +0 -0
  310. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_pr4_status.py +0 -0
  311. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_probe_audit_isolation.py +0 -0
  312. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_quarantine.py +0 -0
  313. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_reason_code.py +0 -0
  314. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_reason_code_hitl_grant_1596.py +0 -0
  315. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_refresh.py +0 -0
  316. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_secrets.py +0 -0
  317. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_secrets_reference_resolver.py +0 -0
  318. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_shell_semantic_class.py +0 -0
  319. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_source_labels.py +0 -0
  320. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_sql_semantic_class.py +0 -0
  321. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_synthetic_policy_id_t79.py +0 -0
  322. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_t103_precedence.py +0 -0
  323. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_t104_cache_gc.py +0 -0
  324. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_t108_local_override_audit.py +0 -0
  325. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_t96_single_audit_log.py +0 -0
  326. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_t99_install_prefetch_bundle.py +0 -0
  327. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_tamper.py +0 -0
  328. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_tamper_behavior.py +0 -0
  329. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_tamper_hook.py +0 -0
  330. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_telemetry_consent.py +0 -0
  331. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_tls_ca_bundle.py +0 -0
  332. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_tracecontext.py +0 -0
  333. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_unsafe_int_boundary.py +0 -0
  334. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_update_cmd.py +0 -0
  335. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_upgrade_nudge.py +0 -0
  336. {controlzero-1.13.8 → controlzero-1.13.10}/tests/test_version_aware_fail_closed.py +0 -0
  337. {controlzero-1.13.8 → controlzero-1.13.10}/tools/cz-kiro-adapter +0 -0
@@ -1,6 +1,116 @@
1
1
  # Changelog
2
2
 
3
- ## 1.13.8 (2026-07-27): warn decisions reach the agent + Kiro IDE adapter unblocked (P0)
3
+ ## 1.13.10 (2026-07-30): DLP now covers prompts and tool output, not just tool arguments
4
+
5
+ Until this release, Control Zero scanned the ARGUMENTS of a tool call and
6
+ nothing else. A secret could reach a model or leave the machine by three other
7
+ routes that looked identical to a working setup. All three are closed here.
8
+
9
+ ### Fixed
10
+
11
+ - **A secret pasted into a prompt was sent to the model unscanned (#1966).**
12
+ Prompt submission is now a scanned surface on the hosts that expose one
13
+ (Claude Code `UserPromptSubmit`, Gemini CLI `BeforeAgent`). Previously only
14
+ tool calls were checked, so pasting a key into the chat box egressed it.
15
+
16
+ - **Credentials from dashboard-configured DLP rules were written to the audit
17
+ log in cleartext (#1881 follow-up).** The scanner hashed matches only when
18
+ the rule category was the literal string `secret`; dashboard rules arrive as
19
+ `credential`, so those matches were stored verbatim. The audit trail was
20
+ keeping the secret it had just detected. Now gated on a named set of
21
+ value-bearing categories, so a category added later cannot reach cleartext.
22
+
23
+ - **Spooled audit records were replayed without ever being scanned (#1967).**
24
+ Replay validated JSON shape, tool name and decision -- never payload content
25
+ -- then transmitted the stored bytes as-is, possibly days later on a
26
+ different network. Records written before this release are treated as
27
+ unscanned rather than trusted.
28
+
29
+ - **The auth cache dropped `key_type`, silently disabling the audit
30
+ anti-spoof guarantee.** `key_type == "extension"` is server-authoritative
31
+ for a row's `surface_class`; on every cache HIT the field was empty, so the
32
+ wire-supplied value was honoured instead. The guarantee held only on the
33
+ first request after a cache miss.
34
+
35
+ ### Added
36
+
37
+ - **Sensitive data in tool OUTPUT is now detected and recorded (#1965).**
38
+ Reading a file that contains a secret is caught. The tool call is NOT
39
+ blocked -- an agent reading a file is normal work -- so this is deliberately
40
+ observe-and-record: one deduplicated message per tool call, and an audit row
41
+ carrying `effect=recorded_only` that is never labelled a block.
42
+
43
+ - **DLP runs before export to third-party observability (#1967).** Braintrust
44
+ and Langfuse were receiving full tool arguments and output previews with no
45
+ scan in between.
46
+
47
+ ### Changed
48
+
49
+ - API-key creation rejects a `key_type` outside `{live, test}` with
50
+ `INVALID_CREDENTIAL_KIND` (400) instead of accepting any string and
51
+ interpolating it into the stored key prefix. Bulk extension-key prefixes are
52
+ limited to 1-8 characters of `[a-z0-9-]`, which is what the database column
53
+ actually fits.
54
+
55
+ ### Notes for anyone writing a policy by hand
56
+
57
+ DLP rules go under `dlp_rules:`, and **every rule needs a `pattern:`**. A rule
58
+ without one is dropped silently. Our own published recipe used `dlp:` with
59
+ `pattern_group:` -- neither is read by anything -- so it configured no DLP at
60
+ all. That recipe is corrected, and CI now fails if a shipped example uses a key
61
+ the loader does not consume.
62
+
63
+ ### Not in this release
64
+
65
+ Tool output is observed, not blocked. Control Zero's own audit egress (the path
66
+ where we ship your data to our backend) is still unscanned; that is tracked and
67
+ is the last uncovered egress path.
68
+
69
+ ## 1.13.9 (2026-07-28): Kiro IDE hooks execute on 1.0.x and stay in the correct project
70
+
71
+ ### Fixed
72
+
73
+ - **Kiro IDE hooks no longer appear enabled while silently executing on
74
+ nothing on Kiro 1.0.x (#1952).** The installer now emits `preToolUse` and
75
+ `promptSubmit` hooks in the `.json` format read by Kiro's 1.0 hook loader,
76
+ while retaining the legacy hook files needed by older builds. It does not
77
+ dual-emit `fileEdited`, because both loaders can execute that event and
78
+ doing so would duplicate scans and audit rows. `promptSubmit` now actually
79
+ runs on current Kiro; `preToolUse` is installed in the working format but
80
+ remains disabled by its existing opt-in flag.
81
+ - **Kiro payloads can no longer be claimed by the Claude Code adapter and
82
+ evaluated under the wrong project and DLP profile (#1945).** Kiro IDE 1.0+
83
+ and Kiro CLI v3 use PascalCase agent-harness events that overlap with Claude
84
+ Code. Kiro-specific payload signals now win routing, so attribution, API-key
85
+ selection, policy, and DLP configuration stay with the Kiro integration
86
+ instead of silently crossing tenants.
87
+ - **An inherited `CONTROLZERO_CLIENT` can no longer redirect a Kiro IDE call
88
+ into another host's configuration (#1951).** The Kiro shim now pins its
89
+ client identity rather than preserving a value inherited from the launching
90
+ environment, preventing the same silent wrong-project and wrong-DLP-profile
91
+ outcome.
92
+ - **A DLP mask match on a host that cannot rewrite tool input now tells the
93
+ agent what to fix (#1946).** The denial identifies the affected argument and
94
+ detector type without echoing the sensitive value, so the agent can retry
95
+ with the secret removed instead of dead-ending on a generic unsupported-mask
96
+ message.
97
+
98
+ ### Added
99
+
100
+ - **`controlzero coverage` is generated from the adapters' declared
101
+ capabilities (#1947).** The CLI now exposes each host's enforcement tier,
102
+ deciding and observation events, argument delivery, rewrite support, and
103
+ known gaps from the same data used by adapter contracts.
104
+
105
+ ### Kiro IDE enforcement status
106
+
107
+ Kiro IDE remains `tier=observe`; this release does **not** claim deterministic
108
+ enforcement. It makes the enabled `promptSubmit` hook execute on Kiro 1.0.x and
109
+ puts `preToolUse` one enable flag away from execution. Until that flag is
110
+ enabled and the deterministic block path is verified end to end, the SDK's
111
+ coverage declaration intentionally remains observe-only.
112
+
113
+ ## 1.13.8 (2026-07-27): warn decisions reach the agent + Kiro IDE adapter unblocked (P0)
4
114
 
5
115
  ### Added
6
116
 
@@ -214,7 +324,7 @@
214
324
  "cannot return rewritten tool arguments". **That was wrong**, and correcting
215
325
  it is why this paragraph exists (#202 no-false-claims):
216
326
 
217
- | Host | Can rewrite tool input | Masking behaviour in 1.13.8 |
327
+ | Host | Can rewrite tool input | Masking behaviour in that release |
218
328
  | --- | --- | --- |
219
329
  | Claude Code | Yes -- `hookSpecificOutput.updatedInput` | **Masks and proceeds** |
220
330
  | Gemini CLI | Yes -- `hookSpecificOutput.tool_input` (BeforeTool) | **Masks and proceeds** |
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: controlzero
3
- Version: 1.13.8
3
+ Version: 1.13.10
4
4
  Summary: AI agent governance: policies, audit, and observability for tool calls. Works locally with no signup.
5
5
  Project-URL: Homepage, https://controlzero.ai
6
6
  Project-URL: Documentation, https://docs.controlzero.ai
@@ -130,8 +130,8 @@ Templates available (`controlzero init -t <name>`):
130
130
  - `autogen` — AutoGen starter policy
131
131
  - `codex-cli` — Codex CLI hook starter
132
132
  - `gemini-cli` — Gemini CLI hook starter
133
- - `kiro` — Kiro (AWS) hook starter (CLI: GA; IDE: limited preview)
134
- - `antigravity` — Google Antigravity (IDE + `agy` CLI) hook starter (Beta)
133
+ - `kiro` — Kiro (AWS) hook starter (CLI: GA; IDE: PRIVATE PREVIEW)
134
+ - `antigravity` — Google Antigravity (IDE + `agy` CLI) hook starter (BETA)
135
135
 
136
136
  ## Loading a policy
137
137
 
@@ -75,8 +75,8 @@ Templates available (`controlzero init -t <name>`):
75
75
  - `autogen` — AutoGen starter policy
76
76
  - `codex-cli` — Codex CLI hook starter
77
77
  - `gemini-cli` — Gemini CLI hook starter
78
- - `kiro` — Kiro (AWS) hook starter (CLI: GA; IDE: limited preview)
79
- - `antigravity` — Google Antigravity (IDE + `agy` CLI) hook starter (Beta)
78
+ - `kiro` — Kiro (AWS) hook starter (CLI: GA; IDE: PRIVATE PREVIEW)
79
+ - `antigravity` — Google Antigravity (IDE + `agy` CLI) hook starter (BETA)
80
80
 
81
81
  ## Loading a policy
82
82
 
@@ -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.13.8"
44
+ __version__ = "1.13.10"
45
45
 
46
46
  __all__ = [
47
47
  "Client",
@@ -131,6 +131,51 @@ SYSTEM_MESSAGES: dict[str, dict[str, str]] = {
131
131
  "마스킹되지 않은 값을 전달하는 대신 차단합니다(기본 차단)."
132
132
  ),
133
133
  },
134
+ # DLP mask on a host whose hook protocol cannot accept rewritten tool input
135
+ # (every host except Claude Code + Gemini CLI). The mask escalates to a
136
+ # deny, and THIS is the text the model reads on that deny -- so it is
137
+ # written as an INSTRUCTION to the agent, not as a status line for a human.
138
+ # Goal: the agent re-issues the same call with the sensitive value removed,
139
+ # so the user's task completes in one extra round-trip instead of dead-
140
+ # ending. See controlzero/_internal/dlp_remediation.py.
141
+ #
142
+ # {findings} is TYPE + LOCATION only. It NEVER contains the detected value:
143
+ # echoing the secret into the model's context is the exact disclosure DLP
144
+ # exists to prevent (#1881, "PII never in cleartext").
145
+ #
146
+ # NO ``ko`` ENTRY, deliberately. The Korean pack is a professional draft
147
+ # pending customer review (see the module docstring), and an unreviewed
148
+ # machine translation of a SECURITY INSTRUCTION is worse than the English
149
+ # fallback -- a mistranslated "do not re-send the value" is a leak. Korean
150
+ # locales fall back to ``en``, which is exactly what they get today from the
151
+ # hard-coded English this replaces, so there is no regression to fix later.
152
+ "DLP_MASK_UNSUPPORTED:agent": {
153
+ "en": (
154
+ "Blocked: the tool arguments contain sensitive data, and this "
155
+ "agent host cannot accept redacted arguments, so the call was "
156
+ "blocked instead of masked. Findings: {findings}. To proceed, "
157
+ "retry the SAME call with the sensitive value removed -- refer to "
158
+ "it indirectly (an environment variable the tool expands, a "
159
+ "secret-manager reference, or a file path the tool reads) instead "
160
+ "of inlining it. Do not print, echo, restate, or re-send the value "
161
+ "in any form, including in a summary or an explanation."
162
+ ),
163
+ },
164
+ # Location-free fallback: a mask matched on the flattened argument text but
165
+ # no single argument leaf carries it (a pattern spanning the boundary
166
+ # between two concatenated values). Still actionable, just less precise.
167
+ "DLP_MASK_UNSUPPORTED:base": {
168
+ "en": (
169
+ "Blocked: the tool arguments contain sensitive data, and this "
170
+ "agent host cannot accept redacted arguments, so the call was "
171
+ "blocked instead of masked. To proceed, retry the SAME call with "
172
+ "the sensitive value removed -- refer to it indirectly (an "
173
+ "environment variable the tool expands, a secret-manager "
174
+ "reference, or a file path the tool reads) instead of inlining it. "
175
+ "Do not print, echo, restate, or re-send the value in any form, "
176
+ "including in a summary or an explanation."
177
+ ),
178
+ },
134
179
  # Tamper / quarantine (constructed in client.py + cli/main.py).
135
180
  "BUNDLE_TAMPERED": {
136
181
  "en": (
@@ -0,0 +1,95 @@
1
+ """Observe-only DLP scanning for data produced after a call starts.
2
+
3
+ This deliberately reuses :class:`DLPScanner`. It does not evaluate policy
4
+ effects and cannot return a deny: the data has already been produced.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from typing import Any, Optional
11
+
12
+ from controlzero._internal.dlp_scanner import (
13
+ DLPScanner,
14
+ extract_text_from_args,
15
+ )
16
+ from controlzero._internal.enforcer import (
17
+ DLP_STATUS_CLEAN,
18
+ DLP_STATUS_FINDINGS,
19
+ DLP_STATUS_SKIPPED,
20
+ DLP_STATUS_UNAVAILABLE,
21
+ )
22
+
23
+ DLP_OUTPUT_EFFECT_RECORDED_ONLY = "recorded_only"
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class DLPOutputObservation:
28
+ """Terminal, non-enforcing result of scanning one output payload."""
29
+
30
+ scanned: bool
31
+ status: str
32
+ reason: str
33
+ details: dict[str, Any] = field(default_factory=dict)
34
+ findings: list[dict] = field(default_factory=list)
35
+ effect: str = DLP_OUTPUT_EFFECT_RECORDED_ONLY
36
+
37
+
38
+ def observe_dlp_output(
39
+ scanner: Optional[DLPScanner],
40
+ output: Any,
41
+ ) -> DLPOutputObservation:
42
+ """Scan one complete output and collapse all findings into one observation.
43
+
44
+ The caller owns user messaging and audit emission. Returning one object
45
+ per invocation makes the deduplication boundary explicit: ten matches in
46
+ one payload are still one observation and therefore one notice.
47
+ """
48
+
49
+ if scanner is None:
50
+ return DLPOutputObservation(
51
+ scanned=False,
52
+ status=DLP_STATUS_UNAVAILABLE,
53
+ reason="scanner_uninitialized",
54
+ )
55
+
56
+ try:
57
+ text = extract_text_from_args(output)
58
+ except Exception as exc: # observation must never affect the tool result
59
+ return DLPOutputObservation(
60
+ scanned=False,
61
+ status=DLP_STATUS_UNAVAILABLE,
62
+ reason="output_extraction_error",
63
+ details={"error_type": type(exc).__name__},
64
+ )
65
+
66
+ if not text:
67
+ return DLPOutputObservation(
68
+ scanned=False,
69
+ status=DLP_STATUS_SKIPPED,
70
+ reason="no_scannable_output",
71
+ )
72
+
73
+ try:
74
+ matches = scanner.scan(text)
75
+ except Exception as exc: # observation must never affect the tool result
76
+ return DLPOutputObservation(
77
+ scanned=False,
78
+ status=DLP_STATUS_UNAVAILABLE,
79
+ reason="scanner_error",
80
+ details={"error_type": type(exc).__name__},
81
+ )
82
+
83
+ if not matches:
84
+ return DLPOutputObservation(
85
+ scanned=True,
86
+ status=DLP_STATUS_CLEAN,
87
+ reason="scan_clean",
88
+ )
89
+
90
+ return DLPOutputObservation(
91
+ scanned=True,
92
+ status=DLP_STATUS_FINDINGS,
93
+ reason="scan_findings",
94
+ findings=scanner.get_findings_for_audit(matches),
95
+ )
@@ -0,0 +1,245 @@
1
+ """Agent-actionable denial text for a DLP finding: TYPE + LOCATION, never VALUE.
2
+
3
+ Why this module exists
4
+ ----------------------
5
+ DLP can MASK a secret out of tool arguments, but only on hosts whose hook
6
+ protocol accepts REWRITTEN tool input. Today that is Claude Code and Gemini CLI
7
+ only (``CoverageDecl.can_rewrite_tool_input``); every other host -- Codex CLI,
8
+ Cursor IDE, Cursor CLI, Kiro CLI, Kiro IDE, Antigravity, and the unknown-host
9
+ fallback -- escalates a mask-rule match to a hard DENY.
10
+
11
+ That deny used to say, in full:
12
+
13
+ DLP mask matched but this host cannot rewrite arguments
14
+
15
+ which is true, actionable for nobody, and addressed to the wrong audience. The
16
+ model that issued the call reads it, cannot tell WHICH argument tripped or WHAT
17
+ it tripped on, and typically retries the identical call -- so the user's task
18
+ dead-ends instead of completing.
19
+
20
+ An **agent-actionable denial** closes that gap with no vendor change and no new
21
+ config: the deny reason itself becomes an instruction precise enough that the
22
+ model re-issues the SAME call with the sensitive value removed. One extra
23
+ round-trip instead of a dead end. It works on every non-rewriting host because
24
+ every one of them already routes ``decision.reason`` to a MODEL-visible field:
25
+
26
+ =============== ==========================================================
27
+ host model-visible field carrying ``decision.reason``
28
+ =============== ==========================================================
29
+ codex_cli ``hookSpecificOutput.permissionDecisionReason`` (+ stderr)
30
+ cursor_ide ``agent_message``
31
+ cursor_cli ``agent_message``
32
+ kiro_cli envelope ``reason`` + stderr (exit 2 sends stderr to the model)
33
+ kiro_ide envelope ``reason`` (v1 wraps hook output in ``<HOOK_INSTRUCTION>``)
34
+ antigravity envelope ``reason``
35
+ unknown envelope ``reason``
36
+ =============== ==========================================================
37
+
38
+ So NO adapter needed rewiring for this. The only thing that was missing was
39
+ text worth putting in that channel.
40
+
41
+ THE HARD CONSTRAINT
42
+ -------------------
43
+ **The message must NEVER contain the detected value.** Writing "found AWS key
44
+ AKIA... at offset 34" would echo the secret straight into the model's context
45
+ window -- which is the exact disclosure DLP exists to prevent, and a breach of
46
+ the #1881 "PII never in cleartext" mandate. A deny that leaks the secret to the
47
+ model is strictly worse than the uninformative deny it replaced.
48
+
49
+ Findings are therefore described by TYPE (the rule's name + category, which are
50
+ properties of the RULE definition, never of the matched bytes) and by LOCATION
51
+ (the argument path). Nothing quotes, prefixes, suffixes, hashes-with-a-
52
+ recoverable-prefix, or partially redacts the value; the value never enters this
53
+ module's output by CONSTRUCTION, not by filtering -- there is no code path here
54
+ that reads ``DLPMatch.matched_text``.
55
+
56
+ The one value-derived input is an argument PATH SEGMENT: a dict KEY is attacker/
57
+ model-controlled and could itself BE the secret (``{"AKIA...": "v"}``). That is
58
+ the single echo route, and :func:`_safe_segment` closes it by scanning every
59
+ segment and replacing any that matches a DLP rule with a fixed placeholder. It
60
+ errs toward redacting (an all-caps key like ``ACCESSKEYID`` trips the SWIFT/BIC
61
+ builtin and is redacted); losing a little location precision is the cheap side
62
+ of that trade.
63
+ """
64
+
65
+ from __future__ import annotations
66
+
67
+ from dataclasses import dataclass
68
+ from typing import Any, Optional
69
+
70
+ from controlzero._internal._messages import system_message
71
+
72
+ #: Placeholder for an argument-path segment that is ITSELF sensitive. A dict key
73
+ #: is model-controlled, so ``{"AKIA...": "v"}`` would otherwise route the secret
74
+ #: into the location clause. Value-free and fixed, exactly like the mask
75
+ #: placeholder, so it can never carry a fragment of the original bytes.
76
+ REDACTED_SEGMENT = "<redacted-key>"
77
+
78
+ #: Path segment for a value that is not reachable by a name (a bare scalar
79
+ #: passed as the whole argument object, or a leaf whose parents are all lists).
80
+ ROOT_SEGMENT = "<tool arguments>"
81
+
82
+ #: Cap on how many findings are enumerated. A pathological call can match
83
+ #: hundreds of spans; an unbounded list would bury the instruction it exists to
84
+ #: deliver and burn the model's context. The remainder is reported as a count.
85
+ MAX_LISTED_FINDINGS = 5
86
+
87
+ #: Cap on a rendered argument path. Deeply nested payloads produce long paths
88
+ #: that add no actionable precision past a point.
89
+ MAX_PATH_CHARS = 120
90
+
91
+
92
+ @dataclass(frozen=True)
93
+ class ArgFinding:
94
+ """One sensitive span, described by WHERE it is and WHAT KIND it is.
95
+
96
+ Deliberately carries no field that could hold the matched bytes. Adding one
97
+ would defeat the whole point of this module, so the dataclass shape is the
98
+ enforcement: there is nowhere for a value to live.
99
+ """
100
+
101
+ arg_path: str
102
+ rule_name: str
103
+ category: str
104
+
105
+
106
+ def _safe_segment(scanner: Any, segment: str) -> str:
107
+ """Return ``segment`` unless the segment TEXT is itself sensitive.
108
+
109
+ A dict key comes from the model's own tool call, so it can be the secret
110
+ (``{"ghp_...": "note"}``). Scanning the segment and substituting a fixed
111
+ placeholder is what stops the location clause from becoming the leak.
112
+ """
113
+ if not segment:
114
+ return ROOT_SEGMENT
115
+ try:
116
+ if scanner.scan(segment):
117
+ return REDACTED_SEGMENT
118
+ except Exception: # pragma: no cover - a scanner fault must not leak
119
+ # Fail toward redaction: if we cannot prove the segment is clean, do not
120
+ # print it. This branch mirrors the module-wide asymmetry -- an
121
+ # imprecise location costs a little usefulness, an echoed secret costs
122
+ # everything.
123
+ return REDACTED_SEGMENT
124
+ return segment
125
+
126
+
127
+ def _join(parent: str, child: str) -> str:
128
+ return f"{parent}.{child}" if parent else child
129
+
130
+
131
+ def _walk(
132
+ scanner: Any,
133
+ obj: Any,
134
+ path: str,
135
+ actions: frozenset,
136
+ out: list,
137
+ seen: set,
138
+ ) -> None:
139
+ """Collect ``ArgFinding`` per LEAF so each finding has a real location.
140
+
141
+ Per-leaf, not over the flattened blob ``extract_text_from_args`` produces:
142
+ the flat text's offsets do not map back to an argument, and "somewhere in
143
+ your arguments" is not a location the model can act on.
144
+ """
145
+ if isinstance(obj, dict):
146
+ for key, value in obj.items():
147
+ _walk(scanner, value, _join(path, _safe_segment(scanner, str(key))), actions, out, seen)
148
+ return
149
+ if isinstance(obj, (list, tuple)):
150
+ for index, item in enumerate(obj):
151
+ _walk(scanner, item, f"{path}[{index}]" if path else f"[{index}]", actions, out, seen)
152
+ return
153
+ if obj is None:
154
+ return
155
+ if isinstance(obj, str):
156
+ text = obj
157
+ elif isinstance(obj, (int, float, bool)):
158
+ # Scanned as text for the same reason the masker stringifies scalars:
159
+ # a Korean card / RRN / account number frequently arrives as a JSON
160
+ # NUMBER, and skipping it here would report no location for a finding
161
+ # the engine already decided to block on.
162
+ text = str(obj)
163
+ else:
164
+ return
165
+
166
+ location = path or ROOT_SEGMENT
167
+ if len(location) > MAX_PATH_CHARS:
168
+ location = location[:MAX_PATH_CHARS] + "..."
169
+
170
+ for match in scanner.scan(text):
171
+ if match.action not in actions:
172
+ continue
173
+ # One line per (location, rule) pair. Ten hits of the same rule in one
174
+ # argument is one instruction, not ten.
175
+ key = (location, match.rule_id)
176
+ if key in seen:
177
+ continue
178
+ seen.add(key)
179
+ out.append(
180
+ ArgFinding(
181
+ arg_path=location,
182
+ rule_name=match.rule_name,
183
+ category=match.category,
184
+ )
185
+ )
186
+
187
+
188
+ def locate_findings(
189
+ scanner: Any,
190
+ args: Any,
191
+ *,
192
+ actions: frozenset = frozenset({"mask"}),
193
+ ) -> list:
194
+ """Describe every ``actions`` match in ``args`` by location + type.
195
+
196
+ ``actions`` is a NAMED POSITIVE SET, not a negation of something else, per
197
+ the CLAUDE.md controls mandate: a future DLP action lands OUTSIDE it and is
198
+ simply not described, rather than being silently swept in by an ``!=``.
199
+ """
200
+ out: list = []
201
+ seen: set = set()
202
+ _walk(scanner, args, "", actions, out, seen)
203
+ return out
204
+
205
+
206
+ def _describe(findings: list) -> str:
207
+ """Render the findings clause. TYPE + LOCATION only -- never the value."""
208
+ clauses = [
209
+ f'argument "{f.arg_path}" contains a value matching the '
210
+ f'"{f.rule_name}" {f.category} detector'
211
+ for f in findings[:MAX_LISTED_FINDINGS]
212
+ ]
213
+ overflow = len(findings) - len(clauses)
214
+ if overflow > 0:
215
+ clauses.append(f"and {overflow} further match(es) in other arguments")
216
+ return "; ".join(clauses)
217
+
218
+
219
+ def agent_remediation_message(
220
+ scanner: Any,
221
+ args: Any,
222
+ *,
223
+ actions: frozenset = frozenset({"mask"}),
224
+ locale: Optional[str] = None,
225
+ ) -> str:
226
+ """The deny reason handed to the MODEL on a non-rewriting host.
227
+
228
+ THE single helper this module exists to provide. Returns text that names
229
+ what was found and where, and tells the agent what to do next -- and that,
230
+ by construction, cannot contain the detected value.
231
+
232
+ Falls back to the location-free base message when nothing could be located
233
+ (e.g. a match that only exists across the boundary between two concatenated
234
+ arguments, so no single leaf carries it). Less useful, still never wrong,
235
+ and never a crash on the deny path.
236
+ """
237
+ findings = locate_findings(scanner, args, actions=actions)
238
+ key = "DLP_MASK_UNSUPPORTED:agent" if findings else "DLP_MASK_UNSUPPORTED:base"
239
+ message = system_message(key, locale, findings=_describe(findings))
240
+ # system_message returns None only for an unknown key. Both keys above are
241
+ # in the pack and the pack is pinned by tests, but the deny path must not
242
+ # raise, so a missing entry degrades to the legacy text rather than a
243
+ # traceback that would fail the hook open on hosts that read a crash as a
244
+ # pass.
245
+ return message or "DLP mask matched but this host cannot rewrite arguments"