controlzero 1.13.11__tar.gz → 1.13.12__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 (145) hide show
  1. {controlzero-1.13.11 → controlzero-1.13.12}/.gitignore +14 -1
  2. {controlzero-1.13.11 → controlzero-1.13.12}/CHANGELOG.md +334 -1
  3. {controlzero-1.13.11 → controlzero-1.13.12}/PKG-INFO +1 -1
  4. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/__init__.py +3 -2
  5. controlzero-1.13.12/controlzero/_internal/action_subject.py +474 -0
  6. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/_internal/dlp_output_observer.py +7 -1
  7. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/_internal/dlp_scanner.py +148 -2
  8. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/_internal/enforcement_outcome.py +115 -12
  9. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/_internal/enforcer.py +560 -26
  10. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/_internal/hook_extractors.py +29 -0
  11. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/audit_remote.py +26 -0
  12. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/debug_bundle.py +21 -1
  13. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/doctor.py +84 -23
  14. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/enforcement.py +156 -11
  15. controlzero-1.13.12/controlzero/cli/enforcement_verdict.py +1316 -0
  16. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/hosts/base.py +54 -9
  17. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/hosts/cursor_cli.py +21 -3
  18. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/hosts/kiro.py +77 -7
  19. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/main.py +615 -159
  20. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/setup_wizard.py +55 -9
  21. controlzero-1.13.12/controlzero/cli/templates/kiro/kiro.yaml +176 -0
  22. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/client.py +636 -9
  23. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/hook_integrity.py +36 -4
  24. controlzero-1.13.12/controlzero/host_invocation.py +723 -0
  25. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/integrations/_export_dlp.py +7 -1
  26. {controlzero-1.13.11 → controlzero-1.13.12}/pyproject.toml +17 -1
  27. controlzero-1.13.11/controlzero/cli/templates/kiro/kiro.yaml +0 -102
  28. {controlzero-1.13.11 → controlzero-1.13.12}/LICENSE +0 -0
  29. {controlzero-1.13.11 → controlzero-1.13.12}/README.md +0 -0
  30. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/__main__.py +0 -0
  31. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/_internal/__init__.py +0 -0
  32. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/_internal/_messages.py +0 -0
  33. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/_internal/action_aliases.py +0 -0
  34. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/_internal/action_validator.py +0 -0
  35. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/_internal/bundle.py +0 -0
  36. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/_internal/condition_ops.py +0 -0
  37. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/_internal/credential_hook.py +0 -0
  38. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/_internal/credential_scanner.py +0 -0
  39. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/_internal/credentials_data/__init__.py +0 -0
  40. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/_internal/credentials_data/built_in.yaml +0 -0
  41. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/_internal/dlp_remediation.py +0 -0
  42. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/_internal/io_capture_producer.py +0 -0
  43. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/_internal/source_labels.py +0 -0
  44. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/_internal/tool_extractors.json +0 -0
  45. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/_internal/types.py +0 -0
  46. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/audit_local.py +0 -0
  47. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/canonical.py +0 -0
  48. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/__init__.py +0 -0
  49. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/__main__.py +0 -0
  50. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/_secrets.py +0 -0
  51. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/agent_config.py +0 -0
  52. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/console.py +0 -0
  53. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/exec_cmd.py +0 -0
  54. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/hook_entry.py +0 -0
  55. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/hosts/__init__.py +0 -0
  56. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/hosts/antigravity.py +0 -0
  57. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/hosts/claims.py +0 -0
  58. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/hosts/claude_code.py +0 -0
  59. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/hosts/codex_cli.py +0 -0
  60. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/hosts/cursor.py +0 -0
  61. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/hosts/gemini_cli.py +0 -0
  62. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/hosts/unknown.py +0 -0
  63. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/kiro_adapter.py +0 -0
  64. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/maturity.py +0 -0
  65. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/migrate.py +0 -0
  66. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/output.py +0 -0
  67. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/spool_cmd.py +0 -0
  68. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/telemetry_consent.py +0 -0
  69. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/templates/antigravity/hooks.json +0 -0
  70. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/templates/antigravity.yaml +0 -0
  71. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/templates/autogen.yaml +0 -0
  72. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/templates/claude-code.yaml +0 -0
  73. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/templates/codex-cli.yaml +0 -0
  74. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/templates/cost-cap.yaml +0 -0
  75. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/templates/crewai.yaml +0 -0
  76. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/templates/cursor.yaml +0 -0
  77. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/templates/gemini-cli.yaml +0 -0
  78. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/templates/generic.yaml +0 -0
  79. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook +0 -0
  80. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/templates/kiro/ide-pre-tool-use.kiro.hook +0 -0
  81. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/templates/kiro/ide-prompt-submit.kiro.hook +0 -0
  82. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/templates/langchain.yaml +0 -0
  83. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/templates/mcp.yaml +0 -0
  84. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/templates/rag.yaml +0 -0
  85. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/cli/update_cmd.py +0 -0
  86. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/device.py +0 -0
  87. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/enrollment.py +0 -0
  88. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/error_codes.py +0 -0
  89. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/error_codes.yaml +0 -0
  90. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/errors.py +0 -0
  91. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/hitl/__init__.py +0 -0
  92. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/hitl/grant_protocol.py +0 -0
  93. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/hitl/mock.py +0 -0
  94. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/hitl/pending_approval.py +0 -0
  95. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/hitl/secret_leak_guard.py +0 -0
  96. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/hitl/status.py +0 -0
  97. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/hooks/__init__.py +0 -0
  98. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/hooks/tool_output_handler.py +0 -0
  99. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/hosted_policy.py +0 -0
  100. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/integrations/__init__.py +0 -0
  101. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/integrations/_mask_apply.py +0 -0
  102. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/integrations/anthropic.py +0 -0
  103. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/integrations/autogen.py +0 -0
  104. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/integrations/braintrust.py +0 -0
  105. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/integrations/crewai/__init__.py +0 -0
  106. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/integrations/crewai/agent.py +0 -0
  107. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/integrations/crewai/crew.py +0 -0
  108. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/integrations/crewai/task.py +0 -0
  109. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/integrations/crewai/tool.py +0 -0
  110. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/integrations/google.py +0 -0
  111. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/integrations/google_adk/__init__.py +0 -0
  112. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/integrations/google_adk/agent.py +0 -0
  113. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/integrations/google_adk/tool.py +0 -0
  114. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/integrations/langchain/__init__.py +0 -0
  115. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/integrations/langchain/agent.py +0 -0
  116. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/integrations/langchain/callbacks.py +0 -0
  117. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/integrations/langchain/chain.py +0 -0
  118. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/integrations/langchain/graph.py +0 -0
  119. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/integrations/langchain/modern.py +0 -0
  120. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/integrations/langchain/tool.py +0 -0
  121. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/integrations/langfuse.py +0 -0
  122. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/integrations/litellm.py +0 -0
  123. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/integrations/openai.py +0 -0
  124. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/integrations/pydantic_ai.py +0 -0
  125. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/integrations/vercel_ai.py +0 -0
  126. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/layout_migration.py +0 -0
  127. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/policy_loader.py +0 -0
  128. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/secrets/__init__.py +0 -0
  129. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/secrets/reference.py +0 -0
  130. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/secrets/resolver.py +0 -0
  131. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/secrets/secretstr.py +0 -0
  132. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/spool/__init__.py +0 -0
  133. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/spool/_compress.py +0 -0
  134. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/spool/_constants.py +0 -0
  135. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/spool/_crc32c.py +0 -0
  136. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/spool/_crypto.py +0 -0
  137. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/spool/_frame.py +0 -0
  138. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/spool/_keyring.py +0 -0
  139. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/spool/_metrics.py +0 -0
  140. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/spool/_spool.py +0 -0
  141. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/spool/_state.py +0 -0
  142. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/spool/_uploader.py +0 -0
  143. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/spool/cz-audit-v1.dict +0 -0
  144. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/tamper.py +0 -0
  145. {controlzero-1.13.11 → controlzero-1.13.12}/controlzero/tracecontext.py +0 -0
