controlzero 1.11.8__tar.gz → 1.12.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (286) hide show
  1. {controlzero-1.11.8 → controlzero-1.12.0}/CHANGELOG.md +76 -0
  2. {controlzero-1.11.8 → controlzero-1.12.0}/PKG-INFO +27 -1
  3. {controlzero-1.11.8 → controlzero-1.12.0}/README.md +26 -0
  4. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/__init__.py +1 -1
  5. controlzero-1.12.0/controlzero/_internal/_messages.py +273 -0
  6. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/_internal/bundle.py +15 -0
  7. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/_internal/enforcer.py +105 -39
  8. controlzero-1.12.0/controlzero/_internal/source_labels.py +165 -0
  9. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/_internal/types.py +7 -0
  10. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/audit_remote.py +16 -1
  11. controlzero-1.12.0/controlzero/cli/agent_config.py +221 -0
  12. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/debug_bundle.py +66 -13
  13. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/hosts/base.py +7 -1
  14. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/main.py +821 -99
  15. controlzero-1.12.0/controlzero/cli/output.py +172 -0
  16. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/client.py +43 -4
  17. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/hosted_policy.py +133 -14
  18. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/policy_loader.py +33 -0
  19. {controlzero-1.11.8 → controlzero-1.12.0}/pyproject.toml +1 -1
  20. controlzero-1.12.0/tests/test_audit_project_id_attribution_pr2.py +305 -0
  21. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_cli_debug_bundle.py +2 -1
  22. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_hitl_validator_keys.py +6 -1
  23. controlzero-1.12.0/tests/test_hook_mask_failclosed.py +95 -0
  24. controlzero-1.12.0/tests/test_localized_reason.py +302 -0
  25. controlzero-1.12.0/tests/test_per_client_isolation_1011.py +251 -0
  26. controlzero-1.12.0/tests/test_pr4_install_messaging.py +377 -0
  27. controlzero-1.12.0/tests/test_pr4_output.py +144 -0
  28. controlzero-1.12.0/tests/test_pr4_status.py +160 -0
  29. controlzero-1.12.0/tests/test_source_labels.py +95 -0
  30. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_synthetic_policy_id_t79.py +6 -2
  31. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_t99_install_prefetch_bundle.py +5 -5
  32. {controlzero-1.11.8 → controlzero-1.12.0}/.gitignore +0 -0
  33. {controlzero-1.11.8 → controlzero-1.12.0}/Dockerfile.test +0 -0
  34. {controlzero-1.11.8 → controlzero-1.12.0}/LICENSE +0 -0
  35. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/__main__.py +0 -0
  36. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/_internal/__init__.py +0 -0
  37. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/_internal/action_aliases.py +0 -0
  38. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/_internal/action_validator.py +0 -0
  39. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/_internal/credential_hook.py +0 -0
  40. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/_internal/credential_scanner.py +0 -0
  41. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/_internal/credentials_data/__init__.py +0 -0
  42. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/_internal/credentials_data/built_in.yaml +0 -0
  43. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/_internal/dlp_scanner.py +0 -0
  44. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/_internal/hook_extractors.py +0 -0
  45. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/_internal/io_capture_producer.py +0 -0
  46. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/_internal/tool_extractors.json +0 -0
  47. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/audit_local.py +0 -0
  48. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/canonical.py +0 -0
  49. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/__init__.py +0 -0
  50. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/__main__.py +0 -0
  51. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/_secrets.py +0 -0
  52. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/console.py +0 -0
  53. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/doctor.py +0 -0
  54. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/exec_cmd.py +0 -0
  55. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/hook_entry.py +0 -0
  56. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/hosts/__init__.py +0 -0
  57. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/hosts/antigravity.py +0 -0
  58. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/hosts/claude_code.py +0 -0
  59. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/hosts/codex_cli.py +0 -0
  60. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/hosts/cursor.py +0 -0
  61. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/hosts/cursor_cli.py +0 -0
  62. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/hosts/gemini_cli.py +0 -0
  63. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/hosts/kiro.py +0 -0
  64. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/hosts/unknown.py +0 -0
  65. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/kiro_adapter.py +0 -0
  66. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/migrate.py +0 -0
  67. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/spool_cmd.py +0 -0
  68. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/telemetry_consent.py +0 -0
  69. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/templates/antigravity/hooks.json +0 -0
  70. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/templates/antigravity.yaml +0 -0
  71. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/templates/autogen.yaml +0 -0
  72. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/templates/claude-code.yaml +0 -0
  73. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/templates/codex-cli.yaml +0 -0
  74. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/templates/cost-cap.yaml +0 -0
  75. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/templates/crewai.yaml +0 -0
  76. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/templates/cursor.yaml +0 -0
  77. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/templates/gemini-cli.yaml +0 -0
  78. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/templates/generic.yaml +0 -0
  79. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook +0 -0
  80. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/templates/kiro/ide-pre-tool-use.kiro.hook +0 -0
  81. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/templates/kiro/ide-prompt-submit.kiro.hook +0 -0
  82. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/templates/kiro/kiro.yaml +0 -0
  83. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/templates/langchain.yaml +0 -0
  84. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/templates/mcp.yaml +0 -0
  85. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/templates/rag.yaml +0 -0
  86. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/cli/update_cmd.py +0 -0
  87. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/device.py +0 -0
  88. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/enrollment.py +0 -0
  89. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/error_codes.py +0 -0
  90. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/error_codes.yaml +0 -0
  91. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/errors.py +0 -0
  92. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/hitl/__init__.py +0 -0
  93. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/hitl/grant_protocol.py +0 -0
  94. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/hitl/mock.py +0 -0
  95. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/hitl/pending_approval.py +0 -0
  96. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/hitl/secret_leak_guard.py +0 -0
  97. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/hitl/status.py +0 -0
  98. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/hook_integrity.py +0 -0
  99. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/hooks/__init__.py +0 -0
  100. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/hooks/tool_output_handler.py +0 -0
  101. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/integrations/__init__.py +0 -0
  102. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/integrations/anthropic.py +0 -0
  103. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/integrations/autogen.py +0 -0
  104. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/integrations/braintrust.py +0 -0
  105. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/integrations/crewai/__init__.py +0 -0
  106. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/integrations/crewai/agent.py +0 -0
  107. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/integrations/crewai/crew.py +0 -0
  108. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/integrations/crewai/task.py +0 -0
  109. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/integrations/crewai/tool.py +0 -0
  110. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/integrations/google.py +0 -0
  111. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/integrations/google_adk/__init__.py +0 -0
  112. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/integrations/google_adk/agent.py +0 -0
  113. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/integrations/google_adk/tool.py +0 -0
  114. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/integrations/langchain/__init__.py +0 -0
  115. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/integrations/langchain/agent.py +0 -0
  116. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/integrations/langchain/callbacks.py +0 -0
  117. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/integrations/langchain/chain.py +0 -0
  118. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/integrations/langchain/graph.py +0 -0
  119. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/integrations/langchain/modern.py +0 -0
  120. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/integrations/langchain/tool.py +0 -0
  121. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/integrations/langfuse.py +0 -0
  122. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/integrations/litellm.py +0 -0
  123. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/integrations/openai.py +0 -0
  124. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/integrations/pydantic_ai.py +0 -0
  125. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/integrations/vercel_ai.py +0 -0
  126. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/layout_migration.py +0 -0
  127. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/secrets/__init__.py +0 -0
  128. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/secrets/reference.py +0 -0
  129. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/secrets/resolver.py +0 -0
  130. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/secrets/secretstr.py +0 -0
  131. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/spool/__init__.py +0 -0
  132. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/spool/_compress.py +0 -0
  133. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/spool/_constants.py +0 -0
  134. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/spool/_crc32c.py +0 -0
  135. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/spool/_crypto.py +0 -0
  136. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/spool/_frame.py +0 -0
  137. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/spool/_keyring.py +0 -0
  138. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/spool/_metrics.py +0 -0
  139. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/spool/_spool.py +0 -0
  140. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/spool/_state.py +0 -0
  141. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/spool/_uploader.py +0 -0
  142. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/spool/cz-audit-v1.dict +0 -0
  143. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/tamper.py +0 -0
  144. {controlzero-1.11.8 → controlzero-1.12.0}/controlzero/tracecontext.py +0 -0
  145. {controlzero-1.11.8 → controlzero-1.12.0}/examples/hello_world.py +0 -0
  146. {controlzero-1.11.8 → controlzero-1.12.0}/tests/_fixtures/antigravity-envelope-expected.json +0 -0
  147. {controlzero-1.11.8 → controlzero-1.12.0}/tests/_fixtures/jcs_args_hash_vectors.json +0 -0
  148. {controlzero-1.11.8 → controlzero-1.12.0}/tests/conftest.py +0 -0
  149. {controlzero-1.11.8 → controlzero-1.12.0}/tests/integrations/__init__.py +0 -0
  150. {controlzero-1.11.8 → controlzero-1.12.0}/tests/integrations/test_google.py +0 -0
  151. {controlzero-1.11.8 → controlzero-1.12.0}/tests/parity/action_aliases.json +0 -0
  152. {controlzero-1.11.8 → controlzero-1.12.0}/tests/spool/__init__.py +0 -0
  153. {controlzero-1.11.8 → controlzero-1.12.0}/tests/spool/conftest.py +0 -0
  154. {controlzero-1.11.8 → controlzero-1.12.0}/tests/spool/test_spool_cli.py +0 -0
  155. {controlzero-1.11.8 → controlzero-1.12.0}/tests/spool/test_spool_concurrency.py +0 -0
  156. {controlzero-1.11.8 → controlzero-1.12.0}/tests/spool/test_spool_conformance.py +0 -0
  157. {controlzero-1.11.8 → controlzero-1.12.0}/tests/spool/test_spool_core.py +0 -0
  158. {controlzero-1.11.8 → controlzero-1.12.0}/tests/spool/test_spool_crash.py +0 -0
  159. {controlzero-1.11.8 → controlzero-1.12.0}/tests/spool/test_spool_diskfull.py +0 -0
  160. {controlzero-1.11.8 → controlzero-1.12.0}/tests/spool/test_spool_durable_default_tamper.py +0 -0
  161. {controlzero-1.11.8 → controlzero-1.12.0}/tests/spool/test_spool_keychain_dek.py +0 -0
  162. {controlzero-1.11.8 → controlzero-1.12.0}/tests/spool/test_spool_sink_wiring.py +0 -0
  163. {controlzero-1.11.8 → controlzero-1.12.0}/tests/spool/test_spool_transcript_localack.py +0 -0
  164. {controlzero-1.11.8 → controlzero-1.12.0}/tests/spool/test_spool_uploader.py +0 -0
  165. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_action_aliases.py +0 -0
  166. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_action_canonicalization.py +0 -0
  167. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_action_validator_t86.py +0 -0
  168. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_adapter_interface.py +0 -0
  169. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_agent_name_env.py +0 -0
  170. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_agy_docs_no_exit2_1373.py +0 -0
  171. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_antigravity_adapter.py +0 -0
  172. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_antigravity_envelope_parity.py +0 -0
  173. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_antigravity_ga_blockers_1248.py +0 -0
  174. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_antigravity_hook_check.py +0 -0
  175. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_antigravity_install.py +0 -0
  176. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_antigravity_posttooluse_observe_58.py +0 -0
  177. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_antigravity_tool_vocab_1303.py +0 -0
  178. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_api_key_mask.py +0 -0
  179. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_audit_remote.py +0 -0
  180. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_audit_remote_io_capture.py +0 -0
  181. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_audit_remote_sdk_version.py +0 -0
  182. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_audit_sink_isolation.py +0 -0
  183. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_bundle_cache_freshness_1303.py +0 -0
  184. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_bundle_parser.py +0 -0
  185. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_bundle_translate.py +0 -0
  186. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_canonical_phase1a.py +0 -0
  187. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_capability_matrix_doc.py +0 -0
  188. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_cli_carve_out.py +0 -0
  189. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_cli_extractor_integration.py +0 -0
  190. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_cli_hook.py +0 -0
  191. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_cli_hook_entry.py +0 -0
  192. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_cli_hosted_refresh.py +0 -0
  193. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_cli_init.py +0 -0
  194. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_cli_init_templates.py +0 -0
  195. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_cli_tail.py +0 -0
  196. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_cli_test.py +0 -0
  197. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_cli_validate.py +0 -0
  198. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_coding_agent_hooks.py +0 -0
  199. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_conditions.py +0 -0
  200. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_config_format_parity_1303.py +0 -0
  201. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_conformance.py +0 -0
  202. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_console.py +0 -0
  203. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_credential_hook.py +0 -0
  204. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_cursor_adapter.py +0 -0
  205. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_default_action.py +0 -0
  206. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_device.py +0 -0
  207. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_dlp_mask.py +0 -0
  208. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_dlp_scanner.py +0 -0
  209. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_doctor.py +0 -0
  210. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_engine_version_consistency.py +0 -0
  211. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_enrollment.py +0 -0
  212. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_env_dump_438.py +0 -0
  213. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_epic_1247_bryan_acceptance.py +0 -0
  214. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_error_codes.py +0 -0
  215. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_errors_e_codes.py +0 -0
  216. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_fail_closed_eval.py +0 -0
  217. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_failopen_1303.py +0 -0
  218. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_glob_matching.py +0 -0
  219. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_hitl_5d_email_install.py +0 -0
  220. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_hitl_6a_cli_flag.py +0 -0
  221. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_hitl_6a_exceptions.py +0 -0
  222. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_hitl_6a_get_secret_hitl.py +0 -0
  223. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_hitl_6a_mock_backend.py +0 -0
  224. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_hitl_6a_pending_approval.py +0 -0
  225. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_hitl_6a_request_approval.py +0 -0
  226. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_hitl_6a_secret_leak_guard.py +0 -0
  227. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_hitl_6a_wait.py +0 -0
  228. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_hitl_conformance.py +0 -0
  229. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_hitl_phase2b_protocol.py +0 -0
  230. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_hitl_reason_codes.py +0 -0
  231. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_hook_extractors.py +0 -0
  232. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_hook_integrity.py +0 -0
  233. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_hosted_local_audit_1247.py +0 -0
  234. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_hosted_local_shadow_warn_1265.py +0 -0
  235. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_hosted_policy_e2e.py +0 -0
  236. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_hosts_adapter.py +0 -0
  237. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_hybrid_mode_strict.py +0 -0
  238. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_hybrid_mode_warn.py +0 -0
  239. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_install_hook_command.py +0 -0
  240. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_install_hooks.py +0 -0
  241. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_io_capture_producer.py +0 -0
  242. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_kiro_adapter.py +0 -0
  243. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_kiro_cli_e2e.py +0 -0
  244. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_kiro_cli_hook_pin_1265.py +0 -0
  245. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_kiro_hook_templates.py +0 -0
  246. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_kiro_install.py +0 -0
  247. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_layout_migration_t101.py +0 -0
  248. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_layout_parity_t102.py +0 -0
  249. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_local_mode_dict.py +0 -0
  250. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_local_mode_file_json.py +0 -0
  251. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_local_mode_file_yaml.py +0 -0
  252. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_log_fallback_stderr.py +0 -0
  253. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_log_options_ignored_hosted.py +0 -0
  254. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_log_rotation.py +0 -0
  255. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_migrate.py +0 -0
  256. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_min_sdk_version_gate.py +0 -0
  257. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_multi_client_per_project_175.py +0 -0
  258. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_no_policy_no_key.py +0 -0
  259. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_observe_mode_1247.py +0 -0
  260. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_package_rename_shim.py +0 -0
  261. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_part3_active_policy_count_1303.py +0 -0
  262. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_policy_engine_version_phase1b.py +0 -0
  263. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_policy_freshness.py +0 -0
  264. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_policy_load_failclosed_f3.py +0 -0
  265. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_policy_settings.py +0 -0
  266. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_policy_source_audit.py +0 -0
  267. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_probe_audit_isolation.py +0 -0
  268. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_quarantine.py +0 -0
  269. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_reason_code.py +0 -0
  270. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_refresh.py +0 -0
  271. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_secrets.py +0 -0
  272. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_secrets_reference_resolver.py +0 -0
  273. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_sql_semantic_class.py +0 -0
  274. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_t103_precedence.py +0 -0
  275. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_t104_cache_gc.py +0 -0
  276. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_t108_local_override_audit.py +0 -0
  277. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_t96_single_audit_log.py +0 -0
  278. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_tamper.py +0 -0
  279. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_tamper_behavior.py +0 -0
  280. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_tamper_hook.py +0 -0
  281. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_telemetry_consent.py +0 -0
  282. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_tracecontext.py +0 -0
  283. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_unsafe_int_boundary.py +0 -0
  284. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_update_cmd.py +0 -0
  285. {controlzero-1.11.8 → controlzero-1.12.0}/tests/test_upgrade_nudge.py +0 -0
  286. {controlzero-1.11.8 → controlzero-1.12.0}/tools/cz-kiro-adapter +0 -0
