controlzero 1.5.0__tar.gz → 1.5.1__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 (121) hide show
  1. {controlzero-1.5.0 → controlzero-1.5.1}/CHANGELOG.md +14 -0
  2. {controlzero-1.5.0 → controlzero-1.5.1}/PKG-INFO +1 -1
  3. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/__init__.py +1 -1
  4. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/client.py +14 -1
  5. {controlzero-1.5.0 → controlzero-1.5.1}/pyproject.toml +1 -1
  6. controlzero-1.5.1/tests/test_api_key_mask.py +59 -0
  7. {controlzero-1.5.0 → controlzero-1.5.1}/.gitignore +0 -0
  8. {controlzero-1.5.0 → controlzero-1.5.1}/Dockerfile.test +0 -0
  9. {controlzero-1.5.0 → controlzero-1.5.1}/LICENSE +0 -0
  10. {controlzero-1.5.0 → controlzero-1.5.1}/README.md +0 -0
  11. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/_internal/__init__.py +0 -0
  12. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/_internal/action_aliases.py +0 -0
  13. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/_internal/bundle.py +0 -0
  14. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/_internal/dlp_scanner.py +0 -0
  15. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/_internal/enforcer.py +0 -0
  16. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/_internal/hook_extractors.py +0 -0
  17. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/_internal/tool_extractors.json +0 -0
  18. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/_internal/types.py +0 -0
  19. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/audit_local.py +0 -0
  20. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/audit_remote.py +0 -0
  21. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/cli/__init__.py +0 -0
  22. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/cli/debug_bundle.py +0 -0
  23. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/cli/main.py +0 -0
  24. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/cli/templates/autogen.yaml +0 -0
  25. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/cli/templates/claude-code.yaml +0 -0
  26. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/cli/templates/codex-cli.yaml +0 -0
  27. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/cli/templates/cost-cap.yaml +0 -0
  28. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/cli/templates/crewai.yaml +0 -0
  29. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/cli/templates/cursor.yaml +0 -0
  30. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/cli/templates/gemini-cli.yaml +0 -0
  31. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/cli/templates/generic.yaml +0 -0
  32. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/cli/templates/langchain.yaml +0 -0
  33. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/cli/templates/mcp.yaml +0 -0
  34. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/cli/templates/rag.yaml +0 -0
  35. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/device.py +0 -0
  36. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/enrollment.py +0 -0
  37. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/errors.py +0 -0
  38. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/hosted_policy.py +0 -0
  39. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/__init__.py +0 -0
  40. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/anthropic.py +0 -0
  41. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/autogen.py +0 -0
  42. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/braintrust.py +0 -0
  43. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/crewai/__init__.py +0 -0
  44. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/crewai/agent.py +0 -0
  45. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/crewai/crew.py +0 -0
  46. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/crewai/task.py +0 -0
  47. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/crewai/tool.py +0 -0
  48. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/google.py +0 -0
  49. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/google_adk/__init__.py +0 -0
  50. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/google_adk/agent.py +0 -0
  51. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/google_adk/tool.py +0 -0
  52. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/langchain/__init__.py +0 -0
  53. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/langchain/agent.py +0 -0
  54. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/langchain/callbacks.py +0 -0
  55. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/langchain/chain.py +0 -0
  56. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/langchain/graph.py +0 -0
  57. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/langchain/modern.py +0 -0
  58. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/langchain/tool.py +0 -0
  59. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/langfuse.py +0 -0
  60. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/litellm.py +0 -0
  61. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/openai.py +0 -0
  62. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/pydantic_ai.py +0 -0
  63. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/vercel_ai.py +0 -0
  64. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/policy_loader.py +0 -0
  65. {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/tamper.py +0 -0
  66. {controlzero-1.5.0 → controlzero-1.5.1}/examples/hello_world.py +0 -0
  67. {controlzero-1.5.0 → controlzero-1.5.1}/tests/conftest.py +0 -0
  68. {controlzero-1.5.0 → controlzero-1.5.1}/tests/integrations/__init__.py +0 -0
  69. {controlzero-1.5.0 → controlzero-1.5.1}/tests/integrations/test_google.py +0 -0
  70. {controlzero-1.5.0 → controlzero-1.5.1}/tests/parity/action_aliases.json +0 -0
  71. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_action_aliases.py +0 -0
  72. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_action_canonicalization.py +0 -0
  73. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_agent_name_env.py +0 -0
  74. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_audit_remote.py +0 -0
  75. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_audit_sink_isolation.py +0 -0
  76. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_bundle_parser.py +0 -0
  77. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_bundle_translate.py +0 -0
  78. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_cli_carve_out.py +0 -0
  79. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_cli_debug_bundle.py +0 -0
  80. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_cli_extractor_integration.py +0 -0
  81. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_cli_hook.py +0 -0
  82. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_cli_hosted_refresh.py +0 -0
  83. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_cli_init.py +0 -0
  84. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_cli_init_templates.py +0 -0
  85. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_cli_tail.py +0 -0
  86. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_cli_test.py +0 -0
  87. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_cli_validate.py +0 -0
  88. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_coding_agent_hooks.py +0 -0
  89. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_conditions.py +0 -0
  90. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_default_action.py +0 -0
  91. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_device.py +0 -0
  92. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_dlp_scanner.py +0 -0
  93. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_enrollment.py +0 -0
  94. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_fail_closed_eval.py +0 -0
  95. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_glob_matching.py +0 -0
  96. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_hook_extractors.py +0 -0
  97. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_hosted_policy_e2e.py +0 -0
  98. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_hybrid_mode_strict.py +0 -0
  99. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_hybrid_mode_warn.py +0 -0
  100. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_install_hooks.py +0 -0
  101. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_local_mode_dict.py +0 -0
  102. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_local_mode_file_json.py +0 -0
  103. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_local_mode_file_yaml.py +0 -0
  104. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_log_fallback_stderr.py +0 -0
  105. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_log_options_ignored_hosted.py +0 -0
  106. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_log_rotation.py +0 -0
  107. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_no_policy_no_key.py +0 -0
  108. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_package_rename_shim.py +0 -0
  109. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_policy_freshness.py +0 -0
  110. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_policy_settings.py +0 -0
  111. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_quarantine.py +0 -0
  112. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_reason_code.py +0 -0
  113. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_refresh.py +0 -0
  114. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_sql_semantic_class.py +0 -0
  115. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_synthetic_policy_id_t79.py +0 -0
  116. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_t103_precedence.py +0 -0
  117. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_t104_cache_gc.py +0 -0
  118. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_t108_local_override_audit.py +0 -0
  119. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_tamper.py +0 -0
  120. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_tamper_behavior.py +0 -0
  121. {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_tamper_hook.py +0 -0
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.1 -- 2026-05-12 (SECURITY)
4
+
5
+ ### Fixed
6
+
7
+ - **API key leak in the active-source stderr notification.** The T103
8
+ startup line `controlzero: active policy source = hosted (...)`
9
+ printed the first 14 characters of `CONTROLZERO_API_KEY`, which for
10
+ a `cz_live_...` or `cz_test_...` key meant 6 characters of the
11
+ customer secret reached stderr (visible in terminals, screen shares,
12
+ support transcripts, and CI logs). The hint is now masked to
13
+ `cz_live_***` or `cz_test_***` so the mode is still observable but
14
+ no secret bytes are exposed. Upgrade ASAP if you ran 1.5.0 in any
15
+ environment where stderr is observable. 1.5.0 is yanked on PyPI.
16
+
3
17
  ## 1.5.0 -- 2026-05-12
4
18
 
5
19
  ### Changed (governance posture; opt-out path documented)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: controlzero
3
- Version: 1.5.0
3
+ Version: 1.5.1
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
@@ -28,7 +28,7 @@ from controlzero.errors import (
28
28
  )
29
29
  from controlzero.policy_loader import load_policy
30
30
 
31
- __version__ = "1.5.0"
31
+ __version__ = "1.5.1"
32
32
 
33
33
  __all__ = [
34
34
  "Client",
@@ -73,6 +73,19 @@ _DEFAULT_REFRESH_INTERVAL_SECONDS = 60
73
73
  _refresh_logger = logging.getLogger("controlzero.client.refresh")
74
74
 
75
75
 
76
+ def _mask_api_key(api_key: Optional[str]) -> str:
77
+ # Reveal only the public, non-secret prefix (cz_live_ / cz_test_) and mask
78
+ # the rest. Anything past the prefix is entropy from the customer's secret
79
+ # and must never appear on stderr, in logs, or in support transcripts.
80
+ if not api_key:
81
+ return "***"
82
+ if api_key.startswith("cz_live_"):
83
+ return "cz_live_***"
84
+ if api_key.startswith("cz_test_"):
85
+ return "cz_test_***"
86
+ return "***"
87
+
88
+
76
89
  class Client:
77
90
  """The ControlZero policy client.
78
91
 
@@ -174,7 +187,7 @@ class Client:
174
187
  self._hosted_etag = cb.etag
175
188
  except Exception: # noqa: BLE001
176
189
  self._hosted_etag = None
177
- self._notify_active_source("hosted", source_hint=self._api_key[:14])
190
+ self._notify_active_source("hosted", source_hint=_mask_api_key(self._api_key))
178
191
  else:
179
192
  # Either no api_key, or api_key + LOCAL_OVERRIDE escape hatch.
180
193
  local_source = self._resolve_local_source(None, None)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "controlzero"
7
- version = "1.5.0"
7
+ version = "1.5.1"
8
8
  description = "AI agent governance: policies, audit, and observability for tool calls. Works locally with no signup."
9
9
  readme = "README.md"
10
10
  license = {text = "Apache-2.0"}
@@ -0,0 +1,59 @@
1
+ """Regression tests for the masked API-key hint shown in the active-source
2
+ stderr notification (T103 follow-up, 1.5.1 security fix).
3
+
4
+ Before 1.5.1 the SDK printed ``self._api_key[:14]`` to stderr on every
5
+ Client construction in hosted mode. For a ``cz_live_abcdef123456...``
6
+ key that meant 6 characters of the customer secret leaked to terminals,
7
+ screen shares, support transcripts, and CI logs.
8
+
9
+ The mask MUST:
10
+
11
+ * preserve the public ``cz_live_`` / ``cz_test_`` prefix as a mode signal
12
+ * NEVER emit any character beyond that prefix from the input key
13
+
14
+ These tests verify both invariants with a brute-force substring scan so a
15
+ future regression that switches back to a length-prefix slice fails loud.
16
+ """
17
+
18
+ from controlzero.client import _mask_api_key
19
+
20
+
21
+ def test_mask_live_key_emits_only_public_prefix():
22
+ masked = _mask_api_key("cz_live_7ebef6b600015e3eaeda9149bf6d9c29a")
23
+ assert masked == "cz_live_***"
24
+
25
+
26
+ def test_mask_test_key_emits_only_public_prefix():
27
+ masked = _mask_api_key("cz_test_abcdef0123456789abcdef0123456789")
28
+ assert masked == "cz_test_***"
29
+
30
+
31
+ def test_mask_unknown_prefix_falls_through_to_triple_star():
32
+ masked = _mask_api_key("plain-token-1234567890")
33
+ assert masked == "***"
34
+
35
+
36
+ def test_mask_none_returns_triple_star():
37
+ assert _mask_api_key(None) == "***"
38
+
39
+
40
+ def test_mask_empty_string_returns_triple_star():
41
+ assert _mask_api_key("") == "***"
42
+
43
+
44
+ def test_mask_never_leaks_secret_bytes_brute_force():
45
+ # The secret portion of a real CloudShift production key (anonymised:
46
+ # do not use this value as-is, the customer rotated it after the
47
+ # 1.5.0 -> 1.5.1 incident on 2026-05-12).
48
+ secret_tail = "7ebef6b600015e3eaeda9149bf6d9c29a3a2a7a3075209112afde20888280de0"
49
+ for prefix in ("cz_live_", "cz_test_"):
50
+ key = prefix + secret_tail
51
+ masked = _mask_api_key(key)
52
+ # Scan every 4-char substring of the secret and assert NONE
53
+ # appears in the masked output. 4 chars is enough entropy that
54
+ # an accidental match is vanishingly unlikely.
55
+ for i in range(len(secret_tail) - 3):
56
+ window = secret_tail[i : i + 4]
57
+ assert window not in masked, (
58
+ f"secret bytes {window!r} leaked into masked output {masked!r}"
59
+ )
File without changes
File without changes
File without changes
File without changes