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.
- controlzero-1.4.3/.gitignore +239 -0
- controlzero-1.4.3/CHANGELOG.md +33 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/PKG-INFO +10 -1
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/__init__.py +1 -1
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/_internal/bundle.py +34 -3
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/_internal/enforcer.py +33 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/client.py +20 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/anthropic.py +22 -12
- controlzero-1.4.3/controlzero/integrations/autogen.py +144 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/crewai/agent.py +18 -6
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/crewai/crew.py +30 -9
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/crewai/task.py +20 -6
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/crewai/tool.py +28 -7
- controlzero-1.4.3/controlzero/integrations/google.py +178 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/google_adk/agent.py +10 -3
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/google_adk/tool.py +24 -12
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/langchain/__init__.py +2 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/langchain/agent.py +8 -0
- controlzero-1.4.3/controlzero/integrations/langchain/modern.py +51 -0
- controlzero-1.4.3/controlzero/integrations/litellm.py +271 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/openai.py +23 -10
- controlzero-1.4.3/controlzero/integrations/pydantic_ai.py +194 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/pyproject.toml +23 -1
- controlzero-1.4.3/tests/integrations/__init__.py +0 -0
- controlzero-1.4.3/tests/integrations/test_google.py +96 -0
- controlzero-1.4.3/tests/test_action_canonicalization.py +36 -0
- controlzero-1.4.3/tests/test_agent_name_env.py +83 -0
- controlzero-1.4.3/tests/test_conditions.py +202 -0
- controlzero-1.4.0/controlzero/integrations/google.py +0 -184
- controlzero-1.4.0/controlzero/integrations/litellm.py +0 -137
- controlzero-1.4.0/controlzero.2026-04-07_22-05-15_956171.log +0 -2
- controlzero-1.4.0/controlzero.2026-04-08_16-53-22_852394.log +0 -6
- controlzero-1.4.0/controlzero.log +0 -49
- {controlzero-1.4.0 → controlzero-1.4.3}/Dockerfile.test +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/LICENSE +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/README.md +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/_internal/__init__.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/_internal/dlp_scanner.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/_internal/types.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/audit_local.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/audit_remote.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/cli/__init__.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/cli/main.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/cli/templates/autogen.yaml +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/cli/templates/claude-code.yaml +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/cli/templates/codex-cli.yaml +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/cli/templates/cost-cap.yaml +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/cli/templates/crewai.yaml +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/cli/templates/cursor.yaml +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/cli/templates/gemini-cli.yaml +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/cli/templates/generic.yaml +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/cli/templates/langchain.yaml +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/cli/templates/mcp.yaml +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/cli/templates/rag.yaml +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/device.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/enrollment.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/errors.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/hosted_policy.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/__init__.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/braintrust.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/crewai/__init__.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/google_adk/__init__.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/langchain/callbacks.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/langchain/chain.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/langchain/graph.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/langchain/tool.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/langfuse.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/integrations/vercel_ai.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/policy_loader.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/controlzero/tamper.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/examples/hello_world.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/conftest.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_audit_remote.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_audit_sink_isolation.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_bundle_parser.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_cli_hook.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_cli_hosted_refresh.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_cli_init.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_cli_init_templates.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_cli_tail.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_cli_test.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_cli_validate.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_coding_agent_hooks.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_device.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_dlp_scanner.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_enrollment.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_fail_closed_eval.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_glob_matching.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_hosted_policy_e2e.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_hybrid_mode_strict.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_hybrid_mode_warn.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_install_hooks.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_local_mode_dict.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_local_mode_file_json.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_local_mode_file_yaml.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_log_fallback_stderr.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_log_options_ignored_hosted.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_log_rotation.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_no_policy_no_key.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_package_rename_shim.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_policy_freshness.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_policy_settings.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_quarantine.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_tamper.py +0 -0
- {controlzero-1.4.0 → controlzero-1.4.3}/tests/test_tamper_behavior.py +0 -0
- {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.
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
66
|
-
"agent_id": self._agent_id,
|
|
65
|
+
tags: dict[str, Any] = {
|
|
67
66
|
"provider": "anthropic",
|
|
68
|
-
"
|
|
69
|
-
"
|
|
67
|
+
"agent_id": self._agent_id,
|
|
68
|
+
"action_detail": method,
|
|
70
69
|
}
|
|
71
70
|
tools = kwargs.get("tools")
|
|
72
71
|
if tools:
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
79
|
-
method=
|
|
77
|
+
"llm",
|
|
78
|
+
method="generate",
|
|
80
79
|
raise_on_deny=True,
|
|
81
|
-
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
|
|
108
|
-
method="
|
|
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
|
+
)
|