controlzero 1.13.9__tar.gz → 1.13.11__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 (333) hide show
  1. {controlzero-1.13.9 → controlzero-1.13.11}/CHANGELOG.md +129 -2
  2. {controlzero-1.13.9 → controlzero-1.13.11}/PKG-INFO +3 -3
  3. {controlzero-1.13.9 → controlzero-1.13.11}/README.md +2 -2
  4. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/__init__.py +1 -1
  5. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/action_validator.py +8 -0
  6. controlzero-1.13.11/controlzero/_internal/dlp_output_observer.py +95 -0
  7. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/dlp_scanner.py +110 -30
  8. controlzero-1.13.11/controlzero/_internal/enforcement_outcome.py +543 -0
  9. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/enforcer.py +293 -40
  10. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/audit_remote.py +197 -2
  11. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/debug_bundle.py +6 -1
  12. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/hosts/antigravity.py +142 -36
  13. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/hosts/base.py +103 -1
  14. controlzero-1.13.11/controlzero/cli/hosts/claims.py +566 -0
  15. controlzero-1.13.11/controlzero/cli/hosts/claude_code.py +569 -0
  16. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/hosts/codex_cli.py +14 -1
  17. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/hosts/cursor_cli.py +6 -0
  18. controlzero-1.13.11/controlzero/cli/hosts/gemini_cli.py +221 -0
  19. controlzero-1.13.11/controlzero/cli/hosts/kiro.py +729 -0
  20. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/main.py +1050 -103
  21. controlzero-1.13.11/controlzero/cli/maturity.py +72 -0
  22. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/setup_wizard.py +294 -30
  23. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/spool_cmd.py +1 -1
  24. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/client.py +318 -33
  25. controlzero-1.13.11/controlzero/hook_integrity.py +1315 -0
  26. controlzero-1.13.11/controlzero/integrations/_export_dlp.py +171 -0
  27. controlzero-1.13.11/controlzero/integrations/_mask_apply.py +146 -0
  28. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/anthropic.py +5 -0
  29. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/autogen.py +15 -4
  30. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/braintrust.py +32 -11
  31. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/crewai/agent.py +5 -0
  32. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/crewai/crew.py +15 -0
  33. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/crewai/task.py +30 -0
  34. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/crewai/tool.py +43 -4
  35. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/google.py +5 -0
  36. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/google_adk/agent.py +5 -0
  37. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/google_adk/tool.py +34 -2
  38. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/langchain/agent.py +20 -0
  39. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/langchain/chain.py +25 -0
  40. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/langchain/graph.py +30 -0
  41. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/langchain/tool.py +41 -2
  42. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/langfuse.py +68 -22
  43. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/litellm.py +5 -0
  44. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/openai.py +10 -0
  45. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/pydantic_ai.py +17 -2
  46. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/vercel_ai.py +5 -0
  47. {controlzero-1.13.9 → controlzero-1.13.11}/pyproject.toml +18 -1
  48. controlzero-1.13.9/Dockerfile.test +0 -24
  49. controlzero-1.13.9/controlzero/cli/hosts/claude_code.py +0 -279
  50. controlzero-1.13.9/controlzero/cli/hosts/gemini_cli.py +0 -114
  51. controlzero-1.13.9/controlzero/cli/hosts/kiro.py +0 -364
  52. controlzero-1.13.9/controlzero/hook_integrity.py +0 -680
  53. controlzero-1.13.9/examples/hello_world.py +0 -20
  54. controlzero-1.13.9/ruff.toml +0 -40
  55. controlzero-1.13.9/tests/_fixtures/antigravity-envelope-expected.json +0 -146
  56. controlzero-1.13.9/tests/_fixtures/jcs_args_hash_vectors.json +0 -111
  57. controlzero-1.13.9/tests/conftest.py +0 -68
  58. controlzero-1.13.9/tests/fixtures/adapter-contracts/antigravity.json +0 -21
  59. controlzero-1.13.9/tests/fixtures/adapter-contracts/claude_code.json +0 -31
  60. controlzero-1.13.9/tests/fixtures/adapter-contracts/codex_cli.json +0 -24
  61. controlzero-1.13.9/tests/fixtures/adapter-contracts/cursor.json +0 -31
  62. controlzero-1.13.9/tests/fixtures/adapter-contracts/gemini_cli.json +0 -25
  63. controlzero-1.13.9/tests/fixtures/adapter-contracts/kiro.json +0 -39
  64. controlzero-1.13.9/tests/integrations/__init__.py +0 -0
  65. controlzero-1.13.9/tests/integrations/test_google.py +0 -96
  66. controlzero-1.13.9/tests/parity/action_aliases.json +0 -55
  67. controlzero-1.13.9/tests/spool/__init__.py +0 -0
  68. controlzero-1.13.9/tests/spool/conftest.py +0 -208
  69. controlzero-1.13.9/tests/spool/test_spool_cli.py +0 -168
  70. controlzero-1.13.9/tests/spool/test_spool_concurrency.py +0 -110
  71. controlzero-1.13.9/tests/spool/test_spool_conformance.py +0 -316
  72. controlzero-1.13.9/tests/spool/test_spool_core.py +0 -415
  73. controlzero-1.13.9/tests/spool/test_spool_crash.py +0 -106
  74. controlzero-1.13.9/tests/spool/test_spool_diskfull.py +0 -77
  75. controlzero-1.13.9/tests/spool/test_spool_durable_default_tamper.py +0 -353
  76. controlzero-1.13.9/tests/spool/test_spool_keychain_dek.py +0 -444
  77. controlzero-1.13.9/tests/spool/test_spool_sink_wiring.py +0 -671
  78. controlzero-1.13.9/tests/spool/test_spool_transcript_localack.py +0 -102
  79. controlzero-1.13.9/tests/spool/test_spool_uploader.py +0 -569
  80. controlzero-1.13.9/tests/test_action_aliases.py +0 -224
  81. controlzero-1.13.9/tests/test_action_canonicalization.py +0 -36
  82. controlzero-1.13.9/tests/test_action_validator_t86.py +0 -129
  83. controlzero-1.13.9/tests/test_adapter_contracts.py +0 -215
  84. controlzero-1.13.9/tests/test_adapter_interface.py +0 -402
  85. controlzero-1.13.9/tests/test_agent_name_env.py +0 -83
  86. controlzero-1.13.9/tests/test_agy_docs_no_exit2_1373.py +0 -131
  87. controlzero-1.13.9/tests/test_antigravity_adapter.py +0 -531
  88. controlzero-1.13.9/tests/test_antigravity_envelope_parity.py +0 -108
  89. controlzero-1.13.9/tests/test_antigravity_ga_blockers_1248.py +0 -565
  90. controlzero-1.13.9/tests/test_antigravity_hook_check.py +0 -164
  91. controlzero-1.13.9/tests/test_antigravity_install.py +0 -431
  92. controlzero-1.13.9/tests/test_antigravity_posttooluse_observe_58.py +0 -295
  93. controlzero-1.13.9/tests/test_antigravity_tool_vocab_1303.py +0 -322
  94. controlzero-1.13.9/tests/test_api_key_mask.py +0 -59
  95. controlzero-1.13.9/tests/test_api_url_config.py +0 -72
  96. controlzero-1.13.9/tests/test_audit_project_id_attribution_pr2.py +0 -304
  97. controlzero-1.13.9/tests/test_audit_remote.py +0 -715
  98. controlzero-1.13.9/tests/test_audit_remote_io_capture.py +0 -141
  99. controlzero-1.13.9/tests/test_audit_remote_sdk_version.py +0 -144
  100. controlzero-1.13.9/tests/test_audit_sink_isolation.py +0 -67
  101. controlzero-1.13.9/tests/test_bundle_cache_freshness_1303.py +0 -302
  102. controlzero-1.13.9/tests/test_bundle_parser.py +0 -238
  103. controlzero-1.13.9/tests/test_bundle_translate.py +0 -263
  104. controlzero-1.13.9/tests/test_canonical_phase1a.py +0 -230
  105. controlzero-1.13.9/tests/test_capability_matrix_doc.py +0 -175
  106. controlzero-1.13.9/tests/test_cli_carve_out.py +0 -513
  107. controlzero-1.13.9/tests/test_cli_coverage.py +0 -62
  108. controlzero-1.13.9/tests/test_cli_debug_bundle.py +0 -390
  109. controlzero-1.13.9/tests/test_cli_extractor_integration.py +0 -527
  110. controlzero-1.13.9/tests/test_cli_hook.py +0 -437
  111. controlzero-1.13.9/tests/test_cli_hook_entry.py +0 -189
  112. controlzero-1.13.9/tests/test_cli_hosted_refresh.py +0 -103
  113. controlzero-1.13.9/tests/test_cli_init.py +0 -43
  114. controlzero-1.13.9/tests/test_cli_init_templates.py +0 -24
  115. controlzero-1.13.9/tests/test_cli_tail.py +0 -33
  116. controlzero-1.13.9/tests/test_cli_test.py +0 -59
  117. controlzero-1.13.9/tests/test_cli_validate.py +0 -37
  118. controlzero-1.13.9/tests/test_coding_agent_hooks.py +0 -894
  119. controlzero-1.13.9/tests/test_condition_operators_1495.py +0 -191
  120. controlzero-1.13.9/tests/test_conditions.py +0 -200
  121. controlzero-1.13.9/tests/test_config_format_parity_1303.py +0 -145
  122. controlzero-1.13.9/tests/test_conformance.py +0 -335
  123. controlzero-1.13.9/tests/test_console.py +0 -87
  124. controlzero-1.13.9/tests/test_credential_hook.py +0 -737
  125. controlzero-1.13.9/tests/test_cross_adapter_shell_enforcement.py +0 -334
  126. controlzero-1.13.9/tests/test_cursor_adapter.py +0 -373
  127. controlzero-1.13.9/tests/test_dashboard_shell_template_1904.py +0 -746
  128. controlzero-1.13.9/tests/test_default_action.py +0 -505
  129. controlzero-1.13.9/tests/test_deny_rule_reconciliation_1011.py +0 -247
  130. controlzero-1.13.9/tests/test_deny_wins_default_catchall_1620.py +0 -130
  131. controlzero-1.13.9/tests/test_device.py +0 -357
  132. controlzero-1.13.9/tests/test_dlp_agent_remediation_1938.py +0 -366
  133. controlzero-1.13.9/tests/test_dlp_mask.py +0 -519
  134. controlzero-1.13.9/tests/test_dlp_mask_hosts_1938.py +0 -164
  135. controlzero-1.13.9/tests/test_dlp_origin_contract.py +0 -620
  136. controlzero-1.13.9/tests/test_dlp_scan_coverage_1927_1931.py +0 -634
  137. controlzero-1.13.9/tests/test_dlp_scanner.py +0 -1215
  138. controlzero-1.13.9/tests/test_doctor.py +0 -150
  139. controlzero-1.13.9/tests/test_engine_version_consistency.py +0 -82
  140. controlzero-1.13.9/tests/test_enrollment.py +0 -820
  141. controlzero-1.13.9/tests/test_env_dump_438.py +0 -148
  142. controlzero-1.13.9/tests/test_epic_1247_bryan_acceptance.py +0 -531
  143. controlzero-1.13.9/tests/test_error_codes.py +0 -122
  144. controlzero-1.13.9/tests/test_errors_e_codes.py +0 -301
  145. controlzero-1.13.9/tests/test_fail_closed_eval.py +0 -19
  146. controlzero-1.13.9/tests/test_failopen_1303.py +0 -96
  147. controlzero-1.13.9/tests/test_git_target_1734.py +0 -242
  148. controlzero-1.13.9/tests/test_glob_matching.py +0 -287
  149. controlzero-1.13.9/tests/test_hitl_5d_email_install.py +0 -137
  150. controlzero-1.13.9/tests/test_hitl_6a_cli_flag.py +0 -306
  151. controlzero-1.13.9/tests/test_hitl_6a_exceptions.py +0 -166
  152. controlzero-1.13.9/tests/test_hitl_6a_get_secret_hitl.py +0 -672
  153. controlzero-1.13.9/tests/test_hitl_6a_mock_backend.py +0 -342
  154. controlzero-1.13.9/tests/test_hitl_6a_pending_approval.py +0 -362
  155. controlzero-1.13.9/tests/test_hitl_6a_request_approval.py +0 -729
  156. controlzero-1.13.9/tests/test_hitl_6a_secret_leak_guard.py +0 -334
  157. controlzero-1.13.9/tests/test_hitl_6a_wait.py +0 -673
  158. controlzero-1.13.9/tests/test_hitl_conformance.py +0 -286
  159. controlzero-1.13.9/tests/test_hitl_phase2b_protocol.py +0 -643
  160. controlzero-1.13.9/tests/test_hitl_reason_codes.py +0 -78
  161. controlzero-1.13.9/tests/test_hitl_validator_keys.py +0 -271
  162. controlzero-1.13.9/tests/test_hook_command_pinning_1705.py +0 -325
  163. controlzero-1.13.9/tests/test_hook_extractors.py +0 -566
  164. controlzero-1.13.9/tests/test_hook_integrity.py +0 -521
  165. controlzero-1.13.9/tests/test_hook_mask_failclosed.py +0 -95
  166. controlzero-1.13.9/tests/test_hook_warn_surfaces_reason_1903.py +0 -479
  167. controlzero-1.13.9/tests/test_hosted_local_audit_1247.py +0 -219
  168. controlzero-1.13.9/tests/test_hosted_local_shadow_warn_1265.py +0 -265
  169. controlzero-1.13.9/tests/test_hosted_policy_e2e.py +0 -383
  170. controlzero-1.13.9/tests/test_hosts_adapter.py +0 -686
  171. controlzero-1.13.9/tests/test_hybrid_mode_strict.py +0 -90
  172. controlzero-1.13.9/tests/test_hybrid_mode_warn.py +0 -28
  173. controlzero-1.13.9/tests/test_install_api_url.py +0 -69
  174. controlzero-1.13.9/tests/test_install_hook_command.py +0 -145
  175. controlzero-1.13.9/tests/test_install_hooks.py +0 -1244
  176. controlzero-1.13.9/tests/test_io_capture_producer.py +0 -172
  177. controlzero-1.13.9/tests/test_kiro_adapter.py +0 -1422
  178. controlzero-1.13.9/tests/test_kiro_agent_harness_routing.py +0 -196
  179. controlzero-1.13.9/tests/test_kiro_cli_e2e.py +0 -353
  180. controlzero-1.13.9/tests/test_kiro_cli_hook_pin_1265.py +0 -133
  181. controlzero-1.13.9/tests/test_kiro_cli_v210_hooks.py +0 -120
  182. controlzero-1.13.9/tests/test_kiro_hook_templates.py +0 -324
  183. controlzero-1.13.9/tests/test_kiro_ide_v2_hooks_1944.py +0 -480
  184. controlzero-1.13.9/tests/test_kiro_install.py +0 -463
  185. controlzero-1.13.9/tests/test_kiro_install_standardisation.py +0 -218
  186. controlzero-1.13.9/tests/test_kiro_version_schemes.py +0 -123
  187. controlzero-1.13.9/tests/test_layout_migration_t101.py +0 -158
  188. controlzero-1.13.9/tests/test_layout_parity_t102.py +0 -229
  189. controlzero-1.13.9/tests/test_local_mode_dict.py +0 -98
  190. controlzero-1.13.9/tests/test_local_mode_file_json.py +0 -33
  191. controlzero-1.13.9/tests/test_local_mode_file_yaml.py +0 -62
  192. controlzero-1.13.9/tests/test_localized_reason.py +0 -303
  193. controlzero-1.13.9/tests/test_log_fallback_stderr.py +0 -33
  194. controlzero-1.13.9/tests/test_log_options_ignored_hosted.py +0 -50
  195. controlzero-1.13.9/tests/test_log_rotation.py +0 -66
  196. controlzero-1.13.9/tests/test_migrate.py +0 -128
  197. controlzero-1.13.9/tests/test_min_sdk_version_gate.py +0 -206
  198. controlzero-1.13.9/tests/test_multi_client_per_project_175.py +0 -810
  199. controlzero-1.13.9/tests/test_no_policy_no_key.py +0 -28
  200. controlzero-1.13.9/tests/test_observe_mode_1247.py +0 -362
  201. controlzero-1.13.9/tests/test_package_rename_shim.py +0 -41
  202. controlzero-1.13.9/tests/test_part3_active_policy_count_1303.py +0 -142
  203. controlzero-1.13.9/tests/test_per_client_isolation_1011.py +0 -250
  204. controlzero-1.13.9/tests/test_policy_engine_version_phase1b.py +0 -73
  205. controlzero-1.13.9/tests/test_policy_freshness.py +0 -274
  206. controlzero-1.13.9/tests/test_policy_load_failclosed_f3.py +0 -278
  207. controlzero-1.13.9/tests/test_policy_settings.py +0 -84
  208. controlzero-1.13.9/tests/test_policy_source_audit.py +0 -149
  209. controlzero-1.13.9/tests/test_pr4_install_messaging.py +0 -376
  210. controlzero-1.13.9/tests/test_pr4_output.py +0 -144
  211. controlzero-1.13.9/tests/test_pr4_status.py +0 -160
  212. controlzero-1.13.9/tests/test_probe_audit_isolation.py +0 -177
  213. controlzero-1.13.9/tests/test_quarantine.py +0 -191
  214. controlzero-1.13.9/tests/test_reason_code.py +0 -392
  215. controlzero-1.13.9/tests/test_reason_code_hitl_grant_1596.py +0 -34
  216. controlzero-1.13.9/tests/test_refresh.py +0 -567
  217. controlzero-1.13.9/tests/test_secrets.py +0 -225
  218. controlzero-1.13.9/tests/test_secrets_reference_resolver.py +0 -191
  219. controlzero-1.13.9/tests/test_setup_wizard.py +0 -196
  220. controlzero-1.13.9/tests/test_shell_semantic_class.py +0 -243
  221. controlzero-1.13.9/tests/test_source_labels.py +0 -95
  222. controlzero-1.13.9/tests/test_sql_semantic_class.py +0 -212
  223. controlzero-1.13.9/tests/test_synthetic_policy_id_t79.py +0 -324
  224. controlzero-1.13.9/tests/test_t103_precedence.py +0 -164
  225. controlzero-1.13.9/tests/test_t104_cache_gc.py +0 -174
  226. controlzero-1.13.9/tests/test_t108_local_override_audit.py +0 -161
  227. controlzero-1.13.9/tests/test_t96_single_audit_log.py +0 -125
  228. controlzero-1.13.9/tests/test_t99_install_prefetch_bundle.py +0 -182
  229. controlzero-1.13.9/tests/test_tamper.py +0 -203
  230. controlzero-1.13.9/tests/test_tamper_behavior.py +0 -163
  231. controlzero-1.13.9/tests/test_tamper_hook.py +0 -307
  232. controlzero-1.13.9/tests/test_telemetry_consent.py +0 -90
  233. controlzero-1.13.9/tests/test_tls_ca_bundle.py +0 -112
  234. controlzero-1.13.9/tests/test_tracecontext.py +0 -89
  235. controlzero-1.13.9/tests/test_unsafe_int_boundary.py +0 -66
  236. controlzero-1.13.9/tests/test_update_cmd.py +0 -77
  237. controlzero-1.13.9/tests/test_upgrade_nudge.py +0 -54
  238. controlzero-1.13.9/tests/test_version_aware_fail_closed.py +0 -212
  239. controlzero-1.13.9/tools/cz-kiro-adapter +0 -31
  240. {controlzero-1.13.9 → controlzero-1.13.11}/.gitignore +0 -0
  241. {controlzero-1.13.9 → controlzero-1.13.11}/LICENSE +0 -0
  242. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/__main__.py +0 -0
  243. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/__init__.py +0 -0
  244. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/_messages.py +0 -0
  245. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/action_aliases.py +0 -0
  246. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/bundle.py +0 -0
  247. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/condition_ops.py +0 -0
  248. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/credential_hook.py +0 -0
  249. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/credential_scanner.py +0 -0
  250. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/credentials_data/__init__.py +0 -0
  251. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/credentials_data/built_in.yaml +0 -0
  252. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/dlp_remediation.py +0 -0
  253. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/hook_extractors.py +0 -0
  254. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/io_capture_producer.py +0 -0
  255. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/source_labels.py +0 -0
  256. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/tool_extractors.json +0 -0
  257. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/_internal/types.py +0 -0
  258. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/audit_local.py +0 -0
  259. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/canonical.py +0 -0
  260. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/__init__.py +0 -0
  261. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/__main__.py +0 -0
  262. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/_secrets.py +0 -0
  263. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/agent_config.py +0 -0
  264. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/console.py +0 -0
  265. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/doctor.py +0 -0
  266. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/enforcement.py +0 -0
  267. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/exec_cmd.py +0 -0
  268. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/hook_entry.py +0 -0
  269. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/hosts/__init__.py +0 -0
  270. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/hosts/cursor.py +0 -0
  271. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/hosts/unknown.py +0 -0
  272. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/kiro_adapter.py +0 -0
  273. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/migrate.py +0 -0
  274. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/output.py +0 -0
  275. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/telemetry_consent.py +0 -0
  276. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/antigravity/hooks.json +0 -0
  277. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/antigravity.yaml +0 -0
  278. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/autogen.yaml +0 -0
  279. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/claude-code.yaml +0 -0
  280. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/codex-cli.yaml +0 -0
  281. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/cost-cap.yaml +0 -0
  282. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/crewai.yaml +0 -0
  283. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/cursor.yaml +0 -0
  284. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/gemini-cli.yaml +0 -0
  285. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/generic.yaml +0 -0
  286. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook +0 -0
  287. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/kiro/ide-pre-tool-use.kiro.hook +0 -0
  288. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/kiro/ide-prompt-submit.kiro.hook +0 -0
  289. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/kiro/kiro.yaml +0 -0
  290. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/langchain.yaml +0 -0
  291. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/mcp.yaml +0 -0
  292. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/templates/rag.yaml +0 -0
  293. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/cli/update_cmd.py +0 -0
  294. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/device.py +0 -0
  295. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/enrollment.py +0 -0
  296. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/error_codes.py +0 -0
  297. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/error_codes.yaml +0 -0
  298. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/errors.py +0 -0
  299. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/hitl/__init__.py +0 -0
  300. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/hitl/grant_protocol.py +0 -0
  301. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/hitl/mock.py +0 -0
  302. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/hitl/pending_approval.py +0 -0
  303. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/hitl/secret_leak_guard.py +0 -0
  304. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/hitl/status.py +0 -0
  305. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/hooks/__init__.py +0 -0
  306. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/hooks/tool_output_handler.py +0 -0
  307. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/hosted_policy.py +0 -0
  308. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/__init__.py +0 -0
  309. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/crewai/__init__.py +0 -0
  310. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/google_adk/__init__.py +0 -0
  311. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/langchain/__init__.py +0 -0
  312. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/langchain/callbacks.py +0 -0
  313. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/integrations/langchain/modern.py +0 -0
  314. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/layout_migration.py +0 -0
  315. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/policy_loader.py +0 -0
  316. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/secrets/__init__.py +0 -0
  317. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/secrets/reference.py +0 -0
  318. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/secrets/resolver.py +0 -0
  319. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/secrets/secretstr.py +0 -0
  320. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/spool/__init__.py +0 -0
  321. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/spool/_compress.py +0 -0
  322. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/spool/_constants.py +0 -0
  323. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/spool/_crc32c.py +0 -0
  324. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/spool/_crypto.py +0 -0
  325. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/spool/_frame.py +0 -0
  326. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/spool/_keyring.py +0 -0
  327. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/spool/_metrics.py +0 -0
  328. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/spool/_spool.py +0 -0
  329. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/spool/_state.py +0 -0
  330. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/spool/_uploader.py +0 -0
  331. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/spool/cz-audit-v1.dict +0 -0
  332. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/tamper.py +0 -0
  333. {controlzero-1.13.9 → controlzero-1.13.11}/controlzero/tracecontext.py +0 -0
