controlzero 1.9.9__tar.gz → 1.11.4__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 (256) hide show
  1. {controlzero-1.9.9 → controlzero-1.11.4}/CHANGELOG.md +164 -0
  2. {controlzero-1.9.9 → controlzero-1.11.4}/PKG-INFO +2 -2
  3. {controlzero-1.9.9 → controlzero-1.11.4}/README.md +1 -1
  4. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/__init__.py +5 -1
  5. controlzero-1.11.4/controlzero/__main__.py +12 -0
  6. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/_internal/bundle.py +99 -6
  7. controlzero-1.11.4/controlzero/cli/exec_cmd.py +90 -0
  8. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/main.py +311 -3
  9. controlzero-1.11.4/controlzero/cli/update_cmd.py +170 -0
  10. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/client.py +51 -0
  11. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/hosted_policy.py +123 -2
  12. controlzero-1.11.4/controlzero/secrets/__init__.py +54 -0
  13. controlzero-1.11.4/controlzero/secrets/reference.py +127 -0
  14. controlzero-1.11.4/controlzero/secrets/resolver.py +156 -0
  15. controlzero-1.11.4/controlzero/secrets/secretstr.py +72 -0
  16. {controlzero-1.9.9 → controlzero-1.11.4}/pyproject.toml +1 -1
  17. controlzero-1.11.4/tests/test_antigravity_posttooluse_observe_58.py +295 -0
  18. controlzero-1.11.4/tests/test_bundle_cache_freshness_1303.py +303 -0
  19. controlzero-1.11.4/tests/test_hosted_local_shadow_warn_1265.py +265 -0
  20. controlzero-1.11.4/tests/test_kiro_cli_hook_pin_1265.py +121 -0
  21. controlzero-1.11.4/tests/test_part3_active_policy_count_1303.py +142 -0
  22. controlzero-1.11.4/tests/test_secrets_reference_resolver.py +192 -0
  23. controlzero-1.11.4/tests/test_update_cmd.py +76 -0
  24. controlzero-1.11.4/tests/test_upgrade_nudge.py +54 -0
  25. {controlzero-1.9.9 → controlzero-1.11.4}/.gitignore +0 -0
  26. {controlzero-1.9.9 → controlzero-1.11.4}/Dockerfile.test +0 -0
  27. {controlzero-1.9.9 → controlzero-1.11.4}/LICENSE +0 -0
  28. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/_internal/__init__.py +0 -0
  29. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/_internal/action_aliases.py +0 -0
  30. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/_internal/action_validator.py +0 -0
  31. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/_internal/credential_hook.py +0 -0
  32. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/_internal/credential_scanner.py +0 -0
  33. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/_internal/credentials_data/__init__.py +0 -0
  34. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/_internal/credentials_data/built_in.yaml +0 -0
  35. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/_internal/dlp_scanner.py +0 -0
  36. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/_internal/enforcer.py +0 -0
  37. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/_internal/hook_extractors.py +0 -0
  38. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/_internal/tool_extractors.json +0 -0
  39. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/_internal/types.py +0 -0
  40. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/audit_local.py +0 -0
  41. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/audit_remote.py +0 -0
  42. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/canonical.py +0 -0
  43. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/__init__.py +0 -0
  44. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/__main__.py +0 -0
  45. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/_secrets.py +0 -0
  46. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/console.py +0 -0
  47. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/debug_bundle.py +0 -0
  48. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/doctor.py +0 -0
  49. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/hosts/__init__.py +0 -0
  50. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/hosts/antigravity.py +0 -0
  51. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/hosts/base.py +0 -0
  52. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/hosts/claude_code.py +0 -0
  53. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/hosts/codex_cli.py +0 -0
  54. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/hosts/gemini_cli.py +0 -0
  55. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/hosts/kiro.py +0 -0
  56. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/hosts/unknown.py +0 -0
  57. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/kiro_adapter.py +0 -0
  58. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/migrate.py +0 -0
  59. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/spool_cmd.py +0 -0
  60. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/telemetry_consent.py +0 -0
  61. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/templates/antigravity/hooks.json +0 -0
  62. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/templates/antigravity.yaml +0 -0
  63. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/templates/autogen.yaml +0 -0
  64. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/templates/claude-code.yaml +0 -0
  65. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/templates/codex-cli.yaml +0 -0
  66. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/templates/cost-cap.yaml +0 -0
  67. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/templates/crewai.yaml +0 -0
  68. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/templates/cursor.yaml +0 -0
  69. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/templates/gemini-cli.yaml +0 -0
  70. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/templates/generic.yaml +0 -0
  71. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook +0 -0
  72. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/templates/kiro/ide-pre-tool-use.kiro.hook +0 -0
  73. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/templates/kiro/ide-prompt-submit.kiro.hook +0 -0
  74. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/templates/kiro/kiro.yaml +0 -0
  75. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/templates/langchain.yaml +0 -0
  76. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/templates/mcp.yaml +0 -0
  77. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/cli/templates/rag.yaml +0 -0
  78. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/device.py +0 -0
  79. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/enrollment.py +0 -0
  80. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/error_codes.py +0 -0
  81. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/error_codes.yaml +0 -0
  82. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/errors.py +0 -0
  83. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/hitl/__init__.py +0 -0
  84. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/hitl/grant_protocol.py +0 -0
  85. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/hitl/mock.py +0 -0
  86. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/hitl/pending_approval.py +0 -0
  87. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/hitl/secret_leak_guard.py +0 -0
  88. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/hitl/status.py +0 -0
  89. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/hooks/__init__.py +0 -0
  90. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/hooks/tool_output_handler.py +0 -0
  91. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/integrations/__init__.py +0 -0
  92. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/integrations/anthropic.py +0 -0
  93. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/integrations/autogen.py +0 -0
  94. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/integrations/braintrust.py +0 -0
  95. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/integrations/crewai/__init__.py +0 -0
  96. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/integrations/crewai/agent.py +0 -0
  97. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/integrations/crewai/crew.py +0 -0
  98. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/integrations/crewai/task.py +0 -0
  99. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/integrations/crewai/tool.py +0 -0
  100. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/integrations/google.py +0 -0
  101. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/integrations/google_adk/__init__.py +0 -0
  102. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/integrations/google_adk/agent.py +0 -0
  103. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/integrations/google_adk/tool.py +0 -0
  104. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/integrations/langchain/__init__.py +0 -0
  105. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/integrations/langchain/agent.py +0 -0
  106. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/integrations/langchain/callbacks.py +0 -0
  107. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/integrations/langchain/chain.py +0 -0
  108. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/integrations/langchain/graph.py +0 -0
  109. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/integrations/langchain/modern.py +0 -0
  110. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/integrations/langchain/tool.py +0 -0
  111. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/integrations/langfuse.py +0 -0
  112. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/integrations/litellm.py +0 -0
  113. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/integrations/openai.py +0 -0
  114. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/integrations/pydantic_ai.py +0 -0
  115. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/integrations/vercel_ai.py +0 -0
  116. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/layout_migration.py +0 -0
  117. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/policy_loader.py +0 -0
  118. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/spool/__init__.py +0 -0
  119. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/spool/_compress.py +0 -0
  120. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/spool/_constants.py +0 -0
  121. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/spool/_crc32c.py +0 -0
  122. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/spool/_crypto.py +0 -0
  123. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/spool/_frame.py +0 -0
  124. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/spool/_keyring.py +0 -0
  125. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/spool/_metrics.py +0 -0
  126. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/spool/_spool.py +0 -0
  127. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/spool/_state.py +0 -0
  128. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/spool/_uploader.py +0 -0
  129. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/spool/cz-audit-v1.dict +0 -0
  130. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/tamper.py +0 -0
  131. {controlzero-1.9.9 → controlzero-1.11.4}/controlzero/tracecontext.py +0 -0
  132. {controlzero-1.9.9 → controlzero-1.11.4}/examples/hello_world.py +0 -0
  133. {controlzero-1.9.9 → controlzero-1.11.4}/tests/_fixtures/jcs_args_hash_vectors.json +0 -0
  134. {controlzero-1.9.9 → controlzero-1.11.4}/tests/conftest.py +0 -0
  135. {controlzero-1.9.9 → controlzero-1.11.4}/tests/integrations/__init__.py +0 -0
  136. {controlzero-1.9.9 → controlzero-1.11.4}/tests/integrations/test_google.py +0 -0
  137. {controlzero-1.9.9 → controlzero-1.11.4}/tests/parity/action_aliases.json +0 -0
  138. {controlzero-1.9.9 → controlzero-1.11.4}/tests/spool/__init__.py +0 -0
  139. {controlzero-1.9.9 → controlzero-1.11.4}/tests/spool/conftest.py +0 -0
  140. {controlzero-1.9.9 → controlzero-1.11.4}/tests/spool/test_spool_cli.py +0 -0
  141. {controlzero-1.9.9 → controlzero-1.11.4}/tests/spool/test_spool_concurrency.py +0 -0
  142. {controlzero-1.9.9 → controlzero-1.11.4}/tests/spool/test_spool_conformance.py +0 -0
  143. {controlzero-1.9.9 → controlzero-1.11.4}/tests/spool/test_spool_core.py +0 -0
  144. {controlzero-1.9.9 → controlzero-1.11.4}/tests/spool/test_spool_crash.py +0 -0
  145. {controlzero-1.9.9 → controlzero-1.11.4}/tests/spool/test_spool_diskfull.py +0 -0
  146. {controlzero-1.9.9 → controlzero-1.11.4}/tests/spool/test_spool_durable_default_tamper.py +0 -0
  147. {controlzero-1.9.9 → controlzero-1.11.4}/tests/spool/test_spool_keychain_dek.py +0 -0
  148. {controlzero-1.9.9 → controlzero-1.11.4}/tests/spool/test_spool_sink_wiring.py +0 -0
  149. {controlzero-1.9.9 → controlzero-1.11.4}/tests/spool/test_spool_transcript_localack.py +0 -0
  150. {controlzero-1.9.9 → controlzero-1.11.4}/tests/spool/test_spool_uploader.py +0 -0
  151. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_action_aliases.py +0 -0
  152. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_action_canonicalization.py +0 -0
  153. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_action_validator_t86.py +0 -0
  154. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_agent_name_env.py +0 -0
  155. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_antigravity_adapter.py +0 -0
  156. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_antigravity_ga_blockers_1248.py +0 -0
  157. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_antigravity_hook_check.py +0 -0
  158. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_antigravity_install.py +0 -0
  159. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_antigravity_tool_vocab_1303.py +0 -0
  160. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_api_key_mask.py +0 -0
  161. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_audit_remote.py +0 -0
  162. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_audit_remote_sdk_version.py +0 -0
  163. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_audit_sink_isolation.py +0 -0
  164. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_bundle_parser.py +0 -0
  165. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_bundle_translate.py +0 -0
  166. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_canonical_phase1a.py +0 -0
  167. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_cli_carve_out.py +0 -0
  168. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_cli_debug_bundle.py +0 -0
  169. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_cli_extractor_integration.py +0 -0
  170. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_cli_hook.py +0 -0
  171. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_cli_hosted_refresh.py +0 -0
  172. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_cli_init.py +0 -0
  173. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_cli_init_templates.py +0 -0
  174. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_cli_tail.py +0 -0
  175. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_cli_test.py +0 -0
  176. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_cli_validate.py +0 -0
  177. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_coding_agent_hooks.py +0 -0
  178. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_conditions.py +0 -0
  179. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_config_format_parity_1303.py +0 -0
  180. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_conformance.py +0 -0
  181. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_console.py +0 -0
  182. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_credential_hook.py +0 -0
  183. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_default_action.py +0 -0
  184. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_device.py +0 -0
  185. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_dlp_scanner.py +0 -0
  186. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_doctor.py +0 -0
  187. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_engine_version_consistency.py +0 -0
  188. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_enrollment.py +0 -0
  189. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_env_dump_438.py +0 -0
  190. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_epic_1247_bryan_acceptance.py +0 -0
  191. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_error_codes.py +0 -0
  192. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_errors_e_codes.py +0 -0
  193. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_fail_closed_eval.py +0 -0
  194. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_failopen_1303.py +0 -0
  195. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_glob_matching.py +0 -0
  196. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_hitl_5d_email_install.py +0 -0
  197. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_hitl_6a_cli_flag.py +0 -0
  198. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_hitl_6a_exceptions.py +0 -0
  199. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_hitl_6a_get_secret_hitl.py +0 -0
  200. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_hitl_6a_mock_backend.py +0 -0
  201. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_hitl_6a_pending_approval.py +0 -0
  202. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_hitl_6a_request_approval.py +0 -0
  203. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_hitl_6a_secret_leak_guard.py +0 -0
  204. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_hitl_6a_wait.py +0 -0
  205. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_hitl_conformance.py +0 -0
  206. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_hitl_phase2b_protocol.py +0 -0
  207. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_hitl_reason_codes.py +0 -0
  208. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_hitl_validator_keys.py +0 -0
  209. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_hook_extractors.py +0 -0
  210. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_hosted_local_audit_1247.py +0 -0
  211. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_hosted_policy_e2e.py +0 -0
  212. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_hosts_adapter.py +0 -0
  213. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_hybrid_mode_strict.py +0 -0
  214. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_hybrid_mode_warn.py +0 -0
  215. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_install_hook_command.py +0 -0
  216. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_install_hooks.py +0 -0
  217. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_kiro_adapter.py +0 -0
  218. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_kiro_cli_e2e.py +0 -0
  219. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_kiro_hook_templates.py +0 -0
  220. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_kiro_install.py +0 -0
  221. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_layout_migration_t101.py +0 -0
  222. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_layout_parity_t102.py +0 -0
  223. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_local_mode_dict.py +0 -0
  224. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_local_mode_file_json.py +0 -0
  225. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_local_mode_file_yaml.py +0 -0
  226. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_log_fallback_stderr.py +0 -0
  227. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_log_options_ignored_hosted.py +0 -0
  228. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_log_rotation.py +0 -0
  229. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_migrate.py +0 -0
  230. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_min_sdk_version_gate.py +0 -0
  231. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_multi_client_per_project_175.py +0 -0
  232. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_no_policy_no_key.py +0 -0
  233. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_observe_mode_1247.py +0 -0
  234. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_package_rename_shim.py +0 -0
  235. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_policy_engine_version_phase1b.py +0 -0
  236. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_policy_freshness.py +0 -0
  237. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_policy_settings.py +0 -0
  238. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_policy_source_audit.py +0 -0
  239. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_quarantine.py +0 -0
  240. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_reason_code.py +0 -0
  241. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_refresh.py +0 -0
  242. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_secrets.py +0 -0
  243. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_sql_semantic_class.py +0 -0
  244. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_synthetic_policy_id_t79.py +0 -0
  245. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_t103_precedence.py +0 -0
  246. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_t104_cache_gc.py +0 -0
  247. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_t108_local_override_audit.py +0 -0
  248. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_t96_single_audit_log.py +0 -0
  249. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_t99_install_prefetch_bundle.py +0 -0
  250. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_tamper.py +0 -0
  251. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_tamper_behavior.py +0 -0
  252. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_tamper_hook.py +0 -0
  253. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_telemetry_consent.py +0 -0
  254. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_tracecontext.py +0 -0
  255. {controlzero-1.9.9 → controlzero-1.11.4}/tests/test_unsafe_int_boundary.py +0 -0
  256. {controlzero-1.9.9 → controlzero-1.11.4}/tools/cz-kiro-adapter +0 -0
