controlzero 1.13.12__tar.gz → 1.13.13__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 (144) hide show
  1. {controlzero-1.13.12 → controlzero-1.13.13}/CHANGELOG.md +49 -0
  2. {controlzero-1.13.12 → controlzero-1.13.13}/PKG-INFO +1 -1
  3. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/__init__.py +1 -1
  4. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/_internal/action_aliases.py +68 -1
  5. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/_internal/enforcer.py +34 -14
  6. {controlzero-1.13.12 → controlzero-1.13.13}/pyproject.toml +1 -1
  7. {controlzero-1.13.12 → controlzero-1.13.13}/.gitignore +0 -0
  8. {controlzero-1.13.12 → controlzero-1.13.13}/LICENSE +0 -0
  9. {controlzero-1.13.12 → controlzero-1.13.13}/README.md +0 -0
  10. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/__main__.py +0 -0
  11. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/_internal/__init__.py +0 -0
  12. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/_internal/_messages.py +0 -0
  13. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/_internal/action_subject.py +0 -0
  14. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/_internal/action_validator.py +0 -0
  15. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/_internal/bundle.py +0 -0
  16. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/_internal/condition_ops.py +0 -0
  17. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/_internal/credential_hook.py +0 -0
  18. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/_internal/credential_scanner.py +0 -0
  19. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/_internal/credentials_data/__init__.py +0 -0
  20. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/_internal/credentials_data/built_in.yaml +0 -0
  21. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/_internal/dlp_output_observer.py +0 -0
  22. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/_internal/dlp_remediation.py +0 -0
  23. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/_internal/dlp_scanner.py +0 -0
  24. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/_internal/enforcement_outcome.py +0 -0
  25. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/_internal/hook_extractors.py +0 -0
  26. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/_internal/io_capture_producer.py +0 -0
  27. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/_internal/source_labels.py +0 -0
  28. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/_internal/tool_extractors.json +0 -0
  29. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/_internal/types.py +0 -0
  30. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/audit_local.py +0 -0
  31. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/audit_remote.py +0 -0
  32. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/canonical.py +0 -0
  33. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/__init__.py +0 -0
  34. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/__main__.py +0 -0
  35. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/_secrets.py +0 -0
  36. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/agent_config.py +0 -0
  37. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/console.py +0 -0
  38. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/debug_bundle.py +0 -0
  39. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/doctor.py +0 -0
  40. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/enforcement.py +0 -0
  41. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/enforcement_verdict.py +0 -0
  42. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/exec_cmd.py +0 -0
  43. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/hook_entry.py +0 -0
  44. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/hosts/__init__.py +0 -0
  45. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/hosts/antigravity.py +0 -0
  46. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/hosts/base.py +0 -0
  47. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/hosts/claims.py +0 -0
  48. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/hosts/claude_code.py +0 -0
  49. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/hosts/codex_cli.py +0 -0
  50. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/hosts/cursor.py +0 -0
  51. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/hosts/cursor_cli.py +0 -0
  52. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/hosts/gemini_cli.py +0 -0
  53. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/hosts/kiro.py +0 -0
  54. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/hosts/unknown.py +0 -0
  55. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/kiro_adapter.py +0 -0
  56. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/main.py +0 -0
  57. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/maturity.py +0 -0
  58. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/migrate.py +0 -0
  59. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/output.py +0 -0
  60. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/setup_wizard.py +0 -0
  61. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/spool_cmd.py +0 -0
  62. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/telemetry_consent.py +0 -0
  63. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/templates/antigravity/hooks.json +0 -0
  64. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/templates/antigravity.yaml +0 -0
  65. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/templates/autogen.yaml +0 -0
  66. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/templates/claude-code.yaml +0 -0
  67. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/templates/codex-cli.yaml +0 -0
  68. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/templates/cost-cap.yaml +0 -0
  69. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/templates/crewai.yaml +0 -0
  70. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/templates/cursor.yaml +0 -0
  71. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/templates/gemini-cli.yaml +0 -0
  72. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/templates/generic.yaml +0 -0
  73. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook +0 -0
  74. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/templates/kiro/ide-pre-tool-use.kiro.hook +0 -0
  75. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/templates/kiro/ide-prompt-submit.kiro.hook +0 -0
  76. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/templates/kiro/kiro.yaml +0 -0
  77. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/templates/langchain.yaml +0 -0
  78. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/templates/mcp.yaml +0 -0
  79. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/templates/rag.yaml +0 -0
  80. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/cli/update_cmd.py +0 -0
  81. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/client.py +0 -0
  82. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/device.py +0 -0
  83. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/enrollment.py +0 -0
  84. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/error_codes.py +0 -0
  85. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/error_codes.yaml +0 -0
  86. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/errors.py +0 -0
  87. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/hitl/__init__.py +0 -0
  88. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/hitl/grant_protocol.py +0 -0
  89. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/hitl/mock.py +0 -0
  90. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/hitl/pending_approval.py +0 -0
  91. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/hitl/secret_leak_guard.py +0 -0
  92. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/hitl/status.py +0 -0
  93. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/hook_integrity.py +0 -0
  94. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/hooks/__init__.py +0 -0
  95. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/hooks/tool_output_handler.py +0 -0
  96. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/host_invocation.py +0 -0
  97. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/hosted_policy.py +0 -0
  98. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/integrations/__init__.py +0 -0
  99. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/integrations/_export_dlp.py +0 -0
  100. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/integrations/_mask_apply.py +0 -0
  101. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/integrations/anthropic.py +0 -0
  102. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/integrations/autogen.py +0 -0
  103. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/integrations/braintrust.py +0 -0
  104. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/integrations/crewai/__init__.py +0 -0
  105. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/integrations/crewai/agent.py +0 -0
  106. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/integrations/crewai/crew.py +0 -0
  107. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/integrations/crewai/task.py +0 -0
  108. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/integrations/crewai/tool.py +0 -0
  109. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/integrations/google.py +0 -0
  110. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/integrations/google_adk/__init__.py +0 -0
  111. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/integrations/google_adk/agent.py +0 -0
  112. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/integrations/google_adk/tool.py +0 -0
  113. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/integrations/langchain/__init__.py +0 -0
  114. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/integrations/langchain/agent.py +0 -0
  115. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/integrations/langchain/callbacks.py +0 -0
  116. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/integrations/langchain/chain.py +0 -0
  117. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/integrations/langchain/graph.py +0 -0
  118. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/integrations/langchain/modern.py +0 -0
  119. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/integrations/langchain/tool.py +0 -0
  120. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/integrations/langfuse.py +0 -0
  121. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/integrations/litellm.py +0 -0
  122. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/integrations/openai.py +0 -0
  123. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/integrations/pydantic_ai.py +0 -0
  124. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/integrations/vercel_ai.py +0 -0
  125. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/layout_migration.py +0 -0
  126. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/policy_loader.py +0 -0
  127. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/secrets/__init__.py +0 -0
  128. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/secrets/reference.py +0 -0
  129. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/secrets/resolver.py +0 -0
  130. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/secrets/secretstr.py +0 -0
  131. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/spool/__init__.py +0 -0
  132. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/spool/_compress.py +0 -0
  133. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/spool/_constants.py +0 -0
  134. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/spool/_crc32c.py +0 -0
  135. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/spool/_crypto.py +0 -0
  136. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/spool/_frame.py +0 -0
  137. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/spool/_keyring.py +0 -0
  138. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/spool/_metrics.py +0 -0
  139. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/spool/_spool.py +0 -0
  140. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/spool/_state.py +0 -0
  141. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/spool/_uploader.py +0 -0
  142. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/spool/cz-audit-v1.dict +0 -0
  143. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/tamper.py +0 -0
  144. {controlzero-1.13.12 → controlzero-1.13.13}/controlzero/tracecontext.py +0 -0
