controlzero 1.13.9__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 (335) hide show
  1. {controlzero-1.13.9 → controlzero-1.13.10}/CHANGELOG.md +66 -0
  2. {controlzero-1.13.9 → controlzero-1.13.10}/PKG-INFO +3 -3
  3. {controlzero-1.13.9 → controlzero-1.13.10}/README.md +2 -2
  4. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/__init__.py +1 -1
  5. controlzero-1.13.10/controlzero/_internal/dlp_output_observer.py +95 -0
  6. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/dlp_scanner.py +110 -30
  7. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/enforcer.py +16 -11
  8. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/audit_remote.py +197 -2
  9. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/hosts/claude_code.py +263 -1
  10. controlzero-1.13.10/controlzero/cli/hosts/gemini_cli.py +221 -0
  11. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/main.py +480 -62
  12. controlzero-1.13.10/controlzero/cli/maturity.py +72 -0
  13. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/setup_wizard.py +294 -30
  14. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/spool_cmd.py +1 -1
  15. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/client.py +149 -29
  16. controlzero-1.13.10/controlzero/integrations/_export_dlp.py +171 -0
  17. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/braintrust.py +32 -11
  18. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/crewai/task.py +20 -0
  19. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/langfuse.py +68 -22
  20. {controlzero-1.13.9 → controlzero-1.13.10}/pyproject.toml +1 -1
  21. {controlzero-1.13.9 → controlzero-1.13.10}/tests/fixtures/adapter-contracts/claude_code.json +14 -1
  22. {controlzero-1.13.9 → controlzero-1.13.10}/tests/fixtures/adapter-contracts/gemini_cli.json +13 -2
  23. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_cli_hook.py +84 -0
  24. controlzero-1.13.10/tests/test_dlp_audit_hashing_1881.py +88 -0
  25. controlzero-1.13.10/tests/test_dlp_egress_conformance_matrix.py +1091 -0
  26. controlzero-1.13.10/tests/test_dlp_path5_audit_egress_1967.py +174 -0
  27. controlzero-1.13.10/tests/test_dlp_paths_8_9_observe_1967.py +170 -0
  28. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_dlp_scanner.py +17 -14
  29. controlzero-1.13.10/tests/test_feature_parity_maturity.py +187 -0
  30. controlzero-1.13.10/tests/test_integration_export_dlp_1967.py +294 -0
  31. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_pr4_install_messaging.py +5 -1
  32. controlzero-1.13.10/tests/test_prompt_submit_dlp_1966.py +421 -0
  33. controlzero-1.13.10/tests/test_setup_wizard.py +504 -0
  34. controlzero-1.13.9/controlzero/cli/hosts/gemini_cli.py +0 -114
  35. controlzero-1.13.9/tests/test_setup_wizard.py +0 -196
  36. {controlzero-1.13.9 → controlzero-1.13.10}/.gitignore +0 -0
  37. {controlzero-1.13.9 → controlzero-1.13.10}/Dockerfile.test +0 -0
  38. {controlzero-1.13.9 → controlzero-1.13.10}/LICENSE +0 -0
  39. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/__main__.py +0 -0
  40. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/__init__.py +0 -0
  41. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/_messages.py +0 -0
  42. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/action_aliases.py +0 -0
  43. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/action_validator.py +0 -0
  44. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/bundle.py +0 -0
  45. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/condition_ops.py +0 -0
  46. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/credential_hook.py +0 -0
  47. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/credential_scanner.py +0 -0
  48. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/credentials_data/__init__.py +0 -0
  49. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/credentials_data/built_in.yaml +0 -0
  50. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/dlp_remediation.py +0 -0
  51. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/hook_extractors.py +0 -0
  52. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/io_capture_producer.py +0 -0
  53. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/source_labels.py +0 -0
  54. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/tool_extractors.json +0 -0
  55. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/_internal/types.py +0 -0
  56. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/audit_local.py +0 -0
  57. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/canonical.py +0 -0
  58. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/__init__.py +0 -0
  59. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/__main__.py +0 -0
  60. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/_secrets.py +0 -0
  61. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/agent_config.py +0 -0
  62. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/console.py +0 -0
  63. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/debug_bundle.py +0 -0
  64. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/doctor.py +0 -0
  65. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/enforcement.py +0 -0
  66. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/exec_cmd.py +0 -0
  67. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/hook_entry.py +0 -0
  68. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/hosts/__init__.py +0 -0
  69. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/hosts/antigravity.py +0 -0
  70. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/hosts/base.py +0 -0
  71. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/hosts/codex_cli.py +0 -0
  72. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/hosts/cursor.py +0 -0
  73. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/hosts/cursor_cli.py +0 -0
  74. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/hosts/kiro.py +0 -0
  75. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/hosts/unknown.py +0 -0
  76. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/kiro_adapter.py +0 -0
  77. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/migrate.py +0 -0
  78. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/output.py +0 -0
  79. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/telemetry_consent.py +0 -0
  80. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/antigravity/hooks.json +0 -0
  81. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/antigravity.yaml +0 -0
  82. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/autogen.yaml +0 -0
  83. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/claude-code.yaml +0 -0
  84. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/codex-cli.yaml +0 -0
  85. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/cost-cap.yaml +0 -0
  86. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/crewai.yaml +0 -0
  87. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/cursor.yaml +0 -0
  88. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/gemini-cli.yaml +0 -0
  89. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/generic.yaml +0 -0
  90. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook +0 -0
  91. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/kiro/ide-pre-tool-use.kiro.hook +0 -0
  92. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/kiro/ide-prompt-submit.kiro.hook +0 -0
  93. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/kiro/kiro.yaml +0 -0
  94. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/langchain.yaml +0 -0
  95. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/mcp.yaml +0 -0
  96. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/templates/rag.yaml +0 -0
  97. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/cli/update_cmd.py +0 -0
  98. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/device.py +0 -0
  99. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/enrollment.py +0 -0
  100. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/error_codes.py +0 -0
  101. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/error_codes.yaml +0 -0
  102. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/errors.py +0 -0
  103. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/hitl/__init__.py +0 -0
  104. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/hitl/grant_protocol.py +0 -0
  105. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/hitl/mock.py +0 -0
  106. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/hitl/pending_approval.py +0 -0
  107. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/hitl/secret_leak_guard.py +0 -0
  108. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/hitl/status.py +0 -0
  109. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/hook_integrity.py +0 -0
  110. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/hooks/__init__.py +0 -0
  111. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/hooks/tool_output_handler.py +0 -0
  112. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/hosted_policy.py +0 -0
  113. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/__init__.py +0 -0
  114. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/anthropic.py +0 -0
  115. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/autogen.py +0 -0
  116. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/crewai/__init__.py +0 -0
  117. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/crewai/agent.py +0 -0
  118. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/crewai/crew.py +0 -0
  119. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/crewai/tool.py +0 -0
  120. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/google.py +0 -0
  121. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/google_adk/__init__.py +0 -0
  122. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/google_adk/agent.py +0 -0
  123. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/google_adk/tool.py +0 -0
  124. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/langchain/__init__.py +0 -0
  125. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/langchain/agent.py +0 -0
  126. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/langchain/callbacks.py +0 -0
  127. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/langchain/chain.py +0 -0
  128. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/langchain/graph.py +0 -0
  129. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/langchain/modern.py +0 -0
  130. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/langchain/tool.py +0 -0
  131. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/litellm.py +0 -0
  132. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/openai.py +0 -0
  133. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/pydantic_ai.py +0 -0
  134. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/integrations/vercel_ai.py +0 -0
  135. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/layout_migration.py +0 -0
  136. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/policy_loader.py +0 -0
  137. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/secrets/__init__.py +0 -0
  138. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/secrets/reference.py +0 -0
  139. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/secrets/resolver.py +0 -0
  140. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/secrets/secretstr.py +0 -0
  141. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/spool/__init__.py +0 -0
  142. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/spool/_compress.py +0 -0
  143. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/spool/_constants.py +0 -0
  144. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/spool/_crc32c.py +0 -0
  145. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/spool/_crypto.py +0 -0
  146. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/spool/_frame.py +0 -0
  147. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/spool/_keyring.py +0 -0
  148. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/spool/_metrics.py +0 -0
  149. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/spool/_spool.py +0 -0
  150. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/spool/_state.py +0 -0
  151. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/spool/_uploader.py +0 -0
  152. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/spool/cz-audit-v1.dict +0 -0
  153. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/tamper.py +0 -0
  154. {controlzero-1.13.9 → controlzero-1.13.10}/controlzero/tracecontext.py +0 -0
  155. {controlzero-1.13.9 → controlzero-1.13.10}/examples/hello_world.py +0 -0
  156. {controlzero-1.13.9 → controlzero-1.13.10}/ruff.toml +0 -0
  157. {controlzero-1.13.9 → controlzero-1.13.10}/tests/_fixtures/antigravity-envelope-expected.json +0 -0
  158. {controlzero-1.13.9 → controlzero-1.13.10}/tests/_fixtures/jcs_args_hash_vectors.json +0 -0
  159. {controlzero-1.13.9 → controlzero-1.13.10}/tests/conftest.py +0 -0
  160. {controlzero-1.13.9 → controlzero-1.13.10}/tests/fixtures/adapter-contracts/antigravity.json +0 -0
  161. {controlzero-1.13.9 → controlzero-1.13.10}/tests/fixtures/adapter-contracts/codex_cli.json +0 -0
  162. {controlzero-1.13.9 → controlzero-1.13.10}/tests/fixtures/adapter-contracts/cursor.json +0 -0
  163. {controlzero-1.13.9 → controlzero-1.13.10}/tests/fixtures/adapter-contracts/kiro.json +0 -0
  164. {controlzero-1.13.9 → controlzero-1.13.10}/tests/integrations/__init__.py +0 -0
  165. {controlzero-1.13.9 → controlzero-1.13.10}/tests/integrations/test_google.py +0 -0
  166. {controlzero-1.13.9 → controlzero-1.13.10}/tests/parity/action_aliases.json +0 -0
  167. {controlzero-1.13.9 → controlzero-1.13.10}/tests/spool/__init__.py +0 -0
  168. {controlzero-1.13.9 → controlzero-1.13.10}/tests/spool/conftest.py +0 -0
  169. {controlzero-1.13.9 → controlzero-1.13.10}/tests/spool/test_spool_cli.py +0 -0
  170. {controlzero-1.13.9 → controlzero-1.13.10}/tests/spool/test_spool_concurrency.py +0 -0
  171. {controlzero-1.13.9 → controlzero-1.13.10}/tests/spool/test_spool_conformance.py +0 -0
  172. {controlzero-1.13.9 → controlzero-1.13.10}/tests/spool/test_spool_core.py +0 -0
  173. {controlzero-1.13.9 → controlzero-1.13.10}/tests/spool/test_spool_crash.py +0 -0
  174. {controlzero-1.13.9 → controlzero-1.13.10}/tests/spool/test_spool_diskfull.py +0 -0
  175. {controlzero-1.13.9 → controlzero-1.13.10}/tests/spool/test_spool_durable_default_tamper.py +0 -0
  176. {controlzero-1.13.9 → controlzero-1.13.10}/tests/spool/test_spool_keychain_dek.py +0 -0
  177. {controlzero-1.13.9 → controlzero-1.13.10}/tests/spool/test_spool_sink_wiring.py +0 -0
  178. {controlzero-1.13.9 → controlzero-1.13.10}/tests/spool/test_spool_transcript_localack.py +0 -0
  179. {controlzero-1.13.9 → controlzero-1.13.10}/tests/spool/test_spool_uploader.py +0 -0
  180. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_action_aliases.py +0 -0
  181. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_action_canonicalization.py +0 -0
  182. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_action_validator_t86.py +0 -0
  183. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_adapter_contracts.py +0 -0
  184. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_adapter_interface.py +0 -0
  185. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_agent_name_env.py +0 -0
  186. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_agy_docs_no_exit2_1373.py +0 -0
  187. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_antigravity_adapter.py +0 -0
  188. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_antigravity_envelope_parity.py +0 -0
  189. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_antigravity_ga_blockers_1248.py +0 -0
  190. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_antigravity_hook_check.py +0 -0
  191. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_antigravity_install.py +0 -0
  192. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_antigravity_posttooluse_observe_58.py +0 -0
  193. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_antigravity_tool_vocab_1303.py +0 -0
  194. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_api_key_mask.py +0 -0
  195. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_api_url_config.py +0 -0
  196. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_audit_project_id_attribution_pr2.py +0 -0
  197. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_audit_remote.py +0 -0
  198. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_audit_remote_io_capture.py +0 -0
  199. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_audit_remote_sdk_version.py +0 -0
  200. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_audit_sink_isolation.py +0 -0
  201. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_bundle_cache_freshness_1303.py +0 -0
  202. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_bundle_parser.py +0 -0
  203. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_bundle_translate.py +0 -0
  204. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_canonical_phase1a.py +0 -0
  205. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_capability_matrix_doc.py +0 -0
  206. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_cli_carve_out.py +0 -0
  207. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_cli_coverage.py +0 -0
  208. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_cli_debug_bundle.py +0 -0
  209. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_cli_extractor_integration.py +0 -0
  210. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_cli_hook_entry.py +0 -0
  211. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_cli_hosted_refresh.py +0 -0
  212. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_cli_init.py +0 -0
  213. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_cli_init_templates.py +0 -0
  214. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_cli_tail.py +0 -0
  215. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_cli_test.py +0 -0
  216. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_cli_validate.py +0 -0
  217. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_coding_agent_hooks.py +0 -0
  218. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_condition_operators_1495.py +0 -0
  219. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_conditions.py +0 -0
  220. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_config_format_parity_1303.py +0 -0
  221. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_conformance.py +0 -0
  222. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_console.py +0 -0
  223. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_credential_hook.py +0 -0
  224. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_cross_adapter_shell_enforcement.py +0 -0
  225. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_cursor_adapter.py +0 -0
  226. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_dashboard_shell_template_1904.py +0 -0
  227. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_default_action.py +0 -0
  228. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_deny_rule_reconciliation_1011.py +0 -0
  229. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_deny_wins_default_catchall_1620.py +0 -0
  230. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_device.py +0 -0
  231. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_dlp_agent_remediation_1938.py +0 -0
  232. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_dlp_mask.py +0 -0
  233. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_dlp_mask_hosts_1938.py +0 -0
  234. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_dlp_origin_contract.py +0 -0
  235. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_dlp_scan_coverage_1927_1931.py +0 -0
  236. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_doctor.py +0 -0
  237. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_engine_version_consistency.py +0 -0
  238. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_enrollment.py +0 -0
  239. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_env_dump_438.py +0 -0
  240. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_epic_1247_bryan_acceptance.py +0 -0
  241. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_error_codes.py +0 -0
  242. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_errors_e_codes.py +0 -0
  243. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_fail_closed_eval.py +0 -0
  244. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_failopen_1303.py +0 -0
  245. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_git_target_1734.py +0 -0
  246. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_glob_matching.py +0 -0
  247. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hitl_5d_email_install.py +0 -0
  248. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hitl_6a_cli_flag.py +0 -0
  249. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hitl_6a_exceptions.py +0 -0
  250. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hitl_6a_get_secret_hitl.py +0 -0
  251. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hitl_6a_mock_backend.py +0 -0
  252. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hitl_6a_pending_approval.py +0 -0
  253. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hitl_6a_request_approval.py +0 -0
  254. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hitl_6a_secret_leak_guard.py +0 -0
  255. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hitl_6a_wait.py +0 -0
  256. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hitl_conformance.py +0 -0
  257. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hitl_phase2b_protocol.py +0 -0
  258. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hitl_reason_codes.py +0 -0
  259. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hitl_validator_keys.py +0 -0
  260. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hook_command_pinning_1705.py +0 -0
  261. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hook_extractors.py +0 -0
  262. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hook_integrity.py +0 -0
  263. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hook_mask_failclosed.py +0 -0
  264. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hook_warn_surfaces_reason_1903.py +0 -0
  265. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hosted_local_audit_1247.py +0 -0
  266. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hosted_local_shadow_warn_1265.py +0 -0
  267. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hosted_policy_e2e.py +0 -0
  268. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hosts_adapter.py +0 -0
  269. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hybrid_mode_strict.py +0 -0
  270. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_hybrid_mode_warn.py +0 -0
  271. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_install_api_url.py +0 -0
  272. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_install_hook_command.py +0 -0
  273. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_install_hooks.py +0 -0
  274. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_io_capture_producer.py +0 -0
  275. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_kiro_adapter.py +0 -0
  276. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_kiro_agent_harness_routing.py +0 -0
  277. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_kiro_cli_e2e.py +0 -0
  278. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_kiro_cli_hook_pin_1265.py +0 -0
  279. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_kiro_cli_v210_hooks.py +0 -0
  280. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_kiro_hook_templates.py +0 -0
  281. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_kiro_ide_v2_hooks_1944.py +0 -0
  282. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_kiro_install.py +0 -0
  283. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_kiro_install_standardisation.py +0 -0
  284. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_kiro_version_schemes.py +0 -0
  285. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_layout_migration_t101.py +0 -0
  286. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_layout_parity_t102.py +0 -0
  287. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_local_mode_dict.py +0 -0
  288. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_local_mode_file_json.py +0 -0
  289. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_local_mode_file_yaml.py +0 -0
  290. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_localized_reason.py +0 -0
  291. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_log_fallback_stderr.py +0 -0
  292. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_log_options_ignored_hosted.py +0 -0
  293. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_log_rotation.py +0 -0
  294. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_migrate.py +0 -0
  295. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_min_sdk_version_gate.py +0 -0
  296. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_multi_client_per_project_175.py +0 -0
  297. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_no_policy_no_key.py +0 -0
  298. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_observe_mode_1247.py +0 -0
  299. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_package_rename_shim.py +0 -0
  300. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_part3_active_policy_count_1303.py +0 -0
  301. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_per_client_isolation_1011.py +0 -0
  302. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_policy_engine_version_phase1b.py +0 -0
  303. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_policy_freshness.py +0 -0
  304. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_policy_load_failclosed_f3.py +0 -0
  305. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_policy_settings.py +0 -0
  306. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_policy_source_audit.py +0 -0
  307. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_pr4_output.py +0 -0
  308. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_pr4_status.py +0 -0
  309. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_probe_audit_isolation.py +0 -0
  310. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_quarantine.py +0 -0
  311. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_reason_code.py +0 -0
  312. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_reason_code_hitl_grant_1596.py +0 -0
  313. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_refresh.py +0 -0
  314. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_secrets.py +0 -0
  315. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_secrets_reference_resolver.py +0 -0
  316. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_shell_semantic_class.py +0 -0
  317. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_source_labels.py +0 -0
  318. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_sql_semantic_class.py +0 -0
  319. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_synthetic_policy_id_t79.py +0 -0
  320. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_t103_precedence.py +0 -0
  321. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_t104_cache_gc.py +0 -0
  322. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_t108_local_override_audit.py +0 -0
  323. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_t96_single_audit_log.py +0 -0
  324. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_t99_install_prefetch_bundle.py +0 -0
  325. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_tamper.py +0 -0
  326. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_tamper_behavior.py +0 -0
  327. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_tamper_hook.py +0 -0
  328. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_telemetry_consent.py +0 -0
  329. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_tls_ca_bundle.py +0 -0
  330. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_tracecontext.py +0 -0
  331. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_unsafe_int_boundary.py +0 -0
  332. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_update_cmd.py +0 -0
  333. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_upgrade_nudge.py +0 -0
  334. {controlzero-1.13.9 → controlzero-1.13.10}/tests/test_version_aware_fail_closed.py +0 -0
  335. {controlzero-1.13.9 → controlzero-1.13.10}/tools/cz-kiro-adapter +0 -0
