controlzero 1.5.5a1__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.5a1 → controlzero-1.5.7}/CHANGELOG.md +94 -5
- {controlzero-1.5.5a1 → controlzero-1.5.7}/PKG-INFO +2 -1
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/__init__.py +1 -1
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/_internal/bundle.py +1 -1
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/_internal/enforcer.py +4 -5
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/_internal/types.py +1 -1
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/audit_remote.py +41 -2
- controlzero-1.5.7/controlzero/cli/_secrets.py +135 -0
- controlzero-1.5.7/controlzero/cli/console.py +125 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/debug_bundle.py +1 -1
- controlzero-1.5.7/controlzero/cli/doctor.py +309 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/hosts/__init__.py +3 -3
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/hosts/base.py +6 -7
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/hosts/claude_code.py +1 -1
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/hosts/gemini_cli.py +1 -1
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/hosts/unknown.py +2 -3
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/main.py +40 -12
- controlzero-1.5.7/controlzero/cli/migrate.py +200 -0
- controlzero-1.5.7/controlzero/cli/telemetry_consent.py +219 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/client.py +3 -3
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/device.py +2 -2
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/enrollment.py +5 -7
- controlzero-1.5.7/controlzero/error_codes.py +415 -0
- controlzero-1.5.7/controlzero/errors.py +181 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/hosted_policy.py +5 -5
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/braintrust.py +0 -1
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/crewai/agent.py +2 -2
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/crewai/task.py +1 -1
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/crewai/tool.py +2 -2
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/google_adk/agent.py +1 -1
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/langchain/agent.py +7 -4
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/langchain/callbacks.py +1 -1
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/langchain/graph.py +1 -1
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/langchain/tool.py +2 -3
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/langfuse.py +0 -1
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/layout_migration.py +1 -1
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/policy_loader.py +1 -1
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/tamper.py +7 -3
- {controlzero-1.5.5a1 → controlzero-1.5.7}/pyproject.toml +6 -1
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/conftest.py +0 -2
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_api_key_mask.py +5 -5
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_audit_remote.py +8 -12
- controlzero-1.5.7/tests/test_audit_remote_sdk_version.py +145 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_bundle_translate.py +1 -2
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_cli_carve_out.py +10 -11
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_cli_debug_bundle.py +0 -2
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_cli_extractor_integration.py +3 -3
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_cli_hook.py +1 -1
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_cli_hosted_refresh.py +1 -1
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_coding_agent_hooks.py +0 -2
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_conditions.py +0 -1
- controlzero-1.5.7/tests/test_console.py +87 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_default_action.py +1 -2
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_device.py +1 -1
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_dlp_scanner.py +1 -3
- controlzero-1.5.7/tests/test_doctor.py +150 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_enrollment.py +0 -3
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_env_dump_438.py +3 -4
- controlzero-1.5.7/tests/test_error_codes.py +64 -0
- controlzero-1.5.7/tests/test_errors_e_codes.py +209 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_glob_matching.py +3 -3
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_hosted_policy_e2e.py +0 -2
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_hosts_adapter.py +2 -2
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_hybrid_mode_warn.py +0 -2
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_install_hook_command.py +3 -3
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_install_hooks.py +2 -3
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_layout_migration_t101.py +0 -1
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_layout_parity_t102.py +0 -1
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_log_fallback_stderr.py +0 -1
- controlzero-1.5.7/tests/test_migrate.py +128 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_package_rename_shim.py +0 -5
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_policy_freshness.py +0 -3
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_policy_settings.py +0 -2
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_quarantine.py +1 -2
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_reason_code.py +3 -3
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_refresh.py +11 -9
- controlzero-1.5.7/tests/test_secrets.py +227 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_sql_semantic_class.py +1 -1
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_synthetic_policy_id_t79.py +1 -1
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_t103_precedence.py +4 -6
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_t104_cache_gc.py +23 -23
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_t108_local_override_audit.py +0 -1
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_t96_single_audit_log.py +0 -1
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_t99_install_prefetch_bundle.py +0 -1
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_tamper_behavior.py +0 -4
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_tamper_hook.py +0 -2
- controlzero-1.5.7/tests/test_telemetry_consent.py +90 -0
- controlzero-1.5.5a1/controlzero/errors.py +0 -85
- {controlzero-1.5.5a1 → controlzero-1.5.7}/.gitignore +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/Dockerfile.test +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/LICENSE +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/README.md +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/_internal/__init__.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/_internal/action_aliases.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/_internal/dlp_scanner.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/_internal/hook_extractors.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/_internal/tool_extractors.json +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/audit_local.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/__init__.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/hosts/codex_cli.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/templates/autogen.yaml +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/templates/claude-code.yaml +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/templates/codex-cli.yaml +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/templates/cost-cap.yaml +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/templates/crewai.yaml +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/templates/cursor.yaml +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/templates/gemini-cli.yaml +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/templates/generic.yaml +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/templates/langchain.yaml +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/templates/mcp.yaml +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/templates/rag.yaml +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/__init__.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/anthropic.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/autogen.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/crewai/__init__.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/crewai/crew.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/google.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/google_adk/__init__.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/google_adk/tool.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/langchain/__init__.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/langchain/chain.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/langchain/modern.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/litellm.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/openai.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/pydantic_ai.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/vercel_ai.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/examples/hello_world.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/integrations/__init__.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/integrations/test_google.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/parity/action_aliases.json +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_action_aliases.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_action_canonicalization.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_agent_name_env.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_audit_sink_isolation.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_bundle_parser.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_cli_init.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_cli_init_templates.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_cli_tail.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_cli_test.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_cli_validate.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_fail_closed_eval.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_hook_extractors.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_hybrid_mode_strict.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_local_mode_dict.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_local_mode_file_json.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_local_mode_file_yaml.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_log_options_ignored_hosted.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_log_rotation.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_no_policy_no_key.py +0 -0
- {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_tamper.py +0 -0
|
@@ -1,5 +1,94 @@
|
|
|
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
|
+
|
|
26
|
+
## v1.5.6 -- 2026-05-15 (PRIVACY)
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- **Customer names and identifiable references removed from the
|
|
31
|
+
published wheel.** Earlier 1.5.x releases (including 1.5.5)
|
|
32
|
+
carried customer names and individual contributor names in inline
|
|
33
|
+
comments and docstrings as historical context for past incidents.
|
|
34
|
+
This release replaces every such reference with a generic
|
|
35
|
+
technical description (a customer / an enterprise customer / a
|
|
36
|
+
date-stamped incident label) while preserving the technical
|
|
37
|
+
meaning of the comment. No behavior change. Concretely affects
|
|
38
|
+
inline comments in `client.py`, `device.py`, `hosted_policy.py`,
|
|
39
|
+
`enrollment.py`, `cli/main.py`, `cli/_secrets.py`, `cli/debug_bundle.py`,
|
|
40
|
+
`cli/hosts/claude_code.py`, `cli/hosts/gemini_cli.py`,
|
|
41
|
+
`_internal/bundle.py`, `_internal/enforcer.py`. 1.5.5 is yanked
|
|
42
|
+
on PyPI.
|
|
43
|
+
- **Realistic-looking placeholder key in source replaced.** The
|
|
44
|
+
64-hex-char `cz_live_*` string used as a docstring example and
|
|
45
|
+
test fixture has been swapped for an obviously-synthetic
|
|
46
|
+
`cz_live_aaaa...` placeholder. The original value was a test
|
|
47
|
+
fixture, not a real customer key, but the format was close
|
|
48
|
+
enough to a real key that an external reader could not tell.
|
|
49
|
+
|
|
50
|
+
## v1.5.5 -- 2026-05-15 (YANKED)
|
|
51
|
+
|
|
52
|
+
YANKED on 2026-05-15 due to customer-name leakage in inline comments.
|
|
53
|
+
Use 1.5.6 instead.
|
|
54
|
+
|
|
55
|
+
## Unreleased -- Tier 0a security hotfix (2026-05-15)
|
|
56
|
+
|
|
57
|
+
### Security (P0 hotfix for #174)
|
|
58
|
+
|
|
59
|
+
- **New `controlzero doctor` command**. Scans every known coding-agent
|
|
60
|
+
settings file (claude-code, gemini-cli, codex-cli, cursor, windsurf,
|
|
61
|
+
vscode, cline, antigravity, adal, jetbrains) for plaintext `cz_*_*`
|
|
62
|
+
API keys baked into hook commands. Reports findings compiler-style
|
|
63
|
+
with stable E#### error codes. Exit 1 on any ERROR finding so it
|
|
64
|
+
can run in CI / pre-push hooks.
|
|
65
|
+
- **New `controlzero migrate` command**. Auto-rewrites every leaked
|
|
66
|
+
inline hook of the form `CONTROLZERO_API_KEY=cz_live_... controlzero
|
|
67
|
+
hook-check` into the safe `controlzero hook-check` form, and
|
|
68
|
+
persists the recovered key to `~/.controlzero/config.yaml`
|
|
69
|
+
(mode 0o600, parent dir 0o700). Has `--dry-run`. Idempotent.
|
|
70
|
+
- **Stable error-code catalog** (`controlzero.error_codes`). 24 E####
|
|
71
|
+
codes across security / auth / policy / cache / network / hook /
|
|
72
|
+
runtime ranges. Each entry has title, what, fix, and a docs slug
|
|
73
|
+
for the `docs.controlzero.ai/errors/` URL.
|
|
74
|
+
- **`controlzero telemetry`** group: `full` / `anonymous` / `off`.
|
|
75
|
+
Opt-IN per the 2026-05-14 transparency mandate. Default unset =
|
|
76
|
+
silent. Non-interactive shells never prompt. State in
|
|
77
|
+
`~/.controlzero/telemetry.yaml` (mode 0o600).
|
|
78
|
+
- **Tighter local-file permissions**. `_write_api_key_config` now
|
|
79
|
+
enforces 0o600 on `config.yaml` and 0o700 on the parent directory.
|
|
80
|
+
Best-effort on Windows / FAT (silent skip).
|
|
81
|
+
- **Cross-SDK CI contract test**: `scripts/ci/check-no-key-leaks.sh`
|
|
82
|
+
fails the build on any new `cz_(live|test)_*` leak surface across
|
|
83
|
+
Python, Node, Go, frontend, docs.
|
|
84
|
+
- **Rich CLI palette**: doctor + migrate now use the DESIGN.md
|
|
85
|
+
sage-green theme via `controlzero.cli.console`.
|
|
86
|
+
|
|
87
|
+
If you previously ran `controlzero install <agent>` on a version
|
|
88
|
+
older than 1.5.3, your agent settings file likely still contains
|
|
89
|
+
the plaintext key. Run `controlzero doctor` to check; run
|
|
90
|
+
`controlzero migrate` to fix.
|
|
91
|
+
|
|
3
92
|
## 1.5.5a1 (pre-release) -- 2026-05-13
|
|
4
93
|
|
|
5
94
|
This is a **pre-release**. `pip install control-zero` continues to
|
|
@@ -21,7 +110,7 @@ T96 + T101 layout changes in real customer environments.
|
|
|
21
110
|
not contents), and resolved API URL. With `--from-hook` it parses
|
|
22
111
|
a stdin payload so the output reflects what the hook subprocess
|
|
23
112
|
actually sees. Built for fast Windows-hook triage after the
|
|
24
|
-
2026-05-12
|
|
113
|
+
2026-05-12 incident -- a customer can now run a single
|
|
25
114
|
command and send us the JSON instead of guessing which env vars
|
|
26
115
|
their hook sees.
|
|
27
116
|
|
|
@@ -73,7 +162,7 @@ T96 + T101 layout changes in real customer environments.
|
|
|
73
162
|
Anthropic's PreToolUse hook schema only accepts `"approve" | "block"`
|
|
74
163
|
-- the `"allow"` string crashed Claude Code's validator with
|
|
75
164
|
`Hook JSON output validation failed - (root): Invalid input` and
|
|
76
|
-
Claude Code dropped the hook decision (defaults to proceed).
|
|
165
|
+
Claude Code dropped the hook decision (defaults to proceed). A customer
|
|
77
166
|
saw a `Write` correctly blocked, then a `PowerShell` command bypass
|
|
78
167
|
to the same intent. After 1.5.3 the Claude Code adapter renders
|
|
79
168
|
allow as `"approve"` and deny as `"block"`; Gemini CLI / Codex CLI
|
|
@@ -122,7 +211,7 @@ T96 + T101 layout changes in real customer environments.
|
|
|
122
211
|
|
|
123
212
|
### Customer impact
|
|
124
213
|
|
|
125
|
-
|
|
214
|
+
an enterprise customer admin reported on 2026-05-12 that Claude Code on
|
|
126
215
|
Windows was not sending any audit logs to the dashboard, while the
|
|
127
216
|
direct Python SDK script was sending logs without a populated
|
|
128
217
|
SOURCE column. Both symptoms collapse to the two fixes above.
|
|
@@ -192,7 +281,7 @@ GH #424 (umbrella), PRs #425 (precedence), #428 (cache GC), #427
|
|
|
192
281
|
(T87, GH #392). The bundle on disk is encrypted+signed; `cat` returns
|
|
193
282
|
nothing useful, so until now diagnosing a deny-deny incident meant
|
|
194
283
|
shipping the bundle back to engineering or attaching a debugger
|
|
195
|
-
(
|
|
284
|
+
(the deny-deny took ~3 hours to root-cause for exactly this
|
|
196
285
|
reason). The new subcommand reads the matching `bootstrap-<prefix>.json`
|
|
197
286
|
for keys, decrypts and verifies the cached bundle blob via the same
|
|
198
287
|
parser the SDK uses, and prints a human-readable summary: bundle id,
|
|
@@ -247,7 +336,7 @@ GH #424 (umbrella), PRs #425 (precedence), #428 (cache GC), #427
|
|
|
247
336
|
work, modern rules continue to work, and only previously-broken
|
|
248
337
|
legacy rules start matching again.
|
|
249
338
|
|
|
250
|
-
- **Synthetic policy_id sentinels** (T79,
|
|
339
|
+
- **Synthetic policy_id sentinels** (T79, the deny-deny postmortem).
|
|
251
340
|
`PolicyDecision.policy_id` is now stamped with one of six canonical
|
|
252
341
|
`synthetic:*` values whenever the deny was emitted by a fail-closed
|
|
253
342
|
code path (no rule matched, empty bundle, missing bundle, T83-class
|
|
@@ -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
|
|
@@ -28,6 +28,7 @@ Requires-Dist: httpx>=0.25.0
|
|
|
28
28
|
Requires-Dist: loguru>=0.7.0
|
|
29
29
|
Requires-Dist: pydantic>=2.0.0
|
|
30
30
|
Requires-Dist: pyyaml>=6.0
|
|
31
|
+
Requires-Dist: rich>=13.0.0
|
|
31
32
|
Requires-Dist: zstandard>=0.22.0
|
|
32
33
|
Provides-Extra: anthropic
|
|
33
34
|
Requires-Dist: anthropic>=0.25.0; extra == 'anthropic'
|
|
@@ -437,7 +437,7 @@ def translate_to_local_policy(payload: dict) -> dict:
|
|
|
437
437
|
# reason_code so dashboards still bucket it as "nothing
|
|
438
438
|
# attached").
|
|
439
439
|
#
|
|
440
|
-
# Copy-choice note (2026-04-19,
|
|
440
|
+
# Copy-choice note (2026-04-19, the 2026-04-19 P0): the previous
|
|
441
441
|
# message -- "No active policies. Define one in the Control Zero
|
|
442
442
|
# dashboard." -- presumed the user had not defined any policies.
|
|
443
443
|
# That presumption was wrong in the common case: the user had
|
|
@@ -20,10 +20,9 @@ from __future__ import annotations
|
|
|
20
20
|
|
|
21
21
|
import fnmatch
|
|
22
22
|
from dataclasses import dataclass, field
|
|
23
|
-
from typing import
|
|
23
|
+
from typing import Optional
|
|
24
24
|
|
|
25
25
|
from controlzero._internal.dlp_scanner import (
|
|
26
|
-
DLPMatch,
|
|
27
26
|
DLPScanner,
|
|
28
27
|
extract_text_from_args,
|
|
29
28
|
)
|
|
@@ -34,7 +33,7 @@ from controlzero._internal.types import PolicyRule
|
|
|
34
33
|
# so integrations + dashboards can branch on decision provenance
|
|
35
34
|
# without regex-matching the human-readable `reason` string.
|
|
36
35
|
#
|
|
37
|
-
# Added 2026-04-19 after
|
|
36
|
+
# Added 2026-04-19 after the 2026-04-19 P0: the SDK fail-closed with "No
|
|
38
37
|
# active policies. Define one in the Control Zero dashboard." when
|
|
39
38
|
# the user had in fact defined three; the three surfaces disagreed
|
|
40
39
|
# because policy_attachments was empty. reason_code lets the hosted
|
|
@@ -79,7 +78,7 @@ VALID_REASON_CODES = frozenset({
|
|
|
79
78
|
REASON_CODE_LOCAL_OVERRIDE_ACTIVE,
|
|
80
79
|
})
|
|
81
80
|
|
|
82
|
-
# Synthetic policy_id sentinels (T79 /
|
|
81
|
+
# Synthetic policy_id sentinels (T79 / the deny-deny postmortem,
|
|
83
82
|
# 2026-05-11). When a deny is emitted by anything OTHER than a
|
|
84
83
|
# user-authored rule, the SDK stamps the audit row's `policy_id` with
|
|
85
84
|
# one of these `synthetic:*` values so the audit dashboard can render
|
|
@@ -318,7 +317,7 @@ class PolicyEvaluator:
|
|
|
318
317
|
# resources:["*"] for unscoped rules, so prior to this
|
|
319
318
|
# fix every cz.guard() call without an explicit resource
|
|
320
319
|
# was skipping every rule and falling through to the
|
|
321
|
-
# default deny (
|
|
320
|
+
# default deny (the deny-deny incident, 2026-05-10).
|
|
322
321
|
# Logic now: if any pattern in rule.resources is the
|
|
323
322
|
# universal "*", treat the gate as satisfied; otherwise
|
|
324
323
|
# require a caller-supplied resource and glob-match it.
|
|
@@ -24,13 +24,44 @@ import json
|
|
|
24
24
|
import logging
|
|
25
25
|
import platform
|
|
26
26
|
import threading
|
|
27
|
-
import time
|
|
28
27
|
import uuid
|
|
29
28
|
from datetime import datetime, timezone
|
|
29
|
+
from pathlib import Path
|
|
30
30
|
from typing import Optional
|
|
31
31
|
|
|
32
32
|
from controlzero.device import detect_client_name, detect_client_version
|
|
33
33
|
|
|
34
|
+
# #495/v1 (2026-05-14): the normalized controlzero_sdk_version field
|
|
35
|
+
# carried on every audit POST. Format is "<lang>@<version>" so the
|
|
36
|
+
# backend can group/filter across SDKs without ambiguity (a version
|
|
37
|
+
# string alone like "1.9.1" is shared between Python and Node).
|
|
38
|
+
# Capped at 64 chars so a freak __version__ value cannot pollute the
|
|
39
|
+
# LowCardinality column on the backend.
|
|
40
|
+
#
|
|
41
|
+
# Read via importlib.metadata rather than `from controlzero import
|
|
42
|
+
# __version__`: audit_remote.py is imported transitively from
|
|
43
|
+
# controlzero.client.Client, which is imported at the top of
|
|
44
|
+
# controlzero/__init__.py before __version__ is defined. The naive
|
|
45
|
+
# import triggers a circular-import ImportError on package load.
|
|
46
|
+
# importlib.metadata reads from the installed dist-info and avoids
|
|
47
|
+
# the partial-module problem entirely.
|
|
48
|
+
def _resolve_sdk_version_wire() -> str:
|
|
49
|
+
try:
|
|
50
|
+
from importlib.metadata import version as _pkg_version
|
|
51
|
+
v = _pkg_version("controlzero")
|
|
52
|
+
except Exception:
|
|
53
|
+
# Editable install where the dist-info isn't on the metadata
|
|
54
|
+
# path, or any other import-time failure: fall back to empty
|
|
55
|
+
# so the audit pipeline doesn't carry a wrong value.
|
|
56
|
+
return ""
|
|
57
|
+
wire = "python@" + v
|
|
58
|
+
if len(wire) > 64:
|
|
59
|
+
return ""
|
|
60
|
+
return wire
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
_CZ_SDK_VERSION_WIRE = _resolve_sdk_version_wire()
|
|
64
|
+
|
|
34
65
|
logger = logging.getLogger("controlzero.audit_remote")
|
|
35
66
|
|
|
36
67
|
# Buffer limits
|
|
@@ -48,7 +79,7 @@ class RemoteAuditSink:
|
|
|
48
79
|
machine_token: str, # not used for auth header, kept for future
|
|
49
80
|
org_id: str,
|
|
50
81
|
machine_id: str,
|
|
51
|
-
state_dir: Optional[
|
|
82
|
+
state_dir: Optional[Path] = None,
|
|
52
83
|
):
|
|
53
84
|
self._api_url = api_url.rstrip("/")
|
|
54
85
|
self._machine_token = machine_token
|
|
@@ -132,6 +163,10 @@ class RemoteAuditSink:
|
|
|
132
163
|
# coding-agent hook-check calls; SDK library integrations
|
|
133
164
|
# pass an empty string until they adopt the same extractor.
|
|
134
165
|
"extracted_method": entry.get("extracted_method", ""),
|
|
166
|
+
# v1 (2026-05-14): normalized controlzero SDK package version
|
|
167
|
+
# so support can pinpoint which SDK release made this POST.
|
|
168
|
+
# See top of file for the wire-format invariant.
|
|
169
|
+
"controlzero_sdk_version": _CZ_SDK_VERSION_WIRE,
|
|
135
170
|
}
|
|
136
171
|
|
|
137
172
|
# ---- flush mechanics ----
|
|
@@ -312,6 +347,10 @@ class BearerAuditSink:
|
|
|
312
347
|
# coding-agent hook-check calls; SDK library integrations
|
|
313
348
|
# pass an empty string until they adopt the same extractor.
|
|
314
349
|
"extracted_method": entry.get("extracted_method", ""),
|
|
350
|
+
# v1 (2026-05-14): same controlzero SDK version wire field
|
|
351
|
+
# as the BearerAuditSink path above. Kept in lockstep so
|
|
352
|
+
# both sinks produce identical row shapes on the backend.
|
|
353
|
+
"controlzero_sdk_version": _CZ_SDK_VERSION_WIRE,
|
|
315
354
|
}
|
|
316
355
|
|
|
317
356
|
def _flush_async(self) -> None:
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""API key redaction + leak-detection helpers (Tier 0a hotfix 2026-05-15).
|
|
2
|
+
|
|
3
|
+
Single source of truth for:
|
|
4
|
+
|
|
5
|
+
1. Detecting `cz_(live|test)_*` patterns in arbitrary text (used by
|
|
6
|
+
`controlzero doctor` to scan agent settings files + the cross-SDK
|
|
7
|
+
contract test that fails the CI on any leak surface).
|
|
8
|
+
2. Redacting a full key to a last-4 form for display in CLI output
|
|
9
|
+
(`cz_live_***aaaaa`). Matches the gh CLI / Stripe CLI convention.
|
|
10
|
+
|
|
11
|
+
These helpers MUST stay pure (no I/O, no logging) so the contract test
|
|
12
|
+
that depends on `find_key_leaks` can run in a sandbox without touching
|
|
13
|
+
the user's actual filesystem.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
from typing import Iterable, List, NamedTuple
|
|
20
|
+
|
|
21
|
+
# A controlzero API key is 8-char prefix (cz_live_ / cz_test_) + 64 hex
|
|
22
|
+
# characters in production. We also accept shorter trailing alphanumerics
|
|
23
|
+
# so legacy short-form test keys (cz_test_localdev_...) are caught.
|
|
24
|
+
# The pattern is deliberately greedy on the suffix so we catch:
|
|
25
|
+
# cz_live_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
|
26
|
+
# cz_test_localdev_000000000000000000000000
|
|
27
|
+
# cz_test_xxxxxxxx (rarer, but valid)
|
|
28
|
+
# but stops at a non-alphanumeric so we don't gobble surrounding markup.
|
|
29
|
+
_KEY_PATTERN = re.compile(r"cz_(live|test)_[A-Za-z0-9_]{4,}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class KeyMatch(NamedTuple):
|
|
33
|
+
"""One match from `find_key_leaks`. The `key` field is the FULL
|
|
34
|
+
matched substring; callers that surface this in customer output
|
|
35
|
+
must run it through `redact_key` first."""
|
|
36
|
+
|
|
37
|
+
start: int
|
|
38
|
+
end: int
|
|
39
|
+
key: str
|
|
40
|
+
line_number: int
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def find_key_leaks(text: str) -> List[KeyMatch]:
|
|
44
|
+
"""Return every `cz_(live|test)_*` substring in `text`.
|
|
45
|
+
|
|
46
|
+
Used by:
|
|
47
|
+
- `controlzero doctor` to scan ~/.claude/settings.json etc.
|
|
48
|
+
- The cross-SDK contract test to fail the PR on any leak.
|
|
49
|
+
- `controlzero migrate` to identify which entries to rewrite.
|
|
50
|
+
|
|
51
|
+
Pure function. No I/O. Deterministic. Safe to call on arbitrary
|
|
52
|
+
untrusted input (no regex catastrophic backtracking; pattern is
|
|
53
|
+
linear).
|
|
54
|
+
"""
|
|
55
|
+
matches: List[KeyMatch] = []
|
|
56
|
+
if not text:
|
|
57
|
+
return matches
|
|
58
|
+
for m in _KEY_PATTERN.finditer(text):
|
|
59
|
+
# Compute the 1-indexed line number so doctor output reads
|
|
60
|
+
# like a compiler error: `foo.json:42:5`. Counts newlines
|
|
61
|
+
# from start of text to the match start.
|
|
62
|
+
line_number = text.count("\n", 0, m.start()) + 1
|
|
63
|
+
matches.append(
|
|
64
|
+
KeyMatch(
|
|
65
|
+
start=m.start(),
|
|
66
|
+
end=m.end(),
|
|
67
|
+
key=m.group(0),
|
|
68
|
+
line_number=line_number,
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
return matches
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def redact_key(key: str) -> str:
|
|
75
|
+
"""Convert `cz_live_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`
|
|
76
|
+
to `cz_live_***aaaaa` (last-5 of the secret payload).
|
|
77
|
+
|
|
78
|
+
The prefix (`cz_live_` / `cz_test_`) is preserved so the mode is
|
|
79
|
+
still readable: ops can tell at a glance which key family this
|
|
80
|
+
is. The last 5 hex chars are kept so two different keys are
|
|
81
|
+
distinguishable in support tickets without exposing enough to
|
|
82
|
+
reconstruct the key (5 hex chars = 20 bits = 1M combinations,
|
|
83
|
+
not enough to brute-force back to the full key but enough for a
|
|
84
|
+
human to disambiguate "is this customer A's key or customer B's key").
|
|
85
|
+
|
|
86
|
+
Returns the input verbatim if it doesn't match the expected shape,
|
|
87
|
+
so caller-side `print(redact_key(maybe_key))` is always safe.
|
|
88
|
+
"""
|
|
89
|
+
stripped = key.strip()
|
|
90
|
+
m = _KEY_PATTERN.fullmatch(stripped)
|
|
91
|
+
if not m:
|
|
92
|
+
return key
|
|
93
|
+
mode = m.group(1) # 'live' or 'test'
|
|
94
|
+
last5 = stripped[-5:] if len(stripped) >= 5 else stripped
|
|
95
|
+
return f"cz_{mode}_***{last5}"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def redact_text(text: str) -> str:
|
|
99
|
+
"""Apply `redact_key` to every `cz_(live|test)_*` match inside an
|
|
100
|
+
arbitrary string.
|
|
101
|
+
|
|
102
|
+
Used to scrub log lines, error messages, and stderr before the SDK
|
|
103
|
+
prints them. Cheaper than wiring per-call-site escapes; safer too
|
|
104
|
+
because the redaction is centralized.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def _replace(m: re.Match[str]) -> str:
|
|
108
|
+
return redact_key(m.group(0))
|
|
109
|
+
|
|
110
|
+
return _KEY_PATTERN.sub(_replace, text)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# Convenience: scan a list of file paths, return a flat list of
|
|
114
|
+
# (path, KeyMatch) pairs for everything found. doctor + migrate both
|
|
115
|
+
# consume this.
|
|
116
|
+
def scan_files(paths: Iterable[str]) -> List[tuple[str, KeyMatch]]:
|
|
117
|
+
"""Open each path, scan for key leaks, return matches with file
|
|
118
|
+
path attached. Missing files / unreadable files are silently
|
|
119
|
+
skipped -- the caller (doctor) handles "did we find the agent
|
|
120
|
+
config at all" via a separate codepath.
|
|
121
|
+
"""
|
|
122
|
+
import pathlib
|
|
123
|
+
|
|
124
|
+
out: List[tuple[str, KeyMatch]] = []
|
|
125
|
+
for p in paths:
|
|
126
|
+
path = pathlib.Path(p).expanduser()
|
|
127
|
+
if not path.exists():
|
|
128
|
+
continue
|
|
129
|
+
try:
|
|
130
|
+
content = path.read_text(encoding="utf-8", errors="replace")
|
|
131
|
+
except OSError:
|
|
132
|
+
continue
|
|
133
|
+
for match in find_key_leaks(content):
|
|
134
|
+
out.append((str(path), match))
|
|
135
|
+
return out
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Centralized Rich console for the controlzero CLI.
|
|
2
|
+
|
|
3
|
+
Tier 0a hotfix piece 6 (#174 / DESIGN.md): every user-facing CLI
|
|
4
|
+
output goes through this singleton so colors, padding, and spinner
|
|
5
|
+
styles stay consistent across `controlzero doctor`, `controlzero
|
|
6
|
+
migrate`, `controlzero install`, and the install-time consent prompt.
|
|
7
|
+
|
|
8
|
+
Color palette pulled straight from DESIGN.md:
|
|
9
|
+
|
|
10
|
+
- accent: #22c55e sage green (logo, active states, success)
|
|
11
|
+
- success: #22c55e same as accent (allowed, healthy)
|
|
12
|
+
- warning: #eab308 yellow (approaching limits, soft errors)
|
|
13
|
+
- error: #ef4444 red (blocked, critical)
|
|
14
|
+
- text-1: #f0f0f3 primary (headings, values)
|
|
15
|
+
- text-2: #8b8b96 secondary (descriptions)
|
|
16
|
+
- text-3: #7a7a86 tertiary (labels)
|
|
17
|
+
- border: rgba(255,255,255,0.06) (rendered as dim grey in terminal)
|
|
18
|
+
|
|
19
|
+
Use the module-level helpers (`success`, `warn`, `error`, `info`,
|
|
20
|
+
`panel`) rather than constructing rich objects in each command. This
|
|
21
|
+
makes the CLI testable: tests can mock the singleton and assert on
|
|
22
|
+
the rendered strings.
|
|
23
|
+
|
|
24
|
+
If the user has NO_COLOR set, rich's automatic detection kicks in
|
|
25
|
+
and we render plain text -- nothing to do here.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from typing import Optional
|
|
31
|
+
|
|
32
|
+
from rich.console import Console
|
|
33
|
+
from rich.panel import Panel
|
|
34
|
+
from rich.style import Style
|
|
35
|
+
from rich.text import Text
|
|
36
|
+
from rich.theme import Theme
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# DESIGN.md color tokens. Names mirror the CSS custom property names
|
|
40
|
+
# so a designer can grep across the repo and find every place a token
|
|
41
|
+
# is consumed.
|
|
42
|
+
_CZ_THEME = Theme({
|
|
43
|
+
"cz.accent": Style(color="#22c55e", bold=False),
|
|
44
|
+
"cz.success": Style(color="#22c55e", bold=True),
|
|
45
|
+
"cz.warning": Style(color="#eab308", bold=True),
|
|
46
|
+
"cz.error": Style(color="#ef4444", bold=True),
|
|
47
|
+
"cz.text1": Style(color="#f0f0f3"),
|
|
48
|
+
"cz.text2": Style(color="#8b8b96"),
|
|
49
|
+
"cz.text3": Style(color="#7a7a86"),
|
|
50
|
+
"cz.text4": Style(color="#5a5a65", dim=True),
|
|
51
|
+
"cz.code": Style(color="#f0f0f3", bgcolor="#222225"),
|
|
52
|
+
# Compound helpers used by panels.
|
|
53
|
+
"cz.heading": Style(color="#f0f0f3", bold=True),
|
|
54
|
+
"cz.dim_border": Style(color="#5a5a65", dim=True),
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Two consoles so callers can route errors to stderr without
|
|
59
|
+
# leaking color escape codes into a redirected stdout.
|
|
60
|
+
_stdout = Console(theme=_CZ_THEME, highlight=False)
|
|
61
|
+
_stderr = Console(theme=_CZ_THEME, highlight=False, stderr=True)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def stdout() -> Console:
|
|
65
|
+
"""The shared stdout-bound console. Use this for normal CLI
|
|
66
|
+
output. Tests can monkeypatch this to capture rendered output."""
|
|
67
|
+
return _stdout
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def stderr() -> Console:
|
|
71
|
+
"""The shared stderr-bound console. Use this for warnings + errors."""
|
|
72
|
+
return _stderr
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# -----------------------------------------------------------------------------
|
|
76
|
+
# Helpers -- the public surface. Every CLI command should use these
|
|
77
|
+
# rather than touching the underlying console.
|
|
78
|
+
# -----------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def success(message: str) -> None:
|
|
82
|
+
"""Bold sage-green check mark + message. Used for happy-path
|
|
83
|
+
completion lines like 'Installed claude-code hook'.
|
|
84
|
+
"""
|
|
85
|
+
_stdout.print(Text.assemble(("[OK] ", "cz.success"), (message, "cz.text1")))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def warn(message: str) -> None:
|
|
89
|
+
"""Yellow warning. Used for soft errors that don't block the
|
|
90
|
+
operation but the user should know about (e.g. 'shell history
|
|
91
|
+
contains a key')."""
|
|
92
|
+
_stderr.print(Text.assemble(("[WARN] ", "cz.warning"), (message, "cz.text1")))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def error(message: str, code: Optional[str] = None) -> None:
|
|
96
|
+
"""Red error. If a stable E#### code is supplied, prepend it so
|
|
97
|
+
the user can search the docs site for the canonical fix."""
|
|
98
|
+
prefix = f"[ERROR {code}] " if code else "[ERROR] "
|
|
99
|
+
_stderr.print(Text.assemble((prefix, "cz.error"), (message, "cz.text1")))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def info(message: str) -> None:
|
|
103
|
+
"""Plain informational line, no color. Used for hints + status
|
|
104
|
+
lines that aren't the headline output."""
|
|
105
|
+
_stdout.print(Text(message, style="cz.text2"))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def heading(message: str) -> None:
|
|
109
|
+
"""Section heading. Bold, primary text color."""
|
|
110
|
+
_stdout.print(Text(message, style="cz.heading"))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def code(snippet: str) -> Text:
|
|
114
|
+
"""Render a short code snippet (filename, command, key) with the
|
|
115
|
+
code style. Returns a Text so callers can compose it into a
|
|
116
|
+
larger renderable."""
|
|
117
|
+
return Text(snippet, style="cz.code")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def panel(content: str, title: str = "", style: str = "cz.dim_border") -> None:
|
|
121
|
+
"""Outlined box with optional title. Used for the consent prompt
|
|
122
|
+
+ the post-install confirmation summary so they don't drown in
|
|
123
|
+
other terminal output."""
|
|
124
|
+
p = Panel.fit(content, title=title or None, border_style=style)
|
|
125
|
+
_stdout.print(p)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""``controlzero debug bundle`` -- inspect SDK-loaded rules + simulate guard.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
the deny-deny incident (T87, GH #392) burned ~3 hours of root-cause
|
|
4
4
|
work because the bundle on disk is encrypted+signed. ``cat`` returns
|
|
5
5
|
nothing legible; operators had to attach a debugger or ship the bundle
|
|
6
6
|
back to engineering. This module gives them a self-serve alternative.
|