@@ -141,8 +141,21 @@ site/
141
141
 
142
142
  # Rust
143
143
  target/
144
- Cargo.lock
145
144
  *.rlib
145
+ # Cargo.lock is COMMITTED, deliberately. Cargo's guidance is to commit the
146
+ # lockfile for binaries and applications and omit it only for libraries
147
+ # published to a registry. Nothing here is published to crates.io: every
148
+ # package in this repo is either a cdylib/staticlib compiled into the
149
+ # Python/Node SDKs across the FFI boundary (crates/*) or a shipped binary
150
+ # (services/shadow-ai-scout). Both are applications for lockfile purposes.
151
+ #
152
+ # It was ignored until 2026-08-14, which meant every CI run re-resolved
153
+ # dependencies from scratch. A third party publishing a new version could,
154
+ # and did, break main with no commit to this repo: icu_* 2.3.0 shipped with
155
+ # MSRV 1.88 and `cargo clippy` on our pinned 1.86.0 started failing on a
156
+ # tree nobody had touched. It also meant the artifact that was tested was
157
+ # not provably the artifact that shipped, so a dependency audit of one said
158
+ # nothing about the other.
146
159
 
147
160
  # Python virtual environments
148
161
  venv/
@@ -1,5 +1,338 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 1.13.12 (2026-08-16): your audit trail now states whether the HOST enforced, and deny rules that never fired now fire
6
+
7
+ Two things in this release change what a customer can conclude from what we
8
+ ship. Read them before the rest.
9
+
10
+ **1. The audit row used to claim a block the host never performs (#2113, #2114,
11
+ #2236).** The row is the artefact an auditor reads to decide whether a customer
12
+ was protected. Until this release, every decision row that REACHED the host-event
13
+ branch came out `enforcement_outcome: enforced`,
14
+ `enforcement_reason: host_enforced`, `enforced: True` -- and on the observe-only
15
+ surfaces that was false.
16
+
17
+ Stated precisely, because the scope matters: this was never all rows. The
18
+ pre-fix `resolve_enforcement` returned earlier and correctly for a non-blocking
19
+ effect (`not_applicable / effect_not_blocking`), a post-execution event
20
+ (`not_enforced / post_execution_event`) and an unknown tier
21
+ (`indeterminate / unknown_tier`), none of which depend on the declaration. The
22
+ rows that were wrong are the ones those guards let through: a blocking effect,
23
+ on a known tier, at a pre-execution event -- which is exactly the population an
24
+ auditor cares about, because it is the set where a block was supposedly imposed.
25
+
26
+ #2203 taught `resolve_enforcement` to consult each adapter's own
27
+ `CoverageDecl`, then wired it into two of its three consumers: the resolver and
28
+ the deny message. The third -- the audit row -- was missed. `client.py` had been
29
+ reading `raw_event` / `deciding_events` / `observe_events` out of
30
+ `context["cz_enforcement_ctx"]` since #2203, but nothing ever put them there.
31
+ Every row was therefore resolved against an empty
32
+ `EventDeclaration(raw="", deciding=(), observe=())`, `declares_anything` was
33
+ False on every row ever written, so the two declaration-dependent branches --
34
+ `not_enforced / event_declared_observe` and
35
+ `indeterminate / event_not_declared_deciding` -- could not fire at all, and any
36
+ row reaching them fell through to the affirmative default instead.
37
+
38
+ Driven on pristine `origin/main`, `kiro_cli` + `userPromptSubmit`:
39
+
40
+ enforcement_outcome : enforced
41
+ enforcement_reason : host_enforced
42
+ enforced : True
43
+
44
+ `KiroCLIAdapter` declares that exact event OBSERVE-ONLY, and kiro-cli 2.16.2 was
45
+ driven answering the prompt after BOTH an exit-2 hook and a structured stdout
46
+ deny. The same row now reads:
47
+
48
+ enforcement_outcome : not_enforced
49
+ enforcement_reason : event_declared_observe_only
50
+ enforced : False
51
+
52
+ **What this means for you.** Rows already in your audit store that say
53
+ `enforced / host_enforced` for a prompt-submission event on an observe-only host
54
+ are not evidence the call was blocked. Nothing about the host's behaviour
55
+ changed in this release -- kiro-cli did not start or stop enforcing anything.
56
+ What changed is that the record now distinguishes "the host imposed this" from
57
+ "we asked and cannot establish that it was imposed". If you query or alert on
58
+ `enforced`, expect observe-only surfaces to move from True to False, and treat
59
+ that as the field becoming correct rather than as a regression.
60
+
61
+ **2. A deny rule naming a host tool name was inert on the shipped hook path
62
+ (#2245).** `deny: Read` was accepted by `validate`, previewed as DENY by
63
+ `test`, and silently ALLOWED by the real `controlzero hook-check`. Three
64
+ surfaces told the customer they were protected while the enforcement path
65
+ ignored the rule, and the audit row for the allowed call was byte-identical to
66
+ one written with no rule authored at all. Measured across the shipped security
67
+ template -- 11 template tool names -- the base enforced 4 of 11 bare names; this
68
+ release enforces 11 of 11, and `controlzero test` now agrees with the hook
69
+ because both build the subject through the hook's own factory.
70
+
71
+ ### Changed: what an audit row asserts about enforcement (#2113, #2114, #2236)
72
+
73
+ - **Enforcement is now bound to the host's own event declaration.** The
74
+ `cz_enforcement_ctx` keys the audit path has always read are now actually
75
+ populated, so the per-host branch can fire.
76
+
77
+ - **An absent enforcement-context key is silence, not an empty declaration
78
+ (#2218).** "This host declared nothing" and "this host declared an explicitly
79
+ empty deciding set" are different facts and are now encoded in the TYPE rather
80
+ than in a flag, so the two cannot collapse into one another.
81
+
82
+ - **`render()` now receives the host event.** It previously did not, so the WIRE
83
+ posture was resolved against an empty `EventDeclaration` while the audit row
84
+ used the real one. A single `hook-check` run could put
85
+ `indeterminate / event_not_declared_deciding` on the wire and write
86
+ `enforced / host_enforced` to the row -- two answers to "was the customer
87
+ protected?" out of one process, on 7 of the 9 shipped adapters. The method's
88
+ own docstring said this could not happen.
89
+
90
+ - **`CursorCLIAdapter.render` no longer drops the posture.** Its override could
91
+ not accept the event pair and rebuilt its `RenderResult` without
92
+ `enforcement`, silently downgrading the ask-path posture to NOT_FINALIZED. A
93
+ control applied at one point on one path, hidden in a subclass override.
94
+
95
+ - **A contradictory declaration is now a named posture, INDETERMINATE.** An
96
+ adapter declaring one event both deciding and observe was previously resolved
97
+ by EVALUATION ORDER -- `is_declared_observe()` is tested first, so the observe
98
+ branch won by position over a contradiction nobody was told about. A
99
+ contradictory declaration is not evidence the host let the call through, and
100
+ `_enforcement_notice` now carries a specific line for it that says it is our
101
+ defect to fix, instead of inheriting the generic "could not confirm" that
102
+ reads identically to the host merely being unobservable. No shipped adapter is
103
+ currently self-contradictory, and that is asserted in both directions.
104
+
105
+ ### Fixed
106
+
107
+ - **A deny rule naming a HOST tool name was inert on the hook path (#2245).**
108
+ The originally-filed hypothesis -- a colon-less subject -- is REFUTED and
109
+ pinned as such: every matcher formats `tool:method` unconditionally, and
110
+ `Read:*` already matched the empty-method subject `Read:`. The real mechanism
111
+ is an alias-resolution asymmetry: the hook resolved the HOST name to its
112
+ canonical family name (`Read` -> `file_read`) and discarded the host spelling
113
+ before the gate ran, while the rule kept the host spelling. `Bash` survived
114
+ only because it is its own canonical name. The gate is now asked about every
115
+ name the call actually carried -- the host spelling AND its canonical family
116
+ name -- and NEVER about sibling aliases, because the alias map is many-to-one
117
+ and canonicalising the RULE would make `deny: Edit` also block `Write`. Both
118
+ directions are pinned. The widening ships behind a phase-1 monotone-closing
119
+ gate: closing flips ship, opening flips are suppressed and counted as
120
+ `match_semantics=new_suppressed`.
121
+
122
+ - **The #2245 phase gate could not see the context axis, and a deny became an
123
+ allow (#2320).** #2245 injects `subject_origin` into the decision context.
124
+ Rule conditions merge the whole context, so a rule can match on a key that did
125
+ not exist before -- and `_choose_match`'s monotone-closing gate enumerated the
126
+ ACTION axis only, so it could not observe a flip caused that way. Driven on
127
+ `main`:
128
+
129
+ base c99dba92e rc=2 DENY policy_id=old-deny (no match_semantics)
130
+ main 6dd972815 rc=0 ALLOW policy_id=origin-allow match_semantics=unchanged
131
+ fixed rc=2 DENY policy_id=old-deny match_semantics=new_suppressed
132
+
133
+ The audit stamped `unchanged` on the exact call whose effect flipped. That is
134
+ worse than an absent field: the record actively asserted the false thing. The
135
+ fix declares, per `SubjectOrigin`, which context keys pre-existed
136
+ (`PRE_WIDENING_CONTEXT_KEYS`, sourced from the pre-fix tree) and evaluates the
137
+ rule walk against both contexts. It is a positive set with import-time
138
+ exhaustiveness, so a new origin is unshippable until declared. `unchanged` is
139
+ now true BY CONSTRUCTION rather than by assumption, and `match_semantics`
140
+ gains `indeterminate` for "the pre-widening verdict could not be established".
141
+ The 11 x 3 action matrix is unchanged at 33/33, so #2245 loses nothing.
142
+
143
+ - **Kiro sub-agent SPAWNS were not gated at `preToolUse` (#2331).**
144
+ `_KIRO_V3_PRE_TOOL_MATCHERS` wired `execute_bash` / `fs_write` / `fs_read` /
145
+ `use_aws` only, so an operator who wrote `deny: use_subagent` had a rule that
146
+ could not fire. Driven on kiro-cli 2.18.1: with the old matcher list
147
+ `use_subagent` reached `preToolUse` ZERO times and the sub-agent completed;
148
+ with the spawn matchers added it reaches `preToolUse` and exit 2 blocks it
149
+ ("PreToolHook blocked the tool execution").
150
+
151
+ **This is admission control in SCOPE, not enforcement in strength**, and the
152
+ limit was observed rather than theorised: in the same run the model responded
153
+ to the blocked spawn by doing the identical work through its own
154
+ `execute_bash`. The Kiro coverage tier stays `partial`.
155
+
156
+ **`use_subagent` is the only spawn tool established as real on kiro-cli
157
+ 2.18.1.** The matcher tuple carries five names. Do not read that as five
158
+ gateable spawn surfaces:
159
+
160
+ - `delegate` -- INERT. Experimental and disabled; the binary says so verbatim
161
+ ("Delegate tool is experimental and not enabled. Use /experiment to enable
162
+ it").
163
+ - `agent_crew` -- INERT. Present in the binary's alias table but not exposed.
164
+ - `spawn_session` -- INERT, and structurally so: it is not a tool name at all
165
+ but a `command` enum value inside the `session_management` tool, which makes
166
+ it wrong as a MATCHER rather than merely unused.
167
+ - `session_management` -- **status NOT ESTABLISHED.** The driven enumeration
168
+ accounted for the three names above; it recorded nothing either way about
169
+ this one, and the shipped Kiro template still carries commented guidance
170
+ naming it. It is neither claimed as coverage nor asserted to be inert here,
171
+ because neither has been driven. Treat it as unproven until it is.
172
+
173
+ Driven: the agent's own tool enumeration returned 13 tools and none of the
174
+ three named INERT above, and declaring all four explicitly surfaced none of
175
+ them. They are carried so the matcher is already in place if a future kiro-cli
176
+ exposes them. Today only `use_subagent` is shown to gate anything, and this
177
+ release claims nothing more.
178
+
179
+ - **`controlzero verify --prove` certified enforcement from an exit code we
180
+ produce (#2305).** Driven on a box where all seven host binaries were ABSENT,
181
+ with the hook repointed at a script whose entire body is
182
+ `cat > /dev/null; exit 2`, it reported `[GREEN] claude_code -- effective gate
183
+ holds + real deny blocked` and `status: ENFORCING`, exit 0. The same shim
184
+ exiting 0 produced RED, which is what establishes that no host was in the loop
185
+ at any point. Three states that must never collapse -- ENFORCING /
186
+ NOT_ENFORCING / UNVERIFIABLE -- were two, and the collapse always resolved
187
+ optimistically. `ENFORCING` is now reachable from ONE point in one resolver,
188
+ and the gate there is deliberately NOT "some host evidence exists" -- that
189
+ weaker gate shipped in the first draft of this module and was refuted, because
190
+ "a config file has a foreign key" unlocked the same sentence as an executed
191
+ binary. Evidence licenses NAMED claims, and an affirmative requires the
192
+ specific `HOST_PRESENT` claim, plus `effective_state_ok` and `probe_blocked`
193
+ both true -- so the verdict object cannot be constructed while asserting
194
+ something it does not hold. `HostEvidence` is produced by collectors that read
195
+ an artifact the HOST emitted, and evidence collected from one adapter cannot
196
+ certify another. Stated exactly rather than absolutely: this closes the
197
+ accidental paths, not a determined one -- the private sentinel can be imported
198
+ deliberately, so this is a chokepoint, not a proof of unforgeability. Also
199
+ fixed in the same pass: probe
200
+ errors collapsed into "did not block"; `doctor` rendered a bare all-clear
201
+ having examined nothing and swallowed reader exceptions; antigravity presence
202
+ tested a path the installer never writes; and install self-checks claimed a
203
+ host blocked something.
204
+
205
+ - **Cross-SDK prompt vocabulary is now pinned (#2331).** Every test asserted one
206
+ SDK against literals it restated itself, so a rename on either side left both
207
+ suites green -- the #2071 shape that let #2108 be closed while still live in
208
+ Python. The Node suite now EXTRACTS the vocabulary from Python source at run
209
+ time and drives Node against it, asserting an exact-set IFF over a bounded
210
+ pool rather than one-way containment, with a match-count and uniqueness
211
+ assertion on every extraction so a moved symbol fails loudly instead of
212
+ passing vacuously.
213
+
214
+ ### Changed (AUDIT SCHEMA BREAK: the per-finding `masked` boolean is gone)
215
+
216
+ - **A DLP finding's `masked: True` is replaced by `mask_state` (#2096).**
217
+ `masked` was stamped from the RULE'S ACTION (`m.action == "mask"`), which is
218
+ what the rule says to DO -- never evidence that anything was done. A call
219
+ DENIED on `mask_unsupported_on_host` redacted nothing and never ran, yet
220
+ produced a findings array byte-identical to one for a call that really was
221
+ redacted. `args_hash` is taken over the ORIGINAL arguments on every path, so
222
+ nothing else in the row discriminated either. Established by driving
223
+ `Client.guard` against pristine `origin/main`: four deny scenarios and one
224
+ allow scenario all emitted `'masked': True`.
225
+
226
+ A boolean cannot carry this truth, so it is now a closed vocabulary, matching
227
+ the Node SDK exactly and asserted against the shared cross-SDK manifest
228
+ `tests/parity/dlp_control_outcomes.json`:
229
+
230
+ | `mask_state` | meaning |
231
+ | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
232
+ | `applied` | the caller acknowledged consuming the redacted args via `decision.apply_masked_args()`. The ONLY value that asserts that acknowledgement. |
233
+ | `declared_not_confirmed` | the SDK redacted into `decision.masked_args` and the caller declared `supports_arg_rewrite=True`; whether it forwards them is not observable. |
234
+ | `not_applied` | a mask rule matched and NOTHING was redacted (the call was denied instead). |
235
+ | `not_applicable` | the rule's action is `detect`/`block`, or this is a post-execution output observation or a third-party export scan. |
236
+
237
+ Treat any unrecognised value as NOT `applied`.
238
+
239
+ **What `applied` does NOT mean.** It is one link short of "the tool received
240
+ redacted bytes", deliberately. `apply_masked_args()` emits the record and then
241
+ returns the args, so a caller can acknowledge and still forward the originals.
242
+ No value in this vocabulary asserts delivery to the tool, because the SDK
243
+ cannot observe what the caller does next.
244
+
245
+ - **`DLPScanner.get_findings_for_audit(matches)` now REQUIRES a second
246
+ argument**, `mask_state`, and rejects both `"applied"` and any value outside
247
+ the vocabulary with a `TypeError` before a single finding is built. The
248
+ posture for an unknown value is REJECT, not downgrade and not pass-through.
249
+
250
+ - **Every decision audit row now carries `dlp_mask_state`**, including rows with
251
+ no DLP findings at all (`not_applicable`). Absence is not a state: "no mask
252
+ was in play" and "a mask fired and was not honoured" are separate, queryable
253
+ facts.
254
+
255
+ - **`decision.masked_args` is deep-cloned and frozen** before `guard()` returns.
256
+ An in-place mutation through the ordinary mapping and sequence operations
257
+ raises the new, exported `MaskedArgsFrozenError` (a `TypeError` subclass) at
258
+ the line attempting it, rather than succeeding and leaving the
259
+ `dlp_mask_applied` record describing bytes the caller never forwarded. Take
260
+ `dict(decision.masked_args)` for a mutable copy. Chosen over a hash comparison
261
+ deliberately: a comparison detects the tamper only if someone remembers to
262
+ call it.
263
+
264
+ **The guarantee is BOUNDED, and the bound is stated rather than implied.**
265
+ Python has no true deep freeze. The protection covers the JSON-shaped tree,
266
+ which is the only shape a redaction is produced in; it is not a claim that
267
+ every in-place mutation is impossible. Specifically: a `set` / `frozenset`
268
+ leaf and a custom mutable object leaf are returned BY IDENTITY, so mutating
269
+ one the caller still holds does change the tree the acknowledgement returns.
270
+ Both are left alone deliberately -- neither is JSON-representable, so neither
271
+ can survive the RFC 8785 canonicaliser or any host envelope, meaning a mask
272
+ decision cannot legitimately carry one.
273
+
274
+ ### Added
275
+
276
+ - **`decision.apply_masked_args()`** -- the single producer of
277
+ `mask_state: "applied"`. Present only on a decision that carries a redaction;
278
+ `None` everywhere else, so "there is nothing to acknowledge" and "the caller
279
+ declined to acknowledge" stay distinguishable. Calling it emits a
280
+ `dlp_mask_applied` audit event carrying `args_hash` + `masked_args_hash` (the
281
+ pair an auditor joins on to see that the bytes actually changed) and returns
282
+ the frozen redacted arguments.
283
+
284
+ - **`controlzero.MaskedArgsFrozenError`** is exported from the package root.
285
+
286
+ ### Known limitations, stated plainly
287
+
288
+ - The Python SDK's DLP decision fields -- `dlp_findings`, `dlp_status`,
289
+ `dlp_masked` and now `dlp_mask_state` -- are written to the LOCAL audit log
290
+ only. `audit_remote._build_wire_entry` is an explicit allowlist and forwards
291
+ none of them, so none of this is visible in a hosted audit trail today. That
292
+ is pre-existing behaviour and unchanged here; #2096 corrects what the record
293
+ SAYS, not where it is shipped.
294
+
295
+ - The Go SDK's `GetFindingsForAudit` gained the same closed vocabulary, but it
296
+ still has NO first-party caller: the Go `Guard` attaches no findings to any
297
+ decision or audit row, so no Go audit row carries a `mask_state`. That is a
298
+ declared gap, pinned by a test that fails the day anyone wires it up. The Go
299
+ SDK also left the build on 2026-08-14 (#2291) and is not published from this
300
+ release.
301
+
302
+ - **Kiro sub-agent coverage is CONFIG-SCOPED, not absent, and one path stays
303
+ ungated.** It would be wrong in both directions to say sub-agents were
304
+ previously ungoverned. Driven on kiro-cli 2.18.1, 19 legs, each with a nonce
305
+ side-effect proving the call actually ran: a sub-agent that runs an agent
306
+ config carrying our hooks DOES fire `preToolUse` and `postToolUse` inside the
307
+ sub-agent (leg G), and a child hook exiting 2 blocks the sub-agent's own tool
308
+ call (leg J). Hooks resolve from the SUB-AGENT'S OWN config and are not
309
+ inherited from the parent.
310
+
311
+ The genuine gap is leg K: a sub-agent spawned WITHOUT an explicit `agent_name`
312
+ runs an implicit config that carries no hooks, and setting
313
+ `chat.defaultAgent` to a hooked agent does not help -- leg K establishes that
314
+ the implicit agent does not resolve to it, so the path cannot be reached by
315
+ writing hooks anywhere on disk. That is kiro **#7755**, which is OPEN with no
316
+ maintainer response and no assignee. Do not cite #7671 for this: it is CLOSED
317
+ AS COMPLETED, was filed against 0.11.130, and the behaviour it describes
318
+ ("hooks will not trigger in subagents") is contradicted by the 2.18.1 binary.
319
+ The vendor documentation still carries that sentence; for a claim about what
320
+ ships today, the binary outranks the doc.
321
+
322
+ Gating the SPAWN is what makes `deny: use_subagent` reachable, and it narrows
323
+ the ungoverned surface rather than eliminating it.
324
+
325
+ - **Scope of every driven Kiro claim in this release: kiro-cli 2.18.1 on
326
+ macOS**, `chat --no-interactive`, `chat.enableSubagent=true`. It is NOT a
327
+ claim about the whole `>=2.1.0,<3.0.0` range the adapter declares.
328
+
329
+ - **Kiro matches by ALIAS FAMILY, not literal matcher name.** Observed on
330
+ 2.18.1 in two independent pairs: an `execute_bash` matcher fired on
331
+ `tool_name` `shell`, and `fs_write` fired on `write`. Each matcher must stay
332
+ a single tool-name token -- a regex-style `shell|execute_bash` alternation
333
+ matches a tool LITERALLY named that, which is to say nothing at all, and
334
+ would silently disable enforcement.
335
+
3
336
  ## 1.13.11 (2026-08-10): kiro_cli prompts were never actually scanned
4
337
 
5
338
  1.13.10 said "Prompt submission is now a scanned surface on the hosts that
@@ -610,7 +943,7 @@ coverage declaration intentionally remains observe-only.
610
943
 
611
944
  ### Fixed
612
945
 
613
- - **kiro-cli Windows / non-UTF-8 install crash (Ken Lee report).** `controlzero
946
+ - **kiro-cli Windows / non-UTF-8 install crash (customer report).** `controlzero
614
947
  kiro init` captured kiro-cli's output in text mode using the OS locale codec, so
615
948
  kiro-cli's UTF-8 output crashed the install's subprocess reader thread on
616
949
  non-UTF-8 Windows locales (Korean CP949, Japanese SJIS, Chinese GBK). That crash
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: controlzero
3
- Version: 1.13.11
3
+ Version: 1.13.12
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
@@ -19,7 +19,7 @@ for framework examples (LangChain, LangGraph, OpenAI Agents, Anthropic, CrewAI,
19
19
  AutoGen, Pydantic AI, MCP servers, and more).
20
20
  """
21
21
 
22
- from controlzero.client import Client
22
+ from controlzero.client import Client, MaskedArgsFrozenError
23
23
  from controlzero.errors import (
24
24
  PolicyDecision,
25
25
  PolicyDeniedError,
@@ -41,10 +41,11 @@ 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.11"
44
+ __version__ = "1.13.12"
45
45
 
46
46
  __all__ = [
47
47
  "Client",
48
+ "MaskedArgsFrozenError",
48
49
  "PendingApproval",
49
50
  "SecretRef",
50
51
  "SecretStr",