@@ -1,5 +1,71 @@
1
1
  # Changelog
2
2
 
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
+
3
69
  ## 1.13.9 (2026-07-28): Kiro IDE hooks execute on 1.0.x and stay in the correct project
4
70
 
5
71
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: controlzero
3
- Version: 1.13.9
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.9"
44
+ __version__ = "1.13.10"
45
45
 
46
46
  __all__ = [
47
47
  "Client",
@@ -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
+ )
@@ -11,7 +11,7 @@ Two pattern sources:
11
11
  2. Custom rules: loaded from the policy file's `dlp_rules:` section
12
12
  or pulled from backend for enrolled SDKs.
13
13
 
14
- Secret-category matches store a SHA-256 hash of the matched text, never the
14
+ Matched sensitive values store a SHA-256 hash of the matched text, never the
15
15
  plaintext. This is consistent with the browser extension's HMAC approach.
16
16
 
17
17
  Pattern categories:
@@ -28,6 +28,36 @@ import re
28
28
  from dataclasses import dataclass, field
29
29
  from typing import Any, Optional
30
30
 
31
+ # Categories the backend can emit in SDK bundle rules. This is the union of
32
+ # dashboard-authored custom rule categories (validDLPCategories in
33
+ # dlp_handler.go) and canonical built-in detector categories
34
+ # (internal/dlpcatalog/patterns/*.json). Keep this as a named positive set:
35
+ # every accepted category carries a matched sensitive VALUE and must be hashed.
36
+ # A category introduced by a newer backend is rejected at scan time rather than
37
+ # silently taking a plaintext fallback.
38
+ BACKEND_DLP_VALUE_CATEGORIES = frozenset(
39
+ {
40
+ "compliance",
41
+ "contact",
42
+ "credential",
43
+ "crypto",
44
+ "custom",
45
+ "document",
46
+ "financial",
47
+ "ip",
48
+ "medical",
49
+ "national_id",
50
+ "network",
51
+ "pii",
52
+ "secret",
53
+ "tax",
54
+ }
55
+ )
56
+
57
+ # ``healthcare`` is the older SDK-local spelling used by the embedded NPI/DEA
58
+ # built-ins. The backend catalog spells the corresponding category ``medical``.
59
+ SENSITIVE_VALUE_CATEGORIES = BACKEND_DLP_VALUE_CATEGORIES | {"healthcare"}
60
+
31
61
  # ---------------------------------------------------------------------------
