controlzero 1.13.13__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.
Files changed (144) hide show
  1. {controlzero-1.13.13 → controlzero-1.13.14}/CHANGELOG.md +39 -0
  2. {controlzero-1.13.13 → controlzero-1.13.14}/PKG-INFO +1 -1
  3. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/__init__.py +1 -1
  4. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/client.py +24 -0
  5. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/tamper.py +26 -0
  6. {controlzero-1.13.13 → controlzero-1.13.14}/pyproject.toml +1 -1
  7. {controlzero-1.13.13 → controlzero-1.13.14}/.gitignore +0 -0
  8. {controlzero-1.13.13 → controlzero-1.13.14}/LICENSE +0 -0
  9. {controlzero-1.13.13 → controlzero-1.13.14}/README.md +0 -0
  10. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/__main__.py +0 -0
  11. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/_internal/__init__.py +0 -0
  12. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/_internal/_messages.py +0 -0
  13. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/_internal/action_aliases.py +0 -0
  14. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/_internal/action_subject.py +0 -0
  15. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/_internal/action_validator.py +0 -0
  16. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/_internal/bundle.py +0 -0
  17. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/_internal/condition_ops.py +0 -0
  18. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/_internal/credential_hook.py +0 -0
  19. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/_internal/credential_scanner.py +0 -0
  20. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/_internal/credentials_data/__init__.py +0 -0
  21. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/_internal/credentials_data/built_in.yaml +0 -0
  22. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/_internal/dlp_output_observer.py +0 -0
  23. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/_internal/dlp_remediation.py +0 -0
  24. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/_internal/dlp_scanner.py +0 -0
  25. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/_internal/enforcement_outcome.py +0 -0
  26. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/_internal/enforcer.py +0 -0
  27. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/_internal/hook_extractors.py +0 -0
  28. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/_internal/io_capture_producer.py +0 -0
  29. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/_internal/source_labels.py +0 -0
  30. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/_internal/tool_extractors.json +0 -0
  31. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/_internal/types.py +0 -0
  32. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/audit_local.py +0 -0
  33. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/audit_remote.py +0 -0
  34. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/canonical.py +0 -0
  35. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/__init__.py +0 -0
  36. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/__main__.py +0 -0
  37. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/_secrets.py +0 -0
  38. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/agent_config.py +0 -0
  39. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/console.py +0 -0
  40. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/debug_bundle.py +0 -0
  41. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/doctor.py +0 -0
  42. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/enforcement.py +0 -0
  43. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/enforcement_verdict.py +0 -0
  44. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/exec_cmd.py +0 -0
  45. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/hook_entry.py +0 -0
  46. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/hosts/__init__.py +0 -0
  47. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/hosts/antigravity.py +0 -0
  48. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/hosts/base.py +0 -0
  49. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/hosts/claims.py +0 -0
  50. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/hosts/claude_code.py +0 -0
  51. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/hosts/codex_cli.py +0 -0
  52. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/hosts/cursor.py +0 -0
  53. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/hosts/cursor_cli.py +0 -0
  54. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/hosts/gemini_cli.py +0 -0
  55. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/hosts/kiro.py +0 -0
  56. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/hosts/unknown.py +0 -0
  57. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/kiro_adapter.py +0 -0
  58. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/main.py +0 -0
  59. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/maturity.py +0 -0
  60. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/migrate.py +0 -0
  61. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/output.py +0 -0
  62. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/setup_wizard.py +0 -0
  63. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/spool_cmd.py +0 -0
  64. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/telemetry_consent.py +0 -0
  65. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/templates/antigravity/hooks.json +0 -0
  66. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/templates/antigravity.yaml +0 -0
  67. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/templates/autogen.yaml +0 -0
  68. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/templates/claude-code.yaml +0 -0
  69. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/templates/codex-cli.yaml +0 -0
  70. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/templates/cost-cap.yaml +0 -0
  71. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/templates/crewai.yaml +0 -0
  72. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/templates/cursor.yaml +0 -0
  73. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/templates/gemini-cli.yaml +0 -0
  74. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/templates/generic.yaml +0 -0
  75. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook +0 -0
  76. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/templates/kiro/ide-pre-tool-use.kiro.hook +0 -0
  77. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/templates/kiro/ide-prompt-submit.kiro.hook +0 -0
  78. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/templates/kiro/kiro.yaml +0 -0
  79. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/templates/langchain.yaml +0 -0
  80. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/templates/mcp.yaml +0 -0
  81. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/templates/rag.yaml +0 -0
  82. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/cli/update_cmd.py +0 -0
  83. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/device.py +0 -0
  84. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/enrollment.py +0 -0
  85. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/error_codes.py +0 -0
  86. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/error_codes.yaml +0 -0
  87. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/errors.py +0 -0
  88. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/hitl/__init__.py +0 -0
  89. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/hitl/grant_protocol.py +0 -0
  90. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/hitl/mock.py +0 -0
  91. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/hitl/pending_approval.py +0 -0
  92. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/hitl/secret_leak_guard.py +0 -0
  93. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/hitl/status.py +0 -0
  94. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/hook_integrity.py +0 -0
  95. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/hooks/__init__.py +0 -0
  96. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/hooks/tool_output_handler.py +0 -0
  97. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/host_invocation.py +0 -0
  98. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/hosted_policy.py +0 -0
  99. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/integrations/__init__.py +0 -0
  100. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/integrations/_export_dlp.py +0 -0
  101. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/integrations/_mask_apply.py +0 -0
  102. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/integrations/anthropic.py +0 -0
  103. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/integrations/autogen.py +0 -0
  104. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/integrations/braintrust.py +0 -0
  105. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/integrations/crewai/__init__.py +0 -0
  106. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/integrations/crewai/agent.py +0 -0
  107. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/integrations/crewai/crew.py +0 -0
  108. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/integrations/crewai/task.py +0 -0
  109. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/integrations/crewai/tool.py +0 -0
  110. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/integrations/google.py +0 -0
  111. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/integrations/google_adk/__init__.py +0 -0
  112. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/integrations/google_adk/agent.py +0 -0
  113. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/integrations/google_adk/tool.py +0 -0
  114. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/integrations/langchain/__init__.py +0 -0
  115. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/integrations/langchain/agent.py +0 -0
  116. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/integrations/langchain/callbacks.py +0 -0
  117. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/integrations/langchain/chain.py +0 -0
  118. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/integrations/langchain/graph.py +0 -0
  119. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/integrations/langchain/modern.py +0 -0
  120. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/integrations/langchain/tool.py +0 -0
  121. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/integrations/langfuse.py +0 -0
  122. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/integrations/litellm.py +0 -0
  123. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/integrations/openai.py +0 -0
  124. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/integrations/pydantic_ai.py +0 -0
  125. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/integrations/vercel_ai.py +0 -0
  126. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/layout_migration.py +0 -0
  127. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/policy_loader.py +0 -0
  128. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/secrets/__init__.py +0 -0
  129. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/secrets/reference.py +0 -0
  130. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/secrets/resolver.py +0 -0
  131. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/secrets/secretstr.py +0 -0
  132. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/spool/__init__.py +0 -0
  133. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/spool/_compress.py +0 -0
  134. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/spool/_constants.py +0 -0
  135. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/spool/_crc32c.py +0 -0
  136. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/spool/_crypto.py +0 -0
  137. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/spool/_frame.py +0 -0
  138. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/spool/_keyring.py +0 -0
  139. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/spool/_metrics.py +0 -0
  140. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/spool/_spool.py +0 -0
  141. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/spool/_state.py +0 -0
  142. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/spool/_uploader.py +0 -0
  143. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/spool/cz-audit-v1.dict +0 -0
  144. {controlzero-1.13.13 → controlzero-1.13.14}/controlzero/tracecontext.py +0 -0
@@ -2,6 +2,45 @@
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
+
5
44
  ## 1.13.13 (2026-08-16): a read-class METHOD could satisfy an allow the SQL would deny
6
45
 
7
46
  **Security fix. If you use a `database:read` allow rule, read this.**
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: controlzero
3
- Version: 1.13.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
@@ -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.13"
44
+ __version__ = "1.13.14"
45
45
 
46
46
  __all__ = [
47
47
  "Client",
@@ -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.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