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