32
62
  # Built-in patterns grouped by category.
33
63
  # All builtins use action="detect". Admins add action="block" via custom rules.
@@ -575,7 +605,7 @@ class DLPRule:
575
605
  id: str
576
606
  name: str
577
607
  pattern: str
578
- category: str # "pii", "financial", "secret", "healthcare"
608
+ category: str
579
609
  action: str # "detect", "block", "mask"
580
610
  region: str = "global"
581
611
  compiled: Optional[re.Pattern] = field(default=None, repr=False)
@@ -593,14 +623,14 @@ class DLPMatch:
593
623
  rule_name: str
594
624
  category: str
595
625
  action: str
596
- matched_text: str # plaintext for pii/financial, SHA-256 hash for secret
626
+ matched_text: str # SHA-256 hash of the matched sensitive value
597
627
  offset: int
598
628
  count: int = 1
599
629
  # Length of the matched span in the SOURCE text. Carried so the
600
630
  # masker can splice the exact span out of a string value without
601
631
  # re-running the regex. Defaults to len(matched_text) so DLPMatch
602
- # objects built by older callers (and the secret path, where
603
- # matched_text is a hash, not the raw value) still report a sensible
632
+ # objects built by older callers (where matched_text may not be the raw
633
+ # value) still report a sensible
604
634
  # length -- but the masker only ever uses the span length recorded at