@@ -1,5 +1,81 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.12.0 -- 2026-06-25 (per-agent key authority + per-agent bundle cache + whoami)
4
+
5
+ ### Added
6
+
7
+ - **`controlzero whoami`.** Prints which API key the SDK will actually use for a
8
+ surface (per-agent / env / global), its prefix only (never the full key), and
9
+ the project + org it resolves to. Diagnoses the most common "my audit logs are
10
+ missing" cause: a `CONTROLZERO_API_KEY` exported in the shell that maps to a
11
+ different project than the integration you installed, so events land in the
12
+ wrong tenant and the dashboard shows nothing.
13
+ - **Per-agent bundle cache.** Each governed surface now caches its policy bundle
14
+ under `~/.controlzero/integrations/<agent>/cache/` (next to the per-agent
15
+ `config.yaml`), so two surfaces on one machine each bound to their own project
16
+ no longer share a bundle. A read fallback keeps existing installs working on
17
+ upgrade: an empty per-agent cache transparently reads the legacy shared cache
18
+ until the first fetch re-caches it (no fail-closed on upgrade).
19
+
20
+ ### Changed
21
+
22
+ - **Per-agent key is now AUTHORITATIVE in the enforcement hook.** Hook-check
23
+ precedence becomes per-agent (for the active surface) > env > global. A key
24
+ installed via `install <agent> --api-key` / `kiro init --api-key` now wins over
25
+ a stale ambient `CONTROLZERO_API_KEY`, with a loud one-line warning when they
26
+ differ. This closes the silent wrong-tenant bug where a hook spooled + uploaded
27
+ + ingested correctly but attributed the events to whatever key was exported in
28
+ the shell, so the dashboard for the installed project showed "no audit entries".
29
+ Non-hook resolution (`status`, install-time display) is unchanged
30
+ (env > per-agent > global).
31
+ - **Cache bundle extension is now `.czpolicy`** (was `.bin`), matching the UI
32
+ policy export (both are the same signed bundle format). Legacy `.bin` caches
33
+ are still read and garbage-collected, so nothing breaks on upgrade.
34
+
35
+ ## 1.11.9 -- 2026-06-25 (policy-posture remediation: honest hosted/local CLI, per-agent keys, key-validity diagnostics)
36
+
37
+ ### Added
38
+
39
+ - **Severity-level CLI output (`controlzero.cli.output`).** All CLI prose now
40
+ routes through one renderer with INFO / WARNING / SUCCESS / ERROR levels:
41
+ WARNING + ERROR go to stderr, INFO + SUCCESS to stdout, colour honours
42
+ `NO_COLOR` / `CONTROLZERO_NO_COLOR` / `TERM=dumb` / non-TTY. No emojis. The
43
+ hook decision path and wire payloads are untouched -- the renderer is never
44
+ used inside `hook_check`, so decision reasons emit verbatim.
45
+ - **Per-integration (per-agent) API keys.** Each coding agent can bind its own
46
+ project key at `integrations/<agent>/config.yaml`. Resolution order is
47
+ `env (explicit) > per-agent (for that agent) > global`, fully back-compatible
48
+ (no per-agent key -> falls back to the global key). Per-agent dir `0700` /
49
+ file `0600`; the raw key is never echoed. This lets one machine run different
50
+ agents bound to different projects, each enforcing its own hosted bundle.
51
+ - **`controlzero status` rewrite.** Shows resolved runtime truth (mode, config
52
+ source, key, enrolled, audit dest) plus a per-agent bindings table and
53
+ per-key backend validity (active / expired / revoked) so a rejected key is
54
+ diagnosable from the CLI instead of only via the coarse E1101 at first tool
55
+ call (#1472).
56
+
57
+ ### Changed
58
+
59
+ - **Honest hosted/local posture messaging.** One canonical wording each for the
60
+ HOSTED and LOCAL install paths, routed through every installer. The HOSTED
61
+ banner states that the local `policy.yaml` is IGNORED while an API key is set,
62
+ and the unmatched-tool default (DENY fail-closed unless the project is set to
63
+ Allow by default) -- removing the "install said ALLOW ALL but runtime denied"
64
+ ambiguity.
65
+ - **Archive-on-hosted.** When an API key is configured, the local `policy.yaml`
66
+ (and its `.sig`) is moved (not deleted) into `archive/policy-<UTC-ts>.yaml`,
67
+ symlink-safe, every error non-fatal -- a hosted machine never silently runs a
68
+ stale local policy.
69
+
70
+ ### Fixed
71
+
72
+ - **`synthetic:UNATTRIBUTED_DENY`** added to the synthetic policy-id catalog so
73
+ an unattributed fail-closed deny renders a stable, documented policy id across
74
+ every surface instead of an opaque blank (#1472).
75
+ - **Audit attribution.** SDK audit wire builders forward `project_id`
76
+ unconditionally so signed-sink audit events are attributed to the right
77
+ project (pairs with the backend ingest fix).
78
+
3
79
  ## 1.11.8 -- 2026-06-22 (DLP mask enforcement + I/O capture: enrolled-machine capture fix + server-side PII redaction)
4
80
 
5
81
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: controlzero
3
- Version: 1.11.8
3
+ Version: 1.12.0
4
4
  Summary: AI agent governance: policies, audit, and observability for tool calls. Works locally with no signup.
5
5
  Project-URL: Homepage, https://controlzero.ai
6
6
  Project-URL: Documentation, https://docs.controlzero.ai
@@ -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.8"
44
+ __version__ = "1.12.0"
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