controlzero 1.10.1__tar.gz → 1.11.5__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.
- {controlzero-1.10.1 → controlzero-1.11.5}/CHANGELOG.md +157 -1
- {controlzero-1.10.1 → controlzero-1.11.5}/PKG-INFO +2 -2
- {controlzero-1.10.1 → controlzero-1.11.5}/README.md +1 -1
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/__init__.py +5 -1
- controlzero-1.11.5/controlzero/__main__.py +12 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/_internal/bundle.py +99 -6
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/doctor.py +3 -1
- controlzero-1.11.5/controlzero/cli/exec_cmd.py +90 -0
- controlzero-1.11.5/controlzero/cli/hook_entry.py +118 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/hosts/__init__.py +18 -1
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/hosts/antigravity.py +92 -4
- controlzero-1.11.5/controlzero/cli/hosts/base.py +532 -0
- controlzero-1.11.5/controlzero/cli/hosts/claude_code.py +191 -0
- controlzero-1.11.5/controlzero/cli/hosts/codex_cli.py +101 -0
- controlzero-1.11.5/controlzero/cli/hosts/cursor.py +322 -0
- controlzero-1.11.5/controlzero/cli/hosts/cursor_cli.py +223 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/hosts/gemini_cli.py +27 -7
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/hosts/kiro.py +53 -3
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/hosts/unknown.py +18 -2
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/main.py +746 -47
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/client.py +51 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/error_codes.yaml +22 -3
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/hosted_policy.py +84 -2
- controlzero-1.11.5/controlzero/secrets/__init__.py +54 -0
- controlzero-1.11.5/controlzero/secrets/reference.py +127 -0
- controlzero-1.11.5/controlzero/secrets/resolver.py +156 -0
- controlzero-1.11.5/controlzero/secrets/secretstr.py +72 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/pyproject.toml +8 -2
- controlzero-1.11.5/tests/_fixtures/antigravity-envelope-expected.json +146 -0
- controlzero-1.11.5/tests/test_adapter_interface.py +384 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_antigravity_adapter.py +70 -11
- controlzero-1.11.5/tests/test_antigravity_envelope_parity.py +96 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_antigravity_ga_blockers_1248.py +3 -3
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_antigravity_install.py +48 -0
- controlzero-1.11.5/tests/test_antigravity_posttooluse_observe_58.py +295 -0
- controlzero-1.11.5/tests/test_bundle_cache_freshness_1303.py +303 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_cli_hook.py +15 -3
- controlzero-1.11.5/tests/test_cli_hook_entry.py +81 -0
- controlzero-1.11.5/tests/test_cursor_adapter.py +362 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_device.py +25 -38
- controlzero-1.11.5/tests/test_hosted_local_shadow_warn_1265.py +265 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hosts_adapter.py +207 -33
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_install_hooks.py +24 -11
- controlzero-1.11.5/tests/test_kiro_cli_hook_pin_1265.py +121 -0
- controlzero-1.11.5/tests/test_part3_active_policy_count_1303.py +142 -0
- controlzero-1.11.5/tests/test_policy_load_failclosed_f3.py +279 -0
- controlzero-1.11.5/tests/test_secrets_reference_resolver.py +192 -0
- controlzero-1.10.1/controlzero/cli/hosts/base.py +0 -171
- controlzero-1.10.1/controlzero/cli/hosts/claude_code.py +0 -135
- controlzero-1.10.1/controlzero/cli/hosts/codex_cli.py +0 -54
- {controlzero-1.10.1 → controlzero-1.11.5}/.gitignore +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/Dockerfile.test +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/LICENSE +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/_internal/__init__.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/_internal/action_aliases.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/_internal/action_validator.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/_internal/credential_hook.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/_internal/credential_scanner.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/_internal/credentials_data/__init__.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/_internal/credentials_data/built_in.yaml +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/_internal/dlp_scanner.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/_internal/enforcer.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/_internal/hook_extractors.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/_internal/tool_extractors.json +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/_internal/types.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/audit_local.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/audit_remote.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/canonical.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/__init__.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/__main__.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/_secrets.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/console.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/debug_bundle.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/kiro_adapter.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/migrate.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/spool_cmd.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/telemetry_consent.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/antigravity/hooks.json +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/antigravity.yaml +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/autogen.yaml +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/claude-code.yaml +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/codex-cli.yaml +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/cost-cap.yaml +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/crewai.yaml +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/cursor.yaml +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/gemini-cli.yaml +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/generic.yaml +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/kiro/ide-pre-tool-use.kiro.hook +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/kiro/ide-prompt-submit.kiro.hook +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/kiro/kiro.yaml +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/langchain.yaml +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/mcp.yaml +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/templates/rag.yaml +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/cli/update_cmd.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/device.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/enrollment.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/error_codes.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/errors.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/hitl/__init__.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/hitl/grant_protocol.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/hitl/mock.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/hitl/pending_approval.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/hitl/secret_leak_guard.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/hitl/status.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/hooks/__init__.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/hooks/tool_output_handler.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/__init__.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/anthropic.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/autogen.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/braintrust.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/crewai/__init__.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/crewai/agent.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/crewai/crew.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/crewai/task.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/crewai/tool.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/google.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/google_adk/__init__.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/google_adk/agent.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/google_adk/tool.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/langchain/__init__.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/langchain/agent.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/langchain/callbacks.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/langchain/chain.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/langchain/graph.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/langchain/modern.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/langchain/tool.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/langfuse.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/litellm.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/openai.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/pydantic_ai.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/integrations/vercel_ai.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/layout_migration.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/policy_loader.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/spool/__init__.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/spool/_compress.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/spool/_constants.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/spool/_crc32c.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/spool/_crypto.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/spool/_frame.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/spool/_keyring.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/spool/_metrics.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/spool/_spool.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/spool/_state.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/spool/_uploader.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/spool/cz-audit-v1.dict +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/tamper.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/controlzero/tracecontext.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/examples/hello_world.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/_fixtures/jcs_args_hash_vectors.json +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/conftest.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/integrations/__init__.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/integrations/test_google.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/parity/action_aliases.json +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/spool/__init__.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/spool/conftest.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/spool/test_spool_cli.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/spool/test_spool_concurrency.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/spool/test_spool_conformance.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/spool/test_spool_core.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/spool/test_spool_crash.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/spool/test_spool_diskfull.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/spool/test_spool_durable_default_tamper.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/spool/test_spool_keychain_dek.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/spool/test_spool_sink_wiring.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/spool/test_spool_transcript_localack.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/spool/test_spool_uploader.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_action_aliases.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_action_canonicalization.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_action_validator_t86.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_agent_name_env.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_antigravity_hook_check.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_antigravity_tool_vocab_1303.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_api_key_mask.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_audit_remote.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_audit_remote_sdk_version.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_audit_sink_isolation.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_bundle_parser.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_bundle_translate.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_canonical_phase1a.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_cli_carve_out.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_cli_debug_bundle.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_cli_extractor_integration.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_cli_hosted_refresh.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_cli_init.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_cli_init_templates.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_cli_tail.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_cli_test.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_cli_validate.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_coding_agent_hooks.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_conditions.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_config_format_parity_1303.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_conformance.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_console.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_credential_hook.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_default_action.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_dlp_scanner.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_doctor.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_engine_version_consistency.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_enrollment.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_env_dump_438.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_epic_1247_bryan_acceptance.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_error_codes.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_errors_e_codes.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_fail_closed_eval.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_failopen_1303.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_glob_matching.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hitl_5d_email_install.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hitl_6a_cli_flag.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hitl_6a_exceptions.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hitl_6a_get_secret_hitl.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hitl_6a_mock_backend.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hitl_6a_pending_approval.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hitl_6a_request_approval.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hitl_6a_secret_leak_guard.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hitl_6a_wait.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hitl_conformance.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hitl_phase2b_protocol.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hitl_reason_codes.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hitl_validator_keys.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hook_extractors.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hosted_local_audit_1247.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hosted_policy_e2e.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hybrid_mode_strict.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_hybrid_mode_warn.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_install_hook_command.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_kiro_adapter.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_kiro_cli_e2e.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_kiro_hook_templates.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_kiro_install.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_layout_migration_t101.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_layout_parity_t102.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_local_mode_dict.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_local_mode_file_json.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_local_mode_file_yaml.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_log_fallback_stderr.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_log_options_ignored_hosted.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_log_rotation.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_migrate.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_min_sdk_version_gate.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_multi_client_per_project_175.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_no_policy_no_key.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_observe_mode_1247.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_package_rename_shim.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_policy_engine_version_phase1b.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_policy_freshness.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_policy_settings.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_policy_source_audit.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_quarantine.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_reason_code.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_refresh.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_secrets.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_sql_semantic_class.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_synthetic_policy_id_t79.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_t103_precedence.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_t104_cache_gc.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_t108_local_override_audit.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_t96_single_audit_log.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_t99_install_prefetch_bundle.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_tamper.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_tamper_behavior.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_tamper_hook.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_telemetry_consent.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_tracecontext.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_unsafe_int_boundary.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_update_cmd.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tests/test_upgrade_nudge.py +0 -0
- {controlzero-1.10.1 → controlzero-1.11.5}/tools/cz-kiro-adapter +0 -0
|
@@ -1,5 +1,161 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.11.5 -- 2026-06-19 (cross-surface CONTROLZERO_CLIENT foreign-pin fail-open)
|
|
4
|
+
|
|
5
|
+
Closes a routing fail-open where an explicit `CONTROLZERO_CLIENT` pin -- set by
|
|
6
|
+
`controlzero install <host>` so a kiro/codex/antigravity/cursor hook routes to
|
|
7
|
+
the right adapter -- was NOT authoritative. The `ClaudeCodeAdapter` is first in
|
|
8
|
+
the registry and falls through to ambient Anthropic env hints (`CLAUDECODE` /
|
|
9
|
+
`CLAUDE_CODE` / ...), so a correctly installed foreign-host hook that ALSO
|
|
10
|
+
carried an ambient `CLAUDECODE` (e.g. launched from a Claude Code-spawned shell)
|
|
11
|
+
was hijacked to the Claude adapter, which renders the wrong wire shape and reads
|
|
12
|
+
as allow on a destructive tool call.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **Foreign `CONTROLZERO_CLIENT` pin now defers (cross-surface fail-open).**
|
|
17
|
+
Every host adapter's `claim()` defers when `CONTROLZERO_CLIENT` names a
|
|
18
|
+
DIFFERENT client, via the shared `HostAdapter._pinned_elsewhere` helper, so an
|
|
19
|
+
installer-pinned hook can never be hijacked by an ambient `CLAUDECODE`;
|
|
20
|
+
`UnknownHostAdapter` stays the always-claim backstop. A 7-surface guard matrix
|
|
21
|
+
(`tests/test_hosts_adapter.py`) pins it fail-if-reverted. Mirrors the Node SDK
|
|
22
|
+
fix. gh#81.
|
|
23
|
+
|
|
24
|
+
## 1.11.4 -- 2026-06-17 (close the count==0 degraded fail-open + stale-empty-cache replay + cached-bundle integrity, gh#1303)
|
|
25
|
+
|
|
26
|
+
Closes a degraded-bundle fail-open in the count-provenance discriminator plus
|
|
27
|
+
the two residuals tracked on gh#1303 after the 1.9.8 degraded/empty fail-open
|
|
28
|
+
fix. All live in the shared decision core / hosted cache, so all five host
|
|
29
|
+
surfaces (Claude Code, Gemini CLI, Codex CLI, Antigravity, Kiro) inherit them.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **`active_policy_count == 0` no longer treats a degraded bundle as genuinely
|
|
34
|
+
empty (gh#1303 fail-open).** Part 3 made the stamped count authoritative for
|
|
35
|
+
genuine-empty, but `genuinely_empty = (count == 0)` did NOT also require the
|
|
36
|
+
backend's explicit `policies: []`. A bundle with `active_policy_count == 0`
|
|
37
|
+
whose `policies` key was MISSING or non-list (truncated / malformed /
|
|
38
|
+
degraded) therefore read as a genuinely-empty project and fell through to
|
|
39
|
+
OBSERVE -> allow -- the rm-rf fail-open class. Genuine-empty now requires
|
|
40
|
+
`count == 0` AND an explicit `policies: []` (the backend always ships that
|
|
41
|
+
for an empty project -- `bundle_handler.go` builds `Policies` as
|
|
42
|
+
`make([]bundlePolicy, 0)` with no `omitempty`), so a `count == 0` paired with
|
|
43
|
+
a missing/malformed `policies` key now FAILS CLOSED (`BUNDLE_MISSING`). The
|
|
44
|
+
gh#1247 genuine-empty observe posture (count == 0 + explicit `[]`) is
|
|
45
|
+
preserved. The Node and Go SDKs are aligned to the same rule for cross-SDK
|
|
46
|
+
decision parity.
|
|
47
|
+
- **Stale-empty-cache replay window (gh#1303 residual A).** A cached bundle
|
|
48
|
+
whose stamped `metadata.active_policy_count` was a legitimate `0` (the
|
|
49
|
+
project was empty when the bundle was minted) still read as genuinely-empty
|
|
50
|
+
AFTER a policy was later attached -- because a count cannot see time --
|
|
51
|
+
so replaying a stale cached empty bundle could silently OBSERVE-allow a
|
|
52
|
+
project that now HAS a policy. The shared core `translate_to_local_policy`
|
|
53
|
+
now takes a `genuine_empty_is_authoritative` flag; the hosted orchestrator
|
|
54
|
+
drops it to `False` once a cached bundle ages past
|
|
55
|
+
`BUNDLE_CACHE_GENUINE_EMPTY_TTL_S` (the bundle's signed `created_at` vs now).
|
|
56
|
+
A stale empty bundle then FAILS CLOSED (deny / re-fetch, `BUNDLE_MISSING`)
|
|
57
|
+
instead of replaying the gh#1247 observe. A FRESH empty bundle keeps the
|
|
58
|
+
gh#1247 genuine-empty observe posture, and a cached bundle WITH real rules
|
|
59
|
+
is still enforced as the last-known-good fallback at any age.
|
|
60
|
+
- **Cached-bundle integrity verification (gh#1303 residual B).**
|
|
61
|
+
`load_cached_bundle` now verifies `sha256(blob) == the stored .meta
|
|
62
|
+
checksum`. On a mismatch (tampered / corrupt / truncated cache) the cache is
|
|
63
|
+
ignored and the caller re-fetches -- and, if the backend is unreachable,
|
|
64
|
+
fails closed -- rather than silently trusting a blob that no longer matches
|
|
65
|
+
its recorded checksum.
|
|
66
|
+
|
|
67
|
+
## 1.11.3 -- 2026-06-17 (hosted mode no longer silently ignores a local policy file, #1265)
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
|
|
71
|
+
- **Hosted mode now warns when a local policy file is being ignored (#1265
|
|
72
|
+
follow-up).** With an API key set, `controlzero hook-check` enforces the
|
|
73
|
+
HOSTED dashboard bundle and intentionally ignores any local
|
|
74
|
+
`./controlzero.{yaml,yml,json}` or `~/.controlzero/policy.{yaml,yml,json}`
|
|
75
|
+
(T103, so a stale local file cannot silently shadow the dashboard). That
|
|
76
|
+
suppression was SILENT: a customer who dropped a project-local
|
|
77
|
+
`controlzero.yaml` with a deny rule, expecting it to enforce, got no signal
|
|
78
|
+
that hosted mode was winning and the file was inert. The hook now emits a
|
|
79
|
+
loud warning to **stderr** that names the ignored file and explains both
|
|
80
|
+
remedies -- set `CONTROLZERO_LOCAL_OVERRIDE=1` to use the local file, or edit
|
|
81
|
+
the policy in the dashboard. The warning is deduped once per process AND once
|
|
82
|
+
per machine per shadowed path (via a small marker under `~/.controlzero/`), so
|
|
83
|
+
it surfaces the signal without spamming stderr on every tool call in the
|
|
84
|
+
per-call hook-subprocess context. Enforcement behavior is UNCHANGED (hosted
|
|
85
|
+
still wins); the warning never touches stdout, so the exit-0/exit-2 hook
|
|
86
|
+
decision contract that Claude Code / Kiro / Codex read is unaffected.
|
|
87
|
+
|
|
88
|
+
## 1.11.2 -- 2026-06-17 (Antigravity PostToolUse observe-only contract, #58)
|
|
89
|
+
|
|
90
|
+
### Fixed
|
|
91
|
+
|
|
92
|
+
- **PostToolUse is now observe-only on the Antigravity surface (#58).**
|
|
93
|
+
The installer wires `controlzero hook-check` to BOTH the `PreToolUse`
|
|
94
|
+
deciding gate and the `PostToolUse` observe event, but `hook-check` never
|
|
95
|
+
read the event name -- it ran the full deciding evaluator on every payload.
|
|
96
|
+
A `PostToolUse` event whose tool matched a deny rule therefore rendered a
|
|
97
|
+
post-execution `{"decision":"deny"}` (or `force_ask` for a HITL gate): a
|
|
98
|
+
contract-incorrect verdict on an observe-only event (the tool has already
|
|
99
|
+
run and cannot be un-run), and on agy's strict stdout schema a deny it can
|
|
100
|
+
neither honor nor cleanly interpret. `hook-check` now recognizes the
|
|
101
|
+
post-execution events (`PostToolUse` / `PostInvocation`, plus Kiro's
|
|
102
|
+
`postToolUse`) and emits the observe no-op **in the form each host expects**:
|
|
103
|
+
Antigravity (the empty-stdout-is-deny host, `decision_via_exit_code=False`)
|
|
104
|
+
gets an explicit `{"decision":"allow"}`; the exit-code hosts (Claude Code /
|
|
105
|
+
Gemini / Codex / Kiro) get **empty stdout + exit 0**, their documented "no
|
|
106
|
+
opinion, proceed" signal. `PostToolUse` is also a Claude Code event name, so
|
|
107
|
+
this path is reached on Claude / Gemini post payloads too -- emitting a
|
|
108
|
+
decision JSON (Claude's `approve`, ...) there would have changed their
|
|
109
|
+
post-hook stdout contract. The DECIDING gate (`PreToolUse`, and any payload
|
|
110
|
+
without a recognized post-event name) keeps its fail-closed deciding
|
|
111
|
+
semantics unchanged. The fix lives in the shared `hook-check` core, so all
|
|
112
|
+
five governed surfaces inherit it.
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
## 1.11.1 -- 2026-06-17 (kiro-cli hook fail-open fix, #1265)
|
|
116
|
+
|
|
117
|
+
### Fixed
|
|
118
|
+
|
|
119
|
+
- **Kiro CLI hook no longer silently fails open (#1265).**
|
|
120
|
+
`controlzero kiro init` wired a BARE `controlzero hook-check` into
|
|
121
|
+
`~/.kiro/settings.json`. On a venv install (or a machine with a broken /
|
|
122
|
+
shadowing `controlzero` on PATH) kiro-cli's hook subshell resolved the wrong
|
|
123
|
+
interpreter, the hook crashed, and kiro-cli FAILED OPEN -- tools ran with no
|
|
124
|
+
enforcement, silently. The hook command is now **interpreter-pinned**:
|
|
125
|
+
`"<python>" -m controlzero hook-check --strict`, so kiro-cli runs the exact
|
|
126
|
+
Python that installed it regardless of PATH/venv. Strict mode moved from the
|
|
127
|
+
Windows-unparseable `CZ_KIRO_CLI_STRICT=1` bash env-prefix to a portable
|
|
128
|
+
`--strict` flag (the env var is still honored for back-compat). New
|
|
129
|
+
`python -m controlzero` entrypoint. `kiro init` now runs a fail-LOUD
|
|
130
|
+
self-check that WARNS if the installed hook does not block a synthetic deny.
|
|
131
|
+
|
|
132
|
+
## 1.11.0 -- 2026-06-17 (secret references: plaintext out of the agent context)
|
|
133
|
+
|
|
134
|
+
### Added
|
|
135
|
+
|
|
136
|
+
- **Secret references (`czsec://`).** `controlzero.secret_ref(name)` returns an
|
|
137
|
+
opaque, value-free reference you can hold in the model context, put in tool
|
|
138
|
+
arguments, and pass around freely. It performs no fetch and carries no value.
|
|
139
|
+
- **Egress-time resolution.** `controlzero exec -- CMD` and
|
|
140
|
+
`controlzero.secrets.run(cmd, env=...)` resolve any `czsec://` references in
|
|
141
|
+
the argv / environment to plaintext ONLY at the moment a child process is
|
|
142
|
+
spawned, injecting the value into the child's argv / environment. The
|
|
143
|
+
plaintext never re-enters the parent (agent) process. Resolution reuses
|
|
144
|
+
`Client.get_secret`, so it is still policy-gated, HITL-aware, and audited; the
|
|
145
|
+
audit line records the reference + a non-reversible fingerprint, never the
|
|
146
|
+
value.
|
|
147
|
+
- **`SecretStr` taint wrapper.** When a value must live briefly in-process, it is
|
|
148
|
+
wrapped so `repr` / `str` / `format` redact and iteration is refused; the only
|
|
149
|
+
way to get the bytes is `.reveal()`, called at the egress call site.
|
|
150
|
+
|
|
151
|
+
### Changed
|
|
152
|
+
|
|
153
|
+
- **`Client.get_secret` now warns** (once per process) that it returns plaintext
|
|
154
|
+
into the agent context, pointing at `secret_ref` + `controlzero exec`. Set
|
|
155
|
+
`CONTROLZERO_SECRETS_PROTECTED=1` to BLOCK plaintext reads entirely
|
|
156
|
+
(`get_secret` raises before any fetch). Default behaviour is unchanged
|
|
157
|
+
(warn-only); existing callers keep working.
|
|
158
|
+
|
|
3
159
|
## 1.10.1 -- 2026-06-17 (upgrade nudge)
|
|
4
160
|
|
|
5
161
|
### Added
|
|
@@ -580,7 +736,7 @@ secret-value-leak guard that gates every outbound HITL payload.
|
|
|
580
736
|
POSTs a HITL approval request, builds the body from the
|
|
581
737
|
`PolicyDecision`, threads in `X-CZ-Requestor-Email` from
|
|
582
738
|
`~/.controlzero/config.yaml`, sends a per-call `Idempotency-Key`, and
|
|
583
|
-
returns a `PendingApproval`. Maps backend error codes
|
|
739
|
+
returns a `PendingApproval`. Maps backend error codes E1705-E1708 onto
|
|
584
740
|
the matching SDK exceptions. (#577)
|
|
585
741
|
|
|
586
742
|
### Conformance
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: controlzero
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.11.5
|
|
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
|
|
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
|
|
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.
|
|
44
|
+
__version__ = "1.11.5"
|
|
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
|
|
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
|
-
|
|
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:
|
|
572
|
-
#
|
|
573
|
-
#
|
|
574
|
-
#
|
|
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
|
|
@@ -353,7 +353,9 @@ def _antigravity_dry_run_roundtrips() -> bool:
|
|
|
353
353
|
adapter = select_adapter({}, {"CONTROLZERO_CLIENT": "antigravity"})
|
|
354
354
|
if getattr(adapter, "name", "") != "antigravity":
|
|
355
355
|
return False
|
|
356
|
-
|
|
356
|
+
# render_envelope is the per-host wire dict; render() folds it into a
|
|
357
|
+
# RenderResult (exit code + stdout). The dry-run inspects the envelope.
|
|
358
|
+
out = adapter.render_envelope(
|
|
357
359
|
CZDecision(effect="allow", reason="doctor dry-run", reason_code="RULE_MATCH")
|
|
358
360
|
)
|
|
359
361
|
return isinstance(out, dict) and out.get("decision") == "allow"
|
|
@@ -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)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Crash-proof console-script entry point for the ``controlzero`` CLI (#1189).
|
|
2
|
+
|
|
3
|
+
The ``controlzero`` console script declared in ``pyproject.toml`` MUST NOT be
|
|
4
|
+
pointed directly at ``controlzero.cli.main:cli``. The pip-generated wrapper
|
|
5
|
+
imports the target at module load with no guard, so a top-level import crash in
|
|
6
|
+
``controlzero/cli/main.py`` (a missing transitive dependency, a packaging
|
|
7
|
+
defect, a syntax error in any eagerly-imported submodule) raises before
|
|
8
|
+
``cli()`` runs. The wrapper then exits nonzero with a traceback, and a host that
|
|
9
|
+
treats nonzero-with-no-decision as "proceed" (Claude Code's PreToolUse falls
|
|
10
|
+
back to allow on a hook it cannot evaluate) silently lets the tool call
|
|
11
|
+
through. That is a fail-OPEN on the governance hot path -- the exact defect
|
|
12
|
+
QA #1189 flagged.
|
|
13
|
+
|
|
14
|
+
This shim makes the entry point crash-proof:
|
|
15
|
+
|
|
16
|
+
* The heavy import of ``controlzero.cli.main`` happens inside a try/except
|
|
17
|
+
with a ``sys.path`` source-checkout fallback, mirroring
|
|
18
|
+
``tools/cz-kiro-adapter`` so a dev checkout that has not been pip-installed
|
|
19
|
+
still resolves the package.
|
|
20
|
+
* If the import or evaluation of the CLI cannot complete AT ALL and this is a
|
|
21
|
+
hook invocation (``hook-check`` on argv -- the governance hot path), the
|
|
22
|
+
shim fails SAFE: it writes a hard, human-readable error to stderr and exits
|
|
23
|
+
with the host's DENY/block code (Claude Code PreToolUse = exit 2), never a
|
|
24
|
+
silent allow.
|
|
25
|
+
* For a non-hook invocation (interactive ``controlzero ...`` usage) a broken
|
|
26
|
+
install should surface its traceback so the user can fix it -- there is no
|
|
27
|
+
governance decision at stake -- so the shim re-raises in that case.
|
|
28
|
+
|
|
29
|
+
What this shim does NOT change: the runtime fail-OPEN contract that lives
|
|
30
|
+
inside ``hook_check`` (E1501 bad-JSON, E1502 timeout, empty stdin, missing
|
|
31
|
+
tool_name) is intentional and is preserved. Those paths are reached only after
|
|
32
|
+
the CLI imports successfully and represent a *decision* to pass through, not an
|
|
33
|
+
import crash. This shim only converts the previously-silent import-crash
|
|
34
|
+
fall-through into an explicit fail-safe deny.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import os
|
|
40
|
+
import sys
|
|
41
|
+
|
|
42
|
+
# Host block/deny exit codes for the hook hot path. Claude Code's PreToolUse
|
|
43
|
+
# protocol treats exit 2 as "block"; Kiro reuses the same convention. We do not
|
|
44
|
+
# have the resolved host adapter available before the import succeeds, so we use
|
|
45
|
+
# the lowest-common-denominator block code (2), which every supported host
|
|
46
|
+
# interprets as deny rather than allow.
|
|
47
|
+
_HOOK_DENY_EXIT_CODE = 2
|
|
48
|
+
|
|
49
|
+
_HOOK_SUBCOMMANDS = frozenset({"hook-check"})
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _is_hook_invocation(argv: list[str]) -> bool:
|
|
53
|
+
"""True when the process was launched on the governance hot path.
|
|
54
|
+
|
|
55
|
+
The ``hook-check`` subcommand is what hosts (Claude Code, Kiro, Codex,
|
|
56
|
+
Gemini, Antigravity) wire into their PreToolUse hooks. Detecting it on argv
|
|
57
|
+
lets the shim choose fail-SAFE (deny) on an import crash for hook calls
|
|
58
|
+
while leaving interactive CLI usage to surface its traceback normally.
|
|
59
|
+
"""
|
|
60
|
+
return any(arg in _HOOK_SUBCOMMANDS for arg in argv[1:])
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def cli() -> None:
|
|
64
|
+
"""Crash-proof wrapper around ``controlzero.cli.main:cli``.
|
|
65
|
+
|
|
66
|
+
Defers the heavy import so an import-time crash cannot escape as a bare
|
|
67
|
+
nonzero traceback that a host would mistake for "allow".
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
from controlzero.cli.main import cli as _real_cli
|
|
71
|
+
except ModuleNotFoundError:
|
|
72
|
+
# Possibly a source checkout where the package is not pip-installed.
|
|
73
|
+
# Add the SDK root to sys.path and retry once, mirroring
|
|
74
|
+
# tools/cz-kiro-adapter. controlzero/cli/hook_entry.py -> SDK root is
|
|
75
|
+
# three levels up (controlzero/cli/ -> controlzero/ -> SDK root).
|
|
76
|
+
_sdk_root = os.path.dirname(
|
|
77
|
+
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
78
|
+
)
|
|
79
|
+
if _sdk_root not in sys.path:
|
|
80
|
+
sys.path.insert(0, _sdk_root)
|
|
81
|
+
try:
|
|
82
|
+
from controlzero.cli.main import cli as _real_cli
|
|
83
|
+
except BaseException: # noqa: BLE001 -- crash-proof boundary
|
|
84
|
+
_fail_safe_or_reraise()
|
|
85
|
+
return
|
|
86
|
+
except BaseException: # noqa: BLE001 -- crash-proof boundary
|
|
87
|
+
# Any other top-level import failure (ImportError on a transitive dep,
|
|
88
|
+
# a syntax error in an eagerly-imported submodule, etc).
|
|
89
|
+
_fail_safe_or_reraise()
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
_real_cli()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _fail_safe_or_reraise() -> None:
|
|
96
|
+
"""On import failure: fail-safe DENY for hook calls, re-raise otherwise.
|
|
97
|
+
|
|
98
|
+
Reads the live exception from ``sys.exc_info`` so it can re-raise the
|
|
99
|
+
original traceback for interactive usage.
|
|
100
|
+
"""
|
|
101
|
+
if _is_hook_invocation(sys.argv):
|
|
102
|
+
exc = sys.exc_info()[1]
|
|
103
|
+
# Hard, loud error to stderr -- never a silent allow. The host sees a
|
|
104
|
+
# nonzero block exit code and denies the tool call.
|
|
105
|
+
sys.stderr.write(
|
|
106
|
+
"[Control Zero] hook entry point could not load the policy engine "
|
|
107
|
+
f"({type(exc).__name__}: {exc}); DENYING this tool call (fail-safe). "
|
|
108
|
+
"Reinstall the controlzero package to restore policy enforcement.\n"
|
|
109
|
+
)
|
|
110
|
+
sys.stderr.flush()
|
|
111
|
+
sys.exit(_HOOK_DENY_EXIT_CODE)
|
|
112
|
+
# Not a hook invocation: surface the real traceback so the user can fix
|
|
113
|
+
# their install. No governance decision is at stake here.
|
|
114
|
+
raise
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
if __name__ == "__main__":
|
|
118
|
+
cli()
|
|
@@ -55,9 +55,18 @@ known host treats no-decision as "proceed") and the universal
|
|
|
55
55
|
from __future__ import annotations
|
|
56
56
|
|
|
57
57
|
from controlzero.cli.hosts.antigravity import AntigravityAdapter
|
|
58
|
-
from controlzero.cli.hosts.base import
|
|
58
|
+
from controlzero.cli.hosts.base import (
|
|
59
|
+
AdapterError,
|
|
60
|
+
CoverageDecl,
|
|
61
|
+
CZDecision,
|
|
62
|
+
HostAdapter,
|
|
63
|
+
RenderResult,
|
|
64
|
+
ToolCall,
|
|
65
|
+
)
|
|
59
66
|
from controlzero.cli.hosts.claude_code import ClaudeCodeAdapter
|
|
60
67
|
from controlzero.cli.hosts.codex_cli import CodexCLIAdapter
|
|
68
|
+
from controlzero.cli.hosts.cursor import CursorIDEAdapter
|
|
69
|
+
from controlzero.cli.hosts.cursor_cli import CursorCLIAdapter
|
|
61
70
|
from controlzero.cli.hosts.gemini_cli import GeminiCLIAdapter
|
|
62
71
|
from controlzero.cli.hosts.kiro import KiroCLIAdapter, KiroIDEAdapter
|
|
63
72
|
from controlzero.cli.hosts.unknown import UnknownHostAdapter
|
|
@@ -77,8 +86,10 @@ REGISTRY: list[HostAdapter] = [
|
|
|
77
86
|
CodexCLIAdapter(),
|
|
78
87
|
GeminiCLIAdapter(),
|
|
79
88
|
AntigravityAdapter(), # distinct toolCall / PreToolCall signature
|
|
89
|
+
CursorCLIAdapter(), # beforeShellExecution + top-level `command`, no tool_name (before KiroCLI)
|
|
80
90
|
KiroCLIAdapter(), # camelCase hook_event_name (disjoint from Claude PascalCase)
|
|
81
91
|
KiroIDEAdapter(), # CZ_KIRO_SURFACE=ide / TERM_PROGRAM=kiro (shim-set)
|
|
92
|
+
CursorIDEAdapter(), # cursor_version / generation_id payload marker or CURSOR_* env (after KiroIDE)
|
|
82
93
|
UnknownHostAdapter(), # always claims; must stay at the tail
|
|
83
94
|
]
|
|
84
95
|
|
|
@@ -105,9 +116,15 @@ def select_adapter(payload: dict, env) -> HostAdapter:
|
|
|
105
116
|
|
|
106
117
|
__all__ = [
|
|
107
118
|
"CZDecision",
|
|
119
|
+
"ToolCall",
|
|
120
|
+
"RenderResult",
|
|
121
|
+
"CoverageDecl",
|
|
122
|
+
"AdapterError",
|
|
108
123
|
"HostAdapter",
|
|
109
124
|
"ClaudeCodeAdapter",
|
|
110
125
|
"CodexCLIAdapter",
|
|
126
|
+
"CursorCLIAdapter",
|
|
127
|
+
"CursorIDEAdapter",
|
|
111
128
|
"GeminiCLIAdapter",
|
|
112
129
|
"AntigravityAdapter",
|
|
113
130
|
"KiroCLIAdapter",
|