605
635
  # scan time, never the hash length.
606
636
  length: int = 0
@@ -669,7 +699,7 @@ class DLPScanner:
669
699
  """Number of loaded rules."""
670
700
  return len(self._rules)
671
701
 
672
- def scan(self, text: str) -> list[DLPMatch]:
702
+ def _scan_impl(self, text: str) -> list[DLPMatch]:
673
703
  """Scan text against all loaded DLP rules.
674
704
 
675
705
  Args:
@@ -678,7 +708,7 @@ class DLPScanner:
678
708
 
679
709
  Returns:
680
710
  List of DLPMatch objects for each rule that matched.
681
- For secret-category rules, matched_text is a SHA-256 hash.
711
+ ``matched_text`` is a SHA-256 hash for every supported category.
682
712
  """
683
713
  if not text or not self._rules:
684
714
  return []
@@ -694,13 +724,19 @@ class DLPScanner:
694
724
  for m in found:
695
725
  raw_text = m.group(0)
696
726
 
697
- # Secret-category: hash the matched text, never store plaintext
698
- if rule.category == "secret":
699
- display_text = hashlib.sha256(
700
- raw_text.encode("utf-8")
701
- ).hexdigest()
702
- else:
703
- display_text = raw_text
727
+ # Every supported DLP category carries a sensitive matched
728
+ # VALUE. Enumerate the backend vocabulary positively, and
729
+ # fail loudly on forward drift so a newer category can never
730
+ # inherit a cleartext fallback (#1881, same failure shape as
731
+ # #1927).
732
+ if rule.category not in SENSITIVE_VALUE_CATEGORIES:
733
+ raise ValueError(
734
+ f"unknown DLP value category {rule.category!r}; "
735
+ "refusing to serialize matched text"
736
+ )
737
+ display_text = hashlib.sha256(
738
+ raw_text.encode("utf-8")
739
+ ).hexdigest()
704
740
 
