controlzero 1.11.8__tar.gz → 1.11.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.11.8 → controlzero-1.11.9}/CHANGELOG.md +44 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/PKG-INFO +27 -1
- {controlzero-1.11.8 → controlzero-1.11.9}/README.md +26 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/__init__.py +1 -1
- controlzero-1.11.9/controlzero/_internal/_messages.py +273 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/_internal/bundle.py +15 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/_internal/enforcer.py +105 -39
- controlzero-1.11.9/controlzero/_internal/source_labels.py +165 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/_internal/types.py +7 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/audit_remote.py +16 -1
- controlzero-1.11.9/controlzero/cli/agent_config.py +171 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/hosts/base.py +7 -1
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/main.py +693 -90
- controlzero-1.11.9/controlzero/cli/output.py +172 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/client.py +43 -4
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/policy_loader.py +33 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/pyproject.toml +1 -1
- controlzero-1.11.9/tests/test_audit_project_id_attribution_pr2.py +305 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_hitl_validator_keys.py +6 -1
- controlzero-1.11.9/tests/test_hook_mask_failclosed.py +95 -0
- controlzero-1.11.9/tests/test_localized_reason.py +302 -0
- controlzero-1.11.9/tests/test_pr4_install_messaging.py +377 -0
- controlzero-1.11.9/tests/test_pr4_output.py +144 -0
- controlzero-1.11.9/tests/test_pr4_status.py +139 -0
- controlzero-1.11.9/tests/test_source_labels.py +95 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_synthetic_policy_id_t79.py +6 -2
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_t99_install_prefetch_bundle.py +5 -5
- {controlzero-1.11.8 → controlzero-1.11.9}/.gitignore +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/Dockerfile.test +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/LICENSE +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/__main__.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/_internal/__init__.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/_internal/action_aliases.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/_internal/action_validator.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/_internal/credential_hook.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/_internal/credential_scanner.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/_internal/credentials_data/__init__.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/_internal/credentials_data/built_in.yaml +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/_internal/dlp_scanner.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/_internal/hook_extractors.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/_internal/io_capture_producer.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/_internal/tool_extractors.json +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/audit_local.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/canonical.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/__init__.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/__main__.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/_secrets.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/console.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/debug_bundle.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/doctor.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/exec_cmd.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/hook_entry.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/hosts/__init__.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/hosts/antigravity.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/hosts/claude_code.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/hosts/codex_cli.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/hosts/cursor.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/hosts/cursor_cli.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/hosts/gemini_cli.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/hosts/kiro.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/hosts/unknown.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/kiro_adapter.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/migrate.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/spool_cmd.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/telemetry_consent.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/templates/antigravity/hooks.json +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/templates/antigravity.yaml +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/templates/autogen.yaml +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/templates/claude-code.yaml +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/templates/codex-cli.yaml +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/templates/cost-cap.yaml +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/templates/crewai.yaml +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/templates/cursor.yaml +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/templates/gemini-cli.yaml +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/templates/generic.yaml +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/templates/kiro/ide-pre-tool-use.kiro.hook +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/templates/kiro/ide-prompt-submit.kiro.hook +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/templates/kiro/kiro.yaml +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/templates/langchain.yaml +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/templates/mcp.yaml +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/templates/rag.yaml +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/cli/update_cmd.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/device.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/enrollment.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/error_codes.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/error_codes.yaml +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/errors.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/hitl/__init__.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/hitl/grant_protocol.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/hitl/mock.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/hitl/pending_approval.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/hitl/secret_leak_guard.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/hitl/status.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/hook_integrity.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/hooks/__init__.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/hooks/tool_output_handler.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/hosted_policy.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/integrations/__init__.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/integrations/anthropic.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/integrations/autogen.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/integrations/braintrust.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/integrations/crewai/__init__.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/integrations/crewai/agent.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/integrations/crewai/crew.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/integrations/crewai/task.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/integrations/crewai/tool.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/integrations/google.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/integrations/google_adk/__init__.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/integrations/google_adk/agent.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/integrations/google_adk/tool.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/integrations/langchain/__init__.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/integrations/langchain/agent.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/integrations/langchain/callbacks.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/integrations/langchain/chain.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/integrations/langchain/graph.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/integrations/langchain/modern.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/integrations/langchain/tool.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/integrations/langfuse.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/integrations/litellm.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/integrations/openai.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/integrations/pydantic_ai.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/integrations/vercel_ai.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/layout_migration.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/secrets/__init__.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/secrets/reference.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/secrets/resolver.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/secrets/secretstr.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/spool/__init__.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/spool/_compress.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/spool/_constants.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/spool/_crc32c.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/spool/_crypto.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/spool/_frame.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/spool/_keyring.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/spool/_metrics.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/spool/_spool.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/spool/_state.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/spool/_uploader.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/spool/cz-audit-v1.dict +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/tamper.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/controlzero/tracecontext.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/examples/hello_world.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/_fixtures/antigravity-envelope-expected.json +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/_fixtures/jcs_args_hash_vectors.json +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/conftest.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/integrations/__init__.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/integrations/test_google.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/parity/action_aliases.json +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/spool/__init__.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/spool/conftest.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/spool/test_spool_cli.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/spool/test_spool_concurrency.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/spool/test_spool_conformance.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/spool/test_spool_core.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/spool/test_spool_crash.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/spool/test_spool_diskfull.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/spool/test_spool_durable_default_tamper.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/spool/test_spool_keychain_dek.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/spool/test_spool_sink_wiring.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/spool/test_spool_transcript_localack.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/spool/test_spool_uploader.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_action_aliases.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_action_canonicalization.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_action_validator_t86.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_adapter_interface.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_agent_name_env.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_agy_docs_no_exit2_1373.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_antigravity_adapter.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_antigravity_envelope_parity.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_antigravity_ga_blockers_1248.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_antigravity_hook_check.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_antigravity_install.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_antigravity_posttooluse_observe_58.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_antigravity_tool_vocab_1303.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_api_key_mask.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_audit_remote.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_audit_remote_io_capture.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_audit_remote_sdk_version.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_audit_sink_isolation.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_bundle_cache_freshness_1303.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_bundle_parser.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_bundle_translate.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_canonical_phase1a.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_capability_matrix_doc.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_cli_carve_out.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_cli_debug_bundle.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_cli_extractor_integration.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_cli_hook.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_cli_hook_entry.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_cli_hosted_refresh.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_cli_init.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_cli_init_templates.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_cli_tail.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_cli_test.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_cli_validate.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_coding_agent_hooks.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_conditions.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_config_format_parity_1303.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_conformance.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_console.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_credential_hook.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_cursor_adapter.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_default_action.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_device.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_dlp_mask.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_dlp_scanner.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_doctor.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_engine_version_consistency.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_enrollment.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_env_dump_438.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_epic_1247_bryan_acceptance.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_error_codes.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_errors_e_codes.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_fail_closed_eval.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_failopen_1303.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_glob_matching.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_hitl_5d_email_install.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_hitl_6a_cli_flag.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_hitl_6a_exceptions.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_hitl_6a_get_secret_hitl.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_hitl_6a_mock_backend.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_hitl_6a_pending_approval.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_hitl_6a_request_approval.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_hitl_6a_secret_leak_guard.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_hitl_6a_wait.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_hitl_conformance.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_hitl_phase2b_protocol.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_hitl_reason_codes.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_hook_extractors.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_hook_integrity.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_hosted_local_audit_1247.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_hosted_local_shadow_warn_1265.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_hosted_policy_e2e.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_hosts_adapter.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_hybrid_mode_strict.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_hybrid_mode_warn.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_install_hook_command.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_install_hooks.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_io_capture_producer.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_kiro_adapter.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_kiro_cli_e2e.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_kiro_cli_hook_pin_1265.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_kiro_hook_templates.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_kiro_install.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_layout_migration_t101.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_layout_parity_t102.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_local_mode_dict.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_local_mode_file_json.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_local_mode_file_yaml.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_log_fallback_stderr.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_log_options_ignored_hosted.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_log_rotation.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_migrate.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_min_sdk_version_gate.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_multi_client_per_project_175.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_no_policy_no_key.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_observe_mode_1247.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_package_rename_shim.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_part3_active_policy_count_1303.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_policy_engine_version_phase1b.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_policy_freshness.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_policy_load_failclosed_f3.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_policy_settings.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_policy_source_audit.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_probe_audit_isolation.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_quarantine.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_reason_code.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_refresh.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_secrets.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_secrets_reference_resolver.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_sql_semantic_class.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_t103_precedence.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_t104_cache_gc.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_t108_local_override_audit.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_t96_single_audit_log.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_tamper.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_tamper_behavior.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_tamper_hook.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_telemetry_consent.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_tracecontext.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_unsafe_int_boundary.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_update_cmd.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tests/test_upgrade_nudge.py +0 -0
- {controlzero-1.11.8 → controlzero-1.11.9}/tools/cz-kiro-adapter +0 -0
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.11.9 -- 2026-06-25 (policy-posture remediation: honest hosted/local CLI, per-agent keys, key-validity diagnostics)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Severity-level CLI output (`controlzero.cli.output`).** All CLI prose now
|
|
8
|
+
routes through one renderer with INFO / WARNING / SUCCESS / ERROR levels:
|
|
9
|
+
WARNING + ERROR go to stderr, INFO + SUCCESS to stdout, colour honours
|
|
10
|
+
`NO_COLOR` / `CONTROLZERO_NO_COLOR` / `TERM=dumb` / non-TTY. No emojis. The
|
|
11
|
+
hook decision path and wire payloads are untouched -- the renderer is never
|
|
12
|
+
used inside `hook_check`, so decision reasons emit verbatim.
|
|
13
|
+
- **Per-integration (per-agent) API keys.** Each coding agent can bind its own
|
|
14
|
+
project key at `integrations/<agent>/config.yaml`. Resolution order is
|
|
15
|
+
`env (explicit) > per-agent (for that agent) > global`, fully back-compatible
|
|
16
|
+
(no per-agent key -> falls back to the global key). Per-agent dir `0700` /
|
|
17
|
+
file `0600`; the raw key is never echoed. This lets one machine run different
|
|
18
|
+
agents bound to different projects, each enforcing its own hosted bundle.
|
|
19
|
+
- **`controlzero status` rewrite.** Shows resolved runtime truth (mode, config
|
|
20
|
+
source, key, enrolled, audit dest) plus a per-agent bindings table and
|
|
21
|
+
per-key backend validity (active / expired / revoked) so a rejected key is
|
|
22
|
+
diagnosable from the CLI instead of only via the coarse E1101 at first tool
|
|
23
|
+
call (#1472).
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- **Honest hosted/local posture messaging.** One canonical wording each for the
|
|
28
|
+
HOSTED and LOCAL install paths, routed through every installer. The HOSTED
|
|
29
|
+
banner states that the local `policy.yaml` is IGNORED while an API key is set,
|
|
30
|
+
and the unmatched-tool default (DENY fail-closed unless the project is set to
|
|
31
|
+
Allow by default) -- removing the "install said ALLOW ALL but runtime denied"
|
|
32
|
+
ambiguity.
|
|
33
|
+
- **Archive-on-hosted.** When an API key is configured, the local `policy.yaml`
|
|
34
|
+
(and its `.sig`) is moved (not deleted) into `archive/policy-<UTC-ts>.yaml`,
|
|
35
|
+
symlink-safe, every error non-fatal -- a hosted machine never silently runs a
|
|
36
|
+
stale local policy.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- **`synthetic:UNATTRIBUTED_DENY`** added to the synthetic policy-id catalog so
|
|
41
|
+
an unattributed fail-closed deny renders a stable, documented policy id across
|
|
42
|
+
every surface instead of an opaque blank (#1472).
|
|
43
|
+
- **Audit attribution.** SDK audit wire builders forward `project_id`
|
|
44
|
+
unconditionally so signed-sink audit events are attributed to the right
|
|
45
|
+
project (pairs with the backend ingest fix).
|
|
46
|
+
|
|
3
47
|
## 1.11.8 -- 2026-06-22 (DLP mask enforcement + I/O capture: enrolled-machine capture fix + server-side PII redaction)
|
|
4
48
|
|
|
5
49
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: controlzero
|
|
3
|
-
Version: 1.11.
|
|
3
|
+
Version: 1.11.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
|
|
@@ -185,6 +185,32 @@ rules:
|
|
|
185
185
|
Rules are evaluated top to bottom. The first match wins. If no rule matches,
|
|
186
186
|
the call is denied (fail-closed).
|
|
187
187
|
|
|
188
|
+
## Localized block messages (`reason_localized`)
|
|
189
|
+
|
|
190
|
+
The `reason` shown when a rule fires is plain UTF-8 text, so you can write it
|
|
191
|
+
in any language. To serve more than one language from the same policy, add a
|
|
192
|
+
`reason_localized` map and select a locale with the `CONTROLZERO_LOCALE`
|
|
193
|
+
environment variable (e.g. `ko`, `ko-KR`):
|
|
194
|
+
|
|
195
|
+
```yaml
|
|
196
|
+
version: '1'
|
|
197
|
+
rules:
|
|
198
|
+
- deny: 'delete_*'
|
|
199
|
+
reason: 'Deletes need human approval' # English default
|
|
200
|
+
reason_localized:
|
|
201
|
+
ko: '삭제는 사람의 승인이 필요합니다'
|
|
202
|
+
- allow: '*'
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
- When `CONTROLZERO_LOCALE` is unset (or set to a locale the rule has no entry
|
|
206
|
+
for), the plain `reason` is used -- existing behavior is unchanged.
|
|
207
|
+
- The SDK's own built-in messages (no-rule-match, DLP block, empty/observe
|
|
208
|
+
bundle) ship an English default plus a Korean translation, selected the same
|
|
209
|
+
way. The packs are pure in-binary maps (no runtime i18n dependency), so this
|
|
210
|
+
is air-gap safe.
|
|
211
|
+
- Localization is display-only: it never changes which rule matches or the
|
|
212
|
+
decided effect.
|
|
213
|
+
|
|
188
214
|
## Tamper detection and quarantine
|
|
189
215
|
|
|
190
216
|
The policy YAML supports a `settings:` section that controls how the SDK
|
|
@@ -131,6 +131,32 @@ rules:
|
|
|
131
131
|
Rules are evaluated top to bottom. The first match wins. If no rule matches,
|
|
132
132
|
the call is denied (fail-closed).
|
|
133
133
|
|
|
134
|
+
## Localized block messages (`reason_localized`)
|
|
135
|
+
|
|
136
|
+
The `reason` shown when a rule fires is plain UTF-8 text, so you can write it
|
|
137
|
+
in any language. To serve more than one language from the same policy, add a
|
|
138
|
+
`reason_localized` map and select a locale with the `CONTROLZERO_LOCALE`
|
|
139
|
+
environment variable (e.g. `ko`, `ko-KR`):
|
|
140
|
+
|
|
141
|
+
```yaml
|
|
142
|
+
version: '1'
|
|
143
|
+
rules:
|
|
144
|
+
- deny: 'delete_*'
|
|
145
|
+
reason: 'Deletes need human approval' # English default
|
|
146
|
+
reason_localized:
|
|
147
|
+
ko: '삭제는 사람의 승인이 필요합니다'
|
|
148
|
+
- allow: '*'
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
- When `CONTROLZERO_LOCALE` is unset (or set to a locale the rule has no entry
|
|
152
|
+
for), the plain `reason` is used -- existing behavior is unchanged.
|
|
153
|
+
- The SDK's own built-in messages (no-rule-match, DLP block, empty/observe
|
|
154
|
+
bundle) ship an English default plus a Korean translation, selected the same
|
|
155
|
+
way. The packs are pure in-binary maps (no runtime i18n dependency), so this
|
|
156
|
+
is air-gap safe.
|
|
157
|
+
- Localization is display-only: it never changes which rule matches or the
|
|
158
|
+
decided effect.
|
|
159
|
+
|
|
134
160
|
## Tamper detection and quarantine
|
|
135
161
|
|
|
136
162
|
The policy YAML supports a `settings:` section that controls how the SDK
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""In-binary localization pack for the SDK-GENERATED decision reasons.
|
|
2
|
+
|
|
3
|
+
Why this exists ( checklist #25, gh#1439)
|
|
4
|
+
--------------------------------------------------
|
|
5
|
+
The user-facing ``reason`` string is carried end-to-end as UTF-8, so an
|
|
6
|
+
operator can already localize ANY rule today by typing the target-language
|
|
7
|
+
text into that rule's ``reason:`` -- or, for per-locale overrides, into the
|
|
8
|
+
new ``reason_localized:`` map (a ``{locale: message}`` dict on the rule). That
|
|
9
|
+
covers operator-authored rules. This module covers the OTHER half: the
|
|
10
|
+
messages the SDK itself GENERATES for the no-rule-match / empty-bundle /
|
|
11
|
+
missing-bundle / tampered / DLP paths. Those have no operator-authored text to
|
|
12
|
+
carry, so they need an in-binary pack.
|
|
13
|
+
|
|
14
|
+
No runtime i18n library is used: these are pure dict lookups, which keeps the
|
|
15
|
+
SDK air-gap safe (the PoC ships self-hosted with no network).
|
|
16
|
+
|
|
17
|
+
Contract
|
|
18
|
+
--------
|
|
19
|
+
- The ``en`` value is the single source of truth for the inline paths that now
|
|
20
|
+
read it (no-match, DLP, tamper, quarantine): it MUST stay byte-identical to
|
|
21
|
+
the legacy hard-coded string so existing consumers that regex-match the
|
|
22
|
+
``reason`` text are unaffected. ``tests/test_localized_reason.py`` pins this.
|
|
23
|
+
- English remains the literal DEFAULT whenever ``CONTROLZERO_LOCALE`` is unset
|
|
24
|
+
or a locale key is missing from a pack entry.
|
|
25
|
+
- For the synthetic empty/missing-bundle rules the English text stays on the
|
|
26
|
+
rule's ``reason`` (unchanged bundle output); the pack supplies ONLY the
|
|
27
|
+
localized (non-en) override, looked up by the rule's ``reason_code``.
|
|
28
|
+
|
|
29
|
+
KOREAN COPY IS A PROFESSIONAL DRAFT and MUST be reviewed/approved by the customer
|
|
30
|
+
before being presented as final. See KO_Localization_2026-06-22 in the project
|
|
31
|
+
vault. The English default is never blocked on that review.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import os
|
|
37
|
+
from typing import Optional
|
|
38
|
+
|
|
39
|
+
DEFAULT_LOCALE = "en"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def resolve_locale(explicit: Optional[str] = None) -> str:
|
|
43
|
+
"""Resolve the active locale.
|
|
44
|
+
|
|
45
|
+
Precedence: explicit arg > ``CONTROLZERO_LOCALE`` env > "" (English).
|
|
46
|
+
Returned lower-cased and stripped so lookups are case-insensitive.
|
|
47
|
+
"""
|
|
48
|
+
if explicit:
|
|
49
|
+
return explicit.strip().lower()
|
|
50
|
+
return (os.environ.get("CONTROLZERO_LOCALE", "") or "").strip().lower()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _locale_candidates(locale: str) -> list[str]:
|
|
54
|
+
"""Return lookup candidates for a locale, most specific first.
|
|
55
|
+
|
|
56
|
+
``ko-KR`` / ``ko_KR`` -> ``["ko-kr", "ko"]`` so a pack keyed by the bare
|
|
57
|
+
primary subtag ("ko") still matches a region-qualified request.
|
|
58
|
+
"""
|
|
59
|
+
loc = (locale or "").strip().lower()
|
|
60
|
+
if not loc:
|
|
61
|
+
return []
|
|
62
|
+
out = [loc]
|
|
63
|
+
for sep in ("-", "_"):
|
|
64
|
+
if sep in loc:
|
|
65
|
+
primary = loc.split(sep, 1)[0]
|
|
66
|
+
if primary and primary not in out:
|
|
67
|
+
out.append(primary)
|
|
68
|
+
break
|
|
69
|
+
return out
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# message key -> {locale -> template}. Templates use str.format placeholders
|
|
73
|
+
# ({action}, {rule_name}, {category}). The English values for the inline paths
|
|
74
|
+
# (NO_RULE_MATCH:*, DLP_BLOCKED:*, BUNDLE_TAMPERED, MACHINE_QUARANTINED) are
|
|
75
|
+
# byte-identical to the legacy strings. For the synthetic-rule codes
|
|
76
|
+
# (OBSERVE_MODE_NO_POLICY, NO_ACTIVE_POLICIES, BUNDLE_MISSING) the English is
|
|
77
|
+
# documentation/parity only -- the live English comes from the rule's reason.
|
|
78
|
+
SYSTEM_MESSAGES: dict[str, dict[str, str]] = {
|
|
79
|
+
# No-rule-match fallthrough (the most common block a Korean operator sees).
|
|
80
|
+
"NO_RULE_MATCH:deny": {
|
|
81
|
+
"en": (
|
|
82
|
+
"No matching policy rule for '{action}' (fail-closed default; "
|
|
83
|
+
"default_on_missing=deny). This tool is neither allowed nor "
|
|
84
|
+
"denied by any rule in your hosted policy, so it is blocked. "
|
|
85
|
+
"Add a rule that allows it (e.g. allow: '*' as a catch-all, or "
|
|
86
|
+
"allow: '{action}'), or set the project/org default to allow, "
|
|
87
|
+
"in the Control Zero dashboard."
|
|
88
|
+
),
|
|
89
|
+
# ko: "'{action}'에 일치하는 정책 규칙이 없어 기본 차단됩니다 ..."
|
|
90
|
+
"ko": (
|
|
91
|
+
"'{action}'에 일치하는 정책 규칙이 없습니다(기본 차단; "
|
|
92
|
+
"default_on_missing=deny). 이 도구는 정책에서 허용되지도 "
|
|
93
|
+
"거부되지도 않았으므로 차단됩니다. Control Zero 대시보드에서 "
|
|
94
|
+
"이를 허용하는 규칙을 추가하거나(예: 포괄 규칙 allow: '*' 또는 "
|
|
95
|
+
"allow: '{action}'), 프로젝트/조직 기본값을 allow로 "
|
|
96
|
+
"설정하십시오."
|
|
97
|
+
),
|
|
98
|
+
},
|
|
99
|
+
"NO_RULE_MATCH:allow": {
|
|
100
|
+
"en": "No matching policy rule for '{action}' (default_action=allow)",
|
|
101
|
+
# ko: "'{action}'에 일치하는 정책 규칙이 없습니다 (default_action=allow)"
|
|
102
|
+
"ko": "'{action}'에 일치하는 정책 규칙이 없습니다(default_action=allow)",
|
|
103
|
+
},
|
|
104
|
+
"NO_RULE_MATCH:warn": {
|
|
105
|
+
"en": "No matching policy rule for '{action}' (default_action=warn)",
|
|
106
|
+
# ko: "'{action}'에 일치하는 정책 규칙이 없습니다 (default_action=warn)"
|
|
107
|
+
"ko": "'{action}'에 일치하는 정책 규칙이 없습니다(default_action=warn)",
|
|
108
|
+
},
|
|
109
|
+
# DLP overrides (the tool-name policy allowed, but a DLP rule vetoed).
|
|
110
|
+
"DLP_BLOCKED:match": {
|
|
111
|
+
"en": (
|
|
112
|
+
"DLP rule '{rule_name}' matched {category} content in tool "
|
|
113
|
+
"arguments"
|
|
114
|
+
),
|
|
115
|
+
# ko: "DLP 규칙 '{rule_name}'이(가) 도구 인자에서 {category} 콘텐츠와 ..."
|
|
116
|
+
"ko": (
|
|
117
|
+
"DLP 규칙 '{rule_name}'이(가) 도구 인자에서 {category} "
|
|
118
|
+
"콘텐츠와 일치하여 호출이 차단되었습니다."
|
|
119
|
+
),
|
|
120
|
+
},
|
|
121
|
+
"DLP_BLOCKED:boundary": {
|
|
122
|
+
"en": (
|
|
123
|
+
"DLP rule '{rule_name}' matched {category} content that spans "
|
|
124
|
+
"argument boundaries and could not be safely masked; blocking "
|
|
125
|
+
"(fail-closed) rather than forwarding an un-redacted value"
|
|
126
|
+
),
|
|
127
|
+
# ko: "DLP 규칙 '{rule_name}'이(가) 인자 경계에 걸친 {category} 콘텐츠와 ..."
|
|
128
|
+
"ko": (
|
|
129
|
+
"DLP 규칙 '{rule_name}'이(가) 인자 경계에 걸쳐 있는 "
|
|
130
|
+
"{category} 콘텐츠와 일치하여 안전하게 마스킹할 수 없으므로, "
|
|
131
|
+
"마스킹되지 않은 값을 전달하는 대신 차단합니다(기본 차단)."
|
|
132
|
+
),
|
|
133
|
+
},
|
|
134
|
+
# Tamper / quarantine (constructed in client.py + cli/main.py).
|
|
135
|
+
"BUNDLE_TAMPERED": {
|
|
136
|
+
"en": (
|
|
137
|
+
"Policy tampering detected. Call blocked by tamper enforcement "
|
|
138
|
+
"policy."
|
|
139
|
+
),
|
|
140
|
+
# ko: "정책 변조가 감지되었습니다. 변조 시행 정책에 의해 호출이 차단되었습니다."
|
|
141
|
+
"ko": (
|
|
142
|
+
"정책 변조가 감지되었습니다. 변조 시행 정책에 의해 호출이 "
|
|
143
|
+
"차단되었습니다."
|
|
144
|
+
),
|
|
145
|
+
},
|
|
146
|
+
# CLI hook-check tamper enforcement (cli/main.py). Carries a {source}
|
|
147
|
+
# discriminator (policy_hmac / audit_chain) and the [Control Zero] prefix.
|
|
148
|
+
"BUNDLE_TAMPERED:cli": {
|
|
149
|
+
"en": (
|
|
150
|
+
"[Control Zero] Tamper detected ({source}). Blocked by enforcement "
|
|
151
|
+
"policy."
|
|
152
|
+
),
|
|
153
|
+
# ko: "[Control Zero] 변조가 감지되었습니다({source}). 시행 정책에 의해 ..."
|
|
154
|
+
"ko": (
|
|
155
|
+
"[Control Zero] 변조가 감지되었습니다({source}). 시행 정책에 "
|
|
156
|
+
"의해 차단되었습니다."
|
|
157
|
+
),
|
|
158
|
+
},
|
|
159
|
+
"MACHINE_QUARANTINED": {
|
|
160
|
+
"en": (
|
|
161
|
+
"Machine quarantined: policy tampering detected. All calls denied "
|
|
162
|
+
"until recovery."
|
|
163
|
+
),
|
|
164
|
+
# ko: "머신이 격리되었습니다: 정책 변조가 감지되었습니다. 복구될 때까지 ..."
|
|
165
|
+
"ko": (
|
|
166
|
+
"머신이 격리되었습니다: 정책 변조가 감지되었습니다. 복구될 "
|
|
167
|
+
"때까지 모든 호출이 거부됩니다."
|
|
168
|
+
),
|
|
169
|
+
},
|
|
170
|
+
# Synthetic empty / missing-bundle rules. English here is parity-only --
|
|
171
|
+
# the live English comes from the rule's `reason` (bundle output unchanged);
|
|
172
|
+
# the enforcer reads ONLY the localized (non-en) value below, by code.
|
|
173
|
+
"OBSERVE_MODE_NO_POLICY": {
|
|
174
|
+
"en": (
|
|
175
|
+
"OBSERVE MODE: no policies are active on this project, so "
|
|
176
|
+
"Control Zero is monitoring and auditing tool calls but "
|
|
177
|
+
"NOT enforcing -- every call is allowed and logged. Attach "
|
|
178
|
+
"a policy (or set the empty-project default to deny) in the "
|
|
179
|
+
"Control Zero dashboard to start enforcing."
|
|
180
|
+
),
|
|
181
|
+
# ko: "관찰 모드: 이 프로젝트에 활성 정책이 없으므로 ... 차단하지 않습니다 ..."
|
|
182
|
+
"ko": (
|
|
183
|
+
"관찰 모드: 이 프로젝트에 활성 정책이 없으므로 Control Zero는 "
|
|
184
|
+
"도구 호출을 모니터링하고 감사 기록을 남기지만 차단하지는 "
|
|
185
|
+
"않습니다 -- 모든 호출이 허용되고 기록됩니다. 시행을 시작하려면 "
|
|
186
|
+
"Control Zero 대시보드에서 정책을 연결하거나(또는 빈 프로젝트 "
|
|
187
|
+
"기본값을 deny로 설정) 하십시오."
|
|
188
|
+
),
|
|
189
|
+
},
|
|
190
|
+
"NO_ACTIVE_POLICIES": {
|
|
191
|
+
"en": (
|
|
192
|
+
"No policies are active on this project. If the dashboard "
|
|
193
|
+
"shows attached policies, regenerate the policy bundle."
|
|
194
|
+
),
|
|
195
|
+
# ko: "이 프로젝트에 활성 정책이 없습니다. 대시보드에 연결된 정책이 표시되면 ..."
|
|
196
|
+
"ko": (
|
|
197
|
+
"이 프로젝트에 활성 정책이 없습니다. 대시보드에 연결된 정책이 "
|
|
198
|
+
"표시되면 정책 번들을 다시 생성하십시오."
|
|
199
|
+
),
|
|
200
|
+
},
|
|
201
|
+
# BUNDLE_MISSING covers two English variants (degraded bundle vs. bundle
|
|
202
|
+
# not loaded), both reason_code=BUNDLE_MISSING. The localized override is a
|
|
203
|
+
# single accurate general message; the precise English stays on the rule.
|
|
204
|
+
"BUNDLE_MISSING": {
|
|
205
|
+
"en": (
|
|
206
|
+
"Policy bundle could not be loaded or produced zero enforceable "
|
|
207
|
+
"rules. Control Zero is failing CLOSED (deny) rather than allowing "
|
|
208
|
+
"every tool call. Regenerate the policy bundle in the Control Zero "
|
|
209
|
+
"dashboard; contact support if this persists."
|
|
210
|
+
),
|
|
211
|
+
# ko: "정책 번들을 로드할 수 없거나 시행 가능한 규칙을 생성하지 못했습니다 ..."
|
|
212
|
+
"ko": (
|
|
213
|
+
"정책 번들을 로드할 수 없거나 시행 가능한 규칙을 생성하지 "
|
|
214
|
+
"못했습니다(미동기화, 백엔드 연결 불가, 복호화 실패 또는 손상/"
|
|
215
|
+
"부분/오래된 번들). Control Zero는 모든 호출을 허용하는 대신 "
|
|
216
|
+
"차단(기본 차단)합니다. Control Zero 대시보드에서 정책 번들을 "
|
|
217
|
+
"다시 생성하고, 문제가 지속되면 지원팀에 문의하십시오."
|
|
218
|
+
),
|
|
219
|
+
},
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def system_message(key: str, locale: Optional[str] = None, **fmt: object) -> Optional[str]:
|
|
224
|
+
"""Return the fully-resolved system message for ``key``, English-default.
|
|
225
|
+
|
|
226
|
+
Resolution: ``pack[key][locale]`` (region subtag tolerated) falling back to
|
|
227
|
+
``pack[key]["en"]``. Returns ``None`` only when ``key`` is unknown. Used by
|
|
228
|
+
the inline paths (no-match, DLP, tamper, quarantine) where the pack is the
|
|
229
|
+
source of the English text.
|
|
230
|
+
"""
|
|
231
|
+
entry = SYSTEM_MESSAGES.get(key)
|
|
232
|
+
if not entry:
|
|
233
|
+
return None
|
|
234
|
+
template = None
|
|
235
|
+
for cand in _locale_candidates(resolve_locale(locale)):
|
|
236
|
+
if cand in entry:
|
|
237
|
+
template = entry[cand]
|
|
238
|
+
break
|
|
239
|
+
if template is None:
|
|
240
|
+
template = entry.get(DEFAULT_LOCALE)
|
|
241
|
+
if template is None:
|
|
242
|
+
return None
|
|
243
|
+
if fmt:
|
|
244
|
+
try:
|
|
245
|
+
return template.format(**fmt)
|
|
246
|
+
except (KeyError, IndexError):
|
|
247
|
+
return template
|
|
248
|
+
return template
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def localized_override(key: str, locale: Optional[str] = None, **fmt: object) -> Optional[str]:
|
|
252
|
+
"""Return ONLY a non-English localized message for ``key``, else ``None``.
|
|
253
|
+
|
|
254
|
+
Unlike :func:`system_message` this never falls back to English. Used for
|
|
255
|
+
the synthetic empty/missing-bundle rules, whose English text stays on the
|
|
256
|
+
rule's ``reason`` (bundle output unchanged) -- the enforcer applies this
|
|
257
|
+
override only when an actual localized string exists for the active locale.
|
|
258
|
+
"""
|
|
259
|
+
entry = SYSTEM_MESSAGES.get(key)
|
|
260
|
+
if not entry:
|
|
261
|
+
return None
|
|
262
|
+
for cand in _locale_candidates(resolve_locale(locale)):
|
|
263
|
+
if cand == DEFAULT_LOCALE:
|
|
264
|
+
continue
|
|
265
|
+
if cand in entry:
|
|
266
|
+
template = entry[cand]
|
|
267
|
+
if fmt:
|
|
268
|
+
try:
|
|
269
|
+
return template.format(**fmt)
|
|
270
|
+
except (KeyError, IndexError):
|
|
271
|
+
return template
|
|
272
|
+
return template
|
|
273
|
+
return None
|
|
@@ -943,4 +943,19 @@ def _translate_rule(rule: dict, policy_id: str) -> Optional[dict]:
|
|
|
943
943
|
if escalate is not None:
|
|
944
944
|
translated["escalate_on_deny"] = bool(escalate)
|
|
945
945
|
|
|
946
|
+
# #25 (gh#1439): the SAME allowlist-strip trap (the gh#175 /
|
|
947
|
+
# #1303 family). A backend-signed bundle copies only an allowlist of
|
|
948
|
+
# rule fields into the local rule shape; without this line a hosted
|
|
949
|
+
# policy's `reason_localized` map is silently dropped and a Korean
|
|
950
|
+
# operator would see only the English fallback. Forward it intact.
|
|
951
|
+
# (The backend bundle wire struct must ALSO carry the field -- see
|
|
952
|
+
# bundle_handler.go PolicyRule -- or it never reaches this translator.)
|
|
953
|
+
reason_localized = rule.get("reason_localized")
|
|
954
|
+
if isinstance(reason_localized, dict) and reason_localized:
|
|
955
|
+
translated["reason_localized"] = {
|
|
956
|
+
str(k).strip().lower(): str(v)
|
|
957
|
+
for k, v in reason_localized.items()
|
|
958
|
+
if isinstance(k, str) and isinstance(v, str)
|
|
959
|
+
}
|
|
960
|
+
|
|
946
961
|
return translated
|
|
@@ -26,6 +26,11 @@ from controlzero._internal.dlp_scanner import (
|
|
|
26
26
|
DLPScanner,
|
|
27
27
|
extract_text_from_args,
|
|
28
28
|
)
|
|
29
|
+
from controlzero._internal._messages import (
|
|
30
|
+
localized_override,
|
|
31
|
+
resolve_locale,
|
|
32
|
+
system_message,
|
|
33
|
+
)
|
|
29
34
|
from controlzero._internal.types import PolicyRule
|
|
30
35
|
|
|
31
36
|
# Phase 1B (#451): policy engine version stamped onto every
|
|
@@ -149,6 +154,10 @@ SYNTHETIC_BUNDLE_MISSING = "synthetic:BUNDLE_MISSING"
|
|
|
149
154
|
SYNTHETIC_RESOURCE_GATE_SKIP = "synthetic:RESOURCE_GATE_SKIP"
|
|
150
155
|
SYNTHETIC_QUARANTINE = "synthetic:QUARANTINE"
|
|
151
156
|
SYNTHETIC_ENGINE_UNAVAILABLE = "synthetic:ENGINE_UNAVAILABLE"
|
|
157
|
+
# Backend-stamped (audit ingest) on a deny that arrived with an empty
|
|
158
|
+
# policy_id -- e.g. an old field SDK or a CLI fail-closed path. SDKs do not
|
|
159
|
+
# emit it; it is listed here so downstream consumers recognize it.
|
|
160
|
+
SYNTHETIC_UNATTRIBUTED_DENY = "synthetic:UNATTRIBUTED_DENY"
|
|
152
161
|
|
|
153
162
|
VALID_SYNTHETIC_POLICY_IDS = frozenset({
|
|
154
163
|
SYNTHETIC_NO_RULE_MATCH,
|
|
@@ -158,6 +167,7 @@ VALID_SYNTHETIC_POLICY_IDS = frozenset({
|
|
|
158
167
|
SYNTHETIC_RESOURCE_GATE_SKIP,
|
|
159
168
|
SYNTHETIC_QUARANTINE,
|
|
160
169
|
SYNTHETIC_ENGINE_UNAVAILABLE,
|
|
170
|
+
SYNTHETIC_UNATTRIBUTED_DENY,
|
|
161
171
|
})
|
|
162
172
|
|
|
163
173
|
# Canonical bundle-level default values. These must stay in lockstep
|
|
@@ -224,7 +234,7 @@ class PolicyDecision:
|
|
|
224
234
|
reason_code: str = ""
|
|
225
235
|
evaluated_rules: int = 0
|
|
226
236
|
dlp_findings: list[dict] = field(default_factory=list)
|
|
227
|
-
# DLP MASK action (#1278
|
|
237
|
+
# DLP MASK action (#1278 the customer checklist #6, parity with the gateway
|
|
228
238
|
# ``response_dlp.mask_text`` path). When a DLP rule matches with
|
|
229
239
|
# action="mask" on an ALLOW decision, the enforcer redacts the sensitive
|
|
230
240
|
# spans in the tool arguments (modify-and-proceed) and stashes the
|
|
@@ -329,6 +339,7 @@ class PolicyEvaluator:
|
|
|
329
339
|
rules: Optional[list[PolicyRule]] = None,
|
|
330
340
|
dlp_scanner: Optional[DLPScanner] = None,
|
|
331
341
|
default_action: str = DEFAULT_BUNDLE_ACTION,
|
|
342
|
+
locale: Optional[str] = None,
|
|
332
343
|
):
|
|
333
344
|
self._rules: list[PolicyRule] = rules or []
|
|
334
345
|
self._dlp_scanner: Optional[DLPScanner] = dlp_scanner
|
|
@@ -338,6 +349,12 @@ class PolicyEvaluator:
|
|
|
338
349
|
self._default_action: str = (
|
|
339
350
|
default_action if default_action in VALID_DEFAULT_ACTIONS else DEFAULT_BUNDLE_ACTION
|
|
340
351
|
)
|
|
352
|
+
# Active locale for user-facing reason localization (
|
|
353
|
+
# #25, gh#1439). None => read CONTROLZERO_LOCALE at construction.
|
|
354
|
+
# Empty string ("") means English, the default; reason text is
|
|
355
|
+
# then byte-identical to the pre-localization SDK so existing
|
|
356
|
+
# reason-regex consumers are unaffected.
|
|
357
|
+
self._locale: str = resolve_locale(locale)
|
|
341
358
|
|
|
342
359
|
def load(self, rules: list[PolicyRule]) -> None:
|
|
343
360
|
"""Replace the rule set."""
|
|
@@ -365,6 +382,52 @@ class PolicyEvaluator:
|
|
|
365
382
|
"""The effective no-match default action."""
|
|
366
383
|
return self._default_action
|
|
367
384
|
|
|
385
|
+
def set_locale(self, locale: Optional[str]) -> None:
|
|
386
|
+
"""Set the active reason-localization locale (#25, gh#1439).
|
|
387
|
+
|
|
388
|
+
``None`` re-reads ``CONTROLZERO_LOCALE``; an empty string forces
|
|
389
|
+
English. Localization is display-only -- it never changes which
|
|
390
|
+
rule matches or the effect, only the human-readable ``reason`` text.
|
|
391
|
+
"""
|
|
392
|
+
self._locale = resolve_locale(locale)
|
|
393
|
+
|
|
394
|
+
@property
|
|
395
|
+
def locale(self) -> str:
|
|
396
|
+
"""The effective reason-localization locale ("" == English)."""
|
|
397
|
+
return self._locale
|
|
398
|
+
|
|
399
|
+
def _resolve_reason(self, rule: PolicyRule) -> str:
|
|
400
|
+
"""Resolve the human-readable reason for a matched rule, localized.
|
|
401
|
+
|
|
402
|
+
Priority (#25, gh#1439):
|
|
403
|
+
1. ``rule.reason_localized[locale]`` -- operator-authored override.
|
|
404
|
+
2. For SDK-generated synthetic rules (id ``synthetic:*``), the
|
|
405
|
+
in-binary localized system message keyed by ``reason_code``.
|
|
406
|
+
3. The plain ``rule.reason`` (English / operator text) -- the
|
|
407
|
+
DEFAULT whenever locale is unset/English or no override exists,
|
|
408
|
+
so reason-regex consumers see byte-identical output.
|
|
409
|
+
4. Canned ``Matched rule <id>`` when the rule carried no reason.
|
|
410
|
+
|
|
411
|
+
Localization is display-only; this never affects the decision.
|
|
412
|
+
"""
|
|
413
|
+
loc = self._locale
|
|
414
|
+
if loc and loc != "en":
|
|
415
|
+
override = (getattr(rule, "reason_localized", None) or {}).get(loc)
|
|
416
|
+
if override:
|
|
417
|
+
return override
|
|
418
|
+
# Region-subtag tolerance (ko-KR -> ko) for the per-rule map.
|
|
419
|
+
rl = getattr(rule, "reason_localized", None) or {}
|
|
420
|
+
if rl:
|
|
421
|
+
primary = loc.split("-", 1)[0].split("_", 1)[0]
|
|
422
|
+
if primary in rl and rl[primary]:
|
|
423
|
+
return rl[primary]
|
|
424
|
+
rule_id = rule.id or ""
|
|
425
|
+
if rule_id.startswith(SYNTHETIC_POLICY_ID_PREFIX) and getattr(rule, "reason_code", ""):
|
|
426
|
+
sys_msg = localized_override(rule.reason_code, loc)
|
|
427
|
+
if sys_msg:
|
|
428
|
+
return sys_msg
|
|
429
|
+
return rule.reason or f"Matched rule {rule.id or rule.name}".strip()
|
|
430
|
+
|
|
368
431
|
def evaluate(
|
|
369
432
|
self,
|
|
370
433
|
tool: str,
|
|
@@ -513,9 +576,11 @@ class PolicyEvaluator:
|
|
|
513
576
|
decision = PolicyDecision(
|
|
514
577
|
effect=rule.effect,
|
|
515
578
|
policy_id=rule.id or rule.name or None,
|
|
516
|
-
# User-provided reason wins
|
|
517
|
-
# the rule
|
|
518
|
-
|
|
579
|
+
# User-provided reason wins, localized when CONTROLZERO_LOCALE
|
|
580
|
+
# selects a locale the rule (or system pack) has copy for.
|
|
581
|
+
# Falls back to the plain reason / canned text otherwise, so
|
|
582
|
+
# English output is byte-identical to before (#25).
|
|
583
|
+
reason=self._resolve_reason(rule),
|
|
519
584
|
reason_code=decision_code,
|
|
520
585
|
evaluated_rules=evaluated,
|
|
521
586
|
gate_matched=gate_matched_value,
|
|
@@ -555,33 +620,28 @@ class PolicyEvaluator:
|
|
|
555
620
|
# phrase is kept for the deny case so downstream consumers
|
|
556
621
|
# that regex-match the reason string (a pattern we actively
|
|
557
622
|
# discourage -- use reason_code instead) keep working.
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
)
|
|
581
|
-
elif self._default_action == "allow":
|
|
582
|
-
reason = f"No matching policy rule for '{action}' (default_action=allow)"
|
|
583
|
-
else: # "warn"
|
|
584
|
-
reason = f"No matching policy rule for '{action}' (default_action=warn)"
|
|
623
|
+
# #1247 (folded from #1257): a deny-by-default no-rule-match is the
|
|
624
|
+
# single most confusing block a customer hits -- a benign tool (e.g.
|
|
625
|
+
# `bash:find`) is denied not because a rule forbids it but because no
|
|
626
|
+
# rule mentions it and the bundle's default_on_missing is deny. The
|
|
627
|
+
# message names the unmatched action + exact remediation so the deny
|
|
628
|
+
# is self-explaining; it keeps the legacy "fail-closed default"
|
|
629
|
+
# substring for any downstream regex consumer (reason_code=
|
|
630
|
+
# NO_RULE_MATCH is the supported signal). NOTE: this is the NON-EMPTY
|
|
631
|
+
# bundle path -- a genuinely empty bundle short-circuits to the
|
|
632
|
+
# synthetic OBSERVE catch-all above.
|
|
633
|
+
#
|
|
634
|
+
# #25 (gh#1439): the three variant strings now live in the
|
|
635
|
+
# in-binary message pack keyed by NO_RULE_MATCH:<default_action>. The
|
|
636
|
+
# English values are byte-identical to the legacy strings, so this is
|
|
637
|
+
# a no-op when CONTROLZERO_LOCALE is unset; with ko set the Korean
|
|
638
|
+
# variant is surfaced. default_action is always one of deny/allow/warn
|
|
639
|
+
# (coerced in the constructor), so the pack key always resolves.
|
|
640
|
+
reason = system_message(
|
|
641
|
+
f"NO_RULE_MATCH:{self._default_action}",
|
|
642
|
+
self._locale,
|
|
643
|
+
action=action,
|
|
644
|
+
) or f"No matching policy rule for '{action}'"
|
|
585
645
|
|
|
586
646
|
# T79: distinguish the T83-class signature ("a rule's actions
|
|
587
647
|
# matched but its resources gate excluded the call") from the
|
|
@@ -675,9 +735,13 @@ class PolicyEvaluator:
|
|
|
675
735
|
return PolicyDecision(
|
|
676
736
|
effect="deny",
|
|
677
737
|
policy_id=decision.policy_id,
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
738
|
+
# #25 (gh#1439): localized via the in-binary pack;
|
|
739
|
+
# English byte-identical to legacy when locale unset.
|
|
740
|
+
reason=system_message(
|
|
741
|
+
"DLP_BLOCKED:match",
|
|
742
|
+
self._locale,
|
|
743
|
+
rule_name=blocking.rule_name,
|
|
744
|
+
category=blocking.category,
|
|
681
745
|
),
|
|
682
746
|
# DLP_BLOCKED means: the tool-name policy said allow,
|
|
683
747
|
# but a DLP rule found a hit in the arguments and
|
|
@@ -735,11 +799,13 @@ class PolicyEvaluator:
|
|
|
735
799
|
return PolicyDecision(
|
|
736
800
|
effect="deny",
|
|
737
801
|
policy_id=decision.policy_id,
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
"
|
|
742
|
-
|
|
802
|
+
# #25 (gh#1439): localized via the in-binary
|
|
803
|
+
# pack; English byte-identical to legacy when unset.
|
|
804
|
+
reason=system_message(
|
|
805
|
+
"DLP_BLOCKED:boundary",
|
|
806
|
+
self._locale,
|
|
807
|
+
rule_name=offending.rule_name,
|
|
808
|
+
category=offending.category,
|
|
743
809
|
),
|
|
744
810
|
reason_code=REASON_CODE_DLP_BLOCKED,
|
|
745
811
|
evaluated_rules=decision.evaluated_rules,
|