@@ -1,5 +1,169 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.11.4 -- 2026-06-17 (close the count==0 degraded fail-open + stale-empty-cache replay + cached-bundle integrity, gh#1303)
4
+
5
+ Closes a degraded-bundle fail-open in the count-provenance discriminator plus
6
+ the two residuals tracked on gh#1303 after the 1.9.8 degraded/empty fail-open
7
+ fix. All live in the shared decision core / hosted cache, so all five host
8
+ surfaces (Claude Code, Gemini CLI, Codex CLI, Antigravity, Kiro) inherit them.
9
+
10
+ ### Fixed
11
+
12
+ - **`active_policy_count == 0` no longer treats a degraded bundle as genuinely
13
+ empty (gh#1303 fail-open).** Part 3 made the stamped count authoritative for
14
+ genuine-empty, but `genuinely_empty = (count == 0)` did NOT also require the
15
+ backend's explicit `policies: []`. A bundle with `active_policy_count == 0`
16
+ whose `policies` key was MISSING or non-list (truncated / malformed /
17
+ degraded) therefore read as a genuinely-empty project and fell through to
18
+ OBSERVE -> allow -- the rm-rf fail-open class. Genuine-empty now requires
19
+ `count == 0` AND an explicit `policies: []` (the backend always ships that
20
+ for an empty project -- `bundle_handler.go` builds `Policies` as
21
+ `make([]bundlePolicy, 0)` with no `omitempty`), so a `count == 0` paired with
22
+ a missing/malformed `policies` key now FAILS CLOSED (`BUNDLE_MISSING`). The
23
+ gh#1247 genuine-empty observe posture (count == 0 + explicit `[]`) is
24
+ preserved. The Node and Go SDKs are aligned to the same rule for cross-SDK
25
+ decision parity.
26
+ - **Stale-empty-cache replay window (gh#1303 residual A).** A cached bundle
27
+ whose stamped `metadata.active_policy_count` was a legitimate `0` (the
28
+ project was empty when the bundle was minted) still read as genuinely-empty
29
+ AFTER a policy was later attached -- because a count cannot see time --
30
+ so replaying a stale cached empty bundle could silently OBSERVE-allow a
31
+ project that now HAS a policy. The shared core `translate_to_local_policy`
32
+ now takes a `genuine_empty_is_authoritative` flag; the hosted orchestrator
33
+ drops it to `False` once a cached bundle ages past
34
+ `BUNDLE_CACHE_GENUINE_EMPTY_TTL_S` (the bundle's signed `created_at` vs now).
35
+ A stale empty bundle then FAILS CLOSED (deny / re-fetch, `BUNDLE_MISSING`)
36
+ instead of replaying the gh#1247 observe. A FRESH empty bundle keeps the
37
+ gh#1247 genuine-empty observe posture, and a cached bundle WITH real rules
38
+ is still enforced as the last-known-good fallback at any age.
39
+ - **Cached-bundle integrity verification (gh#1303 residual B).**
40
+ `load_cached_bundle` now verifies `sha256(blob) == the stored .meta
41
+ checksum`. On a mismatch (tampered / corrupt / truncated cache) the cache is
42
+ ignored and the caller re-fetches -- and, if the backend is unreachable,
43
+ fails closed -- rather than silently trusting a blob that no longer matches
44
+ its recorded checksum.
45
+
46
+ ## 1.11.3 -- 2026-06-17 (hosted mode no longer silently ignores a local policy file, #1265)
47
+
48
+ ### Fixed
49
+
50
+ - **Hosted mode now warns when a local policy file is being ignored (#1265
51
+ follow-up).** With an API key set, `controlzero hook-check` enforces the
52
+ HOSTED dashboard bundle and intentionally ignores any local
53
+ `./controlzero.{yaml,yml,json}` or `~/.controlzero/policy.{yaml,yml,json}`
54
+ (T103, so a stale local file cannot silently shadow the dashboard). That
55
+ suppression was SILENT: a customer who dropped a project-local
56
+ `controlzero.yaml` with a deny rule, expecting it to enforce, got no signal
57
+ that hosted mode was winning and the file was inert. The hook now emits a
58
+ loud warning to **stderr** that names the ignored file and explains both
59
+ remedies -- set `CONTROLZERO_LOCAL_OVERRIDE=1` to use the local file, or edit
60
+ the policy in the dashboard. The warning is deduped once per process AND once
61
+ per machine per shadowed path (via a small marker under `~/.controlzero/`), so
62
+ it surfaces the signal without spamming stderr on every tool call in the
63
+ per-call hook-subprocess context. Enforcement behavior is UNCHANGED (hosted
64
+ still wins); the warning never touches stdout, so the exit-0/exit-2 hook
65
+ decision contract that Claude Code / Kiro / Codex read is unaffected.
66
+
67
+ ## 1.11.2 -- 2026-06-17 (Antigravity PostToolUse observe-only contract, #58)
68
+
69
+ ### Fixed
70
+
71
+ - **PostToolUse is now observe-only on the Antigravity surface (#58).**
72
+ The installer wires `controlzero hook-check` to BOTH the `PreToolUse`
73
+ deciding gate and the `PostToolUse` observe event, but `hook-check` never
74
+ read the event name -- it ran the full deciding evaluator on every payload.
75
+ A `PostToolUse` event whose tool matched a deny rule therefore rendered a
76
+ post-execution `{"decision":"deny"}` (or `force_ask` for a HITL gate): a
77
+ contract-incorrect verdict on an observe-only event (the tool has already
78
+ run and cannot be un-run), and on agy's strict stdout schema a deny it can
79
+ neither honor nor cleanly interpret. `hook-check` now recognizes the
80
+ post-execution events (`PostToolUse` / `PostInvocation`, plus Kiro's
81
+ `postToolUse`) and emits the observe no-op **in the form each host expects**:
82
+ Antigravity (the empty-stdout-is-deny host, `decision_via_exit_code=False`)
83
+ gets an explicit `{"decision":"allow"}`; the exit-code hosts (Claude Code /
84
+ Gemini / Codex / Kiro) get **empty stdout + exit 0**, their documented "no
85
+ opinion, proceed" signal. `PostToolUse` is also a Claude Code event name, so
86
+ this path is reached on Claude / Gemini post payloads too -- emitting a
87
+ decision JSON (Claude's `approve`, ...) there would have changed their
88
+ post-hook stdout contract. The DECIDING gate (`PreToolUse`, and any payload
89
+ without a recognized post-event name) keeps its fail-closed deciding
90
+ semantics unchanged. The fix lives in the shared `hook-check` core, so all
91
+ five governed surfaces inherit it.
92
+
93
+
94
+ ## 1.11.1 -- 2026-06-17 (kiro-cli hook fail-open fix, #1265)
95
+
96
+ ### Fixed
97
+
98
+ - **Kiro CLI hook no longer silently fails open (#1265).**
99
+ `controlzero kiro init` wired a BARE `controlzero hook-check` into
100
+ `~/.kiro/settings.json`. On a venv install (or a machine with a broken /
101
+ shadowing `controlzero` on PATH) kiro-cli's hook subshell resolved the wrong
102
+ interpreter, the hook crashed, and kiro-cli FAILED OPEN -- tools ran with no
103
+ enforcement, silently. The hook command is now **interpreter-pinned**:
104
+ `"<python>" -m controlzero hook-check --strict`, so kiro-cli runs the exact
105
+ Python that installed it regardless of PATH/venv. Strict mode moved from the
106
+ Windows-unparseable `CZ_KIRO_CLI_STRICT=1` bash env-prefix to a portable
107
+ `--strict` flag (the env var is still honored for back-compat). New
108
+ `python -m controlzero` entrypoint. `kiro init` now runs a fail-LOUD
109
+ self-check that WARNS if the installed hook does not block a synthetic deny.
110
+
111
+ ## 1.11.0 -- 2026-06-17 (secret references: plaintext out of the agent context)
112
+
113
+ ### Added
114
+
115
+ - **Secret references (`czsec://`).** `controlzero.secret_ref(name)` returns an
116
+ opaque, value-free reference you can hold in the model context, put in tool
117
+ arguments, and pass around freely. It performs no fetch and carries no value.
118
+ - **Egress-time resolution.** `controlzero exec -- CMD` and
119
+ `controlzero.secrets.run(cmd, env=...)` resolve any `czsec://` references in
120
+ the argv / environment to plaintext ONLY at the moment a child process is
121
+ spawned, injecting the value into the child's argv / environment. The
122
+ plaintext never re-enters the parent (agent) process. Resolution reuses
123
+ `Client.get_secret`, so it is still policy-gated, HITL-aware, and audited; the
124
+ audit line records the reference + a non-reversible fingerprint, never the
125
+ value.
126
+ - **`SecretStr` taint wrapper.** When a value must live briefly in-process, it is
127
+ wrapped so `repr` / `str` / `format` redact and iteration is refused; the only
128
+ way to get the bytes is `.reveal()`, called at the egress call site.
129
+
130
+ ### Changed
131
+
132
+ - **`Client.get_secret` now warns** (once per process) that it returns plaintext
133
+ into the agent context, pointing at `secret_ref` + `controlzero exec`. Set
134
+ `CONTROLZERO_SECRETS_PROTECTED=1` to BLOCK plaintext reads entirely
135
+ (`get_secret` raises before any fetch). Default behaviour is unchanged
136
+ (warn-only); existing callers keep working.
137
+
138
+ ## 1.10.1 -- 2026-06-17 (upgrade nudge)
139
+
140
+ ### Added
141
+
142
+ - **Soft upgrade nudge.** The backend now stamps
143
+ `metadata.recommended_sdk_version` on every bundle; when the running SDK is
144
+ below it, the SDK emits ONE non-fatal warning per process pointing at
145
+ `controlzero update`. It never raises and never changes enforcement -- it
146
+ just stops a customer stuck on a version that predates a fix (e.g. the
147
+ no-policy->observe posture and self-explaining deny, #1247/#1303) from
148
+ silently sitting on an already-fixed bug. Bundles without the field (older
149
+ backends) are a no-op.
150
+
151
+ ## 1.10.0 -- 2026-06-17 (self-update command)
152
+
153
+ ### Added
154
+
155
+ - **`controlzero update`** -- self-update the SDK to the latest PyPI release.
156
+ Enforcement and audit fixes ship in the SDK, so a customer on an old version
157
+ keeps hitting already-fixed bugs (e.g. the no-policy->observe posture and
158
+ self-explaining deny, #1247/#1303) until they upgrade. This is the
159
+ one-command upgrade path: `controlzero update` checks PyPI, shows
160
+ `current -> latest`, and runs the matching upgrade (pip / pipx / uv,
161
+ detected from how the SDK was installed, always echoing the exact command and
162
+ falling back to a printed manual command on failure). `controlzero update
163
+ --check` reports only (exit 10 when an update is available) for scripts and
164
+ the upgrade nudge; `--yes` skips the prompt. Degrades gracefully and never
165
+ tracebacks when PyPI is unreachable.
166
+
3
167
  ## 1.9.9 -- 2026-06-17 (Antigravity tool vocab, JSON+YAML config parity, spool reliability)
4
168
 
5
169
  Follow-ups to the gh#1303 fail-open work plus two correctness fixes.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: controlzero
3
- Version: 1.9.9
3
+ Version: 1.11.4
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
@@ -333,7 +333,7 @@ Basic flow:
333
333
  ```python
334
334
  from controlzero import Client
335
335
 
336
- cz = Client(api_key="cz_live_...") # approvals require hosted mode
336
+ cz = Client(api_key="cz_live_...") # approvals run on any Postgres-backed deployment: hosted (SaaS), self-managed, or air-gapped
337
337
 
338
338
  decision = cz.guard("delete_file", {"path": "/etc/passwd"})
339
339
  if decision.decision == "deny" and getattr(decision, "hitl_eligible", False):
@@ -279,7 +279,7 @@ Basic flow:
279
279
  ```python
280
280
  from controlzero import Client
281
281
 
282
- cz = Client(api_key="cz_live_...") # approvals require hosted mode
282
+ cz = Client(api_key="cz_live_...") # approvals run on any Postgres-backed deployment: hosted (SaaS), self-managed, or air-gapped
283
283
 
284
284
  decision = cz.guard("delete_file", {"path": "/etc/passwd"})
285
285
  if decision.decision == "deny" and getattr(decision, "hitl_eligible", False):
@@ -39,12 +39,16 @@ from controlzero.hitl.grant_protocol import (
39
39
  REASON_HITL_RETRY_LOOP,
40
40
  )
41
41
  from controlzero.policy_loader import load_policy
42
+ from controlzero.secrets import SecretRef, SecretStr, secret_ref
42
43
 
43
- __version__ = "1.9.9"
44
+ __version__ = "1.11.4"
44
45
 
45
46
  __all__ = [
46
47
  "Client",
47
48
  "PendingApproval",
49
+ "SecretRef",
50
+ "SecretStr",
51
+ "secret_ref",
48
52
  "REASON_HITL_BACKEND_UNREACHABLE",
49
53
  "REASON_HITL_GRANT_APPROVED",
50
54
  "REASON_HITL_GRANT_CANCELED",
@@ -0,0 +1,12 @@
1
+ """Enable ``python -m controlzero`` to run the CLI.
2
+
3
+ The Kiro CLI hook command pins the interpreter as
4
+ ``"<sys.executable>" -m controlzero hook-check --strict`` (#1265) so kiro-cli
5
+ invokes the exact, importable controlzero the user installed, never a bare
6
+ ``controlzero`` that PATH might resolve to a broken or shadowing install.
7
+ """
8
+
9
+ from controlzero.cli.main import cli
10
+
11
+ if __name__ == "__main__":
12
+ cli()
@@ -464,9 +464,55 @@ def check_min_sdk_version(payload: dict, sdk_version: str) -> None:
464
464
  # --- Schema translation ----------------------------------------------------
465
465
 
466
466
 
467
- def translate_to_local_policy(payload: dict) -> dict:
467
+ def _bundle_active_policy_count(payload: dict) -> Optional[int]:
468
+ """Return ``metadata.active_policy_count`` -- the backend's LIVE count of
469
+ active policy attachments at bundle-build time (#1303 part 3) -- or
470
+ ``None`` when the field is absent / malformed (an older backend that
471
+ predates the field).
472
+
473
+ This is the AUTHORITATIVE empty-vs-degraded discriminator. ``0`` means a
474
+ genuinely-empty project (observe, #1247); ``>0`` means policies ARE
475
+ attached, so an empty translated rule set is a degraded / stripped /
476
+ stale bundle and must fail closed. ``None`` makes the caller fall back to
477
+ the older ``policies: []`` shape heuristic. Never raises -- a malformed
478
+ value is treated as absent (fall back), never as a genuine 0.
479
+ """
480
+ metadata = payload.get("metadata")
481
+ if not isinstance(metadata, dict):
482
+ return None
483
+ count = metadata.get("active_policy_count")
484
+ # bool is an int subclass -- reject it so a stray `true` is not read as 1.
485
+ if isinstance(count, bool) or not isinstance(count, int):
486
+ return None
487
+ if count < 0:
488
+ return None
489
+ return count
490
+
491
+
492
+ def translate_to_local_policy(
493
+ payload: dict,
494
+ *,
495
+ genuine_empty_is_authoritative: bool = True,
496
+ ) -> dict:
468
497
  """Translate a decrypted bundle payload to the local policy dict shape.
469
498
 
499
+ Args:
500
+ payload: the decrypted bundle payload dict.
501
+ genuine_empty_is_authoritative: whether a stamped genuinely-empty
502
+ project (``metadata.active_policy_count == 0``, or an explicit
503
+ empty ``policies: []`` on older backends) may be TRUSTED as the
504
+ #1247 observe posture. Defaults to ``True`` -- a freshly-pulled
505
+ bundle is authoritative. The hosted orchestrator passes ``False``
506
+ when serving a STALE CACHED bundle (#1303 residual A: the
507
+ stale-empty-cache replay window). A cached genuine-empty signal
508
+ cannot see time -- a count that was a legitimate 0 when the project
509
+ WAS empty still reads as genuinely empty after policies are later
510
+ attached -- so once the cached bundle ages past the freshness
511
+ bound the empty signal is no longer trustworthy. With this flag
512
+ ``False`` an empty translated rule set fails CLOSED (re-fetch /
513
+ deny) instead of replaying a stale observe-allow. A FRESH empty
514
+ bundle keeps the #1247 genuine-empty observe path intact.
515
+
470
516
  The bundle's `policies` list comes from the backend in the shape
471
517
  produced by :func:`BundleHandler.SDKPull` (Go: bundlePolicy). The
472
518
  local :class:`PolicyEvaluator` expects the input format from
@@ -556,7 +602,49 @@ def translate_to_local_policy(payload: dict) -> dict:
556
602
  if translated is not None:
557
603
  flat.append(translated)
558
604
 
559
- if not flat and not (isinstance(raw_policies, list) and len(raw_policies) == 0):
605
+ # #1303 part 3: the backend stamps metadata.active_policy_count -- the LIVE
606
+ # attachment count at bundle-build time. It is the AUTHORITATIVE
607
+ # discriminator for the DEGRADED direction: >0 means policies ARE attached,
608
+ # so an empty translated rule set is a degraded/stripped bundle -> fail
609
+ # closed (catches a stripped bundle that still ships an explicit
610
+ # `policies: []` while the project HAS attachments, which the shape check
611
+ # alone read as genuinely empty).
612
+ #
613
+ # For the GENUINELY-EMPTY direction the count is NECESSARY BUT NOT
614
+ # SUFFICIENT: a genuine empty project requires count == 0 AND the backend's
615
+ # explicit `policies: []`. The backend ALWAYS ships that explicit empty list
616
+ # for an empty project (bundle_handler.go builds Policies as
617
+ # `make([]bundlePolicy, 0)` and the JSON field has no `omitempty`), so
618
+ # requiring it costs a legitimate empty project nothing. A count == 0 paired
619
+ # with a MISSING or non-list `policies` key is a truncated / malformed /
620
+ # degraded bundle, NOT a genuine empty project -- trusting the count alone
621
+ # there would OBSERVE -> allow-all a degraded bundle (the rm-rf fail-open
622
+ # class, #1303), so it must fail closed below. Count ABSENT (older backend
623
+ # that predates the field) -> fall back to the explicit `policies: []` shape
624
+ # check.
625
+ active_policy_count = _bundle_active_policy_count(payload)
626
+ explicit_empty_policies = isinstance(raw_policies, list) and len(raw_policies) == 0
627
+ if active_policy_count is not None:
628
+ genuinely_empty = active_policy_count == 0 and explicit_empty_policies
629
+ else:
630
+ genuinely_empty = explicit_empty_policies
631
+
632
+ # #1303 residual A (stale-empty-cache replay): the genuine-empty signal
633
+ # (count==0 / explicit `policies: []`) cannot see TIME. A count that was a
634
+ # legitimate 0 from when the project WAS empty still reads as genuinely
635
+ # empty after policies are later attached, so replaying a STALE cached
636
+ # empty bundle would observe-allow a project that now HAS a policy. The
637
+ # hosted orchestrator knows the bundle age (header.created_at) and passes
638
+ # genuine_empty_is_authoritative=False once a cached bundle is past its
639
+ # freshness bound; in that case we DROP the genuine-empty trust so the
640
+ # zero-rule outcome falls through to the BUNDLE_MISSING fail-closed branch
641
+ # below (re-fetch / deny) instead of replaying a stale observe. A FRESH
642
+ # empty bundle (the default, and every newly-pulled bundle) keeps the
643
+ # #1247 genuine-empty observe path.
644
+ if not genuine_empty_is_authoritative:
645
+ genuinely_empty = False
646
+
647
+ if not flat and not genuinely_empty:
560
648
  # #1303 FAIL-OPEN FIX (the empty-vs-degraded boundary). We have zero
561
649
  # translatable rules, but this is NOT a genuinely-empty project: the
562
650
  # payload carried attached policies that produced no enforceable rules,
@@ -568,10 +656,15 @@ def translate_to_local_policy(payload: dict) -> dict:
568
656
  # genuinely-empty observe path (#1247) below is reached ONLY when the
569
657
  # backend shipped an explicit empty list (`policies: []`).
570
658
  #
571
- # NOTE: a stale CACHED bundle that legitimately held `policies: []` from
572
- # when the project WAS empty is NOT caught here (it looks genuinely
573
- # empty); that residual replay window is closed by bundle provenance +
574
- # freshness (#1303 part 3 / backend active_policy_count).
659
+ # NOTE: with active_policy_count (part 3) a stripped bundle that ships
660
+ # `policies: []` while the project HAS attachments now fails closed
661
+ # above (count > 0). The remaining purely-time-based residual -- a
662
+ # stale CACHED bundle whose stamped count was a legitimate 0 from when
663
+ # the project WAS empty -- is now ALSO routed here: the hosted
664
+ # orchestrator drops genuine_empty_is_authoritative once the cached
665
+ # bundle ages past its freshness bound (#1303 residual A), which clears
666
+ # genuinely_empty above so this fail-closed branch is taken instead of
667
+ # the stale observe replay.
575
668
  # Reuse the registered BUNDLE_MISSING reason_code / synthetic id (both
576
669
  # already in VALID_REASON_CODES / VALID_SYNTHETIC_POLICY_IDS) so no new
577
670
  # error-catalog entry is introduced; the human-readable reason names the
@@ -0,0 +1,90 @@
1
+ """``controlzero exec`` -- run a command with ``czsec://`` references resolved
2
+ to plaintext at the spawn boundary, injected into the child ONLY.
3
+
4
+ Usage::
5
+
6
+ controlzero exec --env OPENAI_API_KEY=czsec://prod/openai-key -- \
7
+ python my_agent.py
8
+
9
+ controlzero exec -- curl -H "Authorization: Bearer czsec://prod/token" https://api
10
+
11
+ Any ``czsec://`` reference in ``--env`` values or in the command's arguments is
12
+ resolved (policy-gated + audited, via ``Client.get_secret``) and placed into the
13
+ child process's environment / argv. The reference -- not the secret -- is all
14
+ that ever lived in the agent / model context; the plaintext appears only in the
15
+ child. This process never prints the resolved value.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ import sys
22
+
23
+ import click
24
+
25
+ from controlzero.secrets.reference import find_refs
26
+ from controlzero.secrets.resolver import resolve_env, resolve_refs
27
+
28
+
29
+ @click.command(
30
+ "exec",
31
+ context_settings={"ignore_unknown_options": True, "allow_interspersed_args": False},
32
+ )
33
+ @click.option(
34
+ "--env",
35
+ "env_overrides",
36
+ multiple=True,
37
+ metavar="NAME=VALUE",
38
+ help="Set a child env var; VALUE may be a czsec:// reference. Repeatable.",
39
+ )
40
+ @click.argument("command", nargs=-1, type=click.UNPROCESSED)
41
+ def exec_cmd(env_overrides, command) -> None:
42
+ """Resolve czsec:// references at spawn and exec COMMAND with them."""
43
+ if not command:
44
+ raise click.UsageError("no command given (usage: controlzero exec -- CMD ARGS)")
45
+
46
+ # Parse --env NAME=VALUE overrides.
47
+ overrides = {}
48
+ for item in env_overrides:
49
+ if "=" not in item:
50
+ raise click.UsageError(f"--env expects NAME=VALUE, got {item!r}")
51
+ name, value = item.split("=", 1)
52
+ if not name:
53
+ raise click.UsageError(f"--env name is empty in {item!r}")
54
+ overrides[name] = value
55
+
56
+ # Build a Client only if there is actually something to resolve -- a plain
57
+ # exec with no references should not require an API key.
58
+ has_refs = any(find_refs(v) for v in overrides.values()) or any(
59
+ find_refs(tok) for tok in command
60
+ )
61
+ client = None
62
+ if has_refs:
63
+ from controlzero import Client
64
+
65
+ client = Client()
66
+ if getattr(client, "_api_key", None) is None:
67
+ raise click.ClickException(
68
+ "resolving czsec:// references requires CONTROLZERO_API_KEY "
69
+ "(the backend owns the secret store)"
70
+ )
71
+
72
+ # Resolve at the boundary. These are plaintext, destined for the child.
73
+ try:
74
+ resolved_argv = list(resolve_refs(command, client=client))
75
+ child_env = dict(os.environ)
76
+ if overrides:
77
+ child_env.update(resolve_env(overrides, client=client))
78
+ except Exception as exc: # surface a clean error, never a half-resolved spawn
79
+ raise click.ClickException(f"secret resolution failed: {exc}") from exc
80
+
81
+ # execvpe replaces this interpreter, so no resolved plaintext lingers in a
82
+ # Python frame after the child starts.
83
+ try:
84
+ os.execvpe(resolved_argv[0], resolved_argv, child_env)
85
+ except FileNotFoundError:
86
+ raise click.ClickException(f"command not found: {resolved_argv[0]!r}")
87
+ except OSError as exc:
88
+ raise click.ClickException(f"failed to exec {resolved_argv[0]!r}: {exc}")
89
+ # Unreachable on success (process is replaced).
90
+ sys.exit(127)