controlzero 1.11.6__tar.gz → 1.11.9__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 (285) hide show
  1. {controlzero-1.11.6 → controlzero-1.11.9}/.gitignore +8 -0
  2. {controlzero-1.11.6 → controlzero-1.11.9}/CHANGELOG.md +128 -0
  3. {controlzero-1.11.6 → controlzero-1.11.9}/PKG-INFO +27 -1
  4. {controlzero-1.11.6 → controlzero-1.11.9}/README.md +26 -0
  5. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/__init__.py +1 -1
  6. controlzero-1.11.9/controlzero/_internal/_messages.py +273 -0
  7. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/_internal/bundle.py +15 -0
  8. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/_internal/dlp_scanner.py +151 -3
  9. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/_internal/enforcer.py +193 -39
  10. controlzero-1.11.9/controlzero/_internal/io_capture_producer.py +264 -0
  11. controlzero-1.11.9/controlzero/_internal/source_labels.py +165 -0
  12. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/_internal/types.py +7 -0
  13. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/audit_remote.py +221 -2
  14. controlzero-1.11.9/controlzero/cli/agent_config.py +171 -0
  15. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/hosts/base.py +7 -1
  16. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/main.py +1132 -129
  17. controlzero-1.11.9/controlzero/cli/output.py +172 -0
  18. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/client.py +115 -28
  19. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/enrollment.py +188 -0
  20. controlzero-1.11.9/controlzero/hook_integrity.py +673 -0
  21. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/policy_loader.py +33 -0
  22. {controlzero-1.11.6 → controlzero-1.11.9}/pyproject.toml +1 -1
  23. {controlzero-1.11.6 → controlzero-1.11.9}/tests/spool/test_spool_sink_wiring.py +51 -0
  24. controlzero-1.11.9/tests/test_audit_project_id_attribution_pr2.py +305 -0
  25. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_audit_remote.py +188 -0
  26. controlzero-1.11.9/tests/test_audit_remote_io_capture.py +142 -0
  27. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_canonical_phase1a.py +19 -18
  28. controlzero-1.11.9/tests/test_dlp_mask.py +520 -0
  29. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_enrollment.py +274 -0
  30. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_hitl_validator_keys.py +6 -1
  31. controlzero-1.11.9/tests/test_hook_integrity.py +500 -0
  32. controlzero-1.11.9/tests/test_hook_mask_failclosed.py +95 -0
  33. controlzero-1.11.9/tests/test_io_capture_producer.py +172 -0
  34. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_kiro_cli_e2e.py +49 -1
  35. controlzero-1.11.9/tests/test_localized_reason.py +302 -0
  36. controlzero-1.11.9/tests/test_pr4_install_messaging.py +377 -0
  37. controlzero-1.11.9/tests/test_pr4_output.py +144 -0
  38. controlzero-1.11.9/tests/test_pr4_status.py +139 -0
  39. controlzero-1.11.9/tests/test_source_labels.py +95 -0
  40. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_synthetic_policy_id_t79.py +6 -2
  41. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_t99_install_prefetch_bundle.py +5 -5
  42. {controlzero-1.11.6 → controlzero-1.11.9}/Dockerfile.test +0 -0
  43. {controlzero-1.11.6 → controlzero-1.11.9}/LICENSE +0 -0
  44. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/__main__.py +0 -0
  45. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/_internal/__init__.py +0 -0
  46. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/_internal/action_aliases.py +0 -0
  47. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/_internal/action_validator.py +0 -0
  48. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/_internal/credential_hook.py +0 -0
  49. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/_internal/credential_scanner.py +0 -0
  50. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/_internal/credentials_data/__init__.py +0 -0
  51. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/_internal/credentials_data/built_in.yaml +0 -0
  52. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/_internal/hook_extractors.py +0 -0
  53. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/_internal/tool_extractors.json +0 -0
  54. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/audit_local.py +0 -0
  55. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/canonical.py +0 -0
  56. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/__init__.py +0 -0
  57. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/__main__.py +0 -0
  58. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/_secrets.py +0 -0
  59. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/console.py +0 -0
  60. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/debug_bundle.py +0 -0
  61. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/doctor.py +0 -0
  62. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/exec_cmd.py +0 -0
  63. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/hook_entry.py +0 -0
  64. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/hosts/__init__.py +0 -0
  65. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/hosts/antigravity.py +0 -0
  66. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/hosts/claude_code.py +0 -0
  67. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/hosts/codex_cli.py +0 -0
  68. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/hosts/cursor.py +0 -0
  69. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/hosts/cursor_cli.py +0 -0
  70. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/hosts/gemini_cli.py +0 -0
  71. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/hosts/kiro.py +0 -0
  72. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/hosts/unknown.py +0 -0
  73. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/kiro_adapter.py +0 -0
  74. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/migrate.py +0 -0
  75. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/spool_cmd.py +0 -0
  76. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/telemetry_consent.py +0 -0
  77. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/templates/antigravity/hooks.json +0 -0
  78. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/templates/antigravity.yaml +0 -0
  79. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/templates/autogen.yaml +0 -0
  80. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/templates/claude-code.yaml +0 -0
  81. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/templates/codex-cli.yaml +0 -0
  82. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/templates/cost-cap.yaml +0 -0
  83. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/templates/crewai.yaml +0 -0
  84. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/templates/cursor.yaml +0 -0
  85. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/templates/gemini-cli.yaml +0 -0
  86. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/templates/generic.yaml +0 -0
  87. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook +0 -0
  88. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/templates/kiro/ide-pre-tool-use.kiro.hook +0 -0
  89. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/templates/kiro/ide-prompt-submit.kiro.hook +0 -0
  90. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/templates/kiro/kiro.yaml +0 -0
  91. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/templates/langchain.yaml +0 -0
  92. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/templates/mcp.yaml +0 -0
  93. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/templates/rag.yaml +0 -0
  94. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/cli/update_cmd.py +0 -0
  95. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/device.py +0 -0
  96. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/error_codes.py +0 -0
  97. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/error_codes.yaml +0 -0
  98. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/errors.py +0 -0
  99. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/hitl/__init__.py +0 -0
  100. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/hitl/grant_protocol.py +0 -0
  101. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/hitl/mock.py +0 -0
  102. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/hitl/pending_approval.py +0 -0
  103. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/hitl/secret_leak_guard.py +0 -0
  104. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/hitl/status.py +0 -0
  105. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/hooks/__init__.py +0 -0
  106. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/hooks/tool_output_handler.py +0 -0
  107. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/hosted_policy.py +0 -0
  108. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/integrations/__init__.py +0 -0
  109. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/integrations/anthropic.py +0 -0
  110. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/integrations/autogen.py +0 -0
  111. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/integrations/braintrust.py +0 -0
  112. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/integrations/crewai/__init__.py +0 -0
  113. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/integrations/crewai/agent.py +0 -0
  114. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/integrations/crewai/crew.py +0 -0
  115. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/integrations/crewai/task.py +0 -0
  116. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/integrations/crewai/tool.py +0 -0
  117. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/integrations/google.py +0 -0
  118. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/integrations/google_adk/__init__.py +0 -0
  119. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/integrations/google_adk/agent.py +0 -0
  120. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/integrations/google_adk/tool.py +0 -0
  121. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/integrations/langchain/__init__.py +0 -0
  122. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/integrations/langchain/agent.py +0 -0
  123. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/integrations/langchain/callbacks.py +0 -0
  124. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/integrations/langchain/chain.py +0 -0
  125. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/integrations/langchain/graph.py +0 -0
  126. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/integrations/langchain/modern.py +0 -0
  127. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/integrations/langchain/tool.py +0 -0
  128. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/integrations/langfuse.py +0 -0
  129. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/integrations/litellm.py +0 -0
  130. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/integrations/openai.py +0 -0
  131. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/integrations/pydantic_ai.py +0 -0
  132. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/integrations/vercel_ai.py +0 -0
  133. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/layout_migration.py +0 -0
  134. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/secrets/__init__.py +0 -0
  135. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/secrets/reference.py +0 -0
  136. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/secrets/resolver.py +0 -0
  137. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/secrets/secretstr.py +0 -0
  138. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/spool/__init__.py +0 -0
  139. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/spool/_compress.py +0 -0
  140. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/spool/_constants.py +0 -0
  141. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/spool/_crc32c.py +0 -0
  142. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/spool/_crypto.py +0 -0
  143. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/spool/_frame.py +0 -0
  144. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/spool/_keyring.py +0 -0
  145. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/spool/_metrics.py +0 -0
  146. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/spool/_spool.py +0 -0
  147. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/spool/_state.py +0 -0
  148. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/spool/_uploader.py +0 -0
  149. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/spool/cz-audit-v1.dict +0 -0
  150. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/tamper.py +0 -0
  151. {controlzero-1.11.6 → controlzero-1.11.9}/controlzero/tracecontext.py +0 -0
  152. {controlzero-1.11.6 → controlzero-1.11.9}/examples/hello_world.py +0 -0
  153. {controlzero-1.11.6 → controlzero-1.11.9}/tests/_fixtures/antigravity-envelope-expected.json +0 -0
  154. {controlzero-1.11.6 → controlzero-1.11.9}/tests/_fixtures/jcs_args_hash_vectors.json +0 -0
  155. {controlzero-1.11.6 → controlzero-1.11.9}/tests/conftest.py +0 -0
  156. {controlzero-1.11.6 → controlzero-1.11.9}/tests/integrations/__init__.py +0 -0
  157. {controlzero-1.11.6 → controlzero-1.11.9}/tests/integrations/test_google.py +0 -0
  158. {controlzero-1.11.6 → controlzero-1.11.9}/tests/parity/action_aliases.json +0 -0
  159. {controlzero-1.11.6 → controlzero-1.11.9}/tests/spool/__init__.py +0 -0
  160. {controlzero-1.11.6 → controlzero-1.11.9}/tests/spool/conftest.py +0 -0
  161. {controlzero-1.11.6 → controlzero-1.11.9}/tests/spool/test_spool_cli.py +0 -0
  162. {controlzero-1.11.6 → controlzero-1.11.9}/tests/spool/test_spool_concurrency.py +0 -0
  163. {controlzero-1.11.6 → controlzero-1.11.9}/tests/spool/test_spool_conformance.py +0 -0
  164. {controlzero-1.11.6 → controlzero-1.11.9}/tests/spool/test_spool_core.py +0 -0
  165. {controlzero-1.11.6 → controlzero-1.11.9}/tests/spool/test_spool_crash.py +0 -0
  166. {controlzero-1.11.6 → controlzero-1.11.9}/tests/spool/test_spool_diskfull.py +0 -0
  167. {controlzero-1.11.6 → controlzero-1.11.9}/tests/spool/test_spool_durable_default_tamper.py +0 -0
  168. {controlzero-1.11.6 → controlzero-1.11.9}/tests/spool/test_spool_keychain_dek.py +0 -0
  169. {controlzero-1.11.6 → controlzero-1.11.9}/tests/spool/test_spool_transcript_localack.py +0 -0
  170. {controlzero-1.11.6 → controlzero-1.11.9}/tests/spool/test_spool_uploader.py +0 -0
  171. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_action_aliases.py +0 -0
  172. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_action_canonicalization.py +0 -0
  173. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_action_validator_t86.py +0 -0
  174. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_adapter_interface.py +0 -0
  175. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_agent_name_env.py +0 -0
  176. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_agy_docs_no_exit2_1373.py +0 -0
  177. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_antigravity_adapter.py +0 -0
  178. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_antigravity_envelope_parity.py +0 -0
  179. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_antigravity_ga_blockers_1248.py +0 -0
  180. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_antigravity_hook_check.py +0 -0
  181. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_antigravity_install.py +0 -0
  182. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_antigravity_posttooluse_observe_58.py +0 -0
  183. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_antigravity_tool_vocab_1303.py +0 -0
  184. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_api_key_mask.py +0 -0
  185. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_audit_remote_sdk_version.py +0 -0
  186. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_audit_sink_isolation.py +0 -0
  187. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_bundle_cache_freshness_1303.py +0 -0
  188. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_bundle_parser.py +0 -0
  189. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_bundle_translate.py +0 -0
  190. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_capability_matrix_doc.py +0 -0
  191. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_cli_carve_out.py +0 -0
  192. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_cli_debug_bundle.py +0 -0
  193. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_cli_extractor_integration.py +0 -0
  194. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_cli_hook.py +0 -0
  195. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_cli_hook_entry.py +0 -0
  196. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_cli_hosted_refresh.py +0 -0
  197. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_cli_init.py +0 -0
  198. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_cli_init_templates.py +0 -0
  199. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_cli_tail.py +0 -0
  200. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_cli_test.py +0 -0
  201. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_cli_validate.py +0 -0
  202. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_coding_agent_hooks.py +0 -0
  203. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_conditions.py +0 -0
  204. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_config_format_parity_1303.py +0 -0
  205. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_conformance.py +0 -0
  206. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_console.py +0 -0
  207. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_credential_hook.py +0 -0
  208. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_cursor_adapter.py +0 -0
  209. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_default_action.py +0 -0
  210. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_device.py +0 -0
  211. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_dlp_scanner.py +0 -0
  212. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_doctor.py +0 -0
  213. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_engine_version_consistency.py +0 -0
  214. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_env_dump_438.py +0 -0
  215. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_epic_1247_bryan_acceptance.py +0 -0
  216. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_error_codes.py +0 -0
  217. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_errors_e_codes.py +0 -0
  218. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_fail_closed_eval.py +0 -0
  219. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_failopen_1303.py +0 -0
  220. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_glob_matching.py +0 -0
  221. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_hitl_5d_email_install.py +0 -0
  222. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_hitl_6a_cli_flag.py +0 -0
  223. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_hitl_6a_exceptions.py +0 -0
  224. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_hitl_6a_get_secret_hitl.py +0 -0
  225. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_hitl_6a_mock_backend.py +0 -0
  226. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_hitl_6a_pending_approval.py +0 -0
  227. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_hitl_6a_request_approval.py +0 -0
  228. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_hitl_6a_secret_leak_guard.py +0 -0
  229. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_hitl_6a_wait.py +0 -0
  230. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_hitl_conformance.py +0 -0
  231. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_hitl_phase2b_protocol.py +0 -0
  232. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_hitl_reason_codes.py +0 -0
  233. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_hook_extractors.py +0 -0
  234. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_hosted_local_audit_1247.py +0 -0
  235. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_hosted_local_shadow_warn_1265.py +0 -0
  236. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_hosted_policy_e2e.py +0 -0
  237. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_hosts_adapter.py +0 -0
  238. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_hybrid_mode_strict.py +0 -0
  239. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_hybrid_mode_warn.py +0 -0
  240. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_install_hook_command.py +0 -0
  241. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_install_hooks.py +0 -0
  242. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_kiro_adapter.py +0 -0
  243. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_kiro_cli_hook_pin_1265.py +0 -0
  244. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_kiro_hook_templates.py +0 -0
  245. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_kiro_install.py +0 -0
  246. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_layout_migration_t101.py +0 -0
  247. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_layout_parity_t102.py +0 -0
  248. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_local_mode_dict.py +0 -0
  249. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_local_mode_file_json.py +0 -0
  250. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_local_mode_file_yaml.py +0 -0
  251. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_log_fallback_stderr.py +0 -0
  252. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_log_options_ignored_hosted.py +0 -0
  253. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_log_rotation.py +0 -0
  254. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_migrate.py +0 -0
  255. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_min_sdk_version_gate.py +0 -0
  256. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_multi_client_per_project_175.py +0 -0
  257. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_no_policy_no_key.py +0 -0
  258. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_observe_mode_1247.py +0 -0
  259. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_package_rename_shim.py +0 -0
  260. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_part3_active_policy_count_1303.py +0 -0
  261. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_policy_engine_version_phase1b.py +0 -0
  262. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_policy_freshness.py +0 -0
  263. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_policy_load_failclosed_f3.py +0 -0
  264. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_policy_settings.py +0 -0
  265. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_policy_source_audit.py +0 -0
  266. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_probe_audit_isolation.py +0 -0
  267. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_quarantine.py +0 -0
  268. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_reason_code.py +0 -0
  269. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_refresh.py +0 -0
  270. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_secrets.py +0 -0
  271. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_secrets_reference_resolver.py +0 -0
  272. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_sql_semantic_class.py +0 -0
  273. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_t103_precedence.py +0 -0
  274. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_t104_cache_gc.py +0 -0
  275. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_t108_local_override_audit.py +0 -0
  276. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_t96_single_audit_log.py +0 -0
  277. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_tamper.py +0 -0
  278. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_tamper_behavior.py +0 -0
  279. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_tamper_hook.py +0 -0
  280. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_telemetry_consent.py +0 -0
  281. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_tracecontext.py +0 -0
  282. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_unsafe_int_boundary.py +0 -0
  283. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_update_cmd.py +0 -0
  284. {controlzero-1.11.6 → controlzero-1.11.9}/tests/test_upgrade_nudge.py +0 -0
  285. {controlzero-1.11.6 → controlzero-1.11.9}/tools/cz-kiro-adapter +0 -0
