controlzero 1.13.0__tar.gz → 1.13.2__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 (291) hide show
  1. {controlzero-1.13.0 → controlzero-1.13.2}/CHANGELOG.md +41 -0
  2. {controlzero-1.13.0 → controlzero-1.13.2}/PKG-INFO +1 -1
  3. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/__init__.py +1 -1
  4. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/_internal/enforcer.py +24 -0
  5. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/_internal/hook_extractors.py +455 -0
  6. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/main.py +102 -11
  7. {controlzero-1.13.0 → controlzero-1.13.2}/pyproject.toml +1 -1
  8. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_glob_matching.py +86 -0
  9. controlzero-1.13.2/tests/test_kiro_cli_v210_hooks.py +110 -0
  10. controlzero-1.13.2/tests/test_shell_semantic_class.py +244 -0
  11. {controlzero-1.13.0 → controlzero-1.13.2}/.gitignore +0 -0
  12. {controlzero-1.13.0 → controlzero-1.13.2}/Dockerfile.test +0 -0
  13. {controlzero-1.13.0 → controlzero-1.13.2}/LICENSE +0 -0
  14. {controlzero-1.13.0 → controlzero-1.13.2}/README.md +0 -0
  15. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/__main__.py +0 -0
  16. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/_internal/__init__.py +0 -0
  17. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/_internal/_messages.py +0 -0
  18. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/_internal/action_aliases.py +0 -0
  19. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/_internal/action_validator.py +0 -0
  20. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/_internal/bundle.py +0 -0
  21. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/_internal/condition_ops.py +0 -0
  22. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/_internal/credential_hook.py +0 -0
  23. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/_internal/credential_scanner.py +0 -0
  24. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/_internal/credentials_data/__init__.py +0 -0
  25. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/_internal/credentials_data/built_in.yaml +0 -0
  26. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/_internal/dlp_scanner.py +0 -0
  27. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/_internal/io_capture_producer.py +0 -0
  28. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/_internal/source_labels.py +0 -0
  29. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/_internal/tool_extractors.json +0 -0
  30. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/_internal/types.py +0 -0
  31. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/audit_local.py +0 -0
  32. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/audit_remote.py +0 -0
  33. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/canonical.py +0 -0
  34. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/__init__.py +0 -0
  35. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/__main__.py +0 -0
  36. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/_secrets.py +0 -0
  37. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/agent_config.py +0 -0
  38. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/console.py +0 -0
  39. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/debug_bundle.py +0 -0
  40. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/doctor.py +0 -0
  41. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/exec_cmd.py +0 -0
  42. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/hook_entry.py +0 -0
  43. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/hosts/__init__.py +0 -0
  44. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/hosts/antigravity.py +0 -0
  45. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/hosts/base.py +0 -0
  46. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/hosts/claude_code.py +0 -0
  47. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/hosts/codex_cli.py +0 -0
  48. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/hosts/cursor.py +0 -0
  49. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/hosts/cursor_cli.py +0 -0
  50. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/hosts/gemini_cli.py +0 -0
  51. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/hosts/kiro.py +0 -0
  52. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/hosts/unknown.py +0 -0
  53. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/kiro_adapter.py +0 -0
  54. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/migrate.py +0 -0
  55. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/output.py +0 -0
  56. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/spool_cmd.py +0 -0
  57. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/telemetry_consent.py +0 -0
  58. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/templates/antigravity/hooks.json +0 -0
  59. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/templates/antigravity.yaml +0 -0
  60. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/templates/autogen.yaml +0 -0
  61. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/templates/claude-code.yaml +0 -0
  62. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/templates/codex-cli.yaml +0 -0
  63. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/templates/cost-cap.yaml +0 -0
  64. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/templates/crewai.yaml +0 -0
  65. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/templates/cursor.yaml +0 -0
  66. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/templates/gemini-cli.yaml +0 -0
  67. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/templates/generic.yaml +0 -0
  68. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook +0 -0
  69. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/templates/kiro/ide-pre-tool-use.kiro.hook +0 -0
  70. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/templates/kiro/ide-prompt-submit.kiro.hook +0 -0
  71. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/templates/kiro/kiro.yaml +0 -0
  72. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/templates/langchain.yaml +0 -0
  73. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/templates/mcp.yaml +0 -0
  74. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/templates/rag.yaml +0 -0
  75. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/cli/update_cmd.py +0 -0
  76. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/client.py +0 -0
  77. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/device.py +0 -0
  78. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/enrollment.py +0 -0
  79. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/error_codes.py +0 -0
  80. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/error_codes.yaml +0 -0
  81. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/errors.py +0 -0
  82. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/hitl/__init__.py +0 -0
  83. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/hitl/grant_protocol.py +0 -0
  84. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/hitl/mock.py +0 -0
  85. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/hitl/pending_approval.py +0 -0
  86. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/hitl/secret_leak_guard.py +0 -0
  87. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/hitl/status.py +0 -0
  88. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/hook_integrity.py +0 -0
  89. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/hooks/__init__.py +0 -0
  90. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/hooks/tool_output_handler.py +0 -0
  91. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/hosted_policy.py +0 -0
  92. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/integrations/__init__.py +0 -0
  93. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/integrations/anthropic.py +0 -0
  94. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/integrations/autogen.py +0 -0
  95. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/integrations/braintrust.py +0 -0
  96. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/integrations/crewai/__init__.py +0 -0
  97. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/integrations/crewai/agent.py +0 -0
  98. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/integrations/crewai/crew.py +0 -0
  99. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/integrations/crewai/task.py +0 -0
  100. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/integrations/crewai/tool.py +0 -0
  101. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/integrations/google.py +0 -0
  102. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/integrations/google_adk/__init__.py +0 -0
  103. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/integrations/google_adk/agent.py +0 -0
  104. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/integrations/google_adk/tool.py +0 -0
  105. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/integrations/langchain/__init__.py +0 -0
  106. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/integrations/langchain/agent.py +0 -0
  107. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/integrations/langchain/callbacks.py +0 -0
  108. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/integrations/langchain/chain.py +0 -0
  109. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/integrations/langchain/graph.py +0 -0
  110. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/integrations/langchain/modern.py +0 -0
  111. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/integrations/langchain/tool.py +0 -0
  112. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/integrations/langfuse.py +0 -0
  113. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/integrations/litellm.py +0 -0
  114. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/integrations/openai.py +0 -0
  115. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/integrations/pydantic_ai.py +0 -0
  116. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/integrations/vercel_ai.py +0 -0
  117. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/layout_migration.py +0 -0
  118. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/policy_loader.py +0 -0
  119. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/secrets/__init__.py +0 -0
  120. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/secrets/reference.py +0 -0
  121. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/secrets/resolver.py +0 -0
  122. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/secrets/secretstr.py +0 -0
  123. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/spool/__init__.py +0 -0
  124. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/spool/_compress.py +0 -0
  125. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/spool/_constants.py +0 -0
  126. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/spool/_crc32c.py +0 -0
  127. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/spool/_crypto.py +0 -0
  128. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/spool/_frame.py +0 -0
  129. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/spool/_keyring.py +0 -0
  130. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/spool/_metrics.py +0 -0
  131. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/spool/_spool.py +0 -0
  132. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/spool/_state.py +0 -0
  133. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/spool/_uploader.py +0 -0
  134. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/spool/cz-audit-v1.dict +0 -0
  135. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/tamper.py +0 -0
  136. {controlzero-1.13.0 → controlzero-1.13.2}/controlzero/tracecontext.py +0 -0
  137. {controlzero-1.13.0 → controlzero-1.13.2}/examples/hello_world.py +0 -0
  138. {controlzero-1.13.0 → controlzero-1.13.2}/tests/_fixtures/antigravity-envelope-expected.json +0 -0
  139. {controlzero-1.13.0 → controlzero-1.13.2}/tests/_fixtures/jcs_args_hash_vectors.json +0 -0
  140. {controlzero-1.13.0 → controlzero-1.13.2}/tests/conftest.py +0 -0
  141. {controlzero-1.13.0 → controlzero-1.13.2}/tests/integrations/__init__.py +0 -0
  142. {controlzero-1.13.0 → controlzero-1.13.2}/tests/integrations/test_google.py +0 -0
  143. {controlzero-1.13.0 → controlzero-1.13.2}/tests/parity/action_aliases.json +0 -0
  144. {controlzero-1.13.0 → controlzero-1.13.2}/tests/spool/__init__.py +0 -0
  145. {controlzero-1.13.0 → controlzero-1.13.2}/tests/spool/conftest.py +0 -0
  146. {controlzero-1.13.0 → controlzero-1.13.2}/tests/spool/test_spool_cli.py +0 -0
  147. {controlzero-1.13.0 → controlzero-1.13.2}/tests/spool/test_spool_concurrency.py +0 -0
  148. {controlzero-1.13.0 → controlzero-1.13.2}/tests/spool/test_spool_conformance.py +0 -0
  149. {controlzero-1.13.0 → controlzero-1.13.2}/tests/spool/test_spool_core.py +0 -0
  150. {controlzero-1.13.0 → controlzero-1.13.2}/tests/spool/test_spool_crash.py +0 -0
  151. {controlzero-1.13.0 → controlzero-1.13.2}/tests/spool/test_spool_diskfull.py +0 -0
  152. {controlzero-1.13.0 → controlzero-1.13.2}/tests/spool/test_spool_durable_default_tamper.py +0 -0
  153. {controlzero-1.13.0 → controlzero-1.13.2}/tests/spool/test_spool_keychain_dek.py +0 -0
  154. {controlzero-1.13.0 → controlzero-1.13.2}/tests/spool/test_spool_sink_wiring.py +0 -0
  155. {controlzero-1.13.0 → controlzero-1.13.2}/tests/spool/test_spool_transcript_localack.py +0 -0
  156. {controlzero-1.13.0 → controlzero-1.13.2}/tests/spool/test_spool_uploader.py +0 -0
  157. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_action_aliases.py +0 -0
  158. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_action_canonicalization.py +0 -0
  159. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_action_validator_t86.py +0 -0
  160. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_adapter_interface.py +0 -0
  161. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_agent_name_env.py +0 -0
  162. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_agy_docs_no_exit2_1373.py +0 -0
  163. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_antigravity_adapter.py +0 -0
  164. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_antigravity_envelope_parity.py +0 -0
  165. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_antigravity_ga_blockers_1248.py +0 -0
  166. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_antigravity_hook_check.py +0 -0
  167. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_antigravity_install.py +0 -0
  168. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_antigravity_posttooluse_observe_58.py +0 -0
  169. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_antigravity_tool_vocab_1303.py +0 -0
  170. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_api_key_mask.py +0 -0
  171. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_audit_project_id_attribution_pr2.py +0 -0
  172. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_audit_remote.py +0 -0
  173. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_audit_remote_io_capture.py +0 -0
  174. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_audit_remote_sdk_version.py +0 -0
  175. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_audit_sink_isolation.py +0 -0
  176. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_bundle_cache_freshness_1303.py +0 -0
  177. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_bundle_parser.py +0 -0
  178. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_bundle_translate.py +0 -0
  179. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_canonical_phase1a.py +0 -0
  180. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_capability_matrix_doc.py +0 -0
  181. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_cli_carve_out.py +0 -0
  182. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_cli_debug_bundle.py +0 -0
  183. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_cli_extractor_integration.py +0 -0
  184. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_cli_hook.py +0 -0
  185. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_cli_hook_entry.py +0 -0
  186. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_cli_hosted_refresh.py +0 -0
  187. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_cli_init.py +0 -0
  188. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_cli_init_templates.py +0 -0
  189. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_cli_tail.py +0 -0
  190. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_cli_test.py +0 -0
  191. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_cli_validate.py +0 -0
  192. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_coding_agent_hooks.py +0 -0
  193. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_condition_operators_1495.py +0 -0
  194. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_conditions.py +0 -0
  195. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_config_format_parity_1303.py +0 -0
  196. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_conformance.py +0 -0
  197. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_console.py +0 -0
  198. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_credential_hook.py +0 -0
  199. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_cursor_adapter.py +0 -0
  200. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_default_action.py +0 -0
  201. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_deny_rule_reconciliation_1011.py +0 -0
  202. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_device.py +0 -0
  203. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_dlp_mask.py +0 -0
  204. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_dlp_scanner.py +0 -0
  205. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_doctor.py +0 -0
  206. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_engine_version_consistency.py +0 -0
  207. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_enrollment.py +0 -0
  208. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_env_dump_438.py +0 -0
  209. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_epic_1247_bryan_acceptance.py +0 -0
  210. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_error_codes.py +0 -0
  211. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_errors_e_codes.py +0 -0
  212. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_fail_closed_eval.py +0 -0
  213. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_failopen_1303.py +0 -0
  214. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_hitl_5d_email_install.py +0 -0
  215. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_hitl_6a_cli_flag.py +0 -0
  216. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_hitl_6a_exceptions.py +0 -0
  217. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_hitl_6a_get_secret_hitl.py +0 -0
  218. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_hitl_6a_mock_backend.py +0 -0
  219. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_hitl_6a_pending_approval.py +0 -0
  220. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_hitl_6a_request_approval.py +0 -0
  221. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_hitl_6a_secret_leak_guard.py +0 -0
  222. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_hitl_6a_wait.py +0 -0
  223. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_hitl_conformance.py +0 -0
  224. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_hitl_phase2b_protocol.py +0 -0
  225. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_hitl_reason_codes.py +0 -0
  226. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_hitl_validator_keys.py +0 -0
  227. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_hook_extractors.py +0 -0
  228. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_hook_integrity.py +0 -0
  229. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_hook_mask_failclosed.py +0 -0
  230. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_hosted_local_audit_1247.py +0 -0
  231. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_hosted_local_shadow_warn_1265.py +0 -0
  232. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_hosted_policy_e2e.py +0 -0
  233. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_hosts_adapter.py +0 -0
  234. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_hybrid_mode_strict.py +0 -0
  235. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_hybrid_mode_warn.py +0 -0
  236. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_install_hook_command.py +0 -0
  237. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_install_hooks.py +0 -0
  238. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_io_capture_producer.py +0 -0
  239. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_kiro_adapter.py +0 -0
  240. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_kiro_cli_e2e.py +0 -0
  241. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_kiro_cli_hook_pin_1265.py +0 -0
  242. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_kiro_hook_templates.py +0 -0
  243. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_kiro_install.py +0 -0
  244. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_layout_migration_t101.py +0 -0
  245. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_layout_parity_t102.py +0 -0
  246. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_local_mode_dict.py +0 -0
  247. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_local_mode_file_json.py +0 -0
  248. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_local_mode_file_yaml.py +0 -0
  249. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_localized_reason.py +0 -0
  250. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_log_fallback_stderr.py +0 -0
  251. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_log_options_ignored_hosted.py +0 -0
  252. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_log_rotation.py +0 -0
  253. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_migrate.py +0 -0
  254. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_min_sdk_version_gate.py +0 -0
  255. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_multi_client_per_project_175.py +0 -0
  256. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_no_policy_no_key.py +0 -0
  257. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_observe_mode_1247.py +0 -0
  258. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_package_rename_shim.py +0 -0
  259. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_part3_active_policy_count_1303.py +0 -0
  260. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_per_client_isolation_1011.py +0 -0
  261. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_policy_engine_version_phase1b.py +0 -0
  262. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_policy_freshness.py +0 -0
  263. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_policy_load_failclosed_f3.py +0 -0
  264. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_policy_settings.py +0 -0
  265. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_policy_source_audit.py +0 -0
  266. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_pr4_install_messaging.py +0 -0
  267. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_pr4_output.py +0 -0
  268. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_pr4_status.py +0 -0
  269. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_probe_audit_isolation.py +0 -0
  270. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_quarantine.py +0 -0
  271. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_reason_code.py +0 -0
  272. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_refresh.py +0 -0
  273. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_secrets.py +0 -0
  274. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_secrets_reference_resolver.py +0 -0
  275. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_source_labels.py +0 -0
  276. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_sql_semantic_class.py +0 -0
  277. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_synthetic_policy_id_t79.py +0 -0
  278. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_t103_precedence.py +0 -0
  279. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_t104_cache_gc.py +0 -0
  280. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_t108_local_override_audit.py +0 -0
  281. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_t96_single_audit_log.py +0 -0
  282. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_t99_install_prefetch_bundle.py +0 -0
  283. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_tamper.py +0 -0
  284. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_tamper_behavior.py +0 -0
  285. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_tamper_hook.py +0 -0
  286. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_telemetry_consent.py +0 -0
  287. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_tracecontext.py +0 -0
  288. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_unsafe_int_boundary.py +0 -0
  289. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_update_cmd.py +0 -0
  290. {controlzero-1.13.0 → controlzero-1.13.2}/tests/test_upgrade_nudge.py +0 -0
  291. {controlzero-1.13.0 → controlzero-1.13.2}/tools/cz-kiro-adapter +0 -0
