controlzero 1.4.0__tar.gz → 1.4.3__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 (106) hide show
  1. controlzero-1.4.3/.gitignore +239 -0
  2. controlzero-1.4.3/CHANGELOG.md +33 -0
  3. {controlzero-1.4.0 → controlzero-1.4.3}/PKG-INFO +10 -1
  4. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/__init__.py +1 -1
  5. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/_internal/bundle.py +34 -3
  6. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/_internal/enforcer.py +33 -0
  7. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/client.py +20 -0
  8. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/anthropic.py +22 -12
  9. controlzero-1.4.3/controlzero/integrations/autogen.py +144 -0
  10. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/crewai/agent.py +18 -6
  11. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/crewai/crew.py +30 -9
  12. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/crewai/task.py +20 -6
  13. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/crewai/tool.py +28 -7
  14. controlzero-1.4.3/controlzero/integrations/google.py +178 -0
  15. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/google_adk/agent.py +10 -3
  16. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/google_adk/tool.py +24 -12
  17. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/langchain/__init__.py +2 -0
  18. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/langchain/agent.py +8 -0
  19. controlzero-1.4.3/controlzero/integrations/langchain/modern.py +51 -0
  20. controlzero-1.4.3/controlzero/integrations/litellm.py +271 -0
  21. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/openai.py +23 -10
  22. controlzero-1.4.3/controlzero/integrations/pydantic_ai.py +194 -0
  23. {controlzero-1.4.0 → controlzero-1.4.3}/pyproject.toml +23 -1
  24. controlzero-1.4.3/tests/integrations/__init__.py +0 -0
  25. controlzero-1.4.3/tests/integrations/test_google.py +96 -0
  26. controlzero-1.4.3/tests/test_action_canonicalization.py +36 -0
  27. controlzero-1.4.3/tests/test_agent_name_env.py +83 -0
  28. controlzero-1.4.3/tests/test_conditions.py +202 -0
  29. controlzero-1.4.0/controlzero/integrations/google.py +0 -184
  30. controlzero-1.4.0/controlzero/integrations/litellm.py +0 -137
  31. controlzero-1.4.0/controlzero.2026-04-07_22-05-15_956171.log +0 -2
  32. controlzero-1.4.0/controlzero.2026-04-08_16-53-22_852394.log +0 -6
  33. controlzero-1.4.0/controlzero.log +0 -49
  34. {controlzero-1.4.0 → controlzero-1.4.3}/Dockerfile.test +0 -0
  35. {controlzero-1.4.0 → controlzero-1.4.3}/LICENSE +0 -0
  36. {controlzero-1.4.0 → controlzero-1.4.3}/README.md +0 -0
  37. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/_internal/__init__.py +0 -0
  38. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/_internal/dlp_scanner.py +0 -0
  39. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/_internal/types.py +0 -0
  40. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/audit_local.py +0 -0
  41. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/audit_remote.py +0 -0
  42. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/cli/__init__.py +0 -0
  43. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/cli/main.py +0 -0
  44. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/cli/templates/autogen.yaml +0 -0
  45. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/cli/templates/claude-code.yaml +0 -0
  46. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/cli/templates/codex-cli.yaml +0 -0
  47. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/cli/templates/cost-cap.yaml +0 -0
  48. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/cli/templates/crewai.yaml +0 -0
  49. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/cli/templates/cursor.yaml +0 -0
  50. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/cli/templates/gemini-cli.yaml +0 -0
  51. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/cli/templates/generic.yaml +0 -0
  52. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/cli/templates/langchain.yaml +0 -0
  53. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/cli/templates/mcp.yaml +0 -0
  54. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/cli/templates/rag.yaml +0 -0
  55. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/device.py +0 -0
  56. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/enrollment.py +0 -0
  57. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/errors.py +0 -0
  58. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/hosted_policy.py +0 -0
  59. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/__init__.py +0 -0
  60. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/braintrust.py +0 -0
  61. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/crewai/__init__.py +0 -0
  62. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/google_adk/__init__.py +0 -0
  63. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/langchain/callbacks.py +0 -0
  64. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/langchain/chain.py +0 -0
  65. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/langchain/graph.py +0 -0
  66. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/langchain/tool.py +0 -0
  67. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/langfuse.py +0 -0
  68. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/vercel_ai.py +0 -0
  69. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/policy_loader.py +0 -0
  70. {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/tamper.py +0 -0
  71. {controlzero-1.4.0 → controlzero-1.4.3}/examples/hello_world.py +0 -0
  72. {controlzero-1.4.0 → controlzero-1.4.3}/tests/conftest.py +0 -0
  73. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_audit_remote.py +0 -0
  74. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_audit_sink_isolation.py +0 -0
  75. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_bundle_parser.py +0 -0
  76. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_cli_hook.py +0 -0
  77. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_cli_hosted_refresh.py +0 -0
  78. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_cli_init.py +0 -0
  79. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_cli_init_templates.py +0 -0
  80. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_cli_tail.py +0 -0
  81. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_cli_test.py +0 -0
  82. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_cli_validate.py +0 -0
  83. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_coding_agent_hooks.py +0 -0
  84. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_device.py +0 -0
  85. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_dlp_scanner.py +0 -0
  86. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_enrollment.py +0 -0
  87. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_fail_closed_eval.py +0 -0
  88. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_glob_matching.py +0 -0
  89. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_hosted_policy_e2e.py +0 -0
  90. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_hybrid_mode_strict.py +0 -0
  91. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_hybrid_mode_warn.py +0 -0
  92. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_install_hooks.py +0 -0
  93. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_local_mode_dict.py +0 -0
  94. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_local_mode_file_json.py +0 -0
  95. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_local_mode_file_yaml.py +0 -0
  96. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_log_fallback_stderr.py +0 -0
  97. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_log_options_ignored_hosted.py +0 -0
  98. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_log_rotation.py +0 -0
  99. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_no_policy_no_key.py +0 -0
  100. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_package_rename_shim.py +0 -0
  101. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_policy_freshness.py +0 -0
  102. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_policy_settings.py +0 -0
  103. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_quarantine.py +0 -0
  104. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_tamper.py +0 -0
  105. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_tamper_behavior.py +0 -0
  106. {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_tamper_hook.py +0 -0
@@ -0,0 +1,239 @@
1
+ # Worktrees
2
+ .worktrees/
3
+
4
+ # Transient QA / design captures at repo root.
5
+ # Long-term reference screenshots live under docs-site/static/ or in Obsidian.
6
+ /*.png
7
+ /dashboard-*.md
8
+ /uat-*.png
9
+ /audit-*.png
10
+ /billing-*.png
11
+ /sso-*.png
12
+ /notifications-*.png
13
+ /settings-*.png
14
+
15
+ # Dependencies
16
+ node_modules/
17
+ vendor/
18
+
19
+ # Build outputs
20
+ dist/
21
+ build/
22
+ .next/
23
+ out/
24
+ *.egg-info/
25
+ # Go binaries
26
+ apps/control-zero-platform/backend/server
27
+
28
+ # Test & Coverage
29
+ coverage/
30
+ htmlcov/
31
+ .coverage
32
+ *.lcov
33
+ .pytest_cache/
34
+ .vitest/
35
+ test-results/
36
+ playwright-report/
37
+ .playwright/
38
+ __pycache__/
39
+ *.pyc
40
+ *.pyo
41
+
42
+ # Environment and secrets (deny-all, allow explicitly)
43
+ .env*
44
+ !.envrc
45
+ production-secrets*
46
+ vault-backup.key
47
+ *.pem
48
+ *.key
49
+ *.p12
50
+ *.pfx
51
+ id_rsa
52
+ id_ed25519
53
+ *.keystore
54
+ # Firebase credentials and config (contains API keys / service account)
55
+ firebase.js
56
+ firebase.json
57
+ firebase-service-account*.json
58
+ *-firebase-adminsdk-*.json
59
+ firebase-adminsdk-*.json
60
+ serviceAccountKey.json
61
+ cz-service-account.json
62
+ firebase-credentials.json/
63
+ google-services.json
64
+ GoogleService-Info.plist
65
+ !*.pub
66
+ !.env.example
67
+ !.env.local.example
68
+ !.env.production.template
69
+ !.env.production.example
70
+ !.env.test
71
+
72
+ # Explicitly block files that contain or may contain real credentials (override allowlist above)
73
+ .env.dev
74
+ .env.production
75
+ .env.production.complete
76
+ .env.production.local
77
+ .env.local
78
+ .env.*.local
79
+ *.env.real
80
+ *.env.secret
81
+ .env.vercel
82
+
83
+ # IDE & Editors
84
+ .idea/
85
+ .vscode/
86
+ *.swp
87
+ *.swo
88
+ *.sublime-workspace
89
+ *.sublime-project
90
+
91
+ # OS files
92
+ .DS_Store
93
+ Thumbs.db
94
+ *.log
95
+
96
+ # Go
97
+ *.exe
98
+ *.exe~
99
+ *.dll
100
+ *.so
101
+ *.dylib
102
+
103
+ # Docker
104
+ docker-compose.override.yml
105
+ .docker/
106
+
107
+ # Production logs and certificates
108
+ logs/
109
+ letsencrypt/
110
+ *.log
111
+
112
+ # DLP test reports (generated, not committed)
113
+ reports/
114
+
115
+ # MkDocs
116
+ site/
117
+
118
+ # Turbo (if adopted)
119
+ .turbo/
120
+
121
+ # Changeset
122
+ .changeset/*.md
123
+ !.changeset/config.json
124
+ !.changeset/README.md
125
+
126
+ # Vercel
127
+ .vercel/
128
+
129
+ # Rust
130
+ target/
131
+ Cargo.lock
132
+ *.rlib
133
+
134
+ # Python virtual environments
135
+ venv/
136
+ .venv/
137
+ *.egg-info/
138
+
139
+ # Gateway
140
+ apps/control-zero-gateway/.venv/
141
+ apps/control-zero-gateway/__pycache__/
142
+
143
+ # Selenium test outputs
144
+ tests/selenium/screenshots/
145
+ tests/selenium/report.html
146
+
147
+ # Miscellaneous
148
+ *.bak
149
+ *.tmp
150
+ *.temp
151
+ .cache/
152
+ .vercel
153
+ # Internal documentation (sensitive -- do not track)
154
+ docs/internal/
155
+
156
+ # Playwright MCP
157
+ .playwright-mcp/
158
+
159
+ # Session artifacts (prevent future accumulation)
160
+ *_SUMMARY.md
161
+ *_STATUS.md
162
+ *_COMPLETE.md
163
+
164
+ # E2E test screenshots
165
+ e2e-*.png
166
+ cz-*.png
167
+ .superpowers/
168
+
169
+ # AI tooling directories
170
+ .gemini/
171
+ .claude/launch.json
172
+
173
+ # Docusaurus build cache (should not be tracked)
174
+ docs-site/.docusaurus/
175
+
176
+ # Test/governance tester scripts (generated during testing sessions)
177
+ *_tester.py
178
+ verify_and_screenshot.js
179
+
180
+ # HTML reports generated during testing sessions
181
+ *_AUDIT.html
182
+ *_REPORT.html
183
+ *_ANALYSIS.html
184
+ e2e-report.html
185
+ sit-uat-report.html
186
+ UAT_COMPREHENSIVE_REPORT.html
187
+ uat-screenshots/
188
+
189
+ # Offensive summary reports
190
+ SUMMARY_OFFENSIVE_*.md
191
+ .gstack/
192
+
193
+ # Deployment docs (contain server-specific credentials)
194
+ docs/deployment/
195
+ docs/SSH_RECOVERY_GUIDE.md
196
+ docs/HETZNER_OS_INSTALLATION.md
197
+ docs/BACKUP_RECOVERY.md
198
+ docs/VERCEL_SETUP_WEBAPPS.md
199
+ scripts/fix-ssh-config.sh
200
+ scripts/hetzner-post-install.sh
201
+ docs/knowledge-base/
202
+ docs/superpowers/
203
+ # Air-gap build artifacts (generated tarballs and package directories)
204
+ cz-air-gap-*/
205
+ cz-air-gap-*.tar.gz
206
+ cz-support-*.tar.gz
207
+
208
+ # SSL Proxy -- customer CA certs and generated certs (never track secrets)
209
+ services/ssl-proxy/certs/
210
+
211
+ # UAT screenshot artifacts
212
+ uat-*.png
213
+
214
+ # Stray HTML reports in docs/. These are large exported artifacts
215
+ # (SIT/UAT reports, factsheet, launch readiness) that accumulate
216
+ # untracked and risk being swept up by `git add .`. The
217
+ # sit-uat-report-2026-03-22.html in particular is 128MB and would
218
+ # explode the repo. Real docs go under docs/knowledge-base/ or
219
+ # docs/designs/ instead.
220
+ docs/sit-uat-report-*.html
221
+ docs/launch-readiness-report-*.html
222
+ docs/control-zero-factsheet.html
223
+ docs/control-zero-review.html
224
+
225
+ # Reference screenshots (keep local, not in repo)
226
+ agentdefenders-full.png
227
+ cz-revamp-live.png
228
+
229
+ # Agent worktrees (created by Claude Code during parallel work)
230
+ .claude/worktrees/
231
+
232
+ # direnv + sops secrets. Encrypted files under secrets/ are safe to
233
+ # commit; anything matching *.plain or *.dec is a decryption artifact
234
+ # and must NEVER land in git.
235
+ .envrc.local
236
+ .direnv/
237
+ secrets/*.plain
238
+ secrets/*.dec
239
+ .claude/scheduled_tasks.lock
@@ -0,0 +1,33 @@
1
+ # Changelog
2
+
3
+ ## 1.4.1 -- 2026-04-15
4
+
5
+ ### Added
6
+
7
+ - `agent_name` arg + `CZ_AGENT_NAME` env var contract on the `Client` constructor (issue #71). Order: explicit arg > env > `default-agent`.
8
+ - `CZ_DEBUG=1` (or `true`/`yes`/`on`) flips the controlzero logger to DEBUG at construction. Cheap escape hatch for support.
9
+ - Optional install extras: `controlzero[google]`, `controlzero[openai]`, `controlzero[anthropic]` (issue #68). Pin the matching upstream SDK so users do not see import errors.
10
+ - Cross-SDK action canonicalization parity test (issue #69). Mirrors the same fixture in the Node and Go SDKs.
11
+ - Smoke + denial + error-propagation tests for the `wrap_google` integration (issue #68).
12
+
13
+ ## 1.4.0 -- 2026-04-15
14
+
15
+ ### Breaking changes
16
+
17
+ - Integration wrappers now emit simplified action names (`llm:generate`, `embedding:generate`, `tool:call`) instead of provider-prefixed ones (`llm:openai:chat.completions.create`). Policies targeting the old action names must be updated. Provider and model move into `context` tags. See docs/integrations for current patterns.
18
+ - Google integration rewritten against `google-genai` (deprecated `google.generativeai` removed). Install `google-genai`.
19
+ - `integrations.langchain.agent.GovernedAgent` wrapping `AgentExecutor` is deprecated in LangChain v1.x. Use `integrations.langchain.modern.create_governed_agent`.
20
+
21
+ ### New integrations
22
+
23
+ - `integrations.autogen` - first-party helper for autogen-agentchat v0.7+
24
+ - `integrations.pydantic_ai` - first-party helper for pydantic-ai v1.x
25
+ - `integrations.langchain.modern` - LangGraph create_agent pattern
26
+
27
+ ### New features
28
+
29
+ - Policy rule `conditions` field is now evaluated in the local enforcer. Conditions are matched against merged context + args with glob patterns. All keys must match.
30
+
31
+ ### Enhancements
32
+
33
+ - `integrations.litellm` - async + success/failure hooks for streaming audit.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: controlzero
3
- Version: 1.4.0
3
+ Version: 1.4.3
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
@@ -23,9 +23,14 @@ Classifier: Topic :: Security
23
23
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
24
  Requires-Python: >=3.9
25
25
  Requires-Dist: click>=8.1.0
26
+ Requires-Dist: cryptography>=41.0.0
27
+ Requires-Dist: httpx>=0.25.0
26
28
  Requires-Dist: loguru>=0.7.0
27
29
  Requires-Dist: pydantic>=2.0.0
28
30
  Requires-Dist: pyyaml>=6.0
31
+ Requires-Dist: zstandard>=0.22.0
32
+ Provides-Extra: anthropic
33
+ Requires-Dist: anthropic>=0.25.0; extra == 'anthropic'
29
34
  Provides-Extra: dev
30
35
  Requires-Dist: cryptography>=41.0.0; extra == 'dev'
31
36
  Requires-Dist: httpx>=0.25.0; extra == 'dev'
@@ -35,10 +40,14 @@ Requires-Dist: pytest>=7.0.0; extra == 'dev'
35
40
  Requires-Dist: pyyaml>=6.0; extra == 'dev'
36
41
  Requires-Dist: respx>=0.20.0; extra == 'dev'
37
42
  Requires-Dist: zstandard>=0.22.0; extra == 'dev'
43
+ Provides-Extra: google
44
+ Requires-Dist: google-genai>=0.3.0; extra == 'google'
38
45
  Provides-Extra: hosted
39
46
  Requires-Dist: cryptography>=41.0.0; extra == 'hosted'
40
47
  Requires-Dist: httpx>=0.25.0; extra == 'hosted'
41
48
  Requires-Dist: zstandard>=0.22.0; extra == 'hosted'
49
+ Provides-Extra: openai
50
+ Requires-Dist: openai>=1.0.0; extra == 'openai'
42
51
  Description-Content-Type: text/markdown
43
52
 
44
53
  # control-zero
@@ -28,7 +28,7 @@ from controlzero.errors import (
28
28
  )
29
29
  from controlzero.policy_loader import load_policy
30
30
 
31
- __version__ = "1.3.0"
31
+ __version__ = "1.4.3"
32
32
 
33
33
  __all__ = [
34
34
  "Client",
@@ -299,16 +299,47 @@ def _decrypt_aes_gcm(key: bytes, encrypted: bytes) -> bytes:
299
299
 
300
300
 
301
301
  def _zstd_decompress(data: bytes) -> bytes:
302
- """Zstd decompress. Tries multiple Python bindings for portability."""
302
+ """Zstd decompress. Tries multiple Python bindings for portability.
303
+
304
+ Some backend builds produce zstd frames without the frame content
305
+ size in the header (klauspost/compress EncodeAll variants). In that
306
+ case `zstd.ZstdDecompressor().decompress(data)` raises:
307
+
308
+ zstandard.backend_c.ZstdError:
309
+ could not determine content size in frame header
310
+
311
+ To decompress regardless of whether the size is declared, we fall
312
+ back to the streaming API with a hard upper bound of 16 MiB --
313
+ matching MAX_BUNDLE_BYTES enforced one layer up. This is the
314
+ standard robust-decompression pattern recommended by the zstandard
315
+ maintainers for untrusted / variable-source input. #113.
316
+ """
317
+ # 16 MiB upper bound on decompressed size; matches
318
+ # hosted_policy.MAX_BUNDLE_BYTES. Kept inline to avoid an import
319
+ # cycle between _internal and hosted_policy.
320
+ _MAX_DECOMPRESSED = 16 * 1024 * 1024
321
+
303
322
  # Preferred: `zstandard` is the de-facto PyPI package.
304
323
  try:
324
+ import io
305
325
  import zstandard as zstd
306
326
 
307
- return zstd.ZstdDecompressor().decompress(data)
327
+ dctx = zstd.ZstdDecompressor()
328
+ # Streaming read works for frames that lack the declared
329
+ # content size. Cap the output to prevent a zip-bomb class
330
+ # of attack on a tampered bundle.
331
+ with dctx.stream_reader(io.BytesIO(data)) as reader:
332
+ out = reader.read(_MAX_DECOMPRESSED + 1)
333
+ if len(out) > _MAX_DECOMPRESSED:
334
+ raise BundleVerificationError(
335
+ f"zstd decompressed payload exceeds {_MAX_DECOMPRESSED} byte limit"
336
+ )
337
+ return out
308
338
  except ImportError:
309
339
  pass
310
340
 
311
- # Fallback: `pyzstd`.
341
+ # Fallback: `pyzstd`. Its `decompress()` handles missing content
342
+ # size natively without needing streaming mode.
312
343
  try:
313
344
  import pyzstd
314
345
 
@@ -138,6 +138,8 @@ class PolicyEvaluator:
138
138
  if rule.resources:
139
139
  if not resource or not _glob_any(rule.resources, resource):
140
140
  continue
141
+ if not self._conditions_match(rule.conditions, context, args):
142
+ continue
141
143
  decision = PolicyDecision(
142
144
  effect=rule.effect,
143
145
  policy_id=rule.id or rule.name or None,
@@ -158,6 +160,37 @@ class PolicyEvaluator:
158
160
  evaluated_rules=evaluated,
159
161
  )
160
162
 
163
+ def _conditions_match(
164
+ self,
165
+ rule_conditions: Optional[dict],
166
+ context: Optional[dict],
167
+ args: Optional[dict],
168
+ ) -> bool:
169
+ """Match rule.conditions against merged context+args using fnmatch globs.
170
+
171
+ All condition keys must match. Missing key = no match. Context keys
172
+ override args on collision. Nested ``context['tags']`` dict is flattened.
173
+ """
174
+ if not rule_conditions:
175
+ return True
176
+ import fnmatch as _fn
177
+
178
+ merged: dict = {}
179
+ if args:
180
+ merged.update(args)
181
+ if context:
182
+ merged.update(context)
183
+ tags = context.get("tags") or {}
184
+ if isinstance(tags, dict):
185
+ merged.update(tags)
186
+ for key, pattern in rule_conditions.items():
187
+ value = merged.get(key)
188
+ if value is None:
189
+ return False
190
+ if not _fn.fnmatchcase(str(value), str(pattern)):
191
+ return False
192
+ return True
193
+
161
194
  def _apply_dlp_scan(
162
195
  self, decision: PolicyDecision, args: dict
163
196
  ) -> PolicyDecision:
@@ -83,6 +83,7 @@ class Client:
83
83
  log_retention: str = "30 days",
84
84
  log_compression: Optional[str] = None,
85
85
  log_format: str = "json",
86
+ agent_name: Optional[str] = None,
86
87
  ):
87
88
  if policy is not None and policy_file is not None:
88
89
  raise ValueError(
@@ -92,6 +93,20 @@ class Client:
92
93
  # Resolve API key from arg or env
93
94
  self._api_key = api_key or os.environ.get("CONTROLZERO_API_KEY")
94
95
 
96
+ # Resolve agent identity. Used to tag audit events so multi-agent
97
+ # systems can attribute calls. Order: explicit arg > CZ_AGENT_NAME
98
+ # env > "default-agent".
99
+ self._agent_name = (
100
+ agent_name or os.environ.get("CZ_AGENT_NAME") or "default-agent"
101
+ )
102
+
103
+ # CZ_DEBUG=1 (or true/yes/on) flips the controlzero logger to DEBUG
104
+ # at construction time. Cheap escape hatch when a user reports
105
+ # weird behavior and we want them to send us a verbose log.
106
+ if os.environ.get("CZ_DEBUG", "").lower() in ("1", "true", "yes", "on"):
107
+ import logging as _logging
108
+ _logging.getLogger("controlzero").setLevel(_logging.DEBUG)
109
+
95
110
  # Resolve local policy source
96
111
  local_source = self._resolve_local_source(policy, policy_file)
97
112
 
@@ -215,6 +230,11 @@ class Client:
215
230
 
216
231
  # ---------------- public API ----------------
217
232
 
233
+ @property
234
+ def agent_name(self) -> str:
235
+ """The agent identity attached to audit events for this client."""
236
+ return self._agent_name
237
+
218
238
  def guard(
219
239
  self,
220
240
  tool: str,
@@ -62,29 +62,32 @@ class _WrappedMessages:
62
62
  def _enforce_model(self, kwargs: dict, method: str) -> None:
63
63
  """Enforce policy for a messages API call."""
64
64
  model = kwargs.get("model", "unknown")
65
- context: dict[str, Any] = {
66
- "agent_id": self._agent_id,
65
+ tags: dict[str, Any] = {
67
66
  "provider": "anthropic",
68
- "method": method,
69
- "resource": f"model/{model}",
67
+ "agent_id": self._agent_id,
68
+ "action_detail": method,
70
69
  }
71
70
  tools = kwargs.get("tools")
72
71
  if tools:
73
- context["tool_count"] = len(tools)
74
- context["tool_names"] = [
72
+ tags["tool_count"] = len(tools)
73
+ tags["tool_names"] = [
75
74
  t.get("name", "") for t in tools if isinstance(t, dict)
76
75
  ]
77
76
  self._cz.guard(
78
- tool="llm:anthropic",
79
- method=method,
77
+ "llm",
78
+ method="generate",
80
79
  raise_on_deny=True,
81
- context=context,
80
+ context={
81
+ "resource": f"model/{model}",
82
+ "tags": tags,
83
+ },
82
84
  )
83
85
 
84
86
  def create(self, **kwargs: Any) -> Any:
85
87
  """Enforce policy then delegate to the original create()."""
86
88
  model = kwargs.get("model", "unknown")
87
- self._enforce_model(kwargs, "messages.create")
89
+ self._enforce_model(kwargs, "messages.create") # action_detail tag
90
+
88
91
  start = time.perf_counter()
89
92
  response = self._inner.create(**kwargs)
90
93
  latency_ms = int((time.perf_counter() - start) * 1000)
@@ -104,8 +107,8 @@ class _WrappedMessages:
104
107
  reason="guard passed",
105
108
  )
106
109
  self._cz._audit_decision(
107
- tool="llm:anthropic",
108
- method="messages.create",
110
+ tool="llm",
111
+ method="generate",
109
112
  args={
110
113
  "model": model,
111
114
  "input_tokens": input_tokens,
@@ -113,6 +116,13 @@ class _WrappedMessages:
113
116
  "latency_ms": latency_ms,
114
117
  },
115
118
  decision=decision,
119
+ context={
120
+ "tags": {
121
+ "provider": "anthropic",
122
+ "agent_id": self._agent_id,
123
+ "action_detail": "messages.create",
124
+ },
125
+ },
116
126
  )
117
127
  except Exception:
118
128
  pass
@@ -0,0 +1,144 @@
1
+ """Control Zero integration for AutoGen v0.4+ (autogen-agentchat).
2
+
3
+ Compatible with autogen-agentchat >=0.7.0, tested against 0.7.5.
4
+
5
+ Provides a ``governed_tool`` decorator for standalone AutoGen tool
6
+ functions, and a ``GovernedAssistantAgent`` subclass that auto-wraps
7
+ every tool passed into it.
8
+
9
+ Usage::
10
+
11
+ from autogen_agentchat.agents import AssistantAgent
12
+ from controlzero import Client
13
+ from controlzero.integrations.autogen import (
14
+ governed_tool,
15
+ GovernedAssistantAgent,
16
+ )
17
+
18
+ cz = Client(api_key="cz_live_...")
19
+
20
+ @governed_tool(cz, "search_web", agent_id="researcher")
21
+ async def search_web(query: str) -> str:
22
+ return do_search(query)
23
+
24
+ agent = GovernedAssistantAgent(
25
+ name="researcher",
26
+ model_client=model_client,
27
+ tools=[search_web],
28
+ client=cz,
29
+ agent_id="researcher",
30
+ )
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import functools
36
+ from typing import Any, Callable, Iterable, Optional
37
+
38
+ from controlzero.client import Client
39
+
40
+
41
+ def governed_tool(
42
+ cz: Client,
43
+ tool_name: str,
44
+ method: str = "call",
45
+ agent_id: str = "",
46
+ ) -> Callable:
47
+ """Decorate an async tool function with Control Zero policy enforcement.
48
+
49
+ Args:
50
+ cz: The ``controlzero.Client`` instance.
51
+ tool_name: Logical tool name used for the guard action.
52
+ method: Method name for the guard action. Defaults to ``"call"``.
53
+ agent_id: Optional tag on the audit entry.
54
+
55
+ Returns:
56
+ A decorator that wraps an async function. On invocation the wrapped
57
+ function calls ``cz.guard(...)`` with ``raise_on_deny=True`` and then
58
+ awaits the original function.
59
+ """
60
+ def decorator(fn: Callable) -> Callable:
61
+ @functools.wraps(fn)
62
+ async def wrapped(*args: Any, **kwargs: Any) -> Any:
63
+ cz.guard(
64
+ tool_name,
65
+ method=method,
66
+ args={"args": list(args), "kwargs": kwargs},
67
+ context={
68
+ "tags": {
69
+ "agent_id": agent_id,
70
+ "framework": "autogen",
71
+ },
72
+ },
73
+ raise_on_deny=True,
74
+ )
75
+ return await fn(*args, **kwargs)
76
+
77
+ return wrapped
78
+
79
+ return decorator
80
+
81
+
82
+ class GovernedAssistantAgent:
83
+ """Subclass of ``autogen_agentchat.agents.AssistantAgent`` that auto-wraps tools.
84
+
85
+ Every tool passed via ``tools=[...]`` is transparently wrapped in a
86
+ ``governed_tool`` decorator so the policy is evaluated before the tool
87
+ runs.
88
+
89
+ The import of ``autogen_agentchat`` is deferred to instantiation time so
90
+ that merely importing this module does not require the optional
91
+ dependency.
92
+ """
93
+
94
+ def __new__(
95
+ cls,
96
+ name: str,
97
+ model_client: Any,
98
+ tools: Optional[Iterable[Callable]] = None,
99
+ client: Optional[Client] = None,
100
+ agent_id: str = "",
101
+ **kwargs: Any,
102
+ ) -> Any:
103
+ try:
104
+ from autogen_agentchat.agents import AssistantAgent
105
+ except ImportError as exc:
106
+ raise ImportError(
107
+ "The 'autogen-agentchat' package is required for this integration. "
108
+ "Install it with: pip install autogen-agentchat"
109
+ ) from exc
110
+
111
+ if client is None:
112
+ raise ValueError("GovernedAssistantAgent requires a 'client' argument.")
113
+
114
+ wrapped_tools: list[Callable] = []
115
+ if tools:
116
+ for tool in tools:
117
+ tool_name = getattr(tool, "__name__", None) or getattr(
118
+ tool, "name", "tool"
119
+ )
120
+ # Only wrap async callables; if something is already wrapped
121
+ # (a decorator already applied), it is idempotent because
122
+ # guard() is cheap.
123
+ wrapped = governed_tool(
124
+ client,
125
+ tool_name=tool_name,
126
+ method="call",
127
+ agent_id=agent_id,
128
+ )(tool)
129
+ # Preserve name/description attributes AutoGen might inspect.
130
+ for attr in ("name", "description"):
131
+ if hasattr(tool, attr):
132
+ try:
133
+ setattr(wrapped, attr, getattr(tool, attr))
134
+ except Exception:
135
+ pass
136
+ wrapped_tools.append(wrapped)
137
+
138
+ # Instantiate the real AssistantAgent with the wrapped tools.
139
+ return AssistantAgent(
140
+ name=name,
141
+ model_client=model_client,
142
+ tools=wrapped_tools or None,
143
+ **kwargs,
144
+ )