controlzero 1.9.5__tar.gz → 1.9.6__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.9.5 → controlzero-1.9.6}/CHANGELOG.md +40 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/PKG-INFO +1 -1
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/__init__.py +1 -1
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/doctor.py +225 -2
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/hosts/antigravity.py +58 -15
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/main.py +125 -11
- {controlzero-1.9.5 → controlzero-1.9.6}/pyproject.toml +1 -1
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_antigravity_adapter.py +18 -12
- controlzero-1.9.6/tests/test_antigravity_ga_blockers_1248.py +554 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hosts_adapter.py +6 -5
- {controlzero-1.9.5 → controlzero-1.9.6}/.gitignore +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/Dockerfile.test +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/LICENSE +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/README.md +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/_internal/__init__.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/_internal/action_aliases.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/_internal/action_validator.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/_internal/bundle.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/_internal/credential_hook.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/_internal/credential_scanner.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/_internal/credentials_data/__init__.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/_internal/credentials_data/built_in.yaml +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/_internal/dlp_scanner.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/_internal/enforcer.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/_internal/hook_extractors.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/_internal/tool_extractors.json +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/_internal/types.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/audit_local.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/audit_remote.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/canonical.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/__init__.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/__main__.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/_secrets.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/console.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/debug_bundle.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/hosts/__init__.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/hosts/base.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/hosts/claude_code.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/hosts/codex_cli.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/hosts/gemini_cli.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/hosts/kiro.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/hosts/unknown.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/kiro_adapter.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/migrate.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/spool_cmd.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/telemetry_consent.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/antigravity/hooks.json +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/antigravity.yaml +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/autogen.yaml +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/claude-code.yaml +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/codex-cli.yaml +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/cost-cap.yaml +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/crewai.yaml +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/cursor.yaml +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/gemini-cli.yaml +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/generic.yaml +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/kiro/ide-pre-tool-use.kiro.hook +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/kiro/ide-prompt-submit.kiro.hook +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/kiro/kiro.yaml +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/langchain.yaml +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/mcp.yaml +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/cli/templates/rag.yaml +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/client.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/device.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/enrollment.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/error_codes.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/error_codes.yaml +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/errors.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/hitl/__init__.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/hitl/grant_protocol.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/hitl/mock.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/hitl/pending_approval.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/hitl/secret_leak_guard.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/hitl/status.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/hooks/__init__.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/hooks/tool_output_handler.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/hosted_policy.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/__init__.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/anthropic.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/autogen.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/braintrust.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/crewai/__init__.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/crewai/agent.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/crewai/crew.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/crewai/task.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/crewai/tool.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/google.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/google_adk/__init__.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/google_adk/agent.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/google_adk/tool.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/langchain/__init__.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/langchain/agent.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/langchain/callbacks.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/langchain/chain.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/langchain/graph.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/langchain/modern.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/langchain/tool.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/langfuse.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/litellm.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/openai.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/pydantic_ai.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/integrations/vercel_ai.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/layout_migration.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/policy_loader.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/spool/__init__.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/spool/_compress.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/spool/_constants.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/spool/_crc32c.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/spool/_crypto.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/spool/_frame.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/spool/_keyring.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/spool/_metrics.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/spool/_spool.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/spool/_state.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/spool/_uploader.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/spool/cz-audit-v1.dict +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/tamper.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/controlzero/tracecontext.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/examples/hello_world.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/_fixtures/jcs_args_hash_vectors.json +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/conftest.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/integrations/__init__.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/integrations/test_google.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/parity/action_aliases.json +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/spool/__init__.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/spool/conftest.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/spool/test_spool_cli.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/spool/test_spool_concurrency.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/spool/test_spool_conformance.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/spool/test_spool_core.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/spool/test_spool_crash.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/spool/test_spool_diskfull.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/spool/test_spool_durable_default_tamper.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/spool/test_spool_keychain_dek.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/spool/test_spool_sink_wiring.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/spool/test_spool_transcript_localack.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/spool/test_spool_uploader.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_action_aliases.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_action_canonicalization.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_action_validator_t86.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_agent_name_env.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_antigravity_hook_check.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_antigravity_install.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_api_key_mask.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_audit_remote.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_audit_remote_sdk_version.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_audit_sink_isolation.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_bundle_parser.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_bundle_translate.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_canonical_phase1a.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_cli_carve_out.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_cli_debug_bundle.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_cli_extractor_integration.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_cli_hook.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_cli_hosted_refresh.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_cli_init.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_cli_init_templates.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_cli_tail.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_cli_test.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_cli_validate.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_coding_agent_hooks.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_conditions.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_conformance.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_console.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_credential_hook.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_default_action.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_device.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_dlp_scanner.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_doctor.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_engine_version_consistency.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_enrollment.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_env_dump_438.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_epic_1247_bryan_acceptance.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_error_codes.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_errors_e_codes.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_fail_closed_eval.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_glob_matching.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hitl_5d_email_install.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hitl_6a_cli_flag.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hitl_6a_exceptions.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hitl_6a_get_secret_hitl.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hitl_6a_mock_backend.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hitl_6a_pending_approval.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hitl_6a_request_approval.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hitl_6a_secret_leak_guard.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hitl_6a_wait.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hitl_conformance.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hitl_phase2b_protocol.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hitl_reason_codes.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hitl_validator_keys.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hook_extractors.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hosted_local_audit_1247.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hosted_policy_e2e.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hybrid_mode_strict.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_hybrid_mode_warn.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_install_hook_command.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_install_hooks.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_kiro_adapter.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_kiro_cli_e2e.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_kiro_hook_templates.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_kiro_install.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_layout_migration_t101.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_layout_parity_t102.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_local_mode_dict.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_local_mode_file_json.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_local_mode_file_yaml.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_log_fallback_stderr.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_log_options_ignored_hosted.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_log_rotation.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_migrate.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_min_sdk_version_gate.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_multi_client_per_project_175.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_no_policy_no_key.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_observe_mode_1247.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_package_rename_shim.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_policy_engine_version_phase1b.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_policy_freshness.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_policy_settings.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_policy_source_audit.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_quarantine.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_reason_code.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_refresh.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_secrets.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_sql_semantic_class.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_synthetic_policy_id_t79.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_t103_precedence.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_t104_cache_gc.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_t108_local_override_audit.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_t96_single_audit_log.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_t99_install_prefetch_bundle.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_tamper.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_tamper_behavior.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_tamper_hook.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_telemetry_consent.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_tracecontext.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tests/test_unsafe_int_boundary.py +0 -0
- {controlzero-1.9.5 → controlzero-1.9.6}/tools/cz-kiro-adapter +0 -0
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.6 -- 2026-06-16 (Antigravity GA-blocker hardening, gh#1248 / epic gh#925)
|
|
4
|
+
|
|
5
|
+
Closes 3 of the prod-readiness GA blockers for the Antigravity (`agy`)
|
|
6
|
+
integration. Antigravity stays **BETA** -- GA still requires a manual check
|
|
7
|
+
against a real pinned agy build (agy cannot run headless in CI), which these
|
|
8
|
+
changes do not perform.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Empty-stdout fail-closed on the Antigravity surface (blocker #1).** Every
|
|
13
|
+
`controlzero hook-check` error exit -- stdin read error, empty stdin,
|
|
14
|
+
malformed JSON, missing `tool_name`, and an invalid/unreadable policy --
|
|
15
|
+
previously emitted EMPTY stdout and exited 0. Agy decides from stdout JSON,
|
|
16
|
+
and empty stdout is undocumented (agy *currently* reads it as
|
|
17
|
+
`invalid_args` -> deny, but that is unverified across builds). On the
|
|
18
|
+
Antigravity surface those paths now emit an explicit
|
|
19
|
+
`{"decision":"deny", "reason":...}` so a parse/policy error DENIES
|
|
20
|
+
deterministically. Pass-through hosts (Claude Code / Gemini / Codex) keep
|
|
21
|
+
the documented empty-stdout = "no opinion, proceed" behavior unchanged.
|
|
22
|
+
|
|
23
|
+
- **HITL approval gate no longer silently auto-approvable (blocker #2).** The
|
|
24
|
+
HITL decision token now defaults to `force_ask` (a MANDATORY prompt that
|
|
25
|
+
ignores agy's "Always Allow" cache) instead of `ask` (which a cached
|
|
26
|
+
approval can silently satisfy -- fail-OPEN on an approval-gated destructive
|
|
27
|
+
action). `CZ_ANTIGRAVITY_HITL_DECISION` selects the token; an empty or
|
|
28
|
+
unrecognized value falls back to `force_ask`, NEVER to `ask`. If a pinned
|
|
29
|
+
agy build rejects `force_ask`, set `CZ_ANTIGRAVITY_HITL_DECISION=deny` for a
|
|
30
|
+
hard block-with-reason. `ask`/`allow` remain available only as an explicit,
|
|
31
|
+
knowing operator downgrade.
|
|
32
|
+
|
|
33
|
+
- **`doctor` checks the REAL Antigravity paths and verifies the hook is live
|
|
34
|
+
(blocker #3).** `controlzero doctor` previously checked
|
|
35
|
+
`~/.antigravity/config.json` -- a path the installer never writes. It now
|
|
36
|
+
scans the executed `~/.gemini/config/hooks.json` and the agy TUI mirror
|
|
37
|
+
`~/.gemini/antigravity-cli/hooks.json` (plus the cwd-relative project-scope
|
|
38
|
+
`.agents/hooks.json`), confirms a live `controlzero hook-check` `PreToolUse`
|
|
39
|
+
entry is present (`E1006` when missing), and runs a dry-run hook-check
|
|
40
|
+
round-trip to confirm the adapter renders an explicit decision rather than
|
|
41
|
+
empty stdout (`E1007` when it does not).
|
|
42
|
+
|
|
3
43
|
## 1.9.5 -- 2026-06-16 (Kiro CLI -> GA, epic gh#877)
|
|
4
44
|
|
|
5
45
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: controlzero
|
|
3
|
-
Version: 1.9.
|
|
3
|
+
Version: 1.9.6
|
|
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
|
|
@@ -82,8 +82,18 @@ def _agent_paths() -> List[_AgentPath]:
|
|
|
82
82
|
"VS Code user settings"),
|
|
83
83
|
_AgentPath("cline", home / "Library" / "Application Support" / "Code" / "User" / "globalStorage" / "saoudrizwan.claude-dev" / "settings" / "cline_mcp_settings.json",
|
|
84
84
|
"Cline MCP settings (macOS)"),
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
# Antigravity (agy): the install writes the EXECUTED hook config to
|
|
86
|
+
# ~/.gemini/config/hooks.json and mirrors it to
|
|
87
|
+
# ~/.gemini/antigravity-cli/hooks.json for the agy TUI /hooks display
|
|
88
|
+
# (antigravity-cli#49). The pre-#1248 doctor checked
|
|
89
|
+
# ~/.antigravity/config.json -- a path the installer NEVER writes -- so
|
|
90
|
+
# it could neither find a leaked key there nor confirm the hook was
|
|
91
|
+
# live. Scan the real executed + mirror paths now. Project scope
|
|
92
|
+
# (<cwd>/.agents/hooks.json) is covered by the cwd-aware live check.
|
|
93
|
+
_AgentPath("antigravity", home / ".gemini" / "config" / "hooks.json",
|
|
94
|
+
"Antigravity PreToolUse hook command"),
|
|
95
|
+
_AgentPath("antigravity", home / ".gemini" / "antigravity-cli" / "hooks.json",
|
|
96
|
+
"Antigravity agy TUI /hooks mirror"),
|
|
87
97
|
_AgentPath("adal", home / ".adal" / "config.json",
|
|
88
98
|
"Adal config"),
|
|
89
99
|
_AgentPath("jetbrains", home / ".config" / "JetBrains" / "mcp.json",
|
|
@@ -243,6 +253,208 @@ def _check_global_environment() -> List[Finding]:
|
|
|
243
253
|
return findings
|
|
244
254
|
|
|
245
255
|
|
|
256
|
+
# -----------------------------------------------------------------------------
|
|
257
|
+
# Antigravity hook liveness (GA blocker #3, issue #1248).
|
|
258
|
+
#
|
|
259
|
+
# The pre-#1248 doctor only scanned files for leaked keys. For Antigravity it
|
|
260
|
+
# also checked the WRONG path (~/.antigravity/config.json), so it gave a user
|
|
261
|
+
# zero signal about whether their governance hook was actually installed and
|
|
262
|
+
# firing. These checks confirm the hook entry is present at the REAL executed
|
|
263
|
+
# path AND that a dry-run hook-check round-trips a valid decision -- the two
|
|
264
|
+
# things that have to be true for the gate to govern a single tool call.
|
|
265
|
+
# -----------------------------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
# The substring that marks a Control Zero hook entry, matching the installer's
|
|
268
|
+
# own self-detection (`_antigravity_hook_is_cz`). Kept in one place so a future
|
|
269
|
+
# command-string change only edits here.
|
|
270
|
+
_CZ_HOOK_MARKER = "controlzero hook-check"
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _antigravity_executed_hook_paths() -> List[Path]:
|
|
274
|
+
"""The hooks.json paths agy actually EXECUTES, in precedence order.
|
|
275
|
+
|
|
276
|
+
Project scope wins for a workspace, so it is checked first; the global
|
|
277
|
+
user file is the fallback. The TUI mirror (~/.gemini/antigravity-cli/...)
|
|
278
|
+
is a display surface, not an executed path, so it is intentionally NOT in
|
|
279
|
+
this list -- a hook present only in the mirror does not fire.
|
|
280
|
+
"""
|
|
281
|
+
home = Path.home()
|
|
282
|
+
return [
|
|
283
|
+
Path.cwd() / ".agents" / "hooks.json", # project scope (<cwd>)
|
|
284
|
+
home / ".gemini" / "config" / "hooks.json", # global user (executed)
|
|
285
|
+
]
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _antigravity_mirror_path() -> Path:
|
|
289
|
+
"""The agy TUI /hooks DISPLAY mirror (antigravity-cli#49).
|
|
290
|
+
|
|
291
|
+
A hook present ONLY here is shown in the agy `/hooks` UI but is NOT
|
|
292
|
+
executed, so it does not fire. Used purely as a presence signal so doctor
|
|
293
|
+
can warn about that display-but-not-live state.
|
|
294
|
+
"""
|
|
295
|
+
return Path.home() / ".gemini" / "antigravity-cli" / "hooks.json"
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _antigravity_hook_command_present(hooks_path: Path) -> bool:
|
|
299
|
+
"""True iff hooks_path has a live PreToolUse Control Zero hook entry.
|
|
300
|
+
|
|
301
|
+
Mirrors the installer's written shape: top-level ``hooks`` ->
|
|
302
|
+
``PreToolUse`` -> list of blocks each with a ``hooks`` list of
|
|
303
|
+
``{type, command, ...}`` entries. A CZ entry is any whose ``command``
|
|
304
|
+
contains ``controlzero hook-check``. Tolerant of hand-edits / partial
|
|
305
|
+
shapes -- anything it cannot parse counts as "not present".
|
|
306
|
+
"""
|
|
307
|
+
import json
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
data = json.loads(hooks_path.read_text(encoding="utf-8"))
|
|
311
|
+
except (OSError, ValueError):
|
|
312
|
+
return False
|
|
313
|
+
if not isinstance(data, dict):
|
|
314
|
+
return False
|
|
315
|
+
hooks = data.get("hooks")
|
|
316
|
+
if not isinstance(hooks, dict):
|
|
317
|
+
return False
|
|
318
|
+
pre = hooks.get("PreToolUse")
|
|
319
|
+
if not isinstance(pre, list):
|
|
320
|
+
return False
|
|
321
|
+
for block in pre:
|
|
322
|
+
if not isinstance(block, dict):
|
|
323
|
+
continue
|
|
324
|
+
block_hooks = block.get("hooks")
|
|
325
|
+
if not isinstance(block_hooks, list):
|
|
326
|
+
# Tolerate a malformed-but-valid-JSON shape (e.g. "hooks": 3);
|
|
327
|
+
# iterating a non-list would raise TypeError and crash doctor.
|
|
328
|
+
continue
|
|
329
|
+
for entry in block_hooks:
|
|
330
|
+
if not isinstance(entry, dict):
|
|
331
|
+
continue
|
|
332
|
+
cmd = entry.get("command")
|
|
333
|
+
if isinstance(cmd, str) and _CZ_HOOK_MARKER in cmd:
|
|
334
|
+
return True
|
|
335
|
+
return False
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _antigravity_dry_run_roundtrips() -> bool:
|
|
339
|
+
"""True iff a dry-run hook-check produces a valid Antigravity decision.
|
|
340
|
+
|
|
341
|
+
This is the "does the hook actually fire" half of the check. We do NOT
|
|
342
|
+
shell out to ``controlzero hook-check`` (slow, and a misconfigured PATH
|
|
343
|
+
would make a working install look broken). Instead we exercise the same
|
|
344
|
+
code path the hook runs: select the Antigravity adapter and render a
|
|
345
|
+
decision, asserting it emits an explicit ``decision`` token (never the
|
|
346
|
+
empty-stdout ``invalid_args`` trap). If the adapter import or render
|
|
347
|
+
raises, the round-trip has failed.
|
|
348
|
+
"""
|
|
349
|
+
try:
|
|
350
|
+
from controlzero.cli.hosts import CZDecision, select_adapter
|
|
351
|
+
|
|
352
|
+
# Force Antigravity selection via the env signal its claim() honors.
|
|
353
|
+
adapter = select_adapter({}, {"CONTROLZERO_CLIENT": "antigravity"})
|
|
354
|
+
if getattr(adapter, "name", "") != "antigravity":
|
|
355
|
+
return False
|
|
356
|
+
out = adapter.render(
|
|
357
|
+
CZDecision(effect="allow", reason="doctor dry-run", reason_code="RULE_MATCH")
|
|
358
|
+
)
|
|
359
|
+
return isinstance(out, dict) and out.get("decision") == "allow"
|
|
360
|
+
except Exception: # noqa: BLE001
|
|
361
|
+
return False
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _check_antigravity_hook_live() -> List[Finding]:
|
|
365
|
+
"""Confirm the Antigravity governance hook is installed AND can fire.
|
|
366
|
+
|
|
367
|
+
Respects agy's file precedence: the highest-precedence EXISTING executed
|
|
368
|
+
file wins (project scope <cwd>/.agents/hooks.json shadows the global
|
|
369
|
+
~/.gemini/config/hooks.json), so that file alone determines liveness.
|
|
370
|
+
|
|
371
|
+
Emits at most one finding:
|
|
372
|
+
|
|
373
|
+
* If the authoritative executed file carries a CZ entry and the dry-run
|
|
374
|
+
round-trips -> no finding (the gate is live).
|
|
375
|
+
* If the authoritative executed file (or only the display mirror) lacks a
|
|
376
|
+
CZ entry -> WARN installed-but-not-live (or never installed). WARN, not
|
|
377
|
+
ERROR: a box that never installed the agy hook is a valid state, and
|
|
378
|
+
doctor exit 1 is reserved for active security problems (leaked keys,
|
|
379
|
+
bad perms).
|
|
380
|
+
* If the CZ entry is present but the dry-run does NOT round-trip -> ERROR:
|
|
381
|
+
the entry exists but the adapter cannot render a decision, so the gate
|
|
382
|
+
would emit empty stdout -> agy invalid_args / undefined behavior.
|
|
383
|
+
"""
|
|
384
|
+
findings: List[Finding] = []
|
|
385
|
+
executed = _antigravity_executed_hook_paths()
|
|
386
|
+
|
|
387
|
+
# agy executes the HIGHEST-PRECEDENCE existing file only (project scope
|
|
388
|
+
# <cwd>/.agents/hooks.json shadows the global ~/.gemini/config/hooks.json).
|
|
389
|
+
# So the FIRST existing executed file is authoritative: if it carries a CZ
|
|
390
|
+
# entry the gate is live; if it does NOT, the workspace is ungoverned even
|
|
391
|
+
# when a lower-precedence file happens to have one. We must NOT fall through
|
|
392
|
+
# to that lower file, or doctor would falsely report "live" (codex P1).
|
|
393
|
+
authoritative = next((p for p in executed if p.exists()), None)
|
|
394
|
+
live_path = (
|
|
395
|
+
authoritative
|
|
396
|
+
if authoritative is not None and _antigravity_hook_command_present(authoritative)
|
|
397
|
+
else None
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
if live_path is None:
|
|
401
|
+
# Surface a not-live WARN when ANY agy hooks.json exists -- the
|
|
402
|
+
# authoritative executed file without a CZ entry, or only the
|
|
403
|
+
# display-only TUI mirror. A hook present only in the mirror (or absent
|
|
404
|
+
# from the executed file that wins) is shown in the agy `/hooks` UI yet
|
|
405
|
+
# does NOT govern tool calls -- exactly the misleading state to flag.
|
|
406
|
+
# Stay silent when no agy file exists at all so a non-Antigravity user
|
|
407
|
+
# gets no spurious warning.
|
|
408
|
+
mirror = _antigravity_mirror_path()
|
|
409
|
+
present = [p for p in executed if p.exists()]
|
|
410
|
+
if mirror.exists():
|
|
411
|
+
present.append(mirror)
|
|
412
|
+
if present:
|
|
413
|
+
mirror_only = all(p == mirror for p in present)
|
|
414
|
+
findings.append(Finding(
|
|
415
|
+
path=str(present[0]),
|
|
416
|
+
line=1,
|
|
417
|
+
col=1,
|
|
418
|
+
severity="WARN",
|
|
419
|
+
code="E1006",
|
|
420
|
+
message=(
|
|
421
|
+
(
|
|
422
|
+
"Antigravity hook appears only in the agy /hooks DISPLAY "
|
|
423
|
+
"mirror (~/.gemini/antigravity-cli/hooks.json) -- that path "
|
|
424
|
+
"is NOT executed, so the governance gate is NOT firing."
|
|
425
|
+
)
|
|
426
|
+
if mirror_only else
|
|
427
|
+
(
|
|
428
|
+
"Antigravity hooks.json exists but has no live "
|
|
429
|
+
f"'{_CZ_HOOK_MARKER}' PreToolUse entry -- the governance "
|
|
430
|
+
"gate is NOT firing for tool calls."
|
|
431
|
+
)
|
|
432
|
+
),
|
|
433
|
+
fix_hint="run `controlzero install antigravity` to (re)install the hook",
|
|
434
|
+
))
|
|
435
|
+
return findings
|
|
436
|
+
|
|
437
|
+
# Hook entry is present -- verify it can actually render a decision.
|
|
438
|
+
if not _antigravity_dry_run_roundtrips():
|
|
439
|
+
findings.append(Finding(
|
|
440
|
+
path=str(live_path),
|
|
441
|
+
line=1,
|
|
442
|
+
col=1,
|
|
443
|
+
severity="ERROR",
|
|
444
|
+
code="E1007",
|
|
445
|
+
message=(
|
|
446
|
+
"Antigravity hook entry is installed but a dry-run hook-check "
|
|
447
|
+
"does NOT round-trip a decision -- the gate would emit empty "
|
|
448
|
+
"stdout (agy reads that as invalid_args / undefined)."
|
|
449
|
+
),
|
|
450
|
+
fix_hint=(
|
|
451
|
+
"reinstall the SDK (`pip install -U control-zero`) and rerun "
|
|
452
|
+
"`controlzero install antigravity`; if it persists, file a bug"
|
|
453
|
+
),
|
|
454
|
+
))
|
|
455
|
+
return findings
|
|
456
|
+
|
|
457
|
+
|
|
246
458
|
# -----------------------------------------------------------------------------
|
|
247
459
|
# Public entrypoint. Wired into the CLI as `controlzero doctor`.
|
|
248
460
|
# -----------------------------------------------------------------------------
|
|
@@ -257,6 +469,7 @@ def run_doctor(verbose: bool = False) -> int:
|
|
|
257
469
|
findings.extend(_check_agent_key_leaks(_agent_paths()))
|
|
258
470
|
findings.extend(_check_config_permissions())
|
|
259
471
|
findings.extend(_check_global_environment())
|
|
472
|
+
findings.extend(_check_antigravity_hook_live())
|
|
260
473
|
|
|
261
474
|
errors = [f for f in findings if f.severity == "ERROR"]
|
|
262
475
|
warns = [f for f in findings if f.severity == "WARN"]
|
|
@@ -272,6 +485,16 @@ def run_doctor(verbose: bool = False) -> int:
|
|
|
272
485
|
for ap in _agent_paths():
|
|
273
486
|
status = "exists" if ap.path.exists() else "absent"
|
|
274
487
|
cz_console.info(f" {ap.agent:14s} {status:7s} {ap.path}")
|
|
488
|
+
_live = next(
|
|
489
|
+
(p for p in _antigravity_executed_hook_paths()
|
|
490
|
+
if p.exists() and _antigravity_hook_command_present(p)),
|
|
491
|
+
None,
|
|
492
|
+
)
|
|
493
|
+
if _live is not None:
|
|
494
|
+
rt = "round-trips" if _antigravity_dry_run_roundtrips() else "BROKEN"
|
|
495
|
+
cz_console.info(f" {'antigravity':14s} {'live':7s} {_live} (dry-run: {rt})")
|
|
496
|
+
else:
|
|
497
|
+
cz_console.info(f" {'antigravity':14s} {'not-live':7s} (no executed PreToolUse hook entry)")
|
|
275
498
|
return 0
|
|
276
499
|
|
|
277
500
|
for f in findings:
|
|
@@ -28,14 +28,21 @@ danicat.dev):
|
|
|
28
28
|
We therefore ALWAYS emit an explicit ``{"decision": ...}`` -- the allow
|
|
29
29
|
path is ``{"decision":"allow"}``, never silence-that-still-prints-{}.
|
|
30
30
|
|
|
31
|
-
HITL mapping: a Control Zero approval gate maps to
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
HITL mapping (GA blocker #2, issue #1248): a Control Zero approval gate maps to
|
|
32
|
+
``force_ask`` by DEFAULT -- a MANDATORY prompt that ignores agy's "Always Allow"
|
|
33
|
+
cache. A plain ``ask`` CAN be silently auto-satisfied by a cached "Always Allow"
|
|
34
|
+
(fail-OPEN on an approval-gated destructive action), so it is NOT the default
|
|
35
|
+
and is NEVER reached by the unrecognized-value fallback. The token is
|
|
36
|
+
configurable via ``CZ_ANTIGRAVITY_HITL_DECISION``:
|
|
37
|
+
|
|
38
|
+
* unset / unrecognized -> ``force_ask`` (fail-closed default).
|
|
39
|
+
* ``force_ask`` -> guaranteed prompt.
|
|
40
|
+
* ``deny`` -> hard block-with-reason. Use this as the fallback when a pinned agy
|
|
41
|
+
build is known to reject ``force_ask``; the gate then blocks rather than
|
|
42
|
+
silently auto-approving.
|
|
43
|
+
* ``ask`` -> the legacy cache-bypassable behavior. Permitted only as an
|
|
44
|
+
explicit, knowing downgrade (fail-open).
|
|
45
|
+
* ``allow`` -> defeats the gate; explicit override only.
|
|
39
46
|
|
|
40
47
|
Subagent tool calls fire the parent ``PreToolUse`` hook, so one adapter
|
|
41
48
|
governs parent + subagents.
|
|
@@ -68,21 +75,57 @@ _ENV_PREFIXES = ("ANTIGRAVITY_", "AGY_")
|
|
|
68
75
|
|
|
69
76
|
# Source-confirmed decision tokens for PreToolUse output.
|
|
70
77
|
_VALID_DECISIONS = frozenset({"allow", "deny", "ask", "force_ask"})
|
|
71
|
-
|
|
78
|
+
|
|
79
|
+
# HITL approval-gate token. GA blocker #2 (issue #1248): a plain ``ask`` CAN be
|
|
80
|
+
# silently auto-satisfied by a cached "Always Allow" -- which is fail-OPEN on an
|
|
81
|
+
# approval-gated destructive action. For a governance gate that is unacceptable,
|
|
82
|
+
# so the safe default for a HITL gate is ``force_ask`` (a MANDATORY prompt that
|
|
83
|
+
# ignores the cache). If a particular agy build rejects ``force_ask`` (older
|
|
84
|
+
# builds may not implement it), the operator must fall back to ``deny`` -- a hard
|
|
85
|
+
# block-with-reason -- NOT to ``ask``. ``ask`` is therefore no longer the default
|
|
86
|
+
# and is never reached by the unrecognized-value fallback; an operator who wants
|
|
87
|
+
# the cache-bypassable behavior must opt into it EXPLICITLY and knowingly.
|
|
88
|
+
_DEFAULT_HITL_DECISION = "force_ask"
|
|
89
|
+
# The fail-closed fallback for a HITL gate when the configured token is empty or
|
|
90
|
+
# unrecognized. Must be a guaranteed-prompt / hard-block token, never ``ask``.
|
|
91
|
+
_HITL_FALLBACK_DECISION = "force_ask"
|
|
92
|
+
# Tokens that are SAFE for a HITL approval gate (cannot be silently
|
|
93
|
+
# auto-approved from a cache). ``ask`` is deliberately excluded -- it is
|
|
94
|
+
# cache-bypassable -- and ``allow`` is excluded because allowing outright defeats
|
|
95
|
+
# the gate. Only an explicit operator override may select a non-safe token.
|
|
96
|
+
_HITL_SAFE_DECISIONS = frozenset({"force_ask", "deny"})
|
|
72
97
|
|
|
73
98
|
|
|
74
99
|
def _resolve_hitl_decision(env: Mapping[str, str] | None = None) -> str:
|
|
75
100
|
"""Resolve the decision token used for a HITL approval gate.
|
|
76
101
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
102
|
+
GA blocker #2 (issue #1248): a HITL gate must never silently auto-approve.
|
|
103
|
+
A plain ``ask`` can be satisfied by agy's cached "Always Allow" (fail-OPEN),
|
|
104
|
+
so it is NOT a safe HITL token. The resolution rules are:
|
|
105
|
+
|
|
106
|
+
* Default (env unset): ``force_ask`` -- a MANDATORY prompt that ignores the
|
|
107
|
+
cache. This is the safe, fail-closed choice.
|
|
108
|
+
* Empty / unrecognized value: falls back to ``force_ask`` (NEVER ``ask``),
|
|
109
|
+
so a typo or an unset shell can never silently downgrade the gate to a
|
|
110
|
+
cache-bypassable token.
|
|
111
|
+
* Explicit operator override via ``CZ_ANTIGRAVITY_HITL_DECISION``:
|
|
112
|
+
- ``force_ask`` -- guaranteed prompt (the default).
|
|
113
|
+
- ``deny`` -- hard block-with-reason. This is the correct fallback when
|
|
114
|
+
a pinned agy build is known to reject ``force_ask`` (which would
|
|
115
|
+
otherwise trip the ``invalid_args`` -> deny trap anyway, but ``deny``
|
|
116
|
+
makes the block explicit and carries the reason).
|
|
117
|
+
- ``ask`` -- the LEGACY cache-bypassable behavior. Permitted only as an
|
|
118
|
+
explicit, knowing downgrade; it is fail-open and must not be used for
|
|
119
|
+
an approval-gated destructive action.
|
|
120
|
+
- ``allow`` -- defeats the gate entirely; permitted only as an explicit
|
|
121
|
+
override (e.g. a soak/observe deployment).
|
|
82
122
|
"""
|
|
83
123
|
src = env if env is not None else os.environ
|
|
84
124
|
raw = (src.get("CZ_ANTIGRAVITY_HITL_DECISION") or "").strip().lower()
|
|
85
|
-
|
|
125
|
+
if raw in _VALID_DECISIONS:
|
|
126
|
+
return raw
|
|
127
|
+
# Empty or unrecognized -> fail closed to a guaranteed-prompt token.
|
|
128
|
+
return _HITL_FALLBACK_DECISION
|
|
86
129
|
|
|
87
130
|
|
|
88
131
|
class AntigravityAdapter(HostAdapter):
|
|
@@ -699,15 +699,83 @@ def hook_check(policy: Optional[str]):
|
|
|
699
699
|
# the hosted/tamper paths, which already fail closed.
|
|
700
700
|
_strict = _hook_check_strict()
|
|
701
701
|
|
|
702
|
+
# GA blocker #1 (issue #1248): empty-stdout fail-closed for Antigravity.
|
|
703
|
+
#
|
|
704
|
+
# Several hook-check early exits below (stdin read error, empty stdin,
|
|
705
|
+
# malformed JSON, missing tool_name, invalid policy) historically printed
|
|
706
|
+
# NOTHING to stdout and exited 0. On Claude Code / Gemini / Codex an empty
|
|
707
|
+
# stdout + exit 0 is the documented "no opinion, proceed" signal, so that is
|
|
708
|
+
# the right behavior there -- bricking those agents on a malformed payload
|
|
709
|
+
# would be a regression.
|
|
710
|
+
#
|
|
711
|
+
# Antigravity is different: it decides from the stdout JSON, and an empty /
|
|
712
|
+
# decision-less stdout is read by agy as ``invalid_args`` and DENIES the tool
|
|
713
|
+
# (cmux#5358). That happens to fail closed, but it is UNDOCUMENTED and
|
|
714
|
+
# UNVERIFIED across builds -- a future agy could read empty stdout as
|
|
715
|
+
# fail-open. For a governance gate the deny must be DETERMINISTIC, so on the
|
|
716
|
+
# Antigravity surface EVERY error exit emits an explicit
|
|
717
|
+
# ``{"decision":"deny", "reason":...}`` rather than relying on the empty-
|
|
718
|
+
# stdout heuristic.
|
|
719
|
+
#
|
|
720
|
+
# These three early exits run BEFORE the payload parses, so the per-payload
|
|
721
|
+
# ``toolCall`` signature is unavailable. Antigravity is still detectable from
|
|
722
|
+
# the environment alone (``CONTROLZERO_CLIENT=antigravity|agy|antigravity-cli``
|
|
723
|
+
# or any ``ANTIGRAVITY_*`` / ``AGY_*`` var), which is exactly what the
|
|
724
|
+
# adapter's ``claim()`` checks. We pass an empty payload so only the env
|
|
725
|
+
# signal can claim -- a non-Antigravity host falls through to the historical
|
|
726
|
+
# pass-through.
|
|
727
|
+
def _failclosed_if_antigravity(reason: str) -> None:
|
|
728
|
+
"""Emit an explicit Antigravity deny iff the host is Antigravity.
|
|
729
|
+
|
|
730
|
+
No-op for every other host (preserves the pass-through contract). The
|
|
731
|
+
deny is rendered through the adapter so the on-the-wire shape is the
|
|
732
|
+
canonical ``{"decision":"deny", "reason":..., "controlzero":{...}}``.
|
|
733
|
+
Antigravity decides from stdout JSON, not the exit code, so the caller
|
|
734
|
+
still exits 0 after this returns. Wrapped in a broad guard: this runs on
|
|
735
|
+
an already-degraded error path, so a failure to import / select / render
|
|
736
|
+
must never raise out of here -- it would turn a clean pass-through exit
|
|
737
|
+
into a traceback. On failure it is a no-op and the caller's exit stands.
|
|
738
|
+
"""
|
|
739
|
+
try:
|
|
740
|
+
from controlzero.cli.hosts import (
|
|
741
|
+
CZDecision as _CZDecision,
|
|
742
|
+
select_adapter as _select_adapter,
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
_early_adapter = _select_adapter({}, os.environ)
|
|
746
|
+
if getattr(_early_adapter, "name", "") != "antigravity":
|
|
747
|
+
return
|
|
748
|
+
_decision = _CZDecision(
|
|
749
|
+
effect="deny",
|
|
750
|
+
reason=f"[Control Zero] {reason}",
|
|
751
|
+
reason_code=REASON_CODE_BUNDLE_MISSING,
|
|
752
|
+
)
|
|
753
|
+
click.echo(json.dumps(_early_adapter.render(_decision)))
|
|
754
|
+
except Exception: # noqa: BLE001
|
|
755
|
+
# Never raise from an error-path fail-closed helper. The caller
|
|
756
|
+
# still exits; on Antigravity a missing decision line falls back to
|
|
757
|
+
# agy's own empty-stdout handling (currently a deny), which is no
|
|
758
|
+
# worse than the pre-fix behavior.
|
|
759
|
+
return
|
|
760
|
+
|
|
702
761
|
# Read stdin -- Claude Code passes the tool payload here
|
|
703
762
|
try:
|
|
704
763
|
raw = sys.stdin.read()
|
|
705
764
|
except (KeyboardInterrupt, EOFError):
|
|
706
|
-
# No payload
|
|
707
|
-
|
|
765
|
+
# No payload. Kiro CLI strict mode fails CLOSED (exit 2); every other
|
|
766
|
+
# host passes through (exit 0). On Antigravity, emit an explicit deny
|
|
767
|
+
# first -- a missing payload must not silently allow on the governance
|
|
768
|
+
# surface.
|
|
769
|
+
if _strict:
|
|
770
|
+
sys.exit(2)
|
|
771
|
+
_failclosed_if_antigravity(
|
|
772
|
+
"Hook received no payload on stdin; denying (fail-closed)."
|
|
773
|
+
)
|
|
774
|
+
sys.exit(0)
|
|
708
775
|
|
|
709
776
|
if not raw.strip():
|
|
710
|
-
# Empty stdin.
|
|
777
|
+
# Empty stdin. Kiro CLI strict mode fails CLOSED with a specific
|
|
778
|
+
# diagnostic; other hosts pass through, denying explicitly on Antigravity.
|
|
711
779
|
if _strict:
|
|
712
780
|
click.echo(
|
|
713
781
|
"controlzero: empty hook payload on stdin; blocking "
|
|
@@ -716,12 +784,18 @@ def hook_check(policy: Optional[str]):
|
|
|
716
784
|
err=True,
|
|
717
785
|
)
|
|
718
786
|
sys.exit(2)
|
|
787
|
+
_failclosed_if_antigravity(
|
|
788
|
+
"Hook received empty stdin; denying (fail-closed)."
|
|
789
|
+
)
|
|
719
790
|
sys.exit(0)
|
|
720
791
|
|
|
721
792
|
try:
|
|
722
793
|
payload = json.loads(raw)
|
|
723
794
|
except json.JSONDecodeError as e:
|
|
724
|
-
# Malformed payload.
|
|
795
|
+
# Malformed payload. Kiro CLI strict mode fails CLOSED on a parse error;
|
|
796
|
+
# other hosts log to stderr and pass through. On Antigravity, a parse
|
|
797
|
+
# error must DENY (a corrupted payload could otherwise smuggle a tool
|
|
798
|
+
# call past the gate).
|
|
725
799
|
if _strict:
|
|
726
800
|
click.echo(
|
|
727
801
|
f"controlzero: hook payload is not JSON ({e}); blocking "
|
|
@@ -730,8 +804,25 @@ def hook_check(policy: Optional[str]):
|
|
|
730
804
|
err=True,
|
|
731
805
|
)
|
|
732
806
|
sys.exit(2)
|
|
733
|
-
# Default: log to stderr and pass through. Never brick the agent.
|
|
734
807
|
click.echo(f"controlzero: hook payload is not JSON ({e}); allowing", err=True)
|
|
808
|
+
_failclosed_if_antigravity(
|
|
809
|
+
f"Hook payload is not valid JSON ({e}); denying (fail-closed)."
|
|
810
|
+
)
|
|
811
|
+
sys.exit(0)
|
|
812
|
+
|
|
813
|
+
if not isinstance(payload, dict):
|
|
814
|
+
# Valid JSON but not an object (a bare list / string / number / null).
|
|
815
|
+
# ``payload.get(...)`` below would AttributeError on a non-dict, and the
|
|
816
|
+
# adapter cannot extract a tool from it. Treat it exactly like a
|
|
817
|
+
# malformed payload: pass-through hosts allow, Antigravity denies
|
|
818
|
+
# explicitly (GA blocker #1, #1248). Done BEFORE select_adapter so a
|
|
819
|
+
# non-dict never reaches normalize_payload / .get().
|
|
820
|
+
click.echo(
|
|
821
|
+
"controlzero: hook payload is not a JSON object; allowing", err=True
|
|
822
|
+
)
|
|
823
|
+
_failclosed_if_antigravity(
|
|
824
|
+
"Hook payload is not a JSON object; denying (fail-closed)."
|
|
825
|
+
)
|
|
735
826
|
sys.exit(0)
|
|
736
827
|
|
|
737
828
|
# 1.5.3+ adapter base: pick the right host-agent adapter BEFORE
|
|
@@ -757,6 +848,14 @@ def hook_check(policy: Optional[str]):
|
|
|
757
848
|
tool_name = payload.get("tool_name") or payload.get("toolName") or ""
|
|
758
849
|
tool_args = payload.get("tool_input") or payload.get("toolInput") or {}
|
|
759
850
|
if not tool_name:
|
|
851
|
+
# Payload parsed but carries no tool to evaluate. Kiro CLI strict mode
|
|
852
|
+
# fails CLOSED (a security hook that cannot see which tool is running
|
|
853
|
+
# must not fail open). Pass-through hosts treat empty stdout / exit 0
|
|
854
|
+
# as "no opinion". On Antigravity that is the undocumented invalid_args
|
|
855
|
+
# trap, so emit an explicit deny instead (GA blocker #1, #1248) -- a
|
|
856
|
+
# tool call whose name we cannot read must not slip past the gate.
|
|
857
|
+
# _adapter is already the correctly selected host adapter here
|
|
858
|
+
# (toolCall- or env-detected for Antigravity).
|
|
760
859
|
if _strict:
|
|
761
860
|
click.echo(
|
|
762
861
|
"controlzero: hook payload missing tool_name; blocking "
|
|
@@ -766,6 +865,12 @@ def hook_check(policy: Optional[str]):
|
|
|
766
865
|
)
|
|
767
866
|
sys.exit(2)
|
|
768
867
|
click.echo("controlzero: hook payload missing tool_name; allowing", err=True)
|
|
868
|
+
if getattr(_adapter, "name", "") == "antigravity":
|
|
869
|
+
click.echo(json.dumps(_adapter.render(CZDecision(
|
|
870
|
+
effect="deny",
|
|
871
|
+
reason="[Control Zero] Hook payload missing tool_name; denying (fail-closed).",
|
|
872
|
+
reason_code=REASON_CODE_BUNDLE_MISSING,
|
|
873
|
+
))))
|
|
769
874
|
sys.exit(0)
|
|
770
875
|
|
|
771
876
|
def _emit_decision(
|
|
@@ -1031,12 +1136,14 @@ def hook_check(policy: Optional[str]):
|
|
|
1031
1136
|
log_path=str(GLOBAL_AUDIT_PATH),
|
|
1032
1137
|
)
|
|
1033
1138
|
except (PolicyLoadError, PolicyValidationError, PermissionError, OSError) as e:
|
|
1034
|
-
# Bad/unreadable policy file.
|
|
1035
|
-
#
|
|
1036
|
-
#
|
|
1037
|
-
# installed
|
|
1038
|
-
#
|
|
1039
|
-
#
|
|
1139
|
+
# Bad/unreadable policy file. Kiro CLI strict mode fails CLOSED with a
|
|
1140
|
+
# diagnostic that distinguishes an unreadable/parse-broken policy from
|
|
1141
|
+
# "no policy installed" (the BUNDLE_MISSING path above, governed by
|
|
1142
|
+
# default_on_missing); an installed-but-unreadable policy is an operator
|
|
1143
|
+
# error a security hook should not silently allow through. Other
|
|
1144
|
+
# pass-through hosts log + allow (do not break the agent on a local-file
|
|
1145
|
+
# glitch). On Antigravity an invalid policy must DENY (GA blocker #1,
|
|
1146
|
+
# #1248): a governance gate whose policy will not parse must fail closed.
|
|
1040
1147
|
if _strict:
|
|
1041
1148
|
click.echo(
|
|
1042
1149
|
f"controlzero: local policy is present but unreadable/invalid "
|
|
@@ -1051,6 +1158,13 @@ def hook_check(policy: Optional[str]):
|
|
|
1051
1158
|
)
|
|
1052
1159
|
sys.exit(2)
|
|
1053
1160
|
click.echo(f"controlzero: policy file invalid ({e}); allowing", err=True)
|
|
1161
|
+
if getattr(_adapter, "name", "") == "antigravity":
|
|
1162
|
+
_emit_decision(
|
|
1163
|
+
effect="deny",
|
|
1164
|
+
reason=f"[Control Zero] Policy file invalid ({e}); denying (fail-closed).",
|
|
1165
|
+
reason_code=REASON_CODE_BUNDLE_MISSING,
|
|
1166
|
+
)
|
|
1167
|
+
sys.exit(2 if _adapter.decision_via_exit_code else 0)
|
|
1054
1168
|
sys.exit(0)
|
|
1055
1169
|
except Exception as e: # noqa: BLE001
|
|
1056
1170
|
# Hosted-pull failure (HostedAuthError, HostedBootstrapError, etc).
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "controlzero"
|
|
7
|
-
version = "1.9.
|
|
7
|
+
version = "1.9.6"
|
|
8
8
|
description = "AI agent governance: policies, audit, and observability for tool calls. Works locally with no signup."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "Apache-2.0"}
|