@@ -2,6 +2,55 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 1.13.13 (2026-08-16): a read-class METHOD could satisfy an allow the SQL would deny
6
+
7
+ **Security fix. If you use a `database:read` allow rule, read this.**
8
+
9
+ Our own documented read-only-database recipe permitted `DROP TABLE`. Driven
10
+ against the published 1.13.12 wheel, using that page's own policy and its own
11
+ verification command:
12
+
13
+ controlzero test database --method query --policy policy.yaml \
14
+ --args '{"sql":"DROP TABLE users"}' -> ALLOW, policy_id=read-only-db
15
+
16
+ same policy, no --method -> DENY
17
+
18
+ The alias table maps `read -> [query, SELECT, ...]`. A caller-supplied
19
+ `method="query"` is itself a read-class alias, so `database:read` was injected
20
+ FROM THE METHOD and satisfied the allow before the SQL extractor's `admin`
21
+ class was consulted. "Most dangerous class wins" is the documented semantics;
22
+ it did not happen whenever a method was supplied -- which is the SDK call shape
23
+ we document (`cz.guard("database", method="query", args={"sql": ...})`).
24
+
25
+ **Not a regression.** The `query -> read` alias predates the recipe. It has
26
+ never worked. All releases up to and including 1.13.12 are affected.
27
+
28
+ **Fixed:** when a method-derived class and an argument-derived class are both
29
+ present, the most dangerous now governs. Method-only calls keep legacy alias
30
+ expansion; concrete SQL overrides a stale caller-supplied semantic context.
31
+ Python and Node.
32
+
33
+ **If you cannot upgrade yet,** put explicit denies ahead of the allow. Verified
34
+ on the vulnerable 1.13.12 build: DROP / DELETE / TRUNCATE / GRANT and the
35
+ `SELECT 1; DROP TABLE x` piggyback all deny, while `SELECT` still allows.
36
+
37
+ rules:
38
+ - id: no-destructive-sql
39
+ deny: 'database:admin'
40
+ - id: no-writes
41
+ deny: 'database:write'
42
+ - id: read-only-db
43
+ allow: 'database:read'
44
+
45
+ A `deny: 'database:admin'` rule was never affected -- it matches the
46
+ argument-derived class regardless of the method. Only the allow-only shape was
47
+ vulnerable.
48
+
49
+ Tests: 124 explicit cells (31 method modes x 4 SQL classes) plus the four
50
+ literal CLI cases, mirrored in Node. Adding a class or a method alias fails the
51
+ matrix until its cells are filled.
52
+
53
+
5
54
  ## 1.13.12 (2026-08-16): your audit trail now states whether the HOST enforced, and deny rules that never fired now fire
6
55
 
7
56
  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.12
3
+ Version: 1.13.13
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
@@ -41,7 +41,7 @@ from controlzero.hitl.grant_protocol import (
41
41
  from controlzero.policy_loader import load_policy
42
42
  from controlzero.secrets import SecretRef, SecretStr, secret_ref
43
43
 
44
- __version__ = "1.13.12"
44
+ __version__ = "1.13.13"
45
45
 
46
46
  __all__ = [
47
47
  "Client",
@@ -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__ = ["TOOL", "expand_candidate_actions", "alias_table_json"]
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. A rule whose actions list
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']`` when
1183
- # the tool is ``database`` and no class was provided.
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 not semantic_action and tool == "database" and args:
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 expand_candidate_actions
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
- legacy_actions = expand_candidate_actions([action, *extra_actions])
1270
- # WIDENED candidate set: every name this call carried, plus the same
1271
- # derived actions. A strict superset of `legacy_actions`, which is
1272
- # what makes the phase-1 comparison below a two-outcome choice rather
1273
- # than an open-ended merge.
1274
- candidate_actions = expand_candidate_actions(
1275
- [*subject.candidates, *extra_actions]
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "controlzero"
7
- version = "1.13.12"
7
+ version = "1.13.13"
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