@@ -1,5 +1,46 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.13.2 (2026-06-28): kiro-cli 2.10 install crash + cross-adapter hook hardening
4
+
5
+ ### Fixed
6
+
7
+ - **kiro-cli Windows / non-UTF-8 install crash (Ken Lee report).** `controlzero
8
+ kiro init` captured kiro-cli's output in text mode using the OS locale codec, so
9
+ kiro-cli's UTF-8 output crashed the install's subprocess reader thread on
10
+ non-UTF-8 Windows locales (Korean CP949, Japanese SJIS, Chinese GBK). That crash
11
+ could leave the governance agent not set as default, so `kiro-cli chat` then ran
12
+ the built-in default agent ungoverned (no hooks, no audit). All kiro-cli
13
+ shell-outs now decode as UTF-8 (`errors="replace"`) and never crash the install.
14
+ - **Codex hook-trust requirement surfaced at install.** Codex trust-gates the
15
+ non-managed `~/.codex/hooks.json` by hash and SKIPS it until trusted; the
16
+ installer now prints a loud ACTION-REQUIRED step (run `/hooks`, trust the Control
17
+ Zero hook, re-trust after every upgrade) instead of implying it enforces.
18
+ - **Gemini defensive hooks gate.** The installer writes `hooksConfig.enabled=true`
19
+ so an admin/older build with hooks gated off does not silently run ungoverned.
20
+
21
+ ### Notes
22
+
23
+ - kiro-cli 2.10 was verified live to FIRE embedded agent-config `preToolUse` hooks;
24
+ the canonical tool-name matchers (`execute_bash`/`fs_write`/`fs_read`/`use_aws`)
25
+ and the `"*"` all-tools token are correct on 2.10 (both new `shell` and legacy
26
+ `execute_bash` names match).
27
+
28
+ ## 1.13.1 (2026-06-27): shell semantic-class enforcement
29
+
30
+ ### Added
31
+
32
+ - **Shell semantic-class extractor (#437/#1459).** The enforcer maps a shell
33
+ command's first meaningful token to a semantic class
34
+ (`file_write`/`file_delete`/`file_read`/`network`), so a `deny: file_write`
35
+ rule also blocks shell-mediated file operations (e.g. `Bash(touch x)` ->
36
+ `file_write`). Additive layer after the per-command action, most-dangerous-wins
37
+ (`file_delete > network > file_write > file_read`), byte-identical to the Node
38
+ and Go SDKs (shared `parity-cases.json`, 118 cases). Closes the cross-OS
39
+ bypass where a denied `Write` was retried as a PowerShell/`cmd`/Unix file
40
+ write through the Bash tool. Unwraps `sh -c`/`powershell -Command`/`cmd /c`
41
+ launchers and prefix wrappers; recurses `find -delete`/`find -exec`;
42
+ whole-line scan for `.NET` method calls and `/dev/tcp/`.
43
+
3
44
  ## 1.13.0 (2026-06-26): policy-condition operator vocab
4
45
 
5
46
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: controlzero
3
- Version: 1.13.0
3
+ Version: 1.13.2
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.0"
44
+ __version__ = "1.13.2"
45
45
 
46
46
  __all__ = [
47
47
  "Client",
@@ -470,6 +470,30 @@ class PolicyEvaluator:
470
470
  cls = sql_semantic_class(sql_text)
471
471
  if cls:
472
472
  semantic_action = f"{tool}:{cls}"
473
+ # Shell semantic-class layer (#437): the SAME additive pattern for
474
+ # shell-mediated file/network ops. When the tool is the canonical
475
+ # shell (`Bash`) and no class was provided, derive it from the
476
+ # command and promote it into the file_write/file_delete/file_read/
477
+ # network NAMESPACE (`<class>:<class>`) -- NOT `Bash:<class>` --
478
+ # so a rule `deny: file_write` (normalized `file_write:*`) ALSO
479
+ # blocks `Bash(touch x)`. Existing `Bash:*` rules are unaffected
480
+ # because the per-command `action` is always tried first. The
481
+ # hook-check CLI provides the class via
482
+ # ``context['action_semantic_class']``; this is the on-the-fly
483
+ # path for direct ``guard("Bash", args={"command": ...})`` callers.
484
+ if not semantic_action and tool == "Bash" and args:
485
+ from controlzero._internal.hook_extractors import shell_semantic_class
486
+ cmd_text = None
487
+ if isinstance(args, dict):
488
+ for _key in ("command", "CommandLine"):
489
+ _v = args.get(_key)
490
+ if isinstance(_v, str) and _v:
491
+ cmd_text = _v
492
+ break
493
+ if cmd_text:
494
+ cls = shell_semantic_class(cmd_text)
495
+ if cls:
496
+ semantic_action = f"{cls}:{cls}"
473
497
  candidate_actions: list[str] = [action]
474
498
  if semantic_action and semantic_action != action:
475
499
  candidate_actions.append(semantic_action)
@@ -161,6 +161,97 @@ _SHELL_DANGER_ORDER = (
161
161
  )
162
162
  _SHELL_DANGER_RANK = {cmd: i for i, cmd in enumerate(_SHELL_DANGER_ORDER)}
163
163
 
164
+ # Mapping from shell command basename -> canonical semantic class (#437).
165
+ #
166
+ # A second, portable layer parallel to most_dangerous_shell_command's
167
+ # per-command method. A shell-mediated file or network operation is
168
+ # promoted into the file_write / file_delete / file_read / network tool
169
+ # namespace so a rule written against `file_write` (or `file_write:*`)
170
+ # ALSO blocks `Bash(touch x)` -- not just `Write`. Existing `Bash:*`
171
+ # rules are unaffected: this class is an ADDITIVE candidate the
172
+ # evaluator tries AFTER the per-command action (mirrors how the SQL
173
+ # semantic class layers on top of the per-keyword method).
174
+ #
175
+ # Keys are lowercased; the lookup lowercases the basenamed first token,
176
+ # so PowerShell PascalCase cmdlets (New-Item) and any case mix resolve.
177
+ # Covers Unix coreutils, PowerShell cmdlets + their built-in aliases, and
178
+ # cmd.exe builtins, because on Windows most agents spawn PowerShell/cmd and
179
+ # wrap commands via the Bash tool (the #1459 production bypass:
180
+ # `Bash("New-Item ... | Out-Null; Set-Content ...")`). Shell output
181
+ # redirection (`>`/`>>`) and .NET method calls are detected separately and
182
+ # ALSO classify. DELIBERATELY EXCLUDED to avoid denying common-benign
183
+ # commands: bare `sc` (sc.exe service control), `mkdir`/`md`, `echo`,
184
+ # `dir`/`ls`/`gci`, `set`, and arg-aware verbs (`certutil`, `sed`, `dd`,
185
+ # `robocopy`, `awk`, `tar`, `openssl`, `git`, `rsync`) handled in a follow-up.
186
+ _SHELL_CLASS_MAP = {
187
+ # ---- file_write: create / append / copy / move-dest / rename ----
188
+ # Unix
189
+ "touch": "file_write", "tee": "file_write", "cp": "file_write",
190
+ "mv": "file_write", "install": "file_write", "ln": "file_write",
191
+ "truncate": "file_write", "sponge": "file_write", "split": "file_write",
192
+ "patch": "file_write",
193
+ # PowerShell cmdlets + aliases (sc EXCLUDED: sc.exe collision)
194
+ "new-item": "file_write", "ni": "file_write",
195
+ "set-content": "file_write", "add-content": "file_write", "ac": "file_write",
196
+ "out-file": "file_write", "tee-object": "file_write",
197
+ "copy-item": "file_write", "cpi": "file_write",
198
+ "move-item": "file_write", "mi": "file_write",
199
+ "rename-item": "file_write", "rni": "file_write",
200
+ "export-csv": "file_write", "epcsv": "file_write",
201
+ "export-clixml": "file_write",
202
+ # cmd.exe
203
+ "copy": "file_write", "xcopy": "file_write", "move": "file_write",
204
+ "ren": "file_write", "rename": "file_write", "mklink": "file_write",
205
+ # ---- file_delete: irreversible removal / truncate-to-empty ----
206
+ # Unix
207
+ "rm": "file_delete", "rmdir": "file_delete", "unlink": "file_delete",
208
+ "shred": "file_delete", "srm": "file_delete", "wipe": "file_delete",
209
+ "trash": "file_delete", "trash-put": "file_delete",
210
+ # PowerShell
211
+ "remove-item": "file_delete", "ri": "file_delete",
212
+ "clear-content": "file_delete", "clc": "file_delete",
213
+ "clear-item": "file_delete", "cli": "file_delete",
214
+ "remove-itemproperty": "file_delete", "rp": "file_delete",
215
+ # cmd.exe
216
+ "del": "file_delete", "erase": "file_delete", "rd": "file_delete",
217
+ "format": "file_delete",
218
+ # ---- file_read: disclosure ----
219
+ # Unix
220
+ "cat": "file_read", "less": "file_read", "more": "file_read",
221
+ "head": "file_read", "tail": "file_read", "grep": "file_read",
222
+ "od": "file_read", "hexdump": "file_read", "strings": "file_read",
223
+ "cut": "file_read", "tac": "file_read", "nl": "file_read",
224
+ "rev": "file_read", "diff": "file_read", "cmp": "file_read",
225
+ "paste": "file_read", "wc": "file_read",
226
+ # PowerShell
227
+ "get-content": "file_read", "gc": "file_read",
228
+ "select-string": "file_read", "sls": "file_read",
229
+ "import-csv": "file_read", "ipcsv": "file_read",
230
+ "import-clixml": "file_read",
231
+ # cmd.exe readers. `type` EXCLUDED: in Bash/Zsh `type foo` is benign command
232
+ # introspection (`type node`), only a file read in cmd/PowerShell -- needs
233
+ # shell context (follow-up). cmd `find` EXCLUDED (ambiguous with Unix find).
234
+ "fc": "file_read", "comp": "file_read", "findstr": "file_read",
235
+ # ---- network: exfiltration / remote transfer / download ----
236
+ # cross-shell
237
+ "curl": "network", "wget": "network", "nc": "network", "ncat": "network",
238
+ "socat": "network", "telnet": "network", "ssh": "network", "scp": "network",
239
+ "sftp": "network", "gh": "network", "ftp": "network", "tftp": "network",
240
+ "smbclient": "network", "lftp": "network", "aria2c": "network",
241
+ # PowerShell (certutil/bitsadmin EXCLUDED: arg-aware, follow-up)
242
+ "invoke-webrequest": "network", "iwr": "network",
243
+ "invoke-restmethod": "network", "irm": "network",
244
+ "start-bitstransfer": "network",
245
+ }
246
+
247
+ # Class danger ordering: lowest index wins when a compound command's
248
+ # segments resolve to different classes. file_delete (irreversible) >
249
+ # network (exfiltration) > file_write (mutation) > file_read
250
+ # (disclosure). A pipeline `curl evil | rm -rf /` therefore resolves to
251
+ # file_delete so a deny-file_delete rule catches the worst class.
252
+ _SHELL_CLASS_DANGER_ORDER = ("file_delete", "network", "file_write", "file_read")
253
+ _SHELL_CLASS_DANGER_RANK = {c: i for i, c in enumerate(_SHELL_CLASS_DANGER_ORDER)}
254
+
164
255
 
165
256
  # ---------------------------------------------------------------------------
166
257
  # SQL literal/comment stripping
@@ -440,6 +531,63 @@ def _first_token_basename(segment: str) -> str:
440
531
  return token
441
532
 
442
533
 
534
+ def _has_file_redirect(segment: str) -> bool:
535
+ """True if ``segment`` redirects output to a FILE (``>`` or ``>>``).
536
+
537
+ A grammar-free scan mirroring the deliberately dumb shell tokenizer
538
+ (#437): a ``>`` is a file redirect unless it is part of a
539
+ comparison / arrow (``->``, ``=>``, ``<>``) or its target is a
540
+ file-descriptor duplication (``2>&1``, ``>&2``). Quote handling is
541
+ intentionally absent, so ``echo "a > b"`` is conservatively
542
+ classified as a write -- the safe failure direction for a deny rule.
543
+
544
+ Byte-identical to the Node ``hasFileRedirect`` and Go
545
+ ``hasFileRedirect`` implementations.
546
+ """
547
+ n = len(segment)
548
+ i = 0
549
+ # Depth of Bash test / arithmetic contexts. A ``>`` inside ``[[ ... ]]``
550
+ # or ``(( ... ))`` is a string/arith comparison, never a file redirect, so
551
+ # ``[[ "$v" > "1.2" ]]`` must not be classified as a write (would wrongly
552
+ # deny a legitimate conditional under ``deny: file_write``).
553
+ test_depth = 0
554
+ while i < n:
555
+ two = segment[i:i + 2]
556
+ if two == "[[" or two == "((":
557
+ test_depth += 1
558
+ i += 2
559
+ continue
560
+ if two == "]]" or two == "))":
561
+ if test_depth > 0:
562
+ test_depth -= 1
563
+ i += 2
564
+ continue
565
+ if segment[i] != ">":
566
+ i += 1
567
+ continue
568
+ if test_depth > 0:
569
+ i += 1
570
+ continue
571
+ prev = segment[i - 1] if i > 0 else ""
572
+ # ``->``, ``=>``, ``<>`` are operators / dup-source, not a redirect.
573
+ if prev in ("-", "=", "<"):
574
+ i += 1
575
+ continue
576
+ j = i + 1
577
+ # Consume the second ``>`` of an append redirect (``>>``).
578
+ if j < n and segment[j] == ">":
579
+ j += 1
580
+ # Skip whitespace between the operator and its target.
581
+ while j < n and segment[j] in (" ", "\t"):
582
+ j += 1
583
+ # A real file redirect has a target that is not ``&`` (which
584
+ # would be an fd duplication like ``2>&1`` / ``>&2``, not a file).
585
+ if j < n and segment[j] != "&":
586
+ return True
587
+ i = j
588
+ return False
589
+
590
+
443
591
  def most_dangerous_shell_command(cmd: str) -> str:
444
592
  """Return the most dangerous shell command across every segment.
445
593
 
@@ -491,6 +639,312 @@ def most_dangerous_shell_command(cmd: str) -> str:
491
639
  return first_segment_token
492
640
 
493
641
 
642
+ # --- #1459 hardening: bounded wrapper/launcher unwrap ----------------------
643
+ # The extractor still does NOT parse shell grammar. It unwraps a fixed,
644
+ # auditable set of evasion forms so a `deny: file_write` rule is not defeated
645
+ # by `sh -c 'touch x'`, `env touch x`, `sudo rm`, `xargs rm`, `busybox touch`,
646
+ # or `find . -delete`. Forms outside this set (interpreter `-c` with arbitrary
647
+ # code, base64|sh, eval of variables, `-EncodedCommand`) remain documented
648
+ # known limitations. Byte-identical across Python/Node/Go; the adversarial
649
+ # cases are pinned in ``shell_semantic_class_cases`` in ``parity-cases.json``.
650
+ _SHELL_LAUNCHERS = frozenset({"sh", "bash", "zsh", "dash", "ash", "ksh", "su"})
651
+ _POWERSHELL_LAUNCHERS = frozenset({"powershell", "pwsh"})
652
+ _CMD_LAUNCHERS = frozenset({"cmd"})
653
+ # Prefix wrappers consume only flags/operands before the real command. Wrappers
654
+ # that take a POSITIONAL arg first (flock <lockfile>, chroot <dir>, script <file>)
655
+ # are excluded -- they need positional handling and would otherwise misread the
656
+ # arg as the command.
657
+ _SHELL_PREFIX_WRAPPERS = frozenset({
658
+ "sudo", "doas", "env", "command", "builtin", "exec", "nohup", "setsid",
659
+ "time", "nice", "ionice", "stdbuf", "timeout", "xargs", "busybox",
660
+ "watch", "unshare", "nsenter", "parallel",
661
+ })
662
+ _POWERSHELL_CMD_FLAGS = frozenset({"-command", "-c"})
663
+ _CMD_LAUNCHER_FLAGS = frozenset({"/c", "/k"})
664
+ _MAX_SHELL_UNWRAP_DEPTH = 5
665
+
666
+ # Whole-line substring scan (NOT first-token): PowerShell .NET method calls and
667
+ # bash /dev/tcp sockets bypass the verb layer. Lowercased substring -> class.
668
+ _DOTNET_WRITE_SUBSTRINGS = (
669
+ "[system.io.file]::writealltext", "[system.io.file]::appendalltext",
670
+ "[system.io.file]::writeallbytes", "[system.io.file]::create",
671
+ "[system.io.file]::copy", "[system.io.file]::move",
672
+ "streamwriter", "filestream",
673
+ )
674
+ _DOTNET_READ_SUBSTRINGS = (
675
+ "[system.io.file]::readalltext", "[system.io.file]::readallbytes",
676
+ "[system.io.file]::openread", "streamreader",
677
+ )
678
+ _DOTNET_DELETE_SUBSTRINGS = (
679
+ "[system.io.file]::delete", "[system.io.directory]::delete",
680
+ )
681
+ _NETWORK_SUBSTRINGS = (
682
+ ".downloadstring", ".downloadfile", ".uploadfile",
683
+ "net.webclient", "net.sockets", "/dev/tcp/", "/dev/udp/",
684
+ )
685
+
686
+
687
+ def _whole_line_class(cmd_lower: str) -> str:
688
+ """Most-dangerous class from a lowercased whole-command substring scan,
689
+ catching .NET method calls (`[System.IO.File]::WriteAllText`, StreamWriter)
690
+ and bash sockets (`/dev/tcp/`) that are not the first token. ``""`` if none."""
691
+ best_rank: Optional[int] = None
692
+ best_class: str = ""
693
+
694
+ def consider(cls: str) -> None:
695
+ nonlocal best_rank, best_class
696
+ rank = _SHELL_CLASS_DANGER_RANK[cls]
697
+ if best_rank is None or rank < best_rank:
698
+ best_rank = rank
699
+ best_class = cls
700
+
701
+ if any(s in cmd_lower for s in _DOTNET_DELETE_SUBSTRINGS):
702
+ consider("file_delete")
703
+ if any(s in cmd_lower for s in _NETWORK_SUBSTRINGS):
704
+ consider("network")
705
+ if any(s in cmd_lower for s in _DOTNET_WRITE_SUBSTRINGS):
706
+ consider("file_write")
707
+ if any(s in cmd_lower for s in _DOTNET_READ_SUBSTRINGS):
708
+ consider("file_read")
709
+ return best_class
710
+
711
+
712
+ def _cmd_launcher_script(tokens: list) -> Optional[str]:
713
+ """Script after cmd.exe's ``/c`` or ``/k`` (`cmd /c <command>`), dequoted."""
714
+ for j, t in enumerate(tokens):
715
+ if t.lower() in _CMD_LAUNCHER_FLAGS:
716
+ if j + 1 < len(tokens):
717
+ return _strip_matched_quotes(" ".join(tokens[j + 1:]))
718
+ return None
719
+ return None
720
+
721
+
722
+ def _basename_lower(token: str) -> str:
723
+ idx = token.rfind("/")
724
+ base = token[idx + 1:] if idx >= 0 else token
725
+ return base.lower()
726
+
727
+
728
+ def _is_env_assignment(token: str) -> bool:
729
+ """True for a ``VAR=value`` env-assignment prefix token."""
730
+ eq = token.find("=")
731
+ if eq <= 0:
732
+ return False
733
+ name = token[:eq]
734
+ if not (name[0].isalpha() or name[0] == "_"):
735
+ return False
736
+ return all(c.isalnum() or c == "_" for c in name)
737
+
738
+
739
+ # Wrapper option flags that take a SEPARATE operand token, so the operand is
740
+ # skipped too (``sudo -u root rm``, ``xargs -I {} rm``, ``nice -n 10 rm``).
741
+ # Deliberately small + unambiguous: ``-i`` (env ignore-env, no operand) is
742
+ # excluded so ``env -i touch x`` still classifies ``touch``.
743
+ _WRAPPER_VALUE_FLAGS = frozenset({"-u", "--user", "-g", "--group", "-I", "-n", "-p"})
744
+
745
+
746
+ def _is_duration_or_number(token: str) -> bool:
747
+ """A bare number or a duration like ``5`` / ``5s`` (timeout's operand)."""
748
+ if not token:
749
+ return False
750
+ body = token[:-1] if (token[-1] in "smhd" and len(token) >= 2) else token
751
+ if not body:
752
+ return False
753
+ has_digit = False
754
+ for c in body:
755
+ if c.isdigit():
756
+ has_digit = True
757
+ elif c != ".":
758
+ return False
759
+ return has_digit
760
+
761
+
762
+ def _is_wrapper_skip_arg(token: str) -> bool:
763
+ """A token consumed while skipping a prefix wrapper's own args: an option
764
+ (``-x``), a ``VAR=val`` assignment, or a duration/number (``timeout 5s``)."""
765
+ if not token:
766
+ return True
767
+ if token[0] == "-":
768
+ return True
769
+ if _is_env_assignment(token):
770
+ return True
771
+ return _is_duration_or_number(token)
772
+
773
+
774
+ def _launcher_script(tokens: list) -> Optional[str]:
775
+ """Script after a shell launcher's ``-c`` flag, including grouped flags
776
+ like ``-ec`` / ``-lc`` (the rest of the segment, dequoted), or ``None``."""
777
+ for j, t in enumerate(tokens):
778
+ low = t.lower()
779
+ is_c = low == "-c" or (
780
+ len(low) >= 2
781
+ and low[0] == "-"
782
+ and low[1] != "-"
783
+ and low.endswith("c")
784
+ and all(ch.isalpha() for ch in low[1:])
785
+ )
786
+ if is_c:
787
+ if j + 1 < len(tokens):
788
+ return _strip_matched_quotes(" ".join(tokens[j + 1:]))
789
+ return None
790
+ return None
791
+
792
+
793
+ def _strip_matched_quotes(s: str) -> str:
794
+ s = s.strip()
795
+ if len(s) >= 2 and s[0] == s[-1] and s[0] in ("'", '"'):
796
+ return s[1:-1]
797
+ return s
798
+
799
+
800
+ def _script_after_flag(tokens: list, flags) -> Optional[str]:
801
+ """The script string following a standalone ``-c`` / ``-Command`` token
802
+ (the rest of the segment, dequoted), or ``None`` if absent."""
803
+ for j, t in enumerate(tokens):
804
+ if t.lower() in flags:
805
+ if j + 1 < len(tokens):
806
+ return _strip_matched_quotes(" ".join(tokens[j + 1:]))
807
+ return None
808
+ return None
809
+
810
+
811
+ def _classify_find(tokens: list, depth: int) -> str:
812
+ """find(1): ``-delete`` is a delete; ``-exec/-execdir CMD ... ;`` classifies
813
+ CMD by recursing through the same unwrap (so ``-exec sh -c 'rm x'`` and
814
+ ``-exec sudo rm`` are caught); a plain search find has no class."""
815
+ if "-delete" in tokens:
816
+ return "file_delete"
817
+ for flag in ("-exec", "-execdir"):
818
+ if flag in tokens:
819
+ j = tokens.index(flag)
820
+ rest = []
821
+ for t in tokens[j + 1:]:
822
+ if t in (";", "\\;", "+"):
823
+ break
824
+ rest.append(t)
825
+ if rest and depth < _MAX_SHELL_UNWRAP_DEPTH:
826
+ return _classify_shell_segment(" ".join(rest), depth + 1)
827
+ return ""
828
+
829
+
830
+ def _classify_shell_segment(seg: str, depth: int) -> str:
831
+ """Most-dangerous class for one already-split segment, after unwrapping a
832
+ bounded set of launchers/prefixes. ``""`` when nothing classifies."""
833
+ best_rank: Optional[int] = None
834
+ best_class: str = ""
835
+
836
+ def consider(cls: str) -> None:
837
+ nonlocal best_rank, best_class
838
+ if not cls:
839
+ return
840
+ rank = _SHELL_CLASS_DANGER_RANK[cls]
841
+ if best_rank is None or rank < best_rank:
842
+ best_rank = rank
843
+ best_class = cls
844
+
845
+ if _has_file_redirect(seg):
846
+ consider("file_write")
847
+
848
+ tokens = seg.split()
849
+ i = 0
850
+ while i < len(tokens) and _is_env_assignment(tokens[i]):
851
+ i += 1
852
+ guard = 0
853
+ while i < len(tokens) and guard < 8:
854
+ guard += 1
855
+ base = _basename_lower(tokens[i])
856
+ if base in _SHELL_LAUNCHERS:
857
+ sub = _launcher_script(tokens[i:])
858
+ if sub is not None and depth < _MAX_SHELL_UNWRAP_DEPTH:
859
+ consider(_shell_semantic_class_inner(sub, depth + 1))
860
+ break
861
+ if base in _POWERSHELL_LAUNCHERS:
862
+ sub = _script_after_flag(tokens[i:], _POWERSHELL_CMD_FLAGS)
863
+ if sub is not None and depth < _MAX_SHELL_UNWRAP_DEPTH:
864
+ consider(_shell_semantic_class_inner(sub, depth + 1))
865
+ break
866
+ if base in _CMD_LAUNCHERS:
867
+ sub = _cmd_launcher_script(tokens[i:])
868
+ if sub is not None and depth < _MAX_SHELL_UNWRAP_DEPTH:
869
+ consider(_shell_semantic_class_inner(sub, depth + 1))
870
+ break
871
+ if base == "find":
872
+ consider(_classify_find(tokens[i:], depth))
873
+ break
874
+ if base in _SHELL_PREFIX_WRAPPERS:
875
+ i += 1
876
+ while i < len(tokens) and _is_wrapper_skip_arg(tokens[i]):
877
+ flag = tokens[i]
878
+ i += 1
879
+ # skip the operand of a value-taking flag (sudo -u USER, etc.)
880
+ if flag in _WRAPPER_VALUE_FLAGS and i < len(tokens) and not tokens[i].startswith("-"):
881
+ i += 1
882
+ continue
883
+ consider(_SHELL_CLASS_MAP.get(base, ""))
884
+ break
885
+
886
+ return best_class
887
+
888
+
889
+ def _shell_semantic_class_inner(cmd: str, depth: int) -> str:
890
+ if not isinstance(cmd, str) or not cmd.strip():
891
+ return ""
892
+ if depth > _MAX_SHELL_UNWRAP_DEPTH:
893
+ return ""
894
+ best_rank: Optional[int] = None
895
+ best_class: str = ""
896
+
897
+ def consider(cls: str) -> None:
898
+ nonlocal best_rank, best_class
899
+ if not cls:
900
+ return
901
+ rank = _SHELL_CLASS_DANGER_RANK[cls]
902
+ if best_rank is None or rank < best_rank:
903
+ best_rank = rank
904
+ best_class = cls
905
+
906
+ # Whole-line scan first (.NET method calls / /dev/tcp are not first tokens).
907
+ consider(_whole_line_class(cmd.lower()))
908
+ for seg in _split_shell_segments(cmd):
909
+ consider(_classify_shell_segment(seg, depth))
910
+ return best_class
911
+
912
+
913
+ def shell_semantic_class(cmd: str) -> str:
914
+ """Return the most-dangerous shell semantic class across every segment.
915
+
916
+ Maps shell-mediated file and network operations to one of
917
+ ``file_write | file_delete | file_read | network`` (or ``""`` when
918
+ no segment classifies). Closes the #437 bypass where a policy rule
919
+ ``deny: file_write`` blocked ``Write(...)`` but NOT the equivalent
920
+ ``Bash(touch x)`` / ``PowerShell(New-Item ...)`` because the shell
921
+ was treated as an opaque tool.
922
+
923
+ Per segment the class is the more-dangerous of:
924
+
925
+ - the basenamed, lowercased first command token looked up in
926
+ ``_SHELL_CLASS_MAP`` (``rm`` -> file_delete, ``curl`` -> network,
927
+ ``New-Item`` -> file_write), and
928
+ - ``file_write`` when the segment redirects output to a file
929
+ (``>`` / ``>>``), so ``cat a > b`` is a write, not a read.
930
+
931
+ Compound commands (pipes, ``;``, ``&&``, ``||``, command
932
+ substitution) reuse :func:`_split_shell_segments` -- the SAME
933
+ tokenizer :func:`most_dangerous_shell_command` uses -- and the
934
+ most-dangerous class across all segments wins (file_delete >
935
+ network > file_write > file_read).
936
+
937
+ Empty / non-string / unrecognized input -> ``""``. The shell grammar
938
+ is deliberately not parsed (no quote stripping, no alias expansion);
939
+ see the spec README "Known limitations".
940
+
941
+ Byte-identical to the Node ``shellSemanticClass`` and Go
942
+ ``ShellSemanticClass`` implementations; parity is asserted by
943
+ ``shell_semantic_class_cases`` in ``parity-cases.json``.
944
+ """
945
+ return _shell_semantic_class_inner(cmd, 0)
946
+
947
+
494
948
  def identity(v: str) -> str:
495
949
  """Return ``v`` unchanged. Used for browser ``action``."""
496
950
  if not isinstance(v, str):
@@ -646,6 +1100,7 @@ __all__ = [
646
1100
  "most_dangerous_shell_command",
647
1101
  "most_dangerous_sql_keyword",
648
1102
  "resolve_canonical_tool",
1103
+ "shell_semantic_class",
649
1104
  "sql_semantic_class",
650
1105
  "tool_name_as_method",
651
1106
  ]