controlzero 1.5.6__tar.gz → 1.5.7__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.6 → controlzero-1.5.7}/CHANGELOG.md +23 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/PKG-INFO +1 -1
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/__init__.py +1 -1
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/cli/hosts/__init__.py +3 -3
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/cli/hosts/base.py +4 -5
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/cli/hosts/unknown.py +2 -3
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/enrollment.py +3 -4
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/hosted_policy.py +2 -2
- {controlzero-1.5.6 → controlzero-1.5.7}/pyproject.toml +1 -1
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_api_key_mask.py +5 -5
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_env_dump_438.py +2 -2
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_install_hook_command.py +2 -2
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_t103_precedence.py +4 -4
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_t104_cache_gc.py +23 -23
- {controlzero-1.5.6 → controlzero-1.5.7}/.gitignore +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/Dockerfile.test +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/LICENSE +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/README.md +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/_internal/__init__.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/_internal/action_aliases.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/_internal/bundle.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/_internal/dlp_scanner.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/_internal/enforcer.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/_internal/hook_extractors.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/_internal/tool_extractors.json +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/_internal/types.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/audit_local.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/audit_remote.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/cli/__init__.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/cli/_secrets.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/cli/console.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/cli/debug_bundle.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/cli/doctor.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/cli/hosts/claude_code.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/cli/hosts/codex_cli.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/cli/hosts/gemini_cli.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/cli/main.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/cli/migrate.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/cli/telemetry_consent.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/cli/templates/autogen.yaml +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/cli/templates/claude-code.yaml +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/cli/templates/codex-cli.yaml +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/cli/templates/cost-cap.yaml +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/cli/templates/crewai.yaml +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/cli/templates/cursor.yaml +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/cli/templates/gemini-cli.yaml +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/cli/templates/generic.yaml +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/cli/templates/langchain.yaml +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/cli/templates/mcp.yaml +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/cli/templates/rag.yaml +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/client.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/device.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/error_codes.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/errors.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/integrations/__init__.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/integrations/anthropic.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/integrations/autogen.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/integrations/braintrust.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/integrations/crewai/__init__.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/integrations/crewai/agent.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/integrations/crewai/crew.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/integrations/crewai/task.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/integrations/crewai/tool.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/integrations/google.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/integrations/google_adk/__init__.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/integrations/google_adk/agent.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/integrations/google_adk/tool.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/integrations/langchain/__init__.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/integrations/langchain/agent.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/integrations/langchain/callbacks.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/integrations/langchain/chain.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/integrations/langchain/graph.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/integrations/langchain/modern.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/integrations/langchain/tool.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/integrations/langfuse.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/integrations/litellm.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/integrations/openai.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/integrations/pydantic_ai.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/integrations/vercel_ai.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/layout_migration.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/policy_loader.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/controlzero/tamper.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/examples/hello_world.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/conftest.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/integrations/__init__.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/integrations/test_google.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/parity/action_aliases.json +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_action_aliases.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_action_canonicalization.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_agent_name_env.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_audit_remote.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_audit_remote_sdk_version.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_audit_sink_isolation.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_bundle_parser.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_bundle_translate.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_cli_carve_out.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_cli_debug_bundle.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_cli_extractor_integration.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_cli_hook.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_cli_hosted_refresh.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_cli_init.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_cli_init_templates.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_cli_tail.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_cli_test.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_cli_validate.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_coding_agent_hooks.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_conditions.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_console.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_default_action.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_device.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_dlp_scanner.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_doctor.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_enrollment.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_error_codes.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_errors_e_codes.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_fail_closed_eval.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_glob_matching.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_hook_extractors.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_hosted_policy_e2e.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_hosts_adapter.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_hybrid_mode_strict.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_hybrid_mode_warn.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_install_hooks.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_layout_migration_t101.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_layout_parity_t102.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_local_mode_dict.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_local_mode_file_json.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_local_mode_file_yaml.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_log_fallback_stderr.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_log_options_ignored_hosted.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_log_rotation.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_migrate.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_no_policy_no_key.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_package_rename_shim.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_policy_freshness.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_policy_settings.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_quarantine.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_reason_code.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_refresh.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_secrets.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_sql_semantic_class.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_synthetic_policy_id_t79.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_t108_local_override_audit.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_t96_single_audit_log.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_t99_install_prefetch_bundle.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_tamper.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_tamper_behavior.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_tamper_hook.py +0 -0
- {controlzero-1.5.6 → controlzero-1.5.7}/tests/test_telemetry_consent.py +0 -0
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v1.5.7 -- 2026-05-16 (PRIVACY)
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **Customer-context comments in the published wheel.** Codex +
|
|
8
|
+
Gemini outside-voice review surfaced several customer-context
|
|
9
|
+
strings + private monorepo path references that v1.5.6 missed:
|
|
10
|
+
- `controlzero/hosted_policy.py` referenced a customer-specific
|
|
11
|
+
possessive when describing the T104 cache-GC scenario. Rewritten
|
|
12
|
+
in neutral phrasing that preserves the technical context.
|
|
13
|
+
- `controlzero/enrollment.py` documented the wire-format contract
|
|
14
|
+
via a private monorepo path. Replaced with a pointer to the
|
|
15
|
+
public docs site.
|
|
16
|
+
- `controlzero/cli/hosts/base.py` and
|
|
17
|
+
`controlzero/cli/hosts/unknown.py` referenced an internal
|
|
18
|
+
backend filename in inline comments. Replaced with a generic
|
|
19
|
+
description of the constraint.
|
|
20
|
+
- Tests `test_t104_cache_gc.py` and `test_t103_precedence.py`
|
|
21
|
+
carried a geographic identifier and a customer-derived test
|
|
22
|
+
name (NOT in the published wheel since tests are excluded, but
|
|
23
|
+
scrubbed for consistency).
|
|
24
|
+
- **No behavior change.** Comments + docstrings only.
|
|
25
|
+
|
|
3
26
|
## v1.5.6 -- 2026-05-15 (PRIVACY)
|
|
4
27
|
|
|
5
28
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: controlzero
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.7
|
|
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
|
|
@@ -36,9 +36,9 @@ A ``HostAdapter`` owns three responsibilities for one host runtime:
|
|
|
36
36
|
(Windows Claude Code is the canonical case).
|
|
37
37
|
2. ``render(decision)`` -- translate the canonical ``CZDecision``
|
|
38
38
|
into the JSON shape this host's hook validator accepts.
|
|
39
|
-
3. ``canonical_source`` -- the backend
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
3. ``canonical_source`` -- the backend-side source alias
|
|
40
|
+
(``claude_code`` / ``gemini_cli`` / ``codex_cli`` / etc.) so the
|
|
41
|
+
audit row's SOURCE column renders correctly.
|
|
42
42
|
|
|
43
43
|
Adding a new host (Cursor, Windsurf, OpenClaw, Antigravity, the
|
|
44
44
|
next agent that ships) is a single file in this package: subclass
|
|
@@ -84,7 +84,7 @@ class HostAdapter:
|
|
|
84
84
|
A subclass needs three things:
|
|
85
85
|
|
|
86
86
|
* ``name`` -- short identifier used in logs ("claude_code").
|
|
87
|
-
* ``canonical_source`` -- backend
|
|
87
|
+
* ``canonical_source`` -- backend-side source alias
|
|
88
88
|
("claude_code" / "gemini_cli" / "codex_cli" / "unknown").
|
|
89
89
|
This is what the audit row's SOURCE column resolves to so
|
|
90
90
|
the dashboard renders the right label.
|
|
@@ -109,10 +109,9 @@ class HostAdapter:
|
|
|
109
109
|
#: Short identifier used in logs / metrics.
|
|
110
110
|
name: str = "base"
|
|
111
111
|
|
|
112
|
-
#: Backend
|
|
113
|
-
#:
|
|
114
|
-
#:
|
|
115
|
-
#: ``"unknown"`` -- subclasses MUST override.
|
|
112
|
+
#: Backend audit source alias. Must be one of the canonical
|
|
113
|
+
#: values the dashboard accepts so the right label renders.
|
|
114
|
+
#: Default is ``"unknown"`` -- subclasses MUST override.
|
|
116
115
|
canonical_source: str = "unknown"
|
|
117
116
|
|
|
118
117
|
# -- detection --------------------------------------------------
|
|
@@ -26,9 +26,8 @@ from controlzero.cli.hosts.base import CZDecision, HostAdapter
|
|
|
26
26
|
|
|
27
27
|
class UnknownHostAdapter(HostAdapter):
|
|
28
28
|
name = "unknown"
|
|
29
|
-
# Canonical source value
|
|
30
|
-
#
|
|
31
|
-
# the dashboard. Better than mislabelling.
|
|
29
|
+
# Canonical source value the dashboard accepts -- renders as
|
|
30
|
+
# ``--`` on the dashboard. Better than mislabelling.
|
|
32
31
|
canonical_source = "unknown"
|
|
33
32
|
|
|
34
33
|
def claim(self, payload: dict, env: Mapping[str, str]) -> bool:
|
|
@@ -19,10 +19,9 @@ private key lives in ``~/.controlzero/machine.key`` with mode 0600.
|
|
|
19
19
|
A follow-up will plug in Keychain/DPAPI/secret-service per platform;
|
|
20
20
|
the file-on-disk fallback stays as the bottom of the chain.
|
|
21
21
|
|
|
22
|
-
Wire-format: see
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
client side.
|
|
22
|
+
Wire-format contract: see the public docs and OpenAPI spec for the
|
|
23
|
+
machine-auth endpoint at https://docs.controlzero.ai. This module is
|
|
24
|
+
the source of truth on the client side.
|
|
26
25
|
"""
|
|
27
26
|
|
|
28
27
|
from __future__ import annotations
|
|
@@ -138,8 +138,8 @@ def gc_stale_cache(active_api_key: str) -> int:
|
|
|
138
138
|
T104 (2026-05-12, customer report): on key rotation the old key's cache files
|
|
139
139
|
(``bootstrap-<oldscope>.json``, ``bundle-<oldscope>.bin``,
|
|
140
140
|
``bundle-<oldscope>.meta``) accumulated forever next to the new key's
|
|
141
|
-
files.
|
|
142
|
-
next to a fresh ``bootstrap
|
|
141
|
+
files. A customer reported a 25-day-old ``bundle-<oldscope>.bin`` left
|
|
142
|
+
next to a fresh ``bootstrap-<newscope>.json`` because the rotated
|
|
143
143
|
key never invalidated the previous cache. We clean up on every fresh
|
|
144
144
|
bootstrap so the cache directory always reflects exactly one active
|
|
145
145
|
key per machine.
|
|
@@ -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.7"
|
|
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"}
|
|
@@ -19,12 +19,12 @@ from controlzero.client import _mask_api_key
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def test_mask_live_key_emits_only_public_prefix():
|
|
22
|
-
masked = _mask_api_key("
|
|
22
|
+
masked = _mask_api_key("cz_live_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
|
23
23
|
assert masked == "cz_live_***"
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
def test_mask_test_key_emits_only_public_prefix():
|
|
27
|
-
masked = _mask_api_key("
|
|
27
|
+
masked = _mask_api_key("cz_test_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
|
|
28
28
|
assert masked == "cz_test_***"
|
|
29
29
|
|
|
30
30
|
|
|
@@ -42,10 +42,10 @@ def test_mask_empty_string_returns_triple_star():
|
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
def test_mask_never_leaks_secret_bytes_brute_force():
|
|
45
|
-
#
|
|
46
|
-
#
|
|
45
|
+
# Obviously-synthetic placeholder fixture. The 4-char window
|
|
46
|
+
# scan downstream remains meaningful: the masked output must not
|
|
47
47
|
# 1.5.0 -> 1.5.1 incident on 2026-05-12).
|
|
48
|
-
secret_tail = "
|
|
48
|
+
secret_tail = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
49
49
|
for prefix in ("cz_live_", "cz_test_"):
|
|
50
50
|
key = prefix + secret_tail
|
|
51
51
|
masked = _mask_api_key(key)
|
|
@@ -66,7 +66,7 @@ def test_env_dump_outputs_valid_json(monkeypatch):
|
|
|
66
66
|
def test_env_dump_redacts_api_key_by_default(monkeypatch):
|
|
67
67
|
monkeypatch.setenv(
|
|
68
68
|
"CONTROLZERO_API_KEY",
|
|
69
|
-
"
|
|
69
|
+
"cz_live_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
70
70
|
)
|
|
71
71
|
result = _invoke([])
|
|
72
72
|
assert result.exit_code == 0
|
|
@@ -74,7 +74,7 @@ def test_env_dump_redacts_api_key_by_default(monkeypatch):
|
|
|
74
74
|
masked = data["env"]["CONTROLZERO_API_KEY"]
|
|
75
75
|
assert masked == "cz_live_***"
|
|
76
76
|
# Defensive: the secret bytes MUST NOT appear anywhere in the dump.
|
|
77
|
-
secret_tail = "
|
|
77
|
+
secret_tail = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
78
78
|
assert secret_tail not in result.output
|
|
79
79
|
|
|
80
80
|
|
|
@@ -30,8 +30,8 @@ from click.testing import CliRunner
|
|
|
30
30
|
from controlzero.cli.main import cli
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
LIVE_KEY = "
|
|
34
|
-
TEST_KEY = "
|
|
33
|
+
LIVE_KEY = "cz_live_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
34
|
+
TEST_KEY = "cz_test_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
def _read_pretooluse_command(settings_path: Path) -> str:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""T103 regression tests: hosted-vs-local precedence.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Customer report 2026-05-12: a stale ~/.controlzero/policy.yaml
|
|
4
4
|
shadowed the dashboard policy with no warning. Local always won over
|
|
5
5
|
hosted when both were present. T103 reverses this so api_key implies
|
|
6
6
|
hosted unless CONTROLZERO_LOCAL_OVERRIDE=1.
|
|
@@ -101,10 +101,10 @@ def test_no_api_key_accepts_json_policy(monkeypatch, tmp_path):
|
|
|
101
101
|
cz.close()
|
|
102
102
|
|
|
103
103
|
|
|
104
|
-
def
|
|
104
|
+
def test_customer_2026_05_12_cwd_yaml_bypassed_when_api_key_set(
|
|
105
105
|
monkeypatch, tmp_path
|
|
106
106
|
):
|
|
107
|
-
"""
|
|
107
|
+
"""Customer report 2026-05-12 regression.
|
|
108
108
|
|
|
109
109
|
Pre-T103: a stale ~/.controlzero/policy.yaml shadowed the hosted
|
|
110
110
|
bundle for 25 days. Post-T103: api_key + cwd policy.yaml + no
|
|
@@ -125,7 +125,7 @@ def test_john_na_regression_cwd_yaml_bypassed_when_api_key_set(
|
|
|
125
125
|
encoding="utf-8",
|
|
126
126
|
)
|
|
127
127
|
monkeypatch.chdir(tmp_path)
|
|
128
|
-
monkeypatch.setenv("CONTROLZERO_API_KEY", "
|
|
128
|
+
monkeypatch.setenv("CONTROLZERO_API_KEY", "cz_live_synthetictest")
|
|
129
129
|
monkeypatch.delenv("CONTROLZERO_LOCAL_OVERRIDE", raising=False)
|
|
130
130
|
|
|
131
131
|
hosted_policy = {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"""T104 regression tests: stale cache GC on key rotation.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
Customer report 2026-05-12: rotated their api_key from one value to
|
|
4
|
+
another. The new key bootstrapped fresh but the prior key's
|
|
5
|
+
``cache/bundle-<oldscope>.bin`` lingered for 25 days next to the new
|
|
6
|
+
key's files. T104 GC's any cache files whose key-scope does NOT match
|
|
7
|
+
the currently-active api_key, but only on a fresh bootstrap (not on a
|
|
8
|
+
cache hit, because that means the active key is unchanged).
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
@@ -47,19 +47,19 @@ def _write_cache_triplet(cache_dir: Path, scope: str) -> tuple[Path, Path, Path]
|
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
def test_gc_removes_files_for_rotated_keys(fake_cache_dir):
|
|
50
|
-
"""Stale triplet for
|
|
51
|
-
_write_cache_triplet(fake_cache_dir, _key_scope("
|
|
50
|
+
"""Stale triplet for the old scope is removed when the new scope is active."""
|
|
51
|
+
_write_cache_triplet(fake_cache_dir, _key_scope("cz_live_oldscope0000"))
|
|
52
52
|
new_files = _write_cache_triplet(
|
|
53
|
-
fake_cache_dir, _key_scope("
|
|
53
|
+
fake_cache_dir, _key_scope("cz_live_newscope0000")
|
|
54
54
|
)
|
|
55
55
|
|
|
56
|
-
removed = gc_stale_cache("
|
|
56
|
+
removed = gc_stale_cache("cz_live_newscope0000")
|
|
57
57
|
|
|
58
|
-
assert removed == 3 # the
|
|
58
|
+
assert removed == 3 # the old-scope triplet
|
|
59
59
|
for stale_name in (
|
|
60
|
-
f"bootstrap-{_key_scope('
|
|
61
|
-
f"bundle-{_key_scope('
|
|
62
|
-
f"bundle-{_key_scope('
|
|
60
|
+
f"bootstrap-{_key_scope('cz_live_oldscope0000')}.json",
|
|
61
|
+
f"bundle-{_key_scope('cz_live_oldscope0000')}.bin",
|
|
62
|
+
f"bundle-{_key_scope('cz_live_oldscope0000')}.meta",
|
|
63
63
|
):
|
|
64
64
|
assert not (fake_cache_dir / stale_name).exists()
|
|
65
65
|
for active in new_files:
|
|
@@ -68,17 +68,17 @@ def test_gc_removes_files_for_rotated_keys(fake_cache_dir):
|
|
|
68
68
|
|
|
69
69
|
def test_gc_is_noop_when_only_active_key_present(fake_cache_dir):
|
|
70
70
|
"""No-op when every file already belongs to the active key."""
|
|
71
|
-
_write_cache_triplet(fake_cache_dir, _key_scope("
|
|
72
|
-
removed = gc_stale_cache("
|
|
71
|
+
_write_cache_triplet(fake_cache_dir, _key_scope("cz_live_newscope0000"))
|
|
72
|
+
removed = gc_stale_cache("cz_live_newscope0000")
|
|
73
73
|
assert removed == 0
|
|
74
74
|
|
|
75
75
|
|
|
76
76
|
def test_gc_leaves_unrelated_files_alone(fake_cache_dir):
|
|
77
77
|
"""Stray user files in the cache dir are preserved.
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
going to delete arbitrary user content even if they put it in
|
|
81
|
-
cache dir.
|
|
79
|
+
A customer's machine had `s3_copy.py` in `~/.controlzero/`; we are
|
|
80
|
+
not going to delete arbitrary user content even if they put it in
|
|
81
|
+
our cache dir.
|
|
82
82
|
"""
|
|
83
83
|
stray = fake_cache_dir / "README.txt"
|
|
84
84
|
stray.write_text("notes", encoding="utf-8")
|
|
@@ -114,7 +114,7 @@ def test_get_or_fetch_bootstrap_triggers_gc_on_fresh_fetch(
|
|
|
114
114
|
fake_cache_dir, monkeypatch
|
|
115
115
|
):
|
|
116
116
|
"""A fresh bootstrap fetch triggers cache GC for rotated keys."""
|
|
117
|
-
stale_scope = _key_scope("
|
|
117
|
+
stale_scope = _key_scope("cz_live_oldscope0000")
|
|
118
118
|
_write_cache_triplet(fake_cache_dir, stale_scope)
|
|
119
119
|
|
|
120
120
|
fresh_keys = BootstrapKeys(
|
|
@@ -126,16 +126,16 @@ def test_get_or_fetch_bootstrap_triggers_gc_on_fresh_fetch(
|
|
|
126
126
|
)
|
|
127
127
|
|
|
128
128
|
def fake_fetch(api_key, api_url=None):
|
|
129
|
-
assert api_key == "
|
|
129
|
+
assert api_key == "cz_live_newscope0000"
|
|
130
130
|
return fresh_keys
|
|
131
131
|
|
|
132
132
|
monkeypatch.setattr(hosted_policy, "fetch_bootstrap", fake_fetch)
|
|
133
133
|
|
|
134
|
-
keys = hosted_policy.get_or_fetch_bootstrap("
|
|
134
|
+
keys = hosted_policy.get_or_fetch_bootstrap("cz_live_newscope0000")
|
|
135
135
|
assert keys.project_id == "proj"
|
|
136
136
|
|
|
137
137
|
# Stale triplet gone, new bootstrap landed.
|
|
138
|
-
new_scope = _key_scope("
|
|
138
|
+
new_scope = _key_scope("cz_live_newscope0000")
|
|
139
139
|
assert not (fake_cache_dir / f"bootstrap-{stale_scope}.json").exists()
|
|
140
140
|
assert not (fake_cache_dir / f"bundle-{stale_scope}.bin").exists()
|
|
141
141
|
assert not (fake_cache_dir / f"bundle-{stale_scope}.meta").exists()
|
|
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
|
|
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
|