controlzero 1.9.7__tar.gz → 1.9.9__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.7 → controlzero-1.9.9}/CHANGELOG.md +61 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/PKG-INFO +9 -7
- {controlzero-1.9.7 → controlzero-1.9.9}/README.md +5 -3
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/__init__.py +1 -1
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/_internal/bundle.py +48 -5
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/_internal/hook_extractors.py +24 -4
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/_internal/tool_extractors.json +5 -4
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/audit_remote.py +26 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/main.py +52 -16
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/antigravity.yaml +35 -13
- {controlzero-1.9.7 → controlzero-1.9.9}/pyproject.toml +9 -4
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_antigravity_hook_check.py +8 -5
- controlzero-1.9.9/tests/test_antigravity_tool_vocab_1303.py +323 -0
- controlzero-1.9.9/tests/test_config_format_parity_1303.py +145 -0
- controlzero-1.9.9/tests/test_failopen_1303.py +96 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/.gitignore +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/Dockerfile.test +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/LICENSE +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/_internal/__init__.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/_internal/action_aliases.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/_internal/action_validator.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/_internal/credential_hook.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/_internal/credential_scanner.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/_internal/credentials_data/__init__.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/_internal/credentials_data/built_in.yaml +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/_internal/dlp_scanner.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/_internal/enforcer.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/_internal/types.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/audit_local.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/canonical.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/__init__.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/__main__.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/_secrets.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/console.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/debug_bundle.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/doctor.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/hosts/__init__.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/hosts/antigravity.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/hosts/base.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/hosts/claude_code.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/hosts/codex_cli.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/hosts/gemini_cli.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/hosts/kiro.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/hosts/unknown.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/kiro_adapter.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/migrate.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/spool_cmd.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/telemetry_consent.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/antigravity/hooks.json +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/autogen.yaml +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/claude-code.yaml +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/codex-cli.yaml +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/cost-cap.yaml +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/crewai.yaml +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/cursor.yaml +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/gemini-cli.yaml +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/generic.yaml +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/kiro/ide-pre-tool-use.kiro.hook +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/kiro/ide-prompt-submit.kiro.hook +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/kiro/kiro.yaml +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/langchain.yaml +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/mcp.yaml +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/cli/templates/rag.yaml +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/client.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/device.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/enrollment.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/error_codes.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/error_codes.yaml +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/errors.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/hitl/__init__.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/hitl/grant_protocol.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/hitl/mock.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/hitl/pending_approval.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/hitl/secret_leak_guard.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/hitl/status.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/hooks/__init__.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/hooks/tool_output_handler.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/hosted_policy.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/__init__.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/anthropic.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/autogen.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/braintrust.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/crewai/__init__.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/crewai/agent.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/crewai/crew.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/crewai/task.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/crewai/tool.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/google.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/google_adk/__init__.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/google_adk/agent.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/google_adk/tool.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/langchain/__init__.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/langchain/agent.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/langchain/callbacks.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/langchain/chain.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/langchain/graph.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/langchain/modern.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/langchain/tool.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/langfuse.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/litellm.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/openai.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/pydantic_ai.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/integrations/vercel_ai.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/layout_migration.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/policy_loader.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/spool/__init__.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/spool/_compress.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/spool/_constants.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/spool/_crc32c.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/spool/_crypto.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/spool/_frame.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/spool/_keyring.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/spool/_metrics.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/spool/_spool.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/spool/_state.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/spool/_uploader.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/spool/cz-audit-v1.dict +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/tamper.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/controlzero/tracecontext.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/examples/hello_world.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/_fixtures/jcs_args_hash_vectors.json +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/conftest.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/integrations/__init__.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/integrations/test_google.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/parity/action_aliases.json +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/spool/__init__.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/spool/conftest.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/spool/test_spool_cli.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/spool/test_spool_concurrency.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/spool/test_spool_conformance.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/spool/test_spool_core.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/spool/test_spool_crash.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/spool/test_spool_diskfull.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/spool/test_spool_durable_default_tamper.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/spool/test_spool_keychain_dek.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/spool/test_spool_sink_wiring.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/spool/test_spool_transcript_localack.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/spool/test_spool_uploader.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_action_aliases.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_action_canonicalization.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_action_validator_t86.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_agent_name_env.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_antigravity_adapter.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_antigravity_ga_blockers_1248.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_antigravity_install.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_api_key_mask.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_audit_remote.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_audit_remote_sdk_version.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_audit_sink_isolation.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_bundle_parser.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_bundle_translate.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_canonical_phase1a.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_cli_carve_out.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_cli_debug_bundle.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_cli_extractor_integration.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_cli_hook.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_cli_hosted_refresh.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_cli_init.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_cli_init_templates.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_cli_tail.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_cli_test.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_cli_validate.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_coding_agent_hooks.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_conditions.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_conformance.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_console.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_credential_hook.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_default_action.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_device.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_dlp_scanner.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_doctor.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_engine_version_consistency.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_enrollment.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_env_dump_438.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_epic_1247_bryan_acceptance.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_error_codes.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_errors_e_codes.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_fail_closed_eval.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_glob_matching.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hitl_5d_email_install.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hitl_6a_cli_flag.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hitl_6a_exceptions.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hitl_6a_get_secret_hitl.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hitl_6a_mock_backend.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hitl_6a_pending_approval.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hitl_6a_request_approval.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hitl_6a_secret_leak_guard.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hitl_6a_wait.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hitl_conformance.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hitl_phase2b_protocol.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hitl_reason_codes.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hitl_validator_keys.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hook_extractors.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hosted_local_audit_1247.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hosted_policy_e2e.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hosts_adapter.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hybrid_mode_strict.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_hybrid_mode_warn.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_install_hook_command.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_install_hooks.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_kiro_adapter.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_kiro_cli_e2e.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_kiro_hook_templates.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_kiro_install.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_layout_migration_t101.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_layout_parity_t102.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_local_mode_dict.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_local_mode_file_json.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_local_mode_file_yaml.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_log_fallback_stderr.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_log_options_ignored_hosted.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_log_rotation.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_migrate.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_min_sdk_version_gate.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_multi_client_per_project_175.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_no_policy_no_key.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_observe_mode_1247.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_package_rename_shim.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_policy_engine_version_phase1b.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_policy_freshness.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_policy_settings.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_policy_source_audit.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_quarantine.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_reason_code.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_refresh.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_secrets.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_sql_semantic_class.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_synthetic_policy_id_t79.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_t103_precedence.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_t104_cache_gc.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_t108_local_override_audit.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_t96_single_audit_log.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_t99_install_prefetch_bundle.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_tamper.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_tamper_behavior.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_tamper_hook.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_telemetry_consent.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_tracecontext.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tests/test_unsafe_int_boundary.py +0 -0
- {controlzero-1.9.7 → controlzero-1.9.9}/tools/cz-kiro-adapter +0 -0
|
@@ -1,5 +1,66 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.9 -- 2026-06-17 (Antigravity tool vocab, JSON+YAML config parity, spool reliability)
|
|
4
|
+
|
|
5
|
+
Follow-ups to the gh#1303 fail-open work plus two correctness fixes.
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **Antigravity enforcement (gh#1303 part 2).** Real Antigravity emits
|
|
10
|
+
`run_command` (args under `CommandLine`) / `ListDir` / `view_file` /
|
|
11
|
+
`write_to_file` / `replace_file_content`, but the inbound alias table and the
|
|
12
|
+
dangerous-shell extractor assumed `run_command`/`read_file` with
|
|
13
|
+
`args.command`. Two compounding fail-opens against allow-by-default: a
|
|
14
|
+
canonical `deny: Bash` never matched Antigravity's `run_command`, and
|
|
15
|
+
`rm -rf` inside `CommandLine` was invisible to the argument-level scanner.
|
|
16
|
+
Added inbound aliases and made `Bash` read either `command` or `CommandLine`
|
|
17
|
+
(first non-empty wins, fail-closed fallback). Inbound-only -- the policy
|
|
18
|
+
vocabulary the four working hosts match is unchanged.
|
|
19
|
+
- **CLI hook path now auto-discovers JSON/YAML policy files (gh#67).** All
|
|
20
|
+
three SDKs already parse `.yaml`/`.yml`/`.json` and the in-process `Client`
|
|
21
|
+
auto-discovers all three in the working directory, but the CLI `hook-check`
|
|
22
|
+
resolver searched only `controlzero.yaml`. A project authored as
|
|
23
|
+
`controlzero.json` (no `.yaml`) was enforced by the `Client` yet INVISIBLE to
|
|
24
|
+
the enforcement hook -- it fell through to the global policy / BUNDLE_MISSING.
|
|
25
|
+
`hook-check` now searches `controlzero.{yaml,yml,json}` in the working
|
|
26
|
+
directory and `policy.{yaml,yml,json}` globally (first existing,
|
|
27
|
+
`.yaml` > `.yml` > `.json`), matching the `Client`.
|
|
28
|
+
- **Offline audit spool reliability on Python 3.13 (gh#1315).** The encrypted
|
|
29
|
+
spool needs `zstandard` and `cryptography`. `zstandard` 0.22.0 has no cp313
|
|
30
|
+
wheel, so on Python 3.13 the import could fail, the error was swallowed in
|
|
31
|
+
`BearerAuditSink._init_spool`, and the spool silently never formed -- hosted
|
|
32
|
+
audit fell back to a non-durable in-memory buffer. The `zstandard` floor is
|
|
33
|
+
raised to `>=0.23.0` (first cp313 wheels), the import failure is now logged
|
|
34
|
+
loudly and actionably (naming the missing dependency), and a
|
|
35
|
+
`_spool_unavailable_reason` is recorded for diagnostics.
|
|
36
|
+
|
|
37
|
+
## 1.9.8 -- 2026-06-17 (P0 enforcement fail-open: degraded/empty bundle, gh#1303)
|
|
38
|
+
|
|
39
|
+
Closes a P0 SECURITY fail-open found via a customer report: a hosted-policy
|
|
40
|
+
customer's destructive tool calls (e.g. `rm -rf`) were intermittently ALLOWED
|
|
41
|
+
across agent surfaces.
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- **Degraded/partial/stale bundle no longer fails OPEN (gh#1303).** The shared
|
|
46
|
+
decision core (`translate_to_local_policy`) synthesized an allow-all OBSERVE
|
|
47
|
+
rule whenever the translated rule set was empty -- but that fired not only for
|
|
48
|
+
a genuinely-empty project (intended observe, gh#1247) but also for a degraded
|
|
49
|
+
bundle (policies attached yet zero translatable rules) or a missing/malformed
|
|
50
|
+
`policies` key (truncated/stale). A customer who HAS a policy could thus get
|
|
51
|
+
allow-all. The translator now distinguishes a genuinely-empty project (the
|
|
52
|
+
backend ships an explicit empty list) -- which still OBSERVES (gh#1247
|
|
53
|
+
preserved) -- from any other zero-rule outcome, which now FAILS CLOSED (deny)
|
|
54
|
+
via `default_on_missing`. This lives in the shared core, so all five host
|
|
55
|
+
surfaces (Claude Code, Gemini CLI, Codex CLI, Antigravity, Kiro) inherit it.
|
|
56
|
+
- **Unrecognized rule effect now fails closed.** A validly-signed rule carrying
|
|
57
|
+
an unknown/future/typo effect was coerced to `allow` (allow-* for its
|
|
58
|
+
pattern); it now defaults to `deny`.
|
|
59
|
+
|
|
60
|
+
Residual stale-empty-cache replay window (backend bundle provenance + cache
|
|
61
|
+
freshness) and the Antigravity tool-vocabulary gap are tracked in gh#1303 and
|
|
62
|
+
land in follow-up releases.
|
|
63
|
+
|
|
3
64
|
## 1.9.7 -- 2026-06-16 (hosted audit delivery for short-lived processes, gh#1292)
|
|
4
65
|
|
|
5
66
|
Fixes a P0 found via a customer report: hosted-mode audit rows were written to
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: controlzero
|
|
3
|
-
Version: 1.9.
|
|
3
|
+
Version: 1.9.9
|
|
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
|
|
@@ -30,7 +30,7 @@ Requires-Dist: pydantic>=2.0.0
|
|
|
30
30
|
Requires-Dist: pyyaml>=6.0
|
|
31
31
|
Requires-Dist: rfc8785<0.2,>=0.1.4
|
|
32
32
|
Requires-Dist: rich>=13.0.0
|
|
33
|
-
Requires-Dist: zstandard>=0.
|
|
33
|
+
Requires-Dist: zstandard>=0.23.0
|
|
34
34
|
Provides-Extra: anthropic
|
|
35
35
|
Requires-Dist: anthropic>=0.25.0; extra == 'anthropic'
|
|
36
36
|
Provides-Extra: dev
|
|
@@ -41,13 +41,13 @@ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
|
41
41
|
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
42
42
|
Requires-Dist: pyyaml>=6.0; extra == 'dev'
|
|
43
43
|
Requires-Dist: respx>=0.20.0; extra == 'dev'
|
|
44
|
-
Requires-Dist: zstandard>=0.
|
|
44
|
+
Requires-Dist: zstandard>=0.23.0; extra == 'dev'
|
|
45
45
|
Provides-Extra: google
|
|
46
46
|
Requires-Dist: google-genai>=0.3.0; extra == 'google'
|
|
47
47
|
Provides-Extra: hosted
|
|
48
48
|
Requires-Dist: cryptography>=41.0.0; extra == 'hosted'
|
|
49
49
|
Requires-Dist: httpx>=0.25.0; extra == 'hosted'
|
|
50
|
-
Requires-Dist: zstandard>=0.
|
|
50
|
+
Requires-Dist: zstandard>=0.23.0; extra == 'hosted'
|
|
51
51
|
Provides-Extra: openai
|
|
52
52
|
Requires-Dist: openai>=1.0.0; extra == 'openai'
|
|
53
53
|
Description-Content-Type: text/markdown
|
|
@@ -93,7 +93,7 @@ Your AI agents call tools. Some of those tools should never be called by an
|
|
|
93
93
|
agent without a human in the loop. `controlzero` is the policy layer between
|
|
94
94
|
the model's output and the tool execution. Decisions are fail-closed by default.
|
|
95
95
|
|
|
96
|
-
You can use it offline with a local YAML file or Python dict. When you want to
|
|
96
|
+
You can use it offline with a local YAML or JSON file or Python dict. When you want to
|
|
97
97
|
share policies across a team or get a hosted audit dashboard, sign up at
|
|
98
98
|
[controlzero.ai](https://controlzero.ai) and set `CONTROLZERO_API_KEY`.
|
|
99
99
|
|
|
@@ -155,8 +155,10 @@ cz = Client(policy_file="./controlzero.yaml")
|
|
|
155
155
|
cz = Client()
|
|
156
156
|
```
|
|
157
157
|
|
|
158
|
-
If
|
|
159
|
-
automatically.
|
|
158
|
+
If a policy file exists in the current directory it is picked up
|
|
159
|
+
automatically -- `controlzero.yaml`, `controlzero.yml`, or `controlzero.json`
|
|
160
|
+
are auto-detected in that order (first existing wins). No environment
|
|
161
|
+
variable needed. The file may be YAML or JSON; both use the identical schema.
|
|
160
162
|
|
|
161
163
|
## Policy schema
|
|
162
164
|
|
|
@@ -39,7 +39,7 @@ Your AI agents call tools. Some of those tools should never be called by an
|
|
|
39
39
|
agent without a human in the loop. `controlzero` is the policy layer between
|
|
40
40
|
the model's output and the tool execution. Decisions are fail-closed by default.
|
|
41
41
|
|
|
42
|
-
You can use it offline with a local YAML file or Python dict. When you want to
|
|
42
|
+
You can use it offline with a local YAML or JSON file or Python dict. When you want to
|
|
43
43
|
share policies across a team or get a hosted audit dashboard, sign up at
|
|
44
44
|
[controlzero.ai](https://controlzero.ai) and set `CONTROLZERO_API_KEY`.
|
|
45
45
|
|
|
@@ -101,8 +101,10 @@ cz = Client(policy_file="./controlzero.yaml")
|
|
|
101
101
|
cz = Client()
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
If
|
|
105
|
-
automatically.
|
|
104
|
+
If a policy file exists in the current directory it is picked up
|
|
105
|
+
automatically -- `controlzero.yaml`, `controlzero.yml`, or `controlzero.json`
|
|
106
|
+
are auto-detected in that order (first existing wins). No environment
|
|
107
|
+
variable needed. The file may be YAML or JSON; both use the identical schema.
|
|
106
108
|
|
|
107
109
|
## Policy schema
|
|
108
110
|
|
|
@@ -527,7 +527,13 @@ def translate_to_local_policy(payload: dict) -> dict:
|
|
|
527
527
|
if default_on_tamper not in VALID_DEFAULT_ON_TAMPER:
|
|
528
528
|
default_on_tamper = DEFAULT_BUNDLE_ON_TAMPER
|
|
529
529
|
|
|
530
|
-
|
|
530
|
+
# #1303: keep the RAW policies value to distinguish a genuinely-empty
|
|
531
|
+
# project (the backend ships an explicit empty list `policies: []`) from a
|
|
532
|
+
# DEGRADED/partial bundle (policies attached but zero translatable rules, or
|
|
533
|
+
# a missing/malformed `policies` key). The former is observe (#1247); the
|
|
534
|
+
# latter must fail CLOSED, never observe-allow.
|
|
535
|
+
raw_policies = payload.get("policies")
|
|
536
|
+
policies = raw_policies or []
|
|
531
537
|
policies = sorted(
|
|
532
538
|
[p for p in policies if isinstance(p, dict)],
|
|
533
539
|
key=lambda p: int(p.get("priority", 100)),
|
|
@@ -550,6 +556,40 @@ def translate_to_local_policy(payload: dict) -> dict:
|
|
|
550
556
|
if translated is not None:
|
|
551
557
|
flat.append(translated)
|
|
552
558
|
|
|
559
|
+
if not flat and not (isinstance(raw_policies, list) and len(raw_policies) == 0):
|
|
560
|
+
# #1303 FAIL-OPEN FIX (the empty-vs-degraded boundary). We have zero
|
|
561
|
+
# translatable rules, but this is NOT a genuinely-empty project: the
|
|
562
|
+
# payload carried attached policies that produced no enforceable rules,
|
|
563
|
+
# OR a missing / non-list `policies` key (truncated / malformed / stale
|
|
564
|
+
# bundle). Treating that as observe would ALLOW EVERY tool call for a
|
|
565
|
+
# customer who HAS a policy -- the reproduced rm-rf fail-open. Fail
|
|
566
|
+
# CLOSED here via default_on_missing (canonical deny), with a distinct
|
|
567
|
+
# reason_code so it is not confused with a genuine empty project. The
|
|
568
|
+
# genuinely-empty observe path (#1247) below is reached ONLY when the
|
|
569
|
+
# backend shipped an explicit empty list (`policies: []`).
|
|
570
|
+
#
|
|
571
|
+
# NOTE: a stale CACHED bundle that legitimately held `policies: []` from
|
|
572
|
+
# when the project WAS empty is NOT caught here (it looks genuinely
|
|
573
|
+
# empty); that residual replay window is closed by bundle provenance +
|
|
574
|
+
# freshness (#1303 part 3 / backend active_policy_count).
|
|
575
|
+
# Reuse the registered BUNDLE_MISSING reason_code / synthetic id (both
|
|
576
|
+
# already in VALID_REASON_CODES / VALID_SYNTHETIC_POLICY_IDS) so no new
|
|
577
|
+
# error-catalog entry is introduced; the human-readable reason names the
|
|
578
|
+
# degraded/partial/stale cause. effect honors default_on_missing (deny).
|
|
579
|
+
flat.append({
|
|
580
|
+
"effect": default_on_missing,
|
|
581
|
+
"action": "*",
|
|
582
|
+
"id": "synthetic:BUNDLE_MISSING",
|
|
583
|
+
"reason": (
|
|
584
|
+
"Your project has attached policies but the resolved bundle "
|
|
585
|
+
"produced zero enforceable rules (a degraded, partial, or stale "
|
|
586
|
+
"bundle). Control Zero is failing CLOSED (deny) rather than "
|
|
587
|
+
"allowing every tool call. Regenerate the policy bundle in the "
|
|
588
|
+
"Control Zero dashboard; contact support if this persists."
|
|
589
|
+
),
|
|
590
|
+
"reason_code": "BUNDLE_MISSING",
|
|
591
|
+
})
|
|
592
|
+
|
|
553
593
|
if not flat:
|
|
554
594
|
# Empty policy set (RESOLVED SUCCESSFULLY, zero translatable
|
|
555
595
|
# rules): synthetic catch-all rule whose posture is driven by
|
|
@@ -711,9 +751,12 @@ def _translate_rule(rule: dict, policy_id: str) -> Optional[dict]:
|
|
|
711
751
|
singular (``tool`` / ``pattern`` / ``action`` / ``match.tool``) forms
|
|
712
752
|
are now supported; plural wins when both are present.
|
|
713
753
|
"""
|
|
714
|
-
# Accept several spellings of "effect". Policy engine has four
|
|
715
|
-
#
|
|
716
|
-
# fail
|
|
754
|
+
# Accept several spellings of "effect". Policy engine has four canonical
|
|
755
|
+
# effects; an UNRECOGNIZED effect on a validly-signed rule (typo, future
|
|
756
|
+
# effect, corruption) must fail CLOSED. Coercing it to "allow" (the old
|
|
757
|
+
# behavior) turned an unknown rule into allow-*-for-its-pattern -- a
|
|
758
|
+
# fail-open for a security gate (#1303). A deny here over-denies that one
|
|
759
|
+
# pattern at worst, which is the safe direction.
|
|
717
760
|
effect_raw = rule.get("effect")
|
|
718
761
|
if not effect_raw:
|
|
719
762
|
# Only fall back to rule["action"] for effect if it looks like
|
|
@@ -722,7 +765,7 @@ def _translate_rule(rule: dict, policy_id: str) -> Optional[dict]:
|
|
|
722
765
|
fallback = rule.get("action")
|
|
723
766
|
if fallback in ("allow", "deny", "warn", "audit"):
|
|
724
767
|
effect_raw = fallback
|
|
725
|
-
effect = effect_raw if effect_raw in ("allow", "deny", "warn", "audit") else "
|
|
768
|
+
effect = effect_raw if effect_raw in ("allow", "deny", "warn", "audit") else "deny"
|
|
726
769
|
|
|
727
770
|
# Tool pattern resolution order:
|
|
728
771
|
# 1. plural ``actions`` (list, as emitted by the backend)
|
|
@@ -566,7 +566,14 @@ def extract_method(
|
|
|
566
566
|
2. If the tool is unknown, method = ``"*"``.
|
|
567
567
|
3. Read ``args_path`` from the entry. If non-null, ``raw =
|
|
568
568
|
args[args_path]``; else ``raw = tool_name`` (used by
|
|
569
|
-
``file_read`` / ``file_write``).
|
|
569
|
+
``file_read`` / ``file_write``). ``args_path`` may be a single
|
|
570
|
+
key (string) or a list of candidate keys, in which case the
|
|
571
|
+
first key that resolves to a non-empty string is used. The list
|
|
572
|
+
form lets one canonical tool read the differently-named command
|
|
573
|
+
argument each host emits -- e.g. the ``Bash`` shell tool reads
|
|
574
|
+
``command`` (Claude Code / Gemini CLI) or ``CommandLine``
|
|
575
|
+
(Antigravity ``run_command``) so the dangerous-command scanner
|
|
576
|
+
fires regardless of which host sent the call.
|
|
570
577
|
4. Apply the ``extract`` function. Non-empty result = method.
|
|
571
578
|
5. Empty result -> ``fallback_method``.
|
|
572
579
|
|
|
@@ -593,11 +600,24 @@ def extract_method(
|
|
|
593
600
|
else:
|
|
594
601
|
if not isinstance(args, dict):
|
|
595
602
|
return canonical, fallback
|
|
596
|
-
|
|
603
|
+
# ``args_path`` is either a single key (string) or a list of
|
|
604
|
+
# candidate keys. The list form lets one canonical tool read
|
|
605
|
+
# the differently-named command argument each host emits --
|
|
606
|
+
# e.g. ``Bash`` reads ``command`` (Claude Code / Gemini CLI)
|
|
607
|
+
# or ``CommandLine`` (Antigravity ``run_command``). The first
|
|
608
|
+
# key that resolves to a non-empty string wins; if none do,
|
|
609
|
+
# fall back so the no-arg path stays fail-closed.
|
|
610
|
+
candidates = (
|
|
611
|
+
args_path if isinstance(args_path, list) else [args_path]
|
|
612
|
+
)
|
|
613
|
+
raw = None
|
|
614
|
+
for key in candidates:
|
|
615
|
+
value = args.get(key)
|
|
616
|
+
if isinstance(value, str) and value != "":
|
|
617
|
+
raw = value
|
|
618
|
+
break
|
|
597
619
|
if raw is None:
|
|
598
620
|
return canonical, fallback
|
|
599
|
-
if not isinstance(raw, str):
|
|
600
|
-
return canonical, fallback
|
|
601
621
|
|
|
602
622
|
func = _EXTRACTORS.get(extract_name)
|
|
603
623
|
if func is None:
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"aliases": ["sql", "Database", "PostgreSQL", "MySQL", "postgres", "sqlite"]
|
|
10
10
|
},
|
|
11
11
|
"Bash": {
|
|
12
|
-
"args_path": "command",
|
|
12
|
+
"args_path": ["command", "CommandLine"],
|
|
13
13
|
"extract": "most_dangerous_shell_command",
|
|
14
14
|
"fallback_method": "*",
|
|
15
15
|
"aliases": [
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"shell",
|
|
18
18
|
"ShellTool",
|
|
19
19
|
"run_shell_command",
|
|
20
|
+
"run_command",
|
|
20
21
|
"PowerShell",
|
|
21
22
|
"powershell",
|
|
22
23
|
"Shell"
|
|
@@ -44,19 +45,19 @@
|
|
|
44
45
|
"args_path": null,
|
|
45
46
|
"extract": "tool_name_as_method",
|
|
46
47
|
"fallback_method": "read",
|
|
47
|
-
"aliases": ["read_file", "Read", "ReadFile", "read_many_files"]
|
|
48
|
+
"aliases": ["read_file", "Read", "ReadFile", "read_many_files", "view_file"]
|
|
48
49
|
},
|
|
49
50
|
"file_write": {
|
|
50
51
|
"args_path": null,
|
|
51
52
|
"extract": "tool_name_as_method",
|
|
52
53
|
"fallback_method": "write",
|
|
53
|
-
"aliases": ["write_file", "Write", "WriteFile", "edit_file", "Edit", "replace", "apply_patch"]
|
|
54
|
+
"aliases": ["write_file", "Write", "WriteFile", "edit_file", "Edit", "replace", "apply_patch", "write_to_file", "replace_file_content"]
|
|
54
55
|
},
|
|
55
56
|
"file_search": {
|
|
56
57
|
"args_path": null,
|
|
57
58
|
"extract": "tool_name_as_method",
|
|
58
59
|
"fallback_method": "search",
|
|
59
|
-
"aliases": ["Grep", "grep_search", "Glob", "glob"]
|
|
60
|
+
"aliases": ["Grep", "grep_search", "Glob", "glob", "ListDir"]
|
|
60
61
|
},
|
|
61
62
|
"task": {
|
|
62
63
|
"args_path": null,
|
|
@@ -301,6 +301,11 @@ class _SpoolWiringMixin:
|
|
|
301
301
|
self._detached_drain_spawned = False
|
|
302
302
|
self._spool = None
|
|
303
303
|
self._spool_mode = "off"
|
|
304
|
+
# #1315: when the durable spool cannot form (e.g. zstandard/cryptography
|
|
305
|
+
# missing or binary-incompatible on this interpreter), record WHY so
|
|
306
|
+
# diagnostics (`controlzero doctor`) can surface it instead of the user
|
|
307
|
+
# silently running on the ephemeral in-memory buffer. None == healthy.
|
|
308
|
+
self._spool_unavailable_reason = None
|
|
304
309
|
# Fast path: flag off/unset, NO hosted default, AND no spool
|
|
305
310
|
# directory on disk -- skip the spool import entirely so the
|
|
306
311
|
# legacy default path stays byte-identical. When default_mode is
|
|
@@ -342,8 +347,29 @@ class _SpoolWiringMixin:
|
|
|
342
347
|
if (self._spool is not None
|
|
343
348
|
and getattr(self._spool, "in_memory_mode", False)):
|
|
344
349
|
self._spool_init_degraded("memory_mode")
|
|
350
|
+
except ImportError as exc:
|
|
351
|
+
# #1315: the durable spool needs `zstandard` (compression) and
|
|
352
|
+
# `cryptography` (AES-GCM). A missing or binary-incompatible wheel
|
|
353
|
+
# (classically zstandard 0.22.0 on Python 3.13 -- no cp313 wheel)
|
|
354
|
+
# raises here BEFORE the spool dir is created. Name the cause loudly
|
|
355
|
+
# so the operator can fix the dependency instead of unknowingly
|
|
356
|
+
# running on the non-durable in-memory buffer. The dependency floor
|
|
357
|
+
# was raised (zstandard>=0.23.0) to prevent the common case.
|
|
358
|
+
self._spool_init_degraded("import_error")
|
|
359
|
+
self._spool_unavailable_reason = f"import_error: {exc}"
|
|
360
|
+
logger.warning(
|
|
361
|
+
"controlzero: audit spool dependencies unavailable (%s); "
|
|
362
|
+
"the encrypted offline spool is DISABLED and audit falls back "
|
|
363
|
+
"to in-memory buffering (durability reduced). Ensure "
|
|
364
|
+
"'zstandard>=0.23.0' and 'cryptography' are installed for this "
|
|
365
|
+
"Python (pip install -U controlzero).",
|
|
366
|
+
exc,
|
|
367
|
+
)
|
|
368
|
+
self._spool = None
|
|
369
|
+
self._spool_mode = "off"
|
|
345
370
|
except Exception as exc: # noqa: BLE001
|
|
346
371
|
self._spool_init_degraded("exception")
|
|
372
|
+
self._spool_unavailable_reason = f"exception: {exc}"
|
|
347
373
|
logger.warning(
|
|
348
374
|
"controlzero: audit spool unavailable (%s); "
|
|
349
375
|
"falling back to in-memory buffering",
|
|
@@ -50,11 +50,47 @@ DEFAULT_TEMPLATES = [
|
|
|
50
50
|
]
|
|
51
51
|
|
|
52
52
|
# Where the global policy lives when controlzero is used as a Claude Code hook.
|
|
53
|
-
# Per-project ./controlzero.yaml takes precedence (the SDK already
|
|
53
|
+
# Per-project ./controlzero.{yaml,yml,json} takes precedence (the SDK already
|
|
54
|
+
# does this).
|
|
54
55
|
GLOBAL_POLICY_DIR = Path.home() / ".controlzero"
|
|
55
56
|
GLOBAL_POLICY_PATH = GLOBAL_POLICY_DIR / "policy.yaml"
|
|
56
57
|
GLOBAL_POLICY_SIG_PATH = GLOBAL_POLICY_DIR / "policy.yaml.sig"
|
|
57
58
|
GLOBAL_TAMPER_KEY_PATH = GLOBAL_POLICY_DIR / "tamper.key"
|
|
59
|
+
|
|
60
|
+
# Policy-file extensions auto-discovered in the hook resolution order, in
|
|
61
|
+
# precedence order. The local policy loader accepts all three (YAML and JSON
|
|
62
|
+
# -- see ``policy_loader._load_from_file``) and the in-process ``Client``
|
|
63
|
+
# already auto-discovers all three in cwd; the CLI hook path must search the
|
|
64
|
+
# same set so a project authored as ``controlzero.json`` is enforced by the
|
|
65
|
+
# hook exactly like ``controlzero.yaml`` (else it falls through to the global
|
|
66
|
+
# policy / BUNDLE_MISSING -- a JSON-config user would otherwise be invisible
|
|
67
|
+
# to enforcement). Keep this list in sync with ``client._resolve_local_source``.
|
|
68
|
+
_POLICY_FILE_EXTS = (".yaml", ".yml", ".json")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _first_existing_policy(directory: Path, basename: str) -> Optional[Path]:
|
|
72
|
+
"""Return the first existing ``<directory>/<basename><ext>`` for ``ext`` in
|
|
73
|
+
:data:`_POLICY_FILE_EXTS` (``.yaml`` -> ``.yml`` -> ``.json``), else None."""
|
|
74
|
+
for ext in _POLICY_FILE_EXTS:
|
|
75
|
+
candidate = directory / f"{basename}{ext}"
|
|
76
|
+
if candidate.exists():
|
|
77
|
+
return candidate
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _first_existing_variant(base: Path) -> Optional[Path]:
|
|
82
|
+
"""Return the first existing ``base`` with each policy extension swapped in
|
|
83
|
+
(``.yaml`` -> ``.yml`` -> ``.json``), else None.
|
|
84
|
+
|
|
85
|
+
Anchored on ``base`` (not its directory) so the global lookup honors a
|
|
86
|
+
monkeypatched ``GLOBAL_POLICY_PATH`` -- the established test contract
|
|
87
|
+
(e.g. ``test_install_hooks`` sets ``GLOBAL_POLICY_PATH`` to a non-existent
|
|
88
|
+
file to assert the missing-policy path)."""
|
|
89
|
+
for ext in _POLICY_FILE_EXTS:
|
|
90
|
+
candidate = base.with_suffix(ext)
|
|
91
|
+
if candidate.exists():
|
|
92
|
+
return candidate
|
|
93
|
+
return None
|
|
58
94
|
GLOBAL_AUDIT_PATH = GLOBAL_POLICY_DIR / "audit.log"
|
|
59
95
|
# Where the hook-check CLI logs surface-level events that don't flow
|
|
60
96
|
# through the policy evaluator -- today just the unenrolled first-run
|
|
@@ -1349,20 +1385,17 @@ def sign_policy(policy: Optional[str], verify_only: bool):
|
|
|
1349
1385
|
|
|
1350
1386
|
|
|
1351
1387
|
def _has_resolvable_policy_file() -> bool:
|
|
1352
|
-
"""True when a ``./controlzero.yaml`` or
|
|
1353
|
-
exists and looks loadable.
|
|
1388
|
+
"""True when a ``./controlzero.{yaml,yml,json}`` or
|
|
1389
|
+
``~/.controlzero/policy.{yaml,yml,json}`` exists and looks loadable.
|
|
1354
1390
|
|
|
1355
1391
|
Scoped to the carve-out logic: "do we already have a policy in
|
|
1356
1392
|
the standard search paths?" If yes, the user is past first-run
|
|
1357
1393
|
and the carve-out banner is noise. Mirrors the happy-path search
|
|
1358
1394
|
order in :func:`_resolve_hook_policy` so the two stay in sync.
|
|
1359
1395
|
"""
|
|
1360
|
-
|
|
1361
|
-
if cwd_policy.exists():
|
|
1362
|
-
return True
|
|
1363
|
-
if GLOBAL_POLICY_PATH.exists():
|
|
1396
|
+
if _first_existing_policy(Path.cwd(), "controlzero") is not None:
|
|
1364
1397
|
return True
|
|
1365
|
-
return
|
|
1398
|
+
return _first_existing_variant(GLOBAL_POLICY_PATH) is not None
|
|
1366
1399
|
|
|
1367
1400
|
|
|
1368
1401
|
def _is_unenrolled_first_run() -> bool:
|
|
@@ -1783,19 +1816,22 @@ def _maybe_refresh_policy(
|
|
|
1783
1816
|
def _resolve_hook_policy(explicit: Optional[str]) -> Optional[Path]:
|
|
1784
1817
|
"""Find a policy file using the hook-time resolution order:
|
|
1785
1818
|
|
|
1786
|
-
1. Explicit --policy argument
|
|
1787
|
-
2. ./controlzero.yaml (per-project)
|
|
1788
|
-
3. ~/.controlzero/policy.yaml (global)
|
|
1819
|
+
1. Explicit --policy argument (any extension; the loader dispatches on it)
|
|
1820
|
+
2. ./controlzero.{yaml,yml,json} (per-project, first existing)
|
|
1821
|
+
3. ~/.controlzero/policy.{yaml,yml,json} (global, first existing)
|
|
1822
|
+
|
|
1823
|
+
The per-project and global searches probe all three extensions so a
|
|
1824
|
+
project authored as ``controlzero.json`` (or ``.yml``) is enforced by the
|
|
1825
|
+
hook identically to ``controlzero.yaml`` -- matching the in-process
|
|
1826
|
+
``Client`` (:func:`client._resolve_local_source`).
|
|
1789
1827
|
"""
|
|
1790
1828
|
if explicit:
|
|
1791
1829
|
p = Path(explicit)
|
|
1792
1830
|
return p if p.exists() else None
|
|
1793
|
-
cwd = Path.cwd()
|
|
1794
|
-
if cwd
|
|
1831
|
+
cwd = _first_existing_policy(Path.cwd(), "controlzero")
|
|
1832
|
+
if cwd is not None:
|
|
1795
1833
|
return cwd
|
|
1796
|
-
|
|
1797
|
-
return GLOBAL_POLICY_PATH
|
|
1798
|
-
return None
|
|
1834
|
+
return _first_existing_variant(GLOBAL_POLICY_PATH)
|
|
1799
1835
|
|
|
1800
1836
|
|
|
1801
1837
|
def _policy_has_deny_rules(policy_path: Path) -> bool:
|
|
@@ -22,13 +22,27 @@
|
|
|
22
22
|
# To see what got blocked (and what was allowed):
|
|
23
23
|
# tail -f ~/.controlzero/audit.log
|
|
24
24
|
#
|
|
25
|
-
# Antigravity
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
25
|
+
# Antigravity (agy) real tool names (as of 2026-06) and the canonical
|
|
26
|
+
# Control Zero tool each one normalizes to. Write your deny rules
|
|
27
|
+
# against the CANONICAL name on the right -- the SDK resolves every
|
|
28
|
+
# agy tool name to its canonical equivalent before the policy engine
|
|
29
|
+
# evaluates anything, so one rule (e.g. `Bash`) covers agy + Claude
|
|
30
|
+
# Code + Gemini CLI + Codex CLI:
|
|
31
|
+
#
|
|
32
|
+
# agy tool name args canonical tool
|
|
33
|
+
# -------------------- ------------------------ --------------
|
|
34
|
+
# run_command CommandLine, Cwd Bash
|
|
35
|
+
# write_to_file TargetFile, CodeContent file_write
|
|
36
|
+
# replace_file_content TargetFile, ...Chunks file_write
|
|
37
|
+
# read_file ... file_read
|
|
38
|
+
# view_file ... file_read
|
|
39
|
+
# ListDir ... file_search
|
|
40
|
+
# browser_* ... (literal / glob)
|
|
41
|
+
# mcp_* tool-specific args (literal / glob)
|
|
42
|
+
#
|
|
43
|
+
# The dangerous-command scanner reads agy's `CommandLine` argument the
|
|
44
|
+
# same way it reads `command` from other hosts, so `Bash:rm` matches a
|
|
45
|
+
# `run_command` whose `CommandLine` is `rm -rf /`.
|
|
32
46
|
#
|
|
33
47
|
# Antigravity packs the call under a top-level `toolCall{name,args}`
|
|
34
48
|
# envelope; the host adapter flattens it to tool_name / tool_input
|
|
@@ -56,17 +70,25 @@ rules:
|
|
|
56
70
|
# DENY: destructive patterns at the tool level
|
|
57
71
|
# ============================================================
|
|
58
72
|
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
73
|
+
# Write rules against the CANONICAL tool name (see the table above).
|
|
74
|
+
# `Bash` covers agy's `run_command`; `file_write` covers
|
|
75
|
+
# `write_to_file` + `replace_file_content`; `file_read` covers
|
|
76
|
+
# `read_file` + `view_file`; `file_search` covers `ListDir`.
|
|
77
|
+
#
|
|
78
|
+
# Argument-level rules also work: the SDK extracts the dangerous
|
|
79
|
+
# command from agy's `CommandLine` argument, so `Bash:rm` blocks an
|
|
80
|
+
# `rm` while leaving the rest of `Bash` allowed.
|
|
81
|
+
#
|
|
82
|
+
# - id: deny-rm
|
|
83
|
+
# deny: 'Bash:rm'
|
|
84
|
+
# reason: 'rm blocked in this workspace'
|
|
63
85
|
#
|
|
64
86
|
# - id: deny-file-writes
|
|
65
|
-
# deny: '
|
|
87
|
+
# deny: 'file_write'
|
|
66
88
|
# reason: 'No unattended file writes in this project'
|
|
67
89
|
#
|
|
68
90
|
# - id: deny-shell-entirely
|
|
69
|
-
# deny: '
|
|
91
|
+
# deny: 'Bash'
|
|
70
92
|
# reason: 'This workspace does not allow shell execution'
|
|
71
93
|
|
|
72
94
|
# ============================================================
|
|
@@ -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.9"
|
|
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"}
|
|
@@ -37,7 +37,12 @@ dependencies = [
|
|
|
37
37
|
# (cryptography is the bulk), acceptable for the UX win.
|
|
38
38
|
"httpx>=0.25.0",
|
|
39
39
|
"cryptography>=41.0.0",
|
|
40
|
-
|
|
40
|
+
# zstandard floor is 0.23.0: it is the first release to ship cp313
|
|
41
|
+
# wheels. With the old >=0.22.0 floor a Python 3.13 environment could
|
|
42
|
+
# resolve 0.22.0 (no cp313 wheel) and fail to import, which silently
|
|
43
|
+
# disabled the encrypted audit spool (the import error was swallowed in
|
|
44
|
+
# BearerAuditSink._init_spool). See #1315.
|
|
45
|
+
"zstandard>=0.23.0",
|
|
41
46
|
# RFC 8785 JSON Canonicalization Scheme. Single source of truth for
|
|
42
47
|
# cross-SDK audit hashing (args_hash field). Same bytes from Python,
|
|
43
48
|
# Node, and Go SDKs for the same input. ~12 KB pure-Python wheel.
|
|
@@ -60,7 +65,7 @@ dependencies = [
|
|
|
60
65
|
hosted = [
|
|
61
66
|
"httpx>=0.25.0",
|
|
62
67
|
"cryptography>=41.0.0",
|
|
63
|
-
"zstandard>=0.
|
|
68
|
+
"zstandard>=0.23.0", # cp313 wheels (#1315)
|
|
64
69
|
]
|
|
65
70
|
google = [
|
|
66
71
|
# New Gemini SDK. The deprecated google-generativeai package is no
|
|
@@ -80,7 +85,7 @@ dev = [
|
|
|
80
85
|
"pyyaml>=6.0",
|
|
81
86
|
"httpx>=0.25.0",
|
|
82
87
|
"cryptography>=41.0.0",
|
|
83
|
-
"zstandard>=0.
|
|
88
|
+
"zstandard>=0.23.0", # cp313 wheels (#1315)
|
|
84
89
|
"respx>=0.20.0",
|
|
85
90
|
]
|
|
86
91
|
|
|
@@ -69,8 +69,10 @@ def test_allow_emits_explicit_allow_decision(tmp_path):
|
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
def test_deny_emits_deny_decision_from_toolcall_and_exits_zero(tmp_path):
|
|
72
|
-
# A deny rule on the
|
|
73
|
-
# reached the engine through the real command path.
|
|
72
|
+
# A deny rule on the CANONICAL tool name proves toolCall normalization
|
|
73
|
+
# reached the engine through the real command path. Since #1303 Part 2
|
|
74
|
+
# agy's `run_command` resolves to canonical `Bash`, so the portable rule
|
|
75
|
+
# is `deny: Bash` (one rule covers agy + Claude Code + Gemini + Codex).
|
|
74
76
|
#
|
|
75
77
|
# CRITICAL: Antigravity decides from the stdout JSON, NOT the exit code. A
|
|
76
78
|
# non-zero exit is read by agy as a fail-closed deny that would OVERRIDE
|
|
@@ -79,7 +81,7 @@ def test_deny_emits_deny_decision_from_toolcall_and_exits_zero(tmp_path):
|
|
|
79
81
|
p = _write_policy(
|
|
80
82
|
tmp_path,
|
|
81
83
|
[
|
|
82
|
-
{"deny": "
|
|
84
|
+
{"deny": "Bash", "reason": "shell blocked in this workspace"},
|
|
83
85
|
{"allow": "*"},
|
|
84
86
|
],
|
|
85
87
|
)
|
|
@@ -126,10 +128,11 @@ def test_decision_is_never_empty_object(tmp_path):
|
|
|
126
128
|
def test_exit_code_convention_is_host_aware(tmp_path):
|
|
127
129
|
# REGRESSION: the host-aware exit code must not change Claude Code's
|
|
128
130
|
# convention. The SAME deny rule exits 0 for Antigravity (decision lives
|
|
129
|
-
# in the JSON) but exits 2 for Claude Code (non-zero = block).
|
|
131
|
+
# in the JSON) but exits 2 for Claude Code (non-zero = block). The rule
|
|
132
|
+
# targets canonical `Bash` (run_command resolves to it since #1303 Part 2).
|
|
130
133
|
p = _write_policy(
|
|
131
134
|
tmp_path,
|
|
132
|
-
[{"deny": "
|
|
135
|
+
[{"deny": "Bash", "reason": "blocked"}, {"allow": "*"}],
|
|
133
136
|
)
|
|
134
137
|
runner = CliRunner()
|
|
135
138
|
|