controlzero 1.13.12__tar.gz → 1.13.14__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.13.12 → controlzero-1.13.14}/CHANGELOG.md +88 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/PKG-INFO +1 -1
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/__init__.py +1 -1
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/_internal/action_aliases.py +68 -1
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/_internal/enforcer.py +34 -14
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/client.py +24 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/tamper.py +26 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/pyproject.toml +1 -1
- {controlzero-1.13.12 → controlzero-1.13.14}/.gitignore +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/LICENSE +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/README.md +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/__main__.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/_internal/__init__.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/_internal/_messages.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/_internal/action_subject.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/_internal/action_validator.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/_internal/bundle.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/_internal/condition_ops.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/_internal/credential_hook.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/_internal/credential_scanner.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/_internal/credentials_data/__init__.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/_internal/credentials_data/built_in.yaml +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/_internal/dlp_output_observer.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/_internal/dlp_remediation.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/_internal/dlp_scanner.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/_internal/enforcement_outcome.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/_internal/hook_extractors.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/_internal/io_capture_producer.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/_internal/source_labels.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/_internal/tool_extractors.json +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/_internal/types.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/audit_local.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/audit_remote.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/canonical.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/__init__.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/__main__.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/_secrets.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/agent_config.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/console.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/debug_bundle.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/doctor.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/enforcement.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/enforcement_verdict.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/exec_cmd.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/hook_entry.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/hosts/__init__.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/hosts/antigravity.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/hosts/base.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/hosts/claims.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/hosts/claude_code.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/hosts/codex_cli.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/hosts/cursor.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/hosts/cursor_cli.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/hosts/gemini_cli.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/hosts/kiro.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/hosts/unknown.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/kiro_adapter.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/main.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/maturity.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/migrate.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/output.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/setup_wizard.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/spool_cmd.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/telemetry_consent.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/templates/antigravity/hooks.json +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/templates/antigravity.yaml +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/templates/autogen.yaml +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/templates/claude-code.yaml +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/templates/codex-cli.yaml +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/templates/cost-cap.yaml +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/templates/crewai.yaml +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/templates/cursor.yaml +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/templates/gemini-cli.yaml +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/templates/generic.yaml +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/templates/kiro/ide-pre-tool-use.kiro.hook +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/templates/kiro/ide-prompt-submit.kiro.hook +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/templates/kiro/kiro.yaml +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/templates/langchain.yaml +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/templates/mcp.yaml +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/templates/rag.yaml +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/update_cmd.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/device.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/enrollment.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/error_codes.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/error_codes.yaml +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/errors.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/hitl/__init__.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/hitl/grant_protocol.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/hitl/mock.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/hitl/pending_approval.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/hitl/secret_leak_guard.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/hitl/status.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/hook_integrity.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/hooks/__init__.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/hooks/tool_output_handler.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/host_invocation.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/hosted_policy.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/integrations/__init__.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/integrations/_export_dlp.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/integrations/_mask_apply.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/integrations/anthropic.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/integrations/autogen.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/integrations/braintrust.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/integrations/crewai/__init__.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/integrations/crewai/agent.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/integrations/crewai/crew.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/integrations/crewai/task.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/integrations/crewai/tool.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/integrations/google.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/integrations/google_adk/__init__.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/integrations/google_adk/agent.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/integrations/google_adk/tool.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/integrations/langchain/__init__.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/integrations/langchain/agent.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/integrations/langchain/callbacks.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/integrations/langchain/chain.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/integrations/langchain/graph.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/integrations/langchain/modern.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/integrations/langchain/tool.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/integrations/langfuse.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/integrations/litellm.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/integrations/openai.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/integrations/pydantic_ai.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/integrations/vercel_ai.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/layout_migration.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/policy_loader.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/secrets/__init__.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/secrets/reference.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/secrets/resolver.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/secrets/secretstr.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/spool/__init__.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/spool/_compress.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/spool/_constants.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/spool/_crc32c.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/spool/_crypto.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/spool/_frame.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/spool/_keyring.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/spool/_metrics.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/spool/_spool.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/spool/_state.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/spool/_uploader.py +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/spool/cz-audit-v1.dict +0 -0
- {controlzero-1.13.12 → controlzero-1.13.14}/controlzero/tracecontext.py +0 -0
|
@@ -2,6 +2,94 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 1.13.14 (2026-08-17): a tampered policy file silently flipped deny to allow
|
|
6
|
+
|
|
7
|
+
**Security fix. If you sign your policy and rely on `tamper_behavior`, read this.**
|
|
8
|
+
|
|
9
|
+
Reproduced against the published 1.13.13 wheel:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
signed policy -> some_tool: deny (correct)
|
|
13
|
+
TAMPERED policy -> some_tool: allow (tamper_behavior: deny was set)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Anyone with write access to the policy file could turn a `deny` into an
|
|
17
|
+
`allow`, and `guard()` obeyed. The `.sig` sidecar sat alongside it, no longer
|
|
18
|
+
matching, and nothing read it.
|
|
19
|
+
|
|
20
|
+
`Client(policy=...).guard()` never verified the file's HMAC. Every piece of the
|
|
21
|
+
tamper machinery existed and worked -- `load_signed_file` correctly rejects a
|
|
22
|
+
mutated envelope, and `guard()` correctly fails closed when told tampering
|
|
23
|
+
occurred -- but nothing connected the two. The tests for each half passed the
|
|
24
|
+
entire time, because each tested its own half.
|
|
25
|
+
|
|
26
|
+
Fixed: `guard()` now discovers tampering from the signed file itself on every
|
|
27
|
+
evaluation and merges the result into a private copy of the context, so a
|
|
28
|
+
caller cannot assert its own tamper state and enforcement and audit consume the
|
|
29
|
+
same fact.
|
|
30
|
+
|
|
31
|
+
**Who is affected:** callers who signed their policy with `controlzero
|
|
32
|
+
sign-policy` and set `tamper_behavior`. An unsigned policy has no signature to
|
|
33
|
+
verify and is unaffected -- that configuration never had the control.
|
|
34
|
+
|
|
35
|
+
**Node has the same gap** and is tracked separately; this release fixes Python
|
|
36
|
+
only.
|
|
37
|
+
|
|
38
|
+
Upgrade:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install --upgrade controlzero
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 1.13.13 (2026-08-16): a read-class METHOD could satisfy an allow the SQL would deny
|
|
45
|
+
|
|
46
|
+
**Security fix. If you use a `database:read` allow rule, read this.**
|
|
47
|
+
|
|
48
|
+
Our own documented read-only-database recipe permitted `DROP TABLE`. Driven
|
|
49
|
+
against the published 1.13.12 wheel, using that page's own policy and its own
|
|
50
|
+
verification command:
|
|
51
|
+
|
|
52
|
+
controlzero test database --method query --policy policy.yaml \
|
|
53
|
+
--args '{"sql":"DROP TABLE users"}' -> ALLOW, policy_id=read-only-db
|
|
54
|
+
|
|
55
|
+
same policy, no --method -> DENY
|
|
56
|
+
|
|
57
|
+
The alias table maps `read -> [query, SELECT, ...]`. A caller-supplied
|
|
58
|
+
`method="query"` is itself a read-class alias, so `database:read` was injected
|
|
59
|
+
FROM THE METHOD and satisfied the allow before the SQL extractor's `admin`
|
|
60
|
+
class was consulted. "Most dangerous class wins" is the documented semantics;
|
|
61
|
+
it did not happen whenever a method was supplied -- which is the SDK call shape
|
|
62
|
+
we document (`cz.guard("database", method="query", args={"sql": ...})`).
|
|
63
|
+
|
|
64
|
+
**Not a regression.** The `query -> read` alias predates the recipe. It has
|
|
65
|
+
never worked. All releases up to and including 1.13.12 are affected.
|
|
66
|
+
|
|
67
|
+
**Fixed:** when a method-derived class and an argument-derived class are both
|
|
68
|
+
present, the most dangerous now governs. Method-only calls keep legacy alias
|
|
69
|
+
expansion; concrete SQL overrides a stale caller-supplied semantic context.
|
|
70
|
+
Python and Node.
|
|
71
|
+
|
|
72
|
+
**If you cannot upgrade yet,** put explicit denies ahead of the allow. Verified
|
|
73
|
+
on the vulnerable 1.13.12 build: DROP / DELETE / TRUNCATE / GRANT and the
|
|
74
|
+
`SELECT 1; DROP TABLE x` piggyback all deny, while `SELECT` still allows.
|
|
75
|
+
|
|
76
|
+
rules:
|
|
77
|
+
- id: no-destructive-sql
|
|
78
|
+
deny: 'database:admin'
|
|
79
|
+
- id: no-writes
|
|
80
|
+
deny: 'database:write'
|
|
81
|
+
- id: read-only-db
|
|
82
|
+
allow: 'database:read'
|
|
83
|
+
|
|
84
|
+
A `deny: 'database:admin'` rule was never affected -- it matches the
|
|
85
|
+
argument-derived class regardless of the method. Only the allow-only shape was
|
|
86
|
+
vulnerable.
|
|
87
|
+
|
|
88
|
+
Tests: 124 explicit cells (31 method modes x 4 SQL classes) plus the four
|
|
89
|
+
literal CLI cases, mirrored in Node. Adding a class or a method alias fails the
|
|
90
|
+
matrix until its cells are filled.
|
|
91
|
+
|
|
92
|
+
|
|
5
93
|
## 1.13.12 (2026-08-16): your audit trail now states whether the HOST enforced, and deny rules that never fired now fire
|
|
6
94
|
|
|
7
95
|
Two things in this release change what a customer can conclude from what we
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: controlzero
|
|
3
|
-
Version: 1.13.
|
|
3
|
+
Version: 1.13.14
|
|
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
|
|
@@ -87,6 +87,68 @@ for cls, aliases in _CLASSES.items():
|
|
|
87
87
|
_CANONICAL_TO_LEGACY[canon] = {_canonical(a) for a in aliases}
|
|
88
88
|
|
|
89
89
|
|
|
90
|
+
# Conflict-only ordering for a caller method versus the class derived from the
|
|
91
|
+
# concrete SQL arguments (#2340). ``admin`` and ``exec`` are both privileged,
|
|
92
|
+
# top-tier classes: neither may be treated as safer than the other. When they
|
|
93
|
+
# conflict, the argument-derived class wins the tie because it describes the
|
|
94
|
+
# SQL that will actually execute; the caller method is compatibility metadata.
|
|
95
|
+
_DATABASE_CLASS_DANGER: dict[str, int] = {
|
|
96
|
+
"read": 0,
|
|
97
|
+
"write": 1,
|
|
98
|
+
"admin": 2,
|
|
99
|
+
"exec": 2,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _database_classes_for_action(action: str) -> set[str]:
|
|
104
|
+
"""Return canonical database classes represented by one action."""
|
|
105
|
+
if not isinstance(action, str) or not action or ":" not in action:
|
|
106
|
+
return set()
|
|
107
|
+
tool, method = action.split(":", 1)
|
|
108
|
+
if tool != TOOL:
|
|
109
|
+
return set()
|
|
110
|
+
if method in _DATABASE_CLASS_DANGER:
|
|
111
|
+
return {method}
|
|
112
|
+
return {
|
|
113
|
+
canonical_action.split(":", 1)[1]
|
|
114
|
+
for canonical_action in _LEGACY_TO_CANONICAL.get(method, ())
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def resolve_database_class_conflict(
|
|
119
|
+
method_action: str,
|
|
120
|
+
argument_action: str,
|
|
121
|
+
) -> str | None:
|
|
122
|
+
"""Return the sole governing action for a method/argument class conflict.
|
|
123
|
+
|
|
124
|
+
``None`` means there is no explicit conflict to resolve: at least one side
|
|
125
|
+
is unclassified, or both sides represent exactly the same single class. In
|
|
126
|
+
those cases callers retain the additive compatibility behavior.
|
|
127
|
+
|
|
128
|
+
A returned action is a canonical ``database:<class>``. The evaluator must
|
|
129
|
+
build candidates from that action alone, making a less-dangerous method
|
|
130
|
+
alias absent rather than hoping a later deny rule detects it.
|
|
131
|
+
"""
|
|
132
|
+
method_classes = _database_classes_for_action(method_action)
|
|
133
|
+
argument_classes = _database_classes_for_action(argument_action)
|
|
134
|
+
|
|
135
|
+
# Explicit positive gate: conflict resolution only applies when BOTH
|
|
136
|
+
# sources carry recognized classes and the argument side is unambiguous.
|
|
137
|
+
if not method_classes or len(argument_classes) != 1:
|
|
138
|
+
return None
|
|
139
|
+
argument_class = next(iter(argument_classes))
|
|
140
|
+
if method_classes == {argument_class}:
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
method_class = max(method_classes, key=_DATABASE_CLASS_DANGER.__getitem__)
|
|
144
|
+
if (
|
|
145
|
+
_DATABASE_CLASS_DANGER[argument_class]
|
|
146
|
+
>= _DATABASE_CLASS_DANGER[method_class]
|
|
147
|
+
):
|
|
148
|
+
return _canonical(argument_class)
|
|
149
|
+
return _canonical(method_class)
|
|
150
|
+
|
|
151
|
+
|
|
90
152
|
def expand_candidate_actions(actions: Iterable[str]) -> list[str]:
|
|
91
153
|
"""Expand an iterable of candidate actions to include every known alias.
|
|
92
154
|
|
|
@@ -162,4 +224,9 @@ def alias_table_json() -> str:
|
|
|
162
224
|
return json.dumps(payload, indent=2, sort_keys=False)
|
|
163
225
|
|
|
164
226
|
|
|
165
|
-
__all__ = [
|
|
227
|
+
__all__ = [
|
|
228
|
+
"TOOL",
|
|
229
|
+
"alias_table_json",
|
|
230
|
+
"expand_candidate_actions",
|
|
231
|
+
"resolve_database_class_conflict",
|
|
232
|
+
]
|
|
@@ -1171,18 +1171,21 @@ class PolicyEvaluator:
|
|
|
1171
1171
|
|
|
1172
1172
|
# Semantic-class layer (#345): when the action carries a
|
|
1173
1173
|
# canonical SQL class (database:read|write|admin|exec) the
|
|
1174
|
-
# evaluator matches rules against BOTH the per-keyword action
|
|
1175
|
-
# above AND this class action.
|
|
1174
|
+
# evaluator normally matches rules against BOTH the per-keyword action
|
|
1175
|
+
# above AND this class action. When recognized method and argument
|
|
1176
|
+
# classes conflict, #2340 replaces both with the most-dangerous class
|
|
1177
|
+
# below. A rule whose actions list
|
|
1176
1178
|
# contains `database:read` therefore fires for any read-shaped
|
|
1177
1179
|
# SQL (SELECT, EXPLAIN, SHOW, CTE, ...) regardless of dialect
|
|
1178
1180
|
# spelling, while existing per-keyword rules
|
|
1179
1181
|
# (`database:DROP`) keep working unchanged. Two sources for
|
|
1180
1182
|
# the class action: the caller may provide it in
|
|
1181
1183
|
# ``context['action_semantic_class']`` (used by the hook-check
|
|
1182
|
-
# CLI), or we derive it on the fly from ``args['sql']
|
|
1183
|
-
#
|
|
1184
|
+
# CLI), or we derive it on the fly from ``args['sql']``. Concrete SQL
|
|
1185
|
+
# arguments are authoritative when both are present; otherwise a caller
|
|
1186
|
+
# could suppress classification by passing a stale context value.
|
|
1184
1187
|
semantic_action = ctx_dict.get("action_semantic_class") or ""
|
|
1185
|
-
if
|
|
1188
|
+
if tool == "database" and args:
|
|
1186
1189
|
from controlzero._internal.hook_extractors import sql_semantic_class
|
|
1187
1190
|
sql_text = args.get("sql") if isinstance(args, dict) else None
|
|
1188
1191
|
if isinstance(sql_text, str) and sql_text:
|
|
@@ -1262,18 +1265,35 @@ class PolicyEvaluator:
|
|
|
1262
1265
|
# ...) keep matching modern SDK calls that emit canonical
|
|
1263
1266
|
# semantic classes (database:read|write|admin|exec), and vice
|
|
1264
1267
|
# versa. NO BREAKING CHANGES contract -- see #389.
|
|
1265
|
-
from controlzero._internal.action_aliases import
|
|
1268
|
+
from controlzero._internal.action_aliases import (
|
|
1269
|
+
expand_candidate_actions,
|
|
1270
|
+
resolve_database_class_conflict,
|
|
1271
|
+
)
|
|
1272
|
+
|
|
1273
|
+
database_class_override = resolve_database_class_conflict(
|
|
1274
|
+
action, semantic_action
|
|
1275
|
+
)
|
|
1266
1276
|
# PRE-#2245 candidate set: the single surviving tool name plus the
|
|
1267
1277
|
# derived actions. Retained so phase 1 can answer "what would this
|
|
1268
1278
|
# policy have decided before the widening" without guessing.
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1279
|
+
if database_class_override is not None:
|
|
1280
|
+
# #2340: a recognized caller method and a recognized SQL-argument
|
|
1281
|
+
# class disagree. Build the candidate universe from the governing
|
|
1282
|
+
# class ONLY. The less-dangerous alias is therefore unmatchable by
|
|
1283
|
+
# construction; it cannot satisfy an early allow before the actual
|
|
1284
|
+
# SQL class is consulted. This is conflict-only, so method-only and
|
|
1285
|
+
# genuinely same-class calls retain the compatibility expansion.
|
|
1286
|
+
legacy_actions = expand_candidate_actions([database_class_override])
|
|
1287
|
+
candidate_actions = list(legacy_actions)
|
|
1288
|
+
else:
|
|
1289
|
+
legacy_actions = expand_candidate_actions([action, *extra_actions])
|
|
1290
|
+
# WIDENED candidate set: every name this call carried, plus the same
|
|
1291
|
+
# derived actions. A strict superset of `legacy_actions`, which is
|
|
1292
|
+
# what makes the phase-1 comparison below a two-outcome choice rather
|
|
1293
|
+
# than an open-ended merge.
|
|
1294
|
+
candidate_actions = expand_candidate_actions(
|
|
1295
|
+
[*subject.candidates, *extra_actions]
|
|
1296
|
+
)
|
|
1277
1297
|
|
|
1278
1298
|
resource = ctx_dict.get("resource")
|
|
1279
1299
|
evaluated = 0
|
|
@@ -698,6 +698,19 @@ class Client:
|
|
|
698
698
|
)
|
|
699
699
|
self._local_override_source_hint = str(local_source) if self._local_override_active else ""
|
|
700
700
|
|
|
701
|
+
# Keep the actual file-backed source so guard() can verify its HMAC on
|
|
702
|
+
# every evaluation. Dict policies and hosted bundles have no local HMAC
|
|
703
|
+
# sidecars; hosted signatures are verified by load_hosted_policy().
|
|
704
|
+
#
|
|
705
|
+
# Accept str as well as Path. `Client(policy="policy.yaml")` is the form
|
|
706
|
+
# every doc example uses, and an isinstance(local_source, Path) test
|
|
707
|
+
# silently skipped tamper verification for it -- the control would have
|
|
708
|
+
# been present, tested, and unreachable for almost every real caller.
|
|
709
|
+
# A dict policy has no file to verify and correctly yields None here.
|
|
710
|
+
self._local_policy_path = (
|
|
711
|
+
Path(local_source) if isinstance(local_source, (str, Path)) else None
|
|
712
|
+
)
|
|
713
|
+
|
|
701
714
|
# Load local policy if present
|
|
702
715
|
self._evaluator: Optional[PolicyEvaluator] = None
|
|
703
716
|
self._policy_settings = PolicySettings()
|
|
@@ -1465,6 +1478,17 @@ class Client:
|
|
|
1465
1478
|
# error is swallowed: the old bundle keeps working.
|
|
1466
1479
|
self._maybe_refresh_policy()
|
|
1467
1480
|
|
|
1481
|
+
# A caller cannot be trusted to report policy tampering in its own
|
|
1482
|
+
# context. Discover it from the signed file and merge the result into a
|
|
1483
|
+
# private copy so both enforcement and audit consume the same fact.
|
|
1484
|
+
if self._local_policy_path is not None:
|
|
1485
|
+
from controlzero.tamper import policy_file_tampered
|
|
1486
|
+
|
|
1487
|
+
if policy_file_tampered(self._local_policy_path):
|
|
1488
|
+
context = dict(context or {})
|
|
1489
|
+
context["tamper_detected"] = True
|
|
1490
|
+
context.setdefault("tamper_source", "policy_hmac")
|
|
1491
|
+
|
|
1468
1492
|
# gh#175 P1.1 audit trail: resolve the client_name +
|
|
1469
1493
|
# project_id once up front so the quarantine / tamper
|
|
1470
1494
|
# fast-paths below stamp the audit row consistently with the
|
|
@@ -326,6 +326,31 @@ def load_or_create_secret(path: Path) -> bytes:
|
|
|
326
326
|
return secret
|
|
327
327
|
|
|
328
328
|
|
|
329
|
+
def policy_file_tampered(path: Path) -> bool:
|
|
330
|
+
"""Return whether a signed local policy no longer matches its HMAC.
|
|
331
|
+
|
|
332
|
+
Unsigned policies remain supported: when either sidecar is absent there is
|
|
333
|
+
no integrity assertion to verify, so this returns False. Once both
|
|
334
|
+
``<policy>.sig`` and ``tamper.key`` exist, a malformed or mismatched
|
|
335
|
+
signature is tampering.
|
|
336
|
+
"""
|
|
337
|
+
policy_path = Path(path)
|
|
338
|
+
signature_path = policy_path.with_suffix(policy_path.suffix + ".sig")
|
|
339
|
+
key_path = policy_path.parent / "tamper.key"
|
|
340
|
+
if not signature_path.exists() or not key_path.exists():
|
|
341
|
+
return False
|
|
342
|
+
|
|
343
|
+
try:
|
|
344
|
+
secret = load_or_create_secret(key_path)
|
|
345
|
+
payload = policy_path.read_bytes()
|
|
346
|
+
signature = signature_path.read_text(encoding="utf-8").strip()
|
|
347
|
+
except (OSError, UnicodeError):
|
|
348
|
+
# Both sidecars assert that verification is required. If they cannot be
|
|
349
|
+
# read, the integrity claim cannot be established, so fail closed.
|
|
350
|
+
return True
|
|
351
|
+
return not verify_bundle(payload, signature, secret)
|
|
352
|
+
|
|
353
|
+
|
|
329
354
|
# Imports below sit mid-module by design: the quarantine block is a
|
|
330
355
|
# self-contained subsystem that came in later than the original file
|
|
331
356
|
# (#383 quarantine support) and was kept locally scoped to make the
|
|
@@ -463,6 +488,7 @@ __all__ = [
|
|
|
463
488
|
"write_signed_file",
|
|
464
489
|
"HashChain",
|
|
465
490
|
"load_or_create_secret",
|
|
491
|
+
"policy_file_tampered",
|
|
466
492
|
"TamperState",
|
|
467
493
|
"report_tamper_alert",
|
|
468
494
|
]
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "controlzero"
|
|
7
|
-
version = "1.13.
|
|
7
|
+
version = "1.13.14"
|
|
8
8
|
description = "AI agent governance: policies, audit, and observability for tool calls. Works locally with no signup."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "Apache-2.0"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{controlzero-1.13.12 → controlzero-1.13.14}/controlzero/_internal/credentials_data/__init__.py
RENAMED
|
File without changes
|
{controlzero-1.13.12 → controlzero-1.13.14}/controlzero/_internal/credentials_data/built_in.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/templates/antigravity/hooks.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{controlzero-1.13.12 → controlzero-1.13.14}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|