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.
Files changed (150) hide show
  1. {controlzero-1.5.5a1 → controlzero-1.5.7}/CHANGELOG.md +94 -5
  2. {controlzero-1.5.5a1 → controlzero-1.5.7}/PKG-INFO +2 -1
  3. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/__init__.py +1 -1
  4. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/_internal/bundle.py +1 -1
  5. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/_internal/enforcer.py +4 -5
  6. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/_internal/types.py +1 -1
  7. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/audit_remote.py +41 -2
  8. controlzero-1.5.7/controlzero/cli/_secrets.py +135 -0
  9. controlzero-1.5.7/controlzero/cli/console.py +125 -0
  10. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/debug_bundle.py +1 -1
  11. controlzero-1.5.7/controlzero/cli/doctor.py +309 -0
  12. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/hosts/__init__.py +3 -3
  13. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/hosts/base.py +6 -7
  14. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/hosts/claude_code.py +1 -1
  15. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/hosts/gemini_cli.py +1 -1
  16. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/hosts/unknown.py +2 -3
  17. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/main.py +40 -12
  18. controlzero-1.5.7/controlzero/cli/migrate.py +200 -0
  19. controlzero-1.5.7/controlzero/cli/telemetry_consent.py +219 -0
  20. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/client.py +3 -3
  21. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/device.py +2 -2
  22. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/enrollment.py +5 -7
  23. controlzero-1.5.7/controlzero/error_codes.py +415 -0
  24. controlzero-1.5.7/controlzero/errors.py +181 -0
  25. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/hosted_policy.py +5 -5
  26. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/braintrust.py +0 -1
  27. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/crewai/agent.py +2 -2
  28. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/crewai/task.py +1 -1
  29. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/crewai/tool.py +2 -2
  30. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/google_adk/agent.py +1 -1
  31. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/langchain/agent.py +7 -4
  32. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/langchain/callbacks.py +1 -1
  33. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/langchain/graph.py +1 -1
  34. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/langchain/tool.py +2 -3
  35. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/langfuse.py +0 -1
  36. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/layout_migration.py +1 -1
  37. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/policy_loader.py +1 -1
  38. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/tamper.py +7 -3
  39. {controlzero-1.5.5a1 → controlzero-1.5.7}/pyproject.toml +6 -1
  40. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/conftest.py +0 -2
  41. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_api_key_mask.py +5 -5
  42. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_audit_remote.py +8 -12
  43. controlzero-1.5.7/tests/test_audit_remote_sdk_version.py +145 -0
  44. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_bundle_translate.py +1 -2
  45. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_cli_carve_out.py +10 -11
  46. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_cli_debug_bundle.py +0 -2
  47. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_cli_extractor_integration.py +3 -3
  48. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_cli_hook.py +1 -1
  49. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_cli_hosted_refresh.py +1 -1
  50. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_coding_agent_hooks.py +0 -2
  51. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_conditions.py +0 -1
  52. controlzero-1.5.7/tests/test_console.py +87 -0
  53. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_default_action.py +1 -2
  54. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_device.py +1 -1
  55. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_dlp_scanner.py +1 -3
  56. controlzero-1.5.7/tests/test_doctor.py +150 -0
  57. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_enrollment.py +0 -3
  58. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_env_dump_438.py +3 -4
  59. controlzero-1.5.7/tests/test_error_codes.py +64 -0
  60. controlzero-1.5.7/tests/test_errors_e_codes.py +209 -0
  61. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_glob_matching.py +3 -3
  62. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_hosted_policy_e2e.py +0 -2
  63. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_hosts_adapter.py +2 -2
  64. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_hybrid_mode_warn.py +0 -2
  65. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_install_hook_command.py +3 -3
  66. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_install_hooks.py +2 -3
  67. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_layout_migration_t101.py +0 -1
  68. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_layout_parity_t102.py +0 -1
  69. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_log_fallback_stderr.py +0 -1
  70. controlzero-1.5.7/tests/test_migrate.py +128 -0
  71. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_package_rename_shim.py +0 -5
  72. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_policy_freshness.py +0 -3
  73. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_policy_settings.py +0 -2
  74. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_quarantine.py +1 -2
  75. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_reason_code.py +3 -3
  76. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_refresh.py +11 -9
  77. controlzero-1.5.7/tests/test_secrets.py +227 -0
  78. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_sql_semantic_class.py +1 -1
  79. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_synthetic_policy_id_t79.py +1 -1
  80. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_t103_precedence.py +4 -6
  81. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_t104_cache_gc.py +23 -23
  82. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_t108_local_override_audit.py +0 -1
  83. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_t96_single_audit_log.py +0 -1
  84. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_t99_install_prefetch_bundle.py +0 -1
  85. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_tamper_behavior.py +0 -4
  86. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_tamper_hook.py +0 -2
  87. controlzero-1.5.7/tests/test_telemetry_consent.py +90 -0
  88. controlzero-1.5.5a1/controlzero/errors.py +0 -85
  89. {controlzero-1.5.5a1 → controlzero-1.5.7}/.gitignore +0 -0
  90. {controlzero-1.5.5a1 → controlzero-1.5.7}/Dockerfile.test +0 -0
  91. {controlzero-1.5.5a1 → controlzero-1.5.7}/LICENSE +0 -0
  92. {controlzero-1.5.5a1 → controlzero-1.5.7}/README.md +0 -0
  93. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/_internal/__init__.py +0 -0
  94. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/_internal/action_aliases.py +0 -0
  95. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/_internal/dlp_scanner.py +0 -0
  96. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/_internal/hook_extractors.py +0 -0
  97. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/_internal/tool_extractors.json +0 -0
  98. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/audit_local.py +0 -0
  99. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/__init__.py +0 -0
  100. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/hosts/codex_cli.py +0 -0
  101. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/templates/autogen.yaml +0 -0
  102. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/templates/claude-code.yaml +0 -0
  103. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/templates/codex-cli.yaml +0 -0
  104. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/templates/cost-cap.yaml +0 -0
  105. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/templates/crewai.yaml +0 -0
  106. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/templates/cursor.yaml +0 -0
  107. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/templates/gemini-cli.yaml +0 -0
  108. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/templates/generic.yaml +0 -0
  109. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/templates/langchain.yaml +0 -0
  110. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/templates/mcp.yaml +0 -0
  111. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/cli/templates/rag.yaml +0 -0
  112. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/__init__.py +0 -0
  113. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/anthropic.py +0 -0
  114. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/autogen.py +0 -0
  115. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/crewai/__init__.py +0 -0
  116. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/crewai/crew.py +0 -0
  117. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/google.py +0 -0
  118. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/google_adk/__init__.py +0 -0
  119. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/google_adk/tool.py +0 -0
  120. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/langchain/__init__.py +0 -0
  121. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/langchain/chain.py +0 -0
  122. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/langchain/modern.py +0 -0
  123. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/litellm.py +0 -0
  124. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/openai.py +0 -0
  125. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/pydantic_ai.py +0 -0
  126. {controlzero-1.5.5a1 → controlzero-1.5.7}/controlzero/integrations/vercel_ai.py +0 -0
  127. {controlzero-1.5.5a1 → controlzero-1.5.7}/examples/hello_world.py +0 -0
  128. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/integrations/__init__.py +0 -0
  129. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/integrations/test_google.py +0 -0
  130. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/parity/action_aliases.json +0 -0
  131. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_action_aliases.py +0 -0
  132. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_action_canonicalization.py +0 -0
  133. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_agent_name_env.py +0 -0
  134. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_audit_sink_isolation.py +0 -0
  135. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_bundle_parser.py +0 -0
  136. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_cli_init.py +0 -0
  137. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_cli_init_templates.py +0 -0
  138. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_cli_tail.py +0 -0
  139. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_cli_test.py +0 -0
  140. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_cli_validate.py +0 -0
  141. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_fail_closed_eval.py +0 -0
  142. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_hook_extractors.py +0 -0
  143. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_hybrid_mode_strict.py +0 -0
  144. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_local_mode_dict.py +0 -0
  145. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_local_mode_file_json.py +0 -0
  146. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_local_mode_file_yaml.py +0 -0
  147. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_log_options_ignored_hosted.py +0 -0
  148. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_log_rotation.py +0 -0
  149. {controlzero-1.5.5a1 → controlzero-1.5.7}/tests/test_no_policy_no_key.py +0 -0
  150. {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 CloudShift incident -- a customer can now run a single
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). CloudShift
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
- CloudShift / kh.lee reported on 2026-05-12 that Claude Code on
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
- (Bryan's deny-deny took ~3 hours to root-cause for exactly this
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, Bryan deny-deny postmortem).
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.5a1
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'
@@ -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.5a1"
31
+ __version__ = "1.5.7"
32
32
 
33
33
  __all__ = [
34
34
  "Client",
@@ -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, Bryan's P0): the previous
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 Any, Optional
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 Bryan's P0: the SDK fail-closed with "No
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 / Bryan deny-deny postmortem,
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 (Bryan deny-deny incident, 2026-05-10).
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.
@@ -1,6 +1,6 @@
1
1
  """Internal type definitions. Public users should import from controlzero, not here."""
2
2
 
3
- from typing import Any, Optional
3
+ from typing import Any
4
4
  from pydantic import BaseModel, Field
5
5
 
6
6
 
@@ -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["Path"] = None,
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
- Bryan's deny-deny incident (T87, GH #392) burned ~3 hours of root-cause
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.