@@ -29,7 +29,15 @@ out/
29
29
  apps/control-zero-platform/backend/server
30
30
 
31
31
  # Test & Coverage
32
+ # NOTE: the bare `coverage/` pattern is unanchored, so git matches a
33
+ # directory named `coverage` at ANY depth -- including the Next.js app
34
+ # route `app/[locale]/governance/coverage/`. That silently kept the
35
+ # Coverage admin page out of every build and 404'd it in prod (#1180).
36
+ # Re-include the application route below so it ships. The brackets in
37
+ # `[locale]` are escaped because git treats an unescaped `[...]` as a
38
+ # character class, which would make the negation silently not match.
32
39
  coverage/
40
+ !apps/control-zero-platform/frontend/app/\[locale\]/governance/coverage/
33
41
  htmlcov/
34
42
  .coverage
35
43
  *.lcov
@@ -1,5 +1,133 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.11.9 -- 2026-06-25 (policy-posture remediation: honest hosted/local CLI, per-agent keys, key-validity diagnostics)
4
+
5
+ ### Added
6
+
7
+ - **Severity-level CLI output (`controlzero.cli.output`).** All CLI prose now
8
+ routes through one renderer with INFO / WARNING / SUCCESS / ERROR levels:
9
+ WARNING + ERROR go to stderr, INFO + SUCCESS to stdout, colour honours
10
+ `NO_COLOR` / `CONTROLZERO_NO_COLOR` / `TERM=dumb` / non-TTY. No emojis. The
11
+ hook decision path and wire payloads are untouched -- the renderer is never
12
+ used inside `hook_check`, so decision reasons emit verbatim.
13
+ - **Per-integration (per-agent) API keys.** Each coding agent can bind its own
14
+ project key at `integrations/<agent>/config.yaml`. Resolution order is
15
+ `env (explicit) > per-agent (for that agent) > global`, fully back-compatible
16
+ (no per-agent key -> falls back to the global key). Per-agent dir `0700` /
17
+ file `0600`; the raw key is never echoed. This lets one machine run different
18
+ agents bound to different projects, each enforcing its own hosted bundle.
19
+ - **`controlzero status` rewrite.** Shows resolved runtime truth (mode, config
20
+ source, key, enrolled, audit dest) plus a per-agent bindings table and
21
+ per-key backend validity (active / expired / revoked) so a rejected key is
22
+ diagnosable from the CLI instead of only via the coarse E1101 at first tool
23
+ call (#1472).
24
+
25
+ ### Changed
26
+
27
+ - **Honest hosted/local posture messaging.** One canonical wording each for the
28
+ HOSTED and LOCAL install paths, routed through every installer. The HOSTED
29
+ banner states that the local `policy.yaml` is IGNORED while an API key is set,
30
+ and the unmatched-tool default (DENY fail-closed unless the project is set to
31
+ Allow by default) -- removing the "install said ALLOW ALL but runtime denied"
32
+ ambiguity.
33
+ - **Archive-on-hosted.** When an API key is configured, the local `policy.yaml`
34
+ (and its `.sig`) is moved (not deleted) into `archive/policy-<UTC-ts>.yaml`,
35
+ symlink-safe, every error non-fatal -- a hosted machine never silently runs a
36
+ stale local policy.
37
+
38
+ ### Fixed
39
+
40
+ - **`synthetic:UNATTRIBUTED_DENY`** added to the synthetic policy-id catalog so
41
+ an unattributed fail-closed deny renders a stable, documented policy id across
42
+ every surface instead of an opaque blank (#1472).
43
+ - **Audit attribution.** SDK audit wire builders forward `project_id`
44
+ unconditionally so signed-sink audit events are attributed to the right
45
+ project (pairs with the backend ingest fix).
46
+
47
+ ## 1.11.8 -- 2026-06-22 (DLP mask enforcement + I/O capture: enrolled-machine capture fix + server-side PII redaction)
48
+
49
+ ### Added
50
+
51
+ - **I/O capture now fires on the enrolled-agent path (#1390 / #1432).** The
52
+ agent-hook I/O capture producer ships input/output payloads on every tool
53
+ call (`capture_resolved` defaults true); the server cascade + master gate
54
+ decide persistence. Pairs with the backend fix that threads the enrolled
55
+ machine's `project_id` through the capture cascade so a project-scoped
56
+ install actually captures (previously the machine-auth ingest hardcoded the
57
+ project override off and captured nothing). The producer performs no
58
+ client-side redaction by design -- secret + structured-PII redaction happen
59
+ server-side before persist (#1433), and captured payloads are sealed under a
60
+ per-record DEK.
61
+
62
+ - **DLP `mask` action enforced in the SDK (#1278 kakaopay checklist #6).**
63
+ The `mask` action now redacts matched sensitive spans in the tool arguments
64
+ (modify-and-proceed) instead of only being recorded as a finding. When a DLP
65
+ rule matches with `action="mask"` on an `allow` decision, the enforcer
66
+ rewrites each sensitive span to a value-free `[REDACTED-<RULE_ID-UPPER>]`
67
+ token, stashes the redacted args on `PolicyDecision.masked_args`, and records
68
+ the placeholder (never the raw value) in `dlp_findings`. The placeholder
69
+ matches the gateway `response_dlp.mask_text` path and the Rust core
70
+ `pii::redact_placeholder` byte-for-byte. `block` still hard-vetoes to deny
71
+ (and wins over a co-matching mask); `detect`, `allow`, `warn`, and
72
+ policy-level deny are unchanged. Byte-identical with the Node SDK via the
73
+ shared `tests/parity/dlp_mask.json` conformance vector. Masking is leak-safe
74
+ on three edge cases code review flagged: non-string scalar leaves (a JSON
75
+ number like `{"card": 4111111111111111}` is masked, not passed through raw),
76
+ overlapping/nested mask spans (coalesced into a single clean placeholder with
77
+ no stale-offset residue), and cross-argument matches (DLP detects on the
78
+ flattened arg text but masks per-leaf; a mask match that spans two args has
79
+ no covering leaf, so the enforcer re-scans the masked args and FAILS CLOSED
80
+ to a `DLP_BLOCKED` deny rather than forwarding an un-redactable value). On
81
+ that fail-closed deny path the audit records ONLY the value-free mask
82
+ placeholder -- a co-matching `detect` rule's raw matched value is excluded so
83
+ no raw PII lands in the audit even on a block (#202).
84
+
85
+ ## 1.11.7 -- 2026-06-22 (agent tamper-resistance: hook-integrity baseline + detect/audit/alert)
86
+
87
+ Adds agent tamper-resistance detection for the coding-agent hooks the SDK
88
+ installs. A security evaluator's first move is to install the guard and then
89
+ defeat it by deleting or editing the hook; before this release that removal was
90
+ silent until the next tool call (which, with the hook gone, never ran). Now it
91
+ is detected, audited, and alerted.
92
+
93
+ ### Added
94
+
95
+ - **Hook-integrity baseline (`controlzero/hook_integrity.py`).** Every
96
+ `install` / `kiro init` host writer (Claude Code, Gemini CLI, Codex CLI,
97
+ Cursor, Antigravity, Kiro CLI v2/v3, Kiro IDE) now records a signed baseline
98
+ (`~/.controlzero/hook_baseline.json`) of the Control Zero hook command(s) and
99
+ a hash of each host config it wrote. The baseline is HMAC-signed with the same
100
+ `tamper.key` the policy-bundle signer uses, so a forged baseline that hides a
101
+ removed hook needs the secret.
102
+ - **Drift detection + audit + alert.** At `hook-check`, `heartbeat`, and a new
103
+ `controlzero verify` command, the SDK compares live host configs against the
104
+ baseline and classifies each as `missing` (hook command removed), `modified`
105
+ (command changed/repointed, OR a load-bearing enablement field flipped to
106
+ disable the hook while the command stays in place), or `config_missing`
107
+ (config file deleted). The `modified` check uses a SHA-256 over the canonical
108
+ Control-Zero region (the hook entries carrying a CZ command WITH their
109
+ enablement siblings: Cursor `failClosed`, Kiro `enabled`, Codex `config.toml`
110
+ `codex_hooks`), so disabling the hook without removing the command string is
111
+ detected -- while an unrelated edit elsewhere in the config does NOT alarm. A
112
+ finding writes a `hook_tamper_detected` / `enforcement_disabled` event to the
113
+ local audit log and fires the existing best-effort `/v1/sdk/tamper-alert`
114
+ backend alert the admin console surfaces. DETECT + AUDIT + ALERT only -- the
115
+ tool decision is not changed (a benign config edit never bricks the agent).
116
+ - **Bypass env-var detection.** Flags enforcement-downgrade environment
117
+ variables (`CONTROLZERO_CREDLEAK_OFF`, `CONTROLZERO_DLP_OFF`,
118
+ `CONTROLZERO_LOCAL_OVERRIDE`, and the general `CONTROLZERO_*_OFF` /
119
+ `*_DISABLE` pattern) at the same surfaces. Benign operational flags
120
+ (`CONTROLZERO_SPOOL_KEYCHAIN_DISABLE`, `CONTROLZERO_QUIET`) are allow-listed
121
+ so legit config never raises a false alert.
122
+ - **`controlzero verify`** command: on-demand / heartbeat integrity check. Exit
123
+ 0 = clean, 3 = tamper detected, 4 = no baseline recorded yet. `--json` for
124
+ machine output; `--no-report` to inspect without auditing/alerting.
125
+
126
+ Honest scope (#202): this is DETECT + AUDIT, not prevention. It makes hook
127
+ removal / modification / disable visible in the audit log and admin console; it
128
+ does not, by itself, stop an operator with write access from removing the hook
129
+ (that needs OS-level immutability / MDM, out of SDK scope).
130
+
3
131
  ## 1.11.6 -- 2026-06-21 (cross-surface enforcement hardening: Kiro v3 + Antigravity honesty + warn-mode)
4
132
 
5
133
  Ships the #1373 / #65 cross-surface enforcement work and follow-ups that landed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: controlzero
3
- Version: 1.11.6
3
+ Version: 1.11.9
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
@@ -185,6 +185,32 @@ rules:
185
185
  Rules are evaluated top to bottom. The first match wins. If no rule matches,
186
186
  the call is denied (fail-closed).
187
187
 
188
+ ## Localized block messages (`reason_localized`)
189
+
190
+ The `reason` shown when a rule fires is plain UTF-8 text, so you can write it
191
+ in any language. To serve more than one language from the same policy, add a
192
+ `reason_localized` map and select a locale with the `CONTROLZERO_LOCALE`
193
+ environment variable (e.g. `ko`, `ko-KR`):
194
+
195
+ ```yaml
196
+ version: '1'
197
+ rules:
198
+ - deny: 'delete_*'
199
+ reason: 'Deletes need human approval' # English default
200
+ reason_localized:
201
+ ko: '삭제는 사람의 승인이 필요합니다'
202
+ - allow: '*'
203
+ ```
204
+
205
+ - When `CONTROLZERO_LOCALE` is unset (or set to a locale the rule has no entry
206
+ for), the plain `reason` is used -- existing behavior is unchanged.
207
+ - The SDK's own built-in messages (no-rule-match, DLP block, empty/observe
208
+ bundle) ship an English default plus a Korean translation, selected the same
209
+ way. The packs are pure in-binary maps (no runtime i18n dependency), so this
210
+ is air-gap safe.
211
+ - Localization is display-only: it never changes which rule matches or the
212
+ decided effect.
213
+
188
214
  ## Tamper detection and quarantine
189
215
 
190
216
  The policy YAML supports a `settings:` section that controls how the SDK
@@ -131,6 +131,32 @@ rules:
131
131
  Rules are evaluated top to bottom. The first match wins. If no rule matches,
132
132
  the call is denied (fail-closed).
133
133
 
134
+ ## Localized block messages (`reason_localized`)
135
+
136
+ The `reason` shown when a rule fires is plain UTF-8 text, so you can write it
137
+ in any language. To serve more than one language from the same policy, add a
138
+ `reason_localized` map and select a locale with the `CONTROLZERO_LOCALE`
139
+ environment variable (e.g. `ko`, `ko-KR`):
140
+
141
+ ```yaml
142
+ version: '1'
143
+ rules:
144
+ - deny: 'delete_*'
145
+ reason: 'Deletes need human approval' # English default
146
+ reason_localized:
147
+ ko: '삭제는 사람의 승인이 필요합니다'
148
+ - allow: '*'
149
+ ```
150
+
151
+ - When `CONTROLZERO_LOCALE` is unset (or set to a locale the rule has no entry
152
+ for), the plain `reason` is used -- existing behavior is unchanged.
153
+ - The SDK's own built-in messages (no-rule-match, DLP block, empty/observe
154
+ bundle) ship an English default plus a Korean translation, selected the same
155
+ way. The packs are pure in-binary maps (no runtime i18n dependency), so this
156
+ is air-gap safe.
157
+ - Localization is display-only: it never changes which rule matches or the
158
+ decided effect.
159
+
134
160
  ## Tamper detection and quarantine
135
161
 
136
162
  The policy YAML supports a `settings:` section that controls how the SDK
@@ -41,7 +41,7 @@ from controlzero.hitl.grant_protocol import (
41
41
  from controlzero.policy_loader import load_policy
42
42
  from controlzero.secrets import SecretRef, SecretStr, secret_ref
43
43
 
44
- __version__ = "1.11.6"
44
+ __version__ = "1.11.9"
45
45
 
46
46
  __all__ = [
47
47
  "Client",
@@ -0,0 +1,273 @@
1
+ """In-binary localization pack for the SDK-GENERATED decision reasons.
2
+
3
+ Why this exists ( checklist #25, gh#1439)
4
+ --------------------------------------------------
5
+ The user-facing ``reason`` string is carried end-to-end as UTF-8, so an
6
+ operator can already localize ANY rule today by typing the target-language
7
+ text into that rule's ``reason:`` -- or, for per-locale overrides, into the
8
+ new ``reason_localized:`` map (a ``{locale: message}`` dict on the rule). That
9
+ covers operator-authored rules. This module covers the OTHER half: the
10
+ messages the SDK itself GENERATES for the no-rule-match / empty-bundle /
11
+ missing-bundle / tampered / DLP paths. Those have no operator-authored text to
12
+ carry, so they need an in-binary pack.
13
+
14
+ No runtime i18n library is used: these are pure dict lookups, which keeps the
15
+ SDK air-gap safe (the PoC ships self-hosted with no network).
16
+
17
+ Contract
18
+ --------
19
+ - The ``en`` value is the single source of truth for the inline paths that now
20
+ read it (no-match, DLP, tamper, quarantine): it MUST stay byte-identical to
21
+ the legacy hard-coded string so existing consumers that regex-match the
22
+ ``reason`` text are unaffected. ``tests/test_localized_reason.py`` pins this.
23
+ - English remains the literal DEFAULT whenever ``CONTROLZERO_LOCALE`` is unset
24
+ or a locale key is missing from a pack entry.
25
+ - For the synthetic empty/missing-bundle rules the English text stays on the
26
+ rule's ``reason`` (unchanged bundle output); the pack supplies ONLY the
27
+ localized (non-en) override, looked up by the rule's ``reason_code``.
28
+
29
+ KOREAN COPY IS A PROFESSIONAL DRAFT and MUST be reviewed/approved by the customer
30
+ before being presented as final. See KO_Localization_2026-06-22 in the project
31
+ vault. The English default is never blocked on that review.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import os
37
+ from typing import Optional
38
+
39
+ DEFAULT_LOCALE = "en"
40
+
41
+
42
+ def resolve_locale(explicit: Optional[str] = None) -> str:
43
+ """Resolve the active locale.
44
+
45
+ Precedence: explicit arg > ``CONTROLZERO_LOCALE`` env > "" (English).
46
+ Returned lower-cased and stripped so lookups are case-insensitive.
47
+ """
48
+ if explicit:
49
+ return explicit.strip().lower()
50
+ return (os.environ.get("CONTROLZERO_LOCALE", "") or "").strip().lower()
51
+
52
+
53
+ def _locale_candidates(locale: str) -> list[str]:
54
+ """Return lookup candidates for a locale, most specific first.
55
+
56
+ ``ko-KR`` / ``ko_KR`` -> ``["ko-kr", "ko"]`` so a pack keyed by the bare
57
+ primary subtag ("ko") still matches a region-qualified request.
58
+ """
59
+ loc = (locale or "").strip().lower()
60
+ if not loc:
61
+ return []
62
+ out = [loc]
63
+ for sep in ("-", "_"):
64
+ if sep in loc:
65
+ primary = loc.split(sep, 1)[0]
66
+ if primary and primary not in out:
67
+ out.append(primary)
68
+ break
69
+ return out
70
+
71
+
72
+ # message key -> {locale -> template}. Templates use str.format placeholders
73
+ # ({action}, {rule_name}, {category}). The English values for the inline paths
74
+ # (NO_RULE_MATCH:*, DLP_BLOCKED:*, BUNDLE_TAMPERED, MACHINE_QUARANTINED) are
75
+ # byte-identical to the legacy strings. For the synthetic-rule codes
76
+ # (OBSERVE_MODE_NO_POLICY, NO_ACTIVE_POLICIES, BUNDLE_MISSING) the English is
77
+ # documentation/parity only -- the live English comes from the rule's reason.
78
+ SYSTEM_MESSAGES: dict[str, dict[str, str]] = {
79
+ # No-rule-match fallthrough (the most common block a Korean operator sees).
80
+ "NO_RULE_MATCH:deny": {
81
+ "en": (
82
+ "No matching policy rule for '{action}' (fail-closed default; "
83
+ "default_on_missing=deny). This tool is neither allowed nor "
84
+ "denied by any rule in your hosted policy, so it is blocked. "
85
+ "Add a rule that allows it (e.g. allow: '*' as a catch-all, or "
86
+ "allow: '{action}'), or set the project/org default to allow, "
87
+ "in the Control Zero dashboard."
88
+ ),
89
+ # ko: "'{action}'에 일치하는 정책 규칙이 없어 기본 차단됩니다 ..."
90
+ "ko": (
91
+ "'{action}'에 일치하는 정책 규칙이 없습니다(기본 차단; "
92
+ "default_on_missing=deny). 이 도구는 정책에서 허용되지도 "
93
+ "거부되지도 않았으므로 차단됩니다. Control Zero 대시보드에서 "
94
+ "이를 허용하는 규칙을 추가하거나(예: 포괄 규칙 allow: '*' 또는 "
95
+ "allow: '{action}'), 프로젝트/조직 기본값을 allow로 "
96
+ "설정하십시오."
97
+ ),
98
+ },
99
+ "NO_RULE_MATCH:allow": {
100
+ "en": "No matching policy rule for '{action}' (default_action=allow)",
101
+ # ko: "'{action}'에 일치하는 정책 규칙이 없습니다 (default_action=allow)"
102
+ "ko": "'{action}'에 일치하는 정책 규칙이 없습니다(default_action=allow)",
103
+ },
104
+ "NO_RULE_MATCH:warn": {
105
+ "en": "No matching policy rule for '{action}' (default_action=warn)",
106
+ # ko: "'{action}'에 일치하는 정책 규칙이 없습니다 (default_action=warn)"
107
+ "ko": "'{action}'에 일치하는 정책 규칙이 없습니다(default_action=warn)",
108
+ },
109
+ # DLP overrides (the tool-name policy allowed, but a DLP rule vetoed).
110
+ "DLP_BLOCKED:match": {
111
+ "en": (
112
+ "DLP rule '{rule_name}' matched {category} content in tool "
113
+ "arguments"
114
+ ),
115
+ # ko: "DLP 규칙 '{rule_name}'이(가) 도구 인자에서 {category} 콘텐츠와 ..."
116
+ "ko": (
117
+ "DLP 규칙 '{rule_name}'이(가) 도구 인자에서 {category} "
118
+ "콘텐츠와 일치하여 호출이 차단되었습니다."
119
+ ),
120
+ },
121
+ "DLP_BLOCKED:boundary": {
122
+ "en": (
123
+ "DLP rule '{rule_name}' matched {category} content that spans "
124
+ "argument boundaries and could not be safely masked; blocking "
125
+ "(fail-closed) rather than forwarding an un-redacted value"
126
+ ),
127
+ # ko: "DLP 규칙 '{rule_name}'이(가) 인자 경계에 걸친 {category} 콘텐츠와 ..."
128
+ "ko": (
129
+ "DLP 규칙 '{rule_name}'이(가) 인자 경계에 걸쳐 있는 "
130
+ "{category} 콘텐츠와 일치하여 안전하게 마스킹할 수 없으므로, "
131
+ "마스킹되지 않은 값을 전달하는 대신 차단합니다(기본 차단)."
132
+ ),
133
+ },
134
+ # Tamper / quarantine (constructed in client.py + cli/main.py).
135
+ "BUNDLE_TAMPERED": {
136
+ "en": (
137
+ "Policy tampering detected. Call blocked by tamper enforcement "
138
+ "policy."
139
+ ),
140
+ # ko: "정책 변조가 감지되었습니다. 변조 시행 정책에 의해 호출이 차단되었습니다."
141
+ "ko": (
142
+ "정책 변조가 감지되었습니다. 변조 시행 정책에 의해 호출이 "
143
+ "차단되었습니다."
144
+ ),
145
+ },
146
+ # CLI hook-check tamper enforcement (cli/main.py). Carries a {source}
147
+ # discriminator (policy_hmac / audit_chain) and the [Control Zero] prefix.
148
+ "BUNDLE_TAMPERED:cli": {
149
+ "en": (
150
+ "[Control Zero] Tamper detected ({source}). Blocked by enforcement "
151
+ "policy."
152
+ ),
153
+ # ko: "[Control Zero] 변조가 감지되었습니다({source}). 시행 정책에 의해 ..."
154
+ "ko": (
155
+ "[Control Zero] 변조가 감지되었습니다({source}). 시행 정책에 "
156
+ "의해 차단되었습니다."
157
+ ),
158
+ },
159
+ "MACHINE_QUARANTINED": {
160
+ "en": (
161
+ "Machine quarantined: policy tampering detected. All calls denied "
162
+ "until recovery."
163
+ ),
164
+ # ko: "머신이 격리되었습니다: 정책 변조가 감지되었습니다. 복구될 때까지 ..."
165
+ "ko": (
166
+ "머신이 격리되었습니다: 정책 변조가 감지되었습니다. 복구될 "
167
+ "때까지 모든 호출이 거부됩니다."
168
+ ),
169
+ },
170
+ # Synthetic empty / missing-bundle rules. English here is parity-only --
171
+ # the live English comes from the rule's `reason` (bundle output unchanged);
172
+ # the enforcer reads ONLY the localized (non-en) value below, by code.
173
+ "OBSERVE_MODE_NO_POLICY": {
174
+ "en": (
175
+ "OBSERVE MODE: no policies are active on this project, so "
176
+ "Control Zero is monitoring and auditing tool calls but "
177
+ "NOT enforcing -- every call is allowed and logged. Attach "
178
+ "a policy (or set the empty-project default to deny) in the "
179
+ "Control Zero dashboard to start enforcing."
180
+ ),
181
+ # ko: "관찰 모드: 이 프로젝트에 활성 정책이 없으므로 ... 차단하지 않습니다 ..."
182
+ "ko": (
183
+ "관찰 모드: 이 프로젝트에 활성 정책이 없으므로 Control Zero는 "
184
+ "도구 호출을 모니터링하고 감사 기록을 남기지만 차단하지는 "
185
+ "않습니다 -- 모든 호출이 허용되고 기록됩니다. 시행을 시작하려면 "
186
+ "Control Zero 대시보드에서 정책을 연결하거나(또는 빈 프로젝트 "
187
+ "기본값을 deny로 설정) 하십시오."
188
+ ),
189
+ },
190
+ "NO_ACTIVE_POLICIES": {
191
+ "en": (
192
+ "No policies are active on this project. If the dashboard "
193
+ "shows attached policies, regenerate the policy bundle."
194
+ ),
195
+ # ko: "이 프로젝트에 활성 정책이 없습니다. 대시보드에 연결된 정책이 표시되면 ..."
196
+ "ko": (
197
+ "이 프로젝트에 활성 정책이 없습니다. 대시보드에 연결된 정책이 "
198
+ "표시되면 정책 번들을 다시 생성하십시오."
199
+ ),
200
+ },
201
+ # BUNDLE_MISSING covers two English variants (degraded bundle vs. bundle
202
+ # not loaded), both reason_code=BUNDLE_MISSING. The localized override is a
203
+ # single accurate general message; the precise English stays on the rule.
204
+ "BUNDLE_MISSING": {
205
+ "en": (
206
+ "Policy bundle could not be loaded or produced zero enforceable "
207
+ "rules. Control Zero is failing CLOSED (deny) rather than allowing "
208
+ "every tool call. Regenerate the policy bundle in the Control Zero "
209
+ "dashboard; contact support if this persists."
210
+ ),
211
+ # ko: "정책 번들을 로드할 수 없거나 시행 가능한 규칙을 생성하지 못했습니다 ..."
212
+ "ko": (
213
+ "정책 번들을 로드할 수 없거나 시행 가능한 규칙을 생성하지 "
214
+ "못했습니다(미동기화, 백엔드 연결 불가, 복호화 실패 또는 손상/"
215
+ "부분/오래된 번들). Control Zero는 모든 호출을 허용하는 대신 "
216
+ "차단(기본 차단)합니다. Control Zero 대시보드에서 정책 번들을 "
217
+ "다시 생성하고, 문제가 지속되면 지원팀에 문의하십시오."
218
+ ),
219
+ },
220
+ }
221
+
222
+
223
+ def system_message(key: str, locale: Optional[str] = None, **fmt: object) -> Optional[str]:
224
+ """Return the fully-resolved system message for ``key``, English-default.
225
+
226
+ Resolution: ``pack[key][locale]`` (region subtag tolerated) falling back to
227
+ ``pack[key]["en"]``. Returns ``None`` only when ``key`` is unknown. Used by
228
+ the inline paths (no-match, DLP, tamper, quarantine) where the pack is the
229
+ source of the English text.
230
+ """
231
+ entry = SYSTEM_MESSAGES.get(key)
232
+ if not entry:
233
+ return None
234
+ template = None
235
+ for cand in _locale_candidates(resolve_locale(locale)):
236
+ if cand in entry:
237
+ template = entry[cand]
238
+ break
239
+ if template is None:
240
+ template = entry.get(DEFAULT_LOCALE)
241
+ if template is None:
242
+ return None
243
+ if fmt:
244
+ try:
245
+ return template.format(**fmt)
246
+ except (KeyError, IndexError):
247
+ return template
248
+ return template
249
+
250
+
251
+ def localized_override(key: str, locale: Optional[str] = None, **fmt: object) -> Optional[str]:
252
+ """Return ONLY a non-English localized message for ``key``, else ``None``.
253
+
254
+ Unlike :func:`system_message` this never falls back to English. Used for
255
+ the synthetic empty/missing-bundle rules, whose English text stays on the
256
+ rule's ``reason`` (bundle output unchanged) -- the enforcer applies this
257
+ override only when an actual localized string exists for the active locale.
258
+ """
259
+ entry = SYSTEM_MESSAGES.get(key)
260
+ if not entry:
261
+ return None
262
+ for cand in _locale_candidates(resolve_locale(locale)):
263
+ if cand == DEFAULT_LOCALE:
264
+ continue
265
+ if cand in entry:
266
+ template = entry[cand]
267
+ if fmt:
268
+ try:
269
+ return template.format(**fmt)
270
+ except (KeyError, IndexError):
271
+ return template
272
+ return template
273
+ return None
@@ -943,4 +943,19 @@ def _translate_rule(rule: dict, policy_id: str) -> Optional[dict]:
943
943
  if escalate is not None:
944
944
  translated["escalate_on_deny"] = bool(escalate)
945
945
 
946
+ # #25 (gh#1439): the SAME allowlist-strip trap (the gh#175 /
947
+ # #1303 family). A backend-signed bundle copies only an allowlist of
948
+ # rule fields into the local rule shape; without this line a hosted
949
+ # policy's `reason_localized` map is silently dropped and a Korean
950
+ # operator would see only the English fallback. Forward it intact.
951
+ # (The backend bundle wire struct must ALSO carry the field -- see
952
+ # bundle_handler.go PolicyRule -- or it never reaches this translator.)
953
+ reason_localized = rule.get("reason_localized")
954
+ if isinstance(reason_localized, dict) and reason_localized:
955
+ translated["reason_localized"] = {
956
+ str(k).strip().lower(): str(v)
957
+ for k, v in reason_localized.items()
958
+ if isinstance(k, str) and isinstance(v, str)
959
+ }
960
+
946
961
  return translated