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.
- {controlzero-1.5.0 → controlzero-1.5.1}/CHANGELOG.md +14 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/PKG-INFO +1 -1
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/__init__.py +1 -1
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/client.py +14 -1
- {controlzero-1.5.0 → controlzero-1.5.1}/pyproject.toml +1 -1
- controlzero-1.5.1/tests/test_api_key_mask.py +59 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/.gitignore +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/Dockerfile.test +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/LICENSE +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/README.md +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/_internal/__init__.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/_internal/action_aliases.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/_internal/bundle.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/_internal/dlp_scanner.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/_internal/enforcer.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/_internal/hook_extractors.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/_internal/tool_extractors.json +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/_internal/types.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/audit_local.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/audit_remote.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/cli/__init__.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/cli/debug_bundle.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/cli/main.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/cli/templates/autogen.yaml +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/cli/templates/claude-code.yaml +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/cli/templates/codex-cli.yaml +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/cli/templates/cost-cap.yaml +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/cli/templates/crewai.yaml +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/cli/templates/cursor.yaml +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/cli/templates/gemini-cli.yaml +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/cli/templates/generic.yaml +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/cli/templates/langchain.yaml +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/cli/templates/mcp.yaml +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/cli/templates/rag.yaml +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/device.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/enrollment.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/errors.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/hosted_policy.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/__init__.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/anthropic.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/autogen.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/braintrust.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/crewai/__init__.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/crewai/agent.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/crewai/crew.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/crewai/task.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/crewai/tool.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/google.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/google_adk/__init__.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/google_adk/agent.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/google_adk/tool.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/langchain/__init__.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/langchain/agent.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/langchain/callbacks.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/langchain/chain.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/langchain/graph.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/langchain/modern.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/langchain/tool.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/langfuse.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/litellm.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/openai.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/pydantic_ai.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/integrations/vercel_ai.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/policy_loader.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/controlzero/tamper.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/examples/hello_world.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/conftest.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/integrations/__init__.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/integrations/test_google.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/parity/action_aliases.json +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_action_aliases.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_action_canonicalization.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_agent_name_env.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_audit_remote.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_audit_sink_isolation.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_bundle_parser.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_bundle_translate.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_cli_carve_out.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_cli_debug_bundle.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_cli_extractor_integration.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_cli_hook.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_cli_hosted_refresh.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_cli_init.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_cli_init_templates.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_cli_tail.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_cli_test.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_cli_validate.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_coding_agent_hooks.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_conditions.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_default_action.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_device.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_dlp_scanner.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_enrollment.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_fail_closed_eval.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_glob_matching.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_hook_extractors.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_hosted_policy_e2e.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_hybrid_mode_strict.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_hybrid_mode_warn.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_install_hooks.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_local_mode_dict.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_local_mode_file_json.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_local_mode_file_yaml.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_log_fallback_stderr.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_log_options_ignored_hosted.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_log_rotation.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_no_policy_no_key.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_package_rename_shim.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_policy_freshness.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_policy_settings.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_quarantine.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_reason_code.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_refresh.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_sql_semantic_class.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_synthetic_policy_id_t79.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_t103_precedence.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_t104_cache_gc.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_t108_local_override_audit.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_tamper.py +0 -0
- {controlzero-1.5.0 → controlzero-1.5.1}/tests/test_tamper_behavior.py +0 -0
- {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.
|
|
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
|
|
@@ -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
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|