705
741
  matches.append(
706
742
  DLPMatch(
@@ -711,7 +747,7 @@ class DLPScanner:
711
747
  matched_text=display_text,
712
748
  offset=m.start(),
713
749
  # Span length in the SOURCE text (NOT len(display_text),
714
- # which for secrets is a 64-char hash). The masker splices
750
+ # which is a 64-char hash). The masker splices
715
751
  # exactly [offset, offset+length) out of the string value.
716
752
  length=len(raw_text),
717
753
  )
@@ -719,6 +755,39 @@ class DLPScanner:
719
755
 
720
756
  return matches
721
757
 
758
+ def scan(self, text: str) -> list[DLPMatch]:
759
+ """Scan text on the ENFORCEMENT path -- "should this call be allowed".
760
+
761
+ A thin wrapper over :meth:`_scan_impl` on purpose. The egress
762
+ conformance matrix measures host-surface coverage by patching THIS
763
+ method, so anything that patches or spies on ``scan`` observes only
764
+ enforcement scanning. :meth:`scan_for_egress` goes straight to
765
+ ``_scan_impl`` and is therefore invisible to that measurement -- which
766
+ is correct, because our own outbound hygiene is not host coverage and
767
+ counting it as such would inflate the published number.
768
+ """
769
+ return self._scan_impl(text)
770
+
771
+ def scan_for_egress(self, text: str) -> list[DLPMatch]:
772
+ """Scan text we are about to SEND OURSELVES, not text we are policing.
773
+
774
+ Same detection, deliberately a different entry point from :meth:`scan`.
775
+
776
+ These are two different jobs. ``scan`` answers "should this tool call be
777
+ allowed" -- a host-surface enforcement question. This answers "are we
778
+ about to put a customer's secret on our own wire" -- a hygiene question
779
+ about Control Zero's own egress (path 5, #1967).
780
+
781
+ Keeping them distinguishable matters beyond tidiness: the egress
782
+ conformance matrix measures host-surface coverage by spying on
783
+ ``scan``. When redaction shared that entry point, a cell with no host
784
+ surface at all started reporting as "scanned", which would have
785
+ promoted a declaration and inflated the published coverage number on
786
+ the strength of our own outbound hygiene. The measurement has to
787
+ measure what it says it measures.
788
+ """
789
+ return self._scan_impl(text)
790
+
722
791
  def has_blocking_match(self, matches: list[DLPMatch]) -> bool:
723
792
  """Check if any match has action='block'."""
724
793
  return any(m.action == "block" for m in matches)
@@ -836,12 +905,12 @@ class DLPScanner:
836
905
  def get_findings_for_audit(self, matches: list[DLPMatch]) -> list[dict]:
837
906
  """Convert matches to audit-safe findings.
838
907
 
839
- For secret-category matches, only count is included (no text).
908
+ For detect/block matches, ``matched_text`` is the SHA-256 hash produced
909
+ by :meth:`scan`, for every supported sensitive-value category.
840
910
  For mask-action matches, the raw value is REPLACED by the value-free
841
- mask placeholder ("audit shows the masked value, never the secret") --
911
+ mask placeholder ("audit shows the masked value, never the value") --
842
912
  consistent with the gateway, which renders a mask as a value-free
843
- ``[REDACTED-...]`` token everywhere. For other (detect) categories the
844
- matched text is included.
913
+ ``[REDACTED-...]`` token everywhere.
845
914
  """
846
915
  findings: list[dict] = []
847
916
  for m in matches:
@@ -854,22 +923,26 @@ class DLPScanner:
854
923
  if m.action == "mask":
855
924
  # Mask is enforcement, not observation: the audit row records
856
925
  # WHAT the value was masked TO (the value-free placeholder),
857
- # never the raw bytes -- even for pii/financial categories that
858
- # would otherwise echo plaintext under "detect".
926
+ # never the raw bytes. Detect/block findings carry a digest,
927
+ # while mask findings carry the actual replacement token.
859
928
  entry["masked"] = True
860
929
  entry["matched_text"] = mask_placeholder(m.rule_id)
861
- elif m.category != "secret":
862
- # Never include matched text for secrets in audit
930
+ else:
931
+ # scan() has already replaced every supported category's raw
932
+ # value with its SHA-256 digest.
863
933
  entry["matched_text"] = m.matched_text
864
934
  findings.append(entry)
865
935
  return findings
866
936
 
867
937
 
868
- def extract_text_from_args(args: Any) -> str:
938
+ def extract_text_from_args(args: Any, *, include_keys: bool = False) -> str:
869
939
  """Recursively extract all string values from a nested dict/list structure.
870
940
 
871
941
  Handles nested dicts, lists, and mixed structures. Non-string leaf
872
- values are converted to their string representation.
942
+ values are converted to their string representation. Mapping keys are
943
+ omitted by default because tool-argument keys are schema, not user data.
944
+ Egress callers that forward user-controlled mappings can set
945
+ ``include_keys=True`` to scan every key that will cross the boundary too.
873
946
 
874
947
  Args:
875
948
  args: The tool arguments (typically a dict from JSON).
@@ -878,20 +951,27 @@ def extract_text_from_args(args: Any) -> str:
878
951
  A single string with all extracted values joined by newlines.
879
952
  """
880
953
  parts: list[str] = []
881
- _extract_recursive(args, parts)
954
+ _extract_recursive(args, parts, include_keys=include_keys)
882
955
  return "\n".join(parts)
883
956
 
884
957
 
885
- def _extract_recursive(obj: Any, parts: list[str]) -> None:
958
+ def _extract_recursive(
959
+ obj: Any,
960
+ parts: list[str],
961
+ *,
962
+ include_keys: bool = False,
963
+ ) -> None:
886
964
  """Walk a nested structure and collect string representations."""
887
965
  if isinstance(obj, str):
888
966
  parts.append(obj)
889
967
  elif isinstance(obj, dict):
890
- for v in obj.values():
891
- _extract_recursive(v, parts)
968
+ for key, value in obj.items():
969
+ if include_keys:
970
+ _extract_recursive(key, parts, include_keys=True)
971
+ _extract_recursive(value, parts, include_keys=include_keys)
892
972
  elif isinstance(obj, (list, tuple)):
893
973
  for item in obj:
894
- _extract_recursive(item, parts)
974
+ _extract_recursive(item, parts, include_keys=include_keys)
895
975
  elif obj is not None:
896
976
  # Convert numbers, bools, etc. to string for scanning
897
977
  parts.append(str(obj))
@@ -634,6 +634,16 @@ class PolicyEvaluator:
634
634
  """Set or replace the DLP scanner."""
635
635
  self._dlp_scanner = scanner
636
636
 
637
+ def observe_output(self, output):
638
+ """DLP-scan already-produced data without changing its disposition.
639
+
640
+ Kept on the evaluator so this path uses the exact scanner instance
641
+ configured for argument DLP, including built-ins and policy rules.
642
+ """
643
+ from controlzero._internal.dlp_output_observer import observe_dlp_output
644
+
645
+ return observe_dlp_output(self._dlp_scanner, output)
646
+
637
647
  def set_default_action(self, action: str) -> None:
638
648
  """Replace the no-match default action.
639
649
 
@@ -1447,16 +1457,11 @@ class PolicyEvaluator:
1447
1457
  residual = self._dlp_scanner.scan(masked_text) if masked_text else []
1448
1458
  if self._dlp_scanner.has_masking_match(residual):
1449
1459
  # Build the deny's audit findings from ONLY the residual
1450
- # MASK-action matches. A co-matching DETECT-action rule (e.g. a
1451
- # detect rule on "SECRET" that also fires on the un-maskable
1452
- # value) would otherwise have get_findings_for_audit echo its
1453
- # RAW matched_text into the audit row -- raw PII in the audit
1454
- # even on a block is a #202 leak (codex re-review of #1427).
1460
+ # MASK-action matches so the row records the exact fail-closed
1461
+ # condition, not unrelated detect findings on the same bytes.
1455
1462
  # Mask findings render as the value-free [REDACTED-...]
1456
- # placeholder, so this records THAT a mask rule fired and was
1457
- # blocked, never the matched bytes. (Block-action matches can't
1458
- # appear here: a block hit is handled earlier and short-circuits
1459
- # to deny before the mask branch.)
1463
+ # placeholder. (Block-action matches cannot appear here: a
1464
+ # block hit short-circuits before the mask branch.)
1460
1465
  residual_mask_only = [m for m in residual if m.action == "mask"]
1461
1466
  offending = residual_mask_only[0]
1462
1467
  return PolicyDecision(
@@ -1479,8 +1484,8 @@ class PolicyEvaluator:
1479
1484
  # Audit the firing MASK rule(s) WITHOUT echoing the raw
1480
1485
  # value: get_findings_for_audit renders mask findings as the
1481
1486
  # value-free placeholder, so the deny row records that a
1482
- # mask rule fired, never the secret. Detect findings (which
1483
- # carry raw text) are deliberately excluded from this path.
1487
+ # mask rule fired, never the sensitive value. Unrelated
1488
+ # detect findings are deliberately excluded from this path.
1484
1489
  dlp_findings=self._dlp_scanner.get_findings_for_audit(
1485
1490
  residual_mask_only
1486
1491
  ),