@@ -1,5 +1,132 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.13.11 (2026-08-10): kiro_cli prompts were never actually scanned
4
+
5
+ 1.13.10 said "Prompt submission is now a scanned surface on the hosts that
6
+ expose one". For `kiro_cli` that was not true, and this release makes it true.
7
+
8
+ ### Fixed
9
+
10
+ - **`kiro_cli` returned an affirmative ALLOW on a prompt it never read (#2108).**
11
+ A `userPromptSubmit` payload carries no `tool_name`, so it took the
12
+ missing-tool_name branch: under `--strict` (what the installer writes) it was
13
+ REFUSED without being scanned, and without `--strict` it was ALLOWED without
14
+ being scanned. Driven against the real kiro-cli 2.16.2 payload, all three of
15
+ a secret-bearing prompt, a clean prompt and an empty prompt produced
16
+ BYTE-IDENTICAL output -- the prompt text was not an input to the decision at
17
+ all -- and NO audit row was written, so the call was unattributable.
18
+ The prompt is now lifted into the `prompt` pseudo-tool and rides the existing
19
+ DLP path. The three cases now differ, and every one is audited.
20
+
21
+ - **An ambient `CLAUDECODE` environment variable stole the routing.** A
22
+ camelCase kiro payload in a shell where `CLAUDECODE=1` was claimed by the
23
+ Claude Code adapter, and the secret never reached the scanner. Anyone running
24
+ kiro-cli inside a Claude Code session had no prompt DLP at all.
25
+
26
+ - **Padding a prompt past the 1 MiB scan cap bypassed DLP entirely.** The
27
+ bounded UTF-8 prefix is now scanned, and a clean prefix is still a deny with
28
+ `skipped/payload_over_size_cap` -- never reported as `clean`.
29
+
30
+ - **`--strict` failed open on a non-dict payload.** It now refuses, like the
31
+ other four unusable-payload branches.
32
+
33
+ - **`prompt` was not a canonical tool**, so authoring `deny: prompt:*` -- the
34
+ exact rule this feature exists for -- printed "not a known canonical action".
35
+
36
+ - **kiro-cli v3 PascalCase prompt events** were uncovered; normalisation is now
37
+ shared by both Kiro adapters so the result does not depend on which adapter
38
+ won the routing race.
39
+
40
+ ### Known limitations, stated plainly
41
+
42
+ - **kiro-cli does not enforce this event.** Driven on 2.16.2: a hook exiting 2
43
+ on `userPromptSubmit` is logged as failed and the prompt reaches the model
44
+ anyway; a structured stdout deny is also ignored. Control Zero SCANS and
45
+ RECORDS the prompt on this host. It does not block it. Nothing in this
46
+ release claims otherwise.
47
+
48
+ - **The raw-prompt marker does not reach the hosted dashboard.** It reaches the
49
+ DLP scanner and the LOCAL audit record; surfacing it remotely needs a backend
50
+ field and is not done here.
51
+
52
+ - **The Node SDK's equivalent fix is still unreachable as installed (#2208).**
53
+ This release fixes Python only.
54
+
55
+ ## 1.13.10 (2026-07-30): DLP now covers prompts and tool output, not just tool arguments
56
+
57
+ Until this release, Control Zero scanned the ARGUMENTS of a tool call and
58
+ nothing else. A secret could reach a model or leave the machine by three other
59
+ routes that looked identical to a working setup. All three are closed here.
60
+
61
+ ### Fixed
62
+
63
+ - **A secret pasted into a prompt was sent to the model unscanned (#1966).**
64
+ Prompt submission is now a scanned surface on the hosts that expose one
65
+ (Claude Code `UserPromptSubmit`, Gemini CLI `BeforeAgent`). Previously only
66
+ tool calls were checked, so pasting a key into the chat box egressed it.
67
+
68
+ - **Credentials from dashboard-configured DLP rules were written to the audit
69
+ log in cleartext (#1881 follow-up).** The scanner hashed matches only when
70
+ the rule category was the literal string `secret`; dashboard rules arrive as
71
+ `credential`, so those matches were stored verbatim. The audit trail was
72
+ keeping the secret it had just detected. Now gated on a named set of
73
+ value-bearing categories, so a category added later cannot reach cleartext.
74
+
75
+ - **Control Zero's own audit egress was shipping your raw tool input and
76
+ output (#1967 path 5).** `input_payload` / `output_payload` carried customer
77
+ content verbatim to our backend, and redaction was deferred to the backend --
78
+ i.e. applied after the bytes had already left your machine. They are now
79
+ scanned and redacted BEFORE the wire, every sensitive span rather than only
80
+ `mask`-action rules, and the payload is DROPPED rather than forwarded if the
81
+ scanner cannot run. `io_dlp_scan_status` is always set, so "not scanned" stays
82
+ distinguishable from "scanned and clean".
83
+
84
+ - **Spooled audit records were replayed without ever being scanned (#1967).**
85
+ Replay validated JSON shape, tool name and decision -- never payload content
86
+ -- then transmitted the stored bytes as-is, possibly days later on a
87
+ different network. Records written before this release are treated as
88
+ unscanned rather than trusted.
89
+
90
+ - **The auth cache dropped `key_type`, silently disabling the audit
91
+ anti-spoof guarantee.** `key_type == "extension"` is server-authoritative
92
+ for a row's `surface_class`; on every cache HIT the field was empty, so the
93
+ wire-supplied value was honoured instead. The guarantee held only on the
94
+ first request after a cache miss.
95
+
96
+ ### Added
97
+
98
+ - **Sensitive data in tool OUTPUT is now detected and recorded (#1965).**
99
+ Reading a file that contains a secret is caught. The tool call is NOT
100
+ blocked -- an agent reading a file is normal work -- so this is deliberately
101
+ observe-and-record: one deduplicated message per tool call, and an audit row
102
+ carrying `effect=recorded_only` that is never labelled a block.
103
+
104
+ - **DLP runs before export to third-party observability (#1967).** Braintrust
105
+ and Langfuse were receiving full tool arguments and output previews with no
106
+ scan in between.
107
+
108
+ ### Changed
109
+
110
+ - API-key creation rejects a `key_type` outside `{live, test}` with
111
+ `INVALID_CREDENTIAL_KIND` (400) instead of accepting any string and
112
+ interpolating it into the stored key prefix. Bulk extension-key prefixes are
113
+ limited to 1-8 characters of `[a-z0-9-]`, which is what the database column
114
+ actually fits.
115
+
116
+ ### Notes for anyone writing a policy by hand
117
+
118
+ DLP rules go under `dlp_rules:`, and **every rule needs a `pattern:`**. A rule
119
+ without one is dropped silently. Our own published recipe used `dlp:` with
120
+ `pattern_group:` -- neither is read by anything -- so it configured no DLP at
121
+ all. That recipe is corrected, and CI now fails if a shipped example uses a key
122
+ the loader does not consume.
123
+
124
+ ### Not in this release
125
+
126
+ Tool output is observed, not blocked. That is the intended behaviour, not a
127
+ limitation awaiting a fix: an agent reading a file is normal work, and blocking
128
+ it would make Control Zero unusable.
129
+
3
130
  ## 1.13.9 (2026-07-28): Kiro IDE hooks execute on 1.0.x and stay in the correct project
4
131
 
5
132
  ### Fixed
@@ -1119,7 +1246,7 @@ changes do not perform.
1119
1246
 
1120
1247
  ## 1.9.3 -- 2026-06-16 (posture release: empty-bundle OBSERVE + self-explaining no-rule-match deny, epic gh#1247)
1121
1248
 
1122
- The consolidated **posture release** for epic gh#1247 (customer Bryan).
1249
+ The consolidated **posture release** for epic gh#1247 (a customer).
1123
1250
  Three customer-visible behaviors land together so the engine's "what
1124
1251
  happens when a tool isn't covered by a rule" story is coherent across
1125
1252
  both SDKs and the backend:
@@ -1134,7 +1261,7 @@ both SDKs and the backend:
1134
1261
 
1135
1262
  - **Hosted no-rule-match deny is now self-explaining (folds gh#1257).** A
1136
1263
  non-empty bundle with `default_action=deny` and zero rule matches (the
1137
- exact shape of Bryan's "Db read only" allow-list, which excludes
1264
+ exact shape of that customer's "Db read only" allow-list, which excludes
1138
1265
  `bash`) used to deny with the bare, bug-looking message
1139
1266
  `No matching policy rule (fail-closed default)` -- no path out. The
1140
1267
  deny reason now NAMES the unmatched action (e.g. `bash:find`) and the
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: controlzero
3
- Version: 1.13.9
3
+ Version: 1.13.11
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.11"
45
45
 
46
46
  __all__ = [
47
47
  "Client",
@@ -44,6 +44,14 @@ _CANONICAL_TOOLS: set[str] = {
44
44
  # were in fact firing correctly (#1904). Warn-only path; no decision
45
45
  # depends on this set.
46
46
  "file_delete", "network",
47
+ # Prompt-submission pseudo-tool (#1966 / #2108). claude_code.py,
48
+ # gemini_cli.py, cursor.py and kiro.py all lift a submitted prompt into
49
+ # tool_name="prompt" so ONE operator-authored `prompt:*` rule governs every
50
+ # host. It was absent here, so writing exactly the rule the feature is for
51
+ # printed "not a known canonical action" -- a correct rule being told it is
52
+ # wrong, which is how an operator concludes the surface is unsupported and
53
+ # stops writing the rule. Warn-only path; no decision depends on this set.
54
+ "prompt",
47
55
  # database aliases
48
56
  "sql", "Database", "PostgreSQL", "MySQL", "postgres", "sqlite",
49
57
  # Bash aliases
@@ -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))