controlzero 1.12.0__tar.gz → 1.13.1__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.12.0 → controlzero-1.13.1}/CHANGELOG.md +52 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/PKG-INFO +1 -1
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/__init__.py +1 -1
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/_internal/bundle.py +17 -0
- controlzero-1.13.1/controlzero/_internal/condition_ops.py +226 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/_internal/enforcer.py +151 -3
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/_internal/hook_extractors.py +455 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/_internal/io_capture_producer.py +16 -9
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/main.py +26 -1
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/policy_loader.py +53 -1
- {controlzero-1.12.0 → controlzero-1.13.1}/pyproject.toml +1 -1
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_coding_agent_hooks.py +11 -8
- controlzero-1.13.1/tests/test_condition_operators_1495.py +192 -0
- controlzero-1.13.1/tests/test_deny_rule_reconciliation_1011.py +247 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_glob_matching.py +86 -0
- controlzero-1.13.1/tests/test_shell_semantic_class.py +244 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/.gitignore +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/Dockerfile.test +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/LICENSE +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/README.md +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/__main__.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/_internal/__init__.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/_internal/_messages.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/_internal/action_aliases.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/_internal/action_validator.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/_internal/credential_hook.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/_internal/credential_scanner.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/_internal/credentials_data/__init__.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/_internal/credentials_data/built_in.yaml +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/_internal/dlp_scanner.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/_internal/source_labels.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/_internal/tool_extractors.json +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/_internal/types.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/audit_local.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/audit_remote.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/canonical.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/__init__.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/__main__.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/_secrets.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/agent_config.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/console.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/debug_bundle.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/doctor.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/exec_cmd.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/hook_entry.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/hosts/__init__.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/hosts/antigravity.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/hosts/base.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/hosts/claude_code.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/hosts/codex_cli.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/hosts/cursor.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/hosts/cursor_cli.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/hosts/gemini_cli.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/hosts/kiro.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/hosts/unknown.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/kiro_adapter.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/migrate.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/output.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/spool_cmd.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/telemetry_consent.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/templates/antigravity/hooks.json +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/templates/antigravity.yaml +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/templates/autogen.yaml +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/templates/claude-code.yaml +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/templates/codex-cli.yaml +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/templates/cost-cap.yaml +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/templates/crewai.yaml +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/templates/cursor.yaml +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/templates/gemini-cli.yaml +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/templates/generic.yaml +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/templates/kiro/ide-pre-tool-use.kiro.hook +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/templates/kiro/ide-prompt-submit.kiro.hook +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/templates/kiro/kiro.yaml +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/templates/langchain.yaml +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/templates/mcp.yaml +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/templates/rag.yaml +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/cli/update_cmd.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/client.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/device.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/enrollment.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/error_codes.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/error_codes.yaml +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/errors.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/hitl/__init__.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/hitl/grant_protocol.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/hitl/mock.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/hitl/pending_approval.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/hitl/secret_leak_guard.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/hitl/status.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/hook_integrity.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/hooks/__init__.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/hooks/tool_output_handler.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/hosted_policy.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/integrations/__init__.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/integrations/anthropic.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/integrations/autogen.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/integrations/braintrust.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/integrations/crewai/__init__.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/integrations/crewai/agent.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/integrations/crewai/crew.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/integrations/crewai/task.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/integrations/crewai/tool.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/integrations/google.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/integrations/google_adk/__init__.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/integrations/google_adk/agent.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/integrations/google_adk/tool.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/integrations/langchain/__init__.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/integrations/langchain/agent.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/integrations/langchain/callbacks.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/integrations/langchain/chain.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/integrations/langchain/graph.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/integrations/langchain/modern.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/integrations/langchain/tool.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/integrations/langfuse.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/integrations/litellm.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/integrations/openai.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/integrations/pydantic_ai.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/integrations/vercel_ai.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/layout_migration.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/secrets/__init__.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/secrets/reference.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/secrets/resolver.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/secrets/secretstr.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/spool/__init__.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/spool/_compress.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/spool/_constants.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/spool/_crc32c.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/spool/_crypto.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/spool/_frame.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/spool/_keyring.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/spool/_metrics.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/spool/_spool.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/spool/_state.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/spool/_uploader.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/spool/cz-audit-v1.dict +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/tamper.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/controlzero/tracecontext.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/examples/hello_world.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/_fixtures/antigravity-envelope-expected.json +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/_fixtures/jcs_args_hash_vectors.json +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/conftest.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/integrations/__init__.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/integrations/test_google.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/parity/action_aliases.json +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/spool/__init__.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/spool/conftest.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/spool/test_spool_cli.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/spool/test_spool_concurrency.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/spool/test_spool_conformance.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/spool/test_spool_core.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/spool/test_spool_crash.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/spool/test_spool_diskfull.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/spool/test_spool_durable_default_tamper.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/spool/test_spool_keychain_dek.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/spool/test_spool_sink_wiring.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/spool/test_spool_transcript_localack.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/spool/test_spool_uploader.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_action_aliases.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_action_canonicalization.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_action_validator_t86.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_adapter_interface.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_agent_name_env.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_agy_docs_no_exit2_1373.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_antigravity_adapter.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_antigravity_envelope_parity.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_antigravity_ga_blockers_1248.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_antigravity_hook_check.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_antigravity_install.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_antigravity_posttooluse_observe_58.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_antigravity_tool_vocab_1303.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_api_key_mask.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_audit_project_id_attribution_pr2.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_audit_remote.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_audit_remote_io_capture.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_audit_remote_sdk_version.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_audit_sink_isolation.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_bundle_cache_freshness_1303.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_bundle_parser.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_bundle_translate.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_canonical_phase1a.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_capability_matrix_doc.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_cli_carve_out.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_cli_debug_bundle.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_cli_extractor_integration.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_cli_hook.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_cli_hook_entry.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_cli_hosted_refresh.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_cli_init.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_cli_init_templates.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_cli_tail.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_cli_test.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_cli_validate.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_conditions.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_config_format_parity_1303.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_conformance.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_console.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_credential_hook.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_cursor_adapter.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_default_action.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_device.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_dlp_mask.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_dlp_scanner.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_doctor.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_engine_version_consistency.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_enrollment.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_env_dump_438.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_epic_1247_bryan_acceptance.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_error_codes.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_errors_e_codes.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_fail_closed_eval.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_failopen_1303.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_hitl_5d_email_install.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_hitl_6a_cli_flag.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_hitl_6a_exceptions.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_hitl_6a_get_secret_hitl.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_hitl_6a_mock_backend.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_hitl_6a_pending_approval.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_hitl_6a_request_approval.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_hitl_6a_secret_leak_guard.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_hitl_6a_wait.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_hitl_conformance.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_hitl_phase2b_protocol.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_hitl_reason_codes.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_hitl_validator_keys.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_hook_extractors.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_hook_integrity.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_hook_mask_failclosed.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_hosted_local_audit_1247.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_hosted_local_shadow_warn_1265.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_hosted_policy_e2e.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_hosts_adapter.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_hybrid_mode_strict.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_hybrid_mode_warn.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_install_hook_command.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_install_hooks.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_io_capture_producer.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_kiro_adapter.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_kiro_cli_e2e.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_kiro_cli_hook_pin_1265.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_kiro_hook_templates.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_kiro_install.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_layout_migration_t101.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_layout_parity_t102.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_local_mode_dict.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_local_mode_file_json.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_local_mode_file_yaml.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_localized_reason.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_log_fallback_stderr.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_log_options_ignored_hosted.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_log_rotation.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_migrate.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_min_sdk_version_gate.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_multi_client_per_project_175.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_no_policy_no_key.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_observe_mode_1247.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_package_rename_shim.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_part3_active_policy_count_1303.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_per_client_isolation_1011.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_policy_engine_version_phase1b.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_policy_freshness.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_policy_load_failclosed_f3.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_policy_settings.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_policy_source_audit.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_pr4_install_messaging.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_pr4_output.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_pr4_status.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_probe_audit_isolation.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_quarantine.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_reason_code.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_refresh.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_secrets.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_secrets_reference_resolver.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_source_labels.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_sql_semantic_class.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_synthetic_policy_id_t79.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_t103_precedence.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_t104_cache_gc.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_t108_local_override_audit.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_t96_single_audit_log.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_t99_install_prefetch_bundle.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_tamper.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_tamper_behavior.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_tamper_hook.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_telemetry_consent.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_tracecontext.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_unsafe_int_boundary.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_update_cmd.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tests/test_upgrade_nudge.py +0 -0
- {controlzero-1.12.0 → controlzero-1.13.1}/tools/cz-kiro-adapter +0 -0
|
@@ -1,5 +1,57 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.13.1 (2026-06-27): shell semantic-class enforcement
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Shell semantic-class extractor (#437/#1459).** The enforcer maps a shell
|
|
8
|
+
command's first meaningful token to a semantic class
|
|
9
|
+
(`file_write`/`file_delete`/`file_read`/`network`), so a `deny: file_write`
|
|
10
|
+
rule also blocks shell-mediated file operations (e.g. `Bash(touch x)` ->
|
|
11
|
+
`file_write`). Additive layer after the per-command action, most-dangerous-wins
|
|
12
|
+
(`file_delete > network > file_write > file_read`), byte-identical to the Node
|
|
13
|
+
and Go SDKs (shared `parity-cases.json`, 118 cases). Closes the cross-OS
|
|
14
|
+
bypass where a denied `Write` was retried as a PowerShell/`cmd`/Unix file
|
|
15
|
+
write through the Bash tool. Unwraps `sh -c`/`powershell -Command`/`cmd /c`
|
|
16
|
+
launchers and prefix wrappers; recurses `find -delete`/`find -exec`;
|
|
17
|
+
whole-line scan for `.NET` method calls and `/dev/tcp/`.
|
|
18
|
+
|
|
19
|
+
## 1.13.0 (2026-06-26): policy-condition operator vocab
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **Condition operators.** A rule condition value may now be a tagged envelope
|
|
24
|
+
`{op, value}` honoring a closed operator set: `equals`, `contains`, `glob`,
|
|
25
|
+
`matches` (regex, BETA), `gt`/`lt`/`gte`/`lte`, `in`/`not_in`,
|
|
26
|
+
`in_range`/`outside_range`, `exists`/`not_exists`. Read-only aliases
|
|
27
|
+
`matches_pattern`/`greater_than`/`less_than` are canonicalized on load, so
|
|
28
|
+
existing dashboard templates work with zero migration. A legacy scalar value
|
|
29
|
+
(a plain string or number) keeps its prior glob/equals behavior exactly, so
|
|
30
|
+
existing policies and signed bundles evaluate unchanged.
|
|
31
|
+
- **File-path conditions.** `file_path` / `path` / `notebook_path` now feed the
|
|
32
|
+
`input_text` condition value, so a condition like "input_text contains .env"
|
|
33
|
+
fires on Read/Write/Edit tool calls.
|
|
34
|
+
|
|
35
|
+
### Notes
|
|
36
|
+
|
|
37
|
+
- Fail-closed: a missing field is no-match for every operator except
|
|
38
|
+
`exists`/`not_exists`; an unknown operator is rejected at load; a malformed
|
|
39
|
+
condition fails closed. `matches` is BETA and length-capped to bound regex
|
|
40
|
+
backtracking. Secret detection stays on the DLP scanner, not a regex condition.
|
|
41
|
+
|
|
42
|
+
## 1.12.1 (2026-06-26): deny rules actually deny
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- **Deny rules now actually deny (#1011).** A policy DENY rule with a condition
|
|
47
|
+
(e.g. "block Bash when the command contains rm -rf") could silently fail to
|
|
48
|
+
match, so the action ran anyway. Reconciled the rule-authoring vocabulary with
|
|
49
|
+
the engine: tool names match case-insensitively (a rule for `bash:*` catches
|
|
50
|
+
the canonical `Bash:rm`), the command text is matched as `input_text` by
|
|
51
|
+
CONTAINS, and conditions are no longer dropped when a hosted policy is
|
|
52
|
+
translated for local enforcement. A present-but-malformed condition now fails
|
|
53
|
+
closed at load instead of silently becoming an unconditional allow.
|
|
54
|
+
|
|
3
55
|
## 1.12.0 -- 2026-06-25 (per-agent key authority + per-agent bundle cache + whoami)
|
|
4
56
|
|
|
5
57
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: controlzero
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.13.1
|
|
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
|
|
@@ -958,4 +958,21 @@ def _translate_rule(rule: dict, policy_id: str) -> Optional[dict]:
|
|
|
958
958
|
if isinstance(k, str) and isinstance(v, str)
|
|
959
959
|
}
|
|
960
960
|
|
|
961
|
+
# #1011 follow-up: the SAME allowlist-strip trap. A hosted rule's
|
|
962
|
+
# `conditions` (e.g. {input_text: "rm -rf"}) was being dropped here, so a
|
|
963
|
+
# condition authored in the dashboard NEVER reached the local enforcer, so
|
|
964
|
+
# the rule silently matched on actions only (or, with a tool-case mismatch,
|
|
965
|
+
# not at all). Forward BOTH alias keys verbatim whenever present (incl. a
|
|
966
|
+
# malformed/falsy shape: [] / "" / 0 / False), and let the loader apply ONE
|
|
967
|
+
# consistent precedence + shape validation (truthy-first; reject any
|
|
968
|
+
# non-dict; a genuine empty dict {} is "no conditions"). Collapsing to a
|
|
969
|
+
# single key HERE re-introduces a divergent precedence where a present `{}`
|
|
970
|
+
# could shadow a real alias (codex re-review), so the bundle stays dumb and
|
|
971
|
+
# the loader is the single source of truth. (The backend wire struct carries
|
|
972
|
+
# Conditions; see bundle_handler.go PolicyRule.)
|
|
973
|
+
if "conditions" in rule:
|
|
974
|
+
translated["conditions"] = rule["conditions"]
|
|
975
|
+
if "when" in rule:
|
|
976
|
+
translated["when"] = rule["when"]
|
|
977
|
+
|
|
961
978
|
return translated
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Policy-condition operator table (single source of truth) for #1011 / #1495.
|
|
2
|
+
|
|
3
|
+
A rule condition value is either a LEGACY SCALAR (string -> glob, number/bool ->
|
|
4
|
+
equals; evaluated in enforcer._conditions_match, byte-identical to pre-#1495) or
|
|
5
|
+
a TAGGED ENVELOPE ``{"op": <name>, "value": <scalar|array>, "case_insensitive"?:
|
|
6
|
+
bool}``. This module evaluates the envelope operators ONLY; the scalar path stays
|
|
7
|
+
in the enforcer so existing policies and signed bundles evaluate unchanged.
|
|
8
|
+
|
|
9
|
+
This is the Python reference; the Node twin (conditionOps.ts) MUST agree
|
|
10
|
+
byte-for-byte (cross-SDK parity is proven by tests/parity/decisions.json). Go is
|
|
11
|
+
paused and not part of this contract; the Go BACKEND engine evaluates only the
|
|
12
|
+
reserved server-side keys (time_range/ip_whitelist/tags) and defers tool-level
|
|
13
|
+
conditions to the SDK (it has no tool args).
|
|
14
|
+
|
|
15
|
+
FAIL-CLOSED LAW: a MISSING field (value is None) is NO-MATCH for EVERY operator
|
|
16
|
+
except ``exists``/``not_exists``. A regex that will not compile, or a numeric op
|
|
17
|
+
on a non-numeric field, is NO-MATCH (never an error that fails open).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import fnmatch
|
|
23
|
+
import re
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
# Read-only aliases canonicalised on evaluation so the shipped templates
|
|
27
|
+
# (matches_pattern / greater_than / less_than) load and evaluate with ZERO data
|
|
28
|
+
# migration. The dashboard going forward emits the canonical names.
|
|
29
|
+
_OP_ALIASES = {
|
|
30
|
+
"matches_pattern": "matches",
|
|
31
|
+
"greater_than": "gt",
|
|
32
|
+
"less_than": "lt",
|
|
33
|
+
"greater_than_or_equal": "gte",
|
|
34
|
+
"less_than_or_equal": "lte",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# The closed operator set the engine honors. The Go publish-time validator (and
|
|
38
|
+
# the frontend) reject any op not in here, so an inert/typo'd operator can never
|
|
39
|
+
# ship.
|
|
40
|
+
KNOWN_OPS = frozenset(
|
|
41
|
+
{
|
|
42
|
+
"glob",
|
|
43
|
+
"equals",
|
|
44
|
+
"contains",
|
|
45
|
+
"matches",
|
|
46
|
+
"gt",
|
|
47
|
+
"lt",
|
|
48
|
+
"gte",
|
|
49
|
+
"lte",
|
|
50
|
+
"in",
|
|
51
|
+
"not_in",
|
|
52
|
+
"outside_range",
|
|
53
|
+
"in_range",
|
|
54
|
+
"exists",
|
|
55
|
+
"not_exists",
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# Reserved structured condition keys are matched by KEY NAME (the Go backend's
|
|
61
|
+
# server-side matchers) and are NEVER interpreted as a {op,value} envelope, even
|
|
62
|
+
# when their value nests a sub-key literally named `op` (e.g. a tag named "op").
|
|
63
|
+
# This closes the tags:{op:...} collision hole. The caller checks the KEY against
|
|
64
|
+
# this set before treating a value as an envelope.
|
|
65
|
+
RESERVED_STRUCTURED_KEYS = frozenset({"time_range", "ip_whitelist", "tags"})
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def canonicalize_op(op: str) -> str:
|
|
69
|
+
"""Map a read alias to its canonical operator name; pass through otherwise."""
|
|
70
|
+
return _OP_ALIASES.get(op, op)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def is_envelope(value: Any) -> bool:
|
|
74
|
+
"""True iff ``value`` is a tagged operator envelope: a dict with a string
|
|
75
|
+
``op`` key. Reserved structured keys are disambiguated by KEY NAME upstream,
|
|
76
|
+
never here, and a runtime args/context value is never treated as an envelope.
|
|
77
|
+
"""
|
|
78
|
+
return isinstance(value, dict) and isinstance(value.get("op"), str)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _as_float(x: Any):
|
|
82
|
+
"""Coerce to float for numeric ops; return None if not numeric (-> NO-MATCH)."""
|
|
83
|
+
if isinstance(x, bool): # bool is an int subclass; never treat as a number here
|
|
84
|
+
return None
|
|
85
|
+
if isinstance(x, (int, float)):
|
|
86
|
+
return float(x)
|
|
87
|
+
if isinstance(x, str):
|
|
88
|
+
try:
|
|
89
|
+
return float(x.strip())
|
|
90
|
+
except ValueError:
|
|
91
|
+
return None
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _as_list(operand: Any) -> list:
|
|
96
|
+
"""Operand for in/not_in: a JSON array, or a legacy comma-separated string
|
|
97
|
+
('Saturday,Sunday') split by this compat shim so the shipped templates work
|
|
98
|
+
unmodified. A None / empty operand yields an EMPTY list (the caller treats an
|
|
99
|
+
empty operand as NO-MATCH, fail-closed (codex P2: a missing `value` must not
|
|
100
|
+
turn `not_in` into match-everything))."""
|
|
101
|
+
if operand is None:
|
|
102
|
+
return []
|
|
103
|
+
if isinstance(operand, (list, tuple)):
|
|
104
|
+
return [x for x in operand if x is not None]
|
|
105
|
+
if isinstance(operand, str):
|
|
106
|
+
return [p.strip() for p in operand.split(",") if p.strip()]
|
|
107
|
+
return [operand]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# matches (regex) is BETA: Python `re` has no per-match timeout, so a pathological
|
|
111
|
+
# author-supplied pattern can backtrack catastrophically against adversarial field
|
|
112
|
+
# input (codex P1 ReDoS). Until a timeout / RE2 binding lands, bound BOTH the
|
|
113
|
+
# scanned input and the pattern length so practical backtracking is capped; the
|
|
114
|
+
# publish-time validator (#1498) also caps the pattern. A regex error stays
|
|
115
|
+
# fail-closed (NO-MATCH).
|
|
116
|
+
_MATCHES_INPUT_CAP = 8192
|
|
117
|
+
_MATCHES_PATTERN_CAP = 1024
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _range_bounds(operand: Any):
|
|
121
|
+
"""[lo, hi] for in_range/outside_range: a 2-element array, or a legacy
|
|
122
|
+
'lo-hi' / 'HH:MM-HH:MM' string. Returns (lo_str, hi_str) or None if malformed.
|
|
123
|
+
Comparison is lexical for HH:MM (24h zero-padded) and numeric otherwise; both
|
|
124
|
+
sides are compared as strings of the same kind, matching the Go matchTimeRange
|
|
125
|
+
lexical compare for time."""
|
|
126
|
+
if isinstance(operand, (list, tuple)) and len(operand) == 2:
|
|
127
|
+
return str(operand[0]), str(operand[1])
|
|
128
|
+
if isinstance(operand, str) and "-" in operand:
|
|
129
|
+
lo, _, hi = operand.partition("-")
|
|
130
|
+
lo, hi = lo.strip(), hi.strip()
|
|
131
|
+
if lo and hi:
|
|
132
|
+
return lo, hi
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _in_range(value: Any, lo: str, hi: str) -> bool:
|
|
137
|
+
"""Inclusive lo<=value<=hi. Numeric when all three parse as numbers, else a
|
|
138
|
+
lexical string compare (so 'HH:MM' 24h times order correctly)."""
|
|
139
|
+
fv, flo, fhi = _as_float(value), _as_float(lo), _as_float(hi)
|
|
140
|
+
if fv is not None and flo is not None and fhi is not None:
|
|
141
|
+
return flo <= fv <= fhi
|
|
142
|
+
sv = str(value)
|
|
143
|
+
return lo <= sv <= hi
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def match_operator(
|
|
147
|
+
op: str, value: Any, operand: Any, case_insensitive: bool = False
|
|
148
|
+
) -> bool:
|
|
149
|
+
"""Evaluate one field-level envelope operator. ``value`` is the field value
|
|
150
|
+
from the merged context+args dict (None if the field is absent); ``operand``
|
|
151
|
+
is the envelope's ``value``. Returns True iff the condition is satisfied.
|
|
152
|
+
Unknown op -> False (fail-closed; the validator rejects it at publish too)."""
|
|
153
|
+
op = canonicalize_op(op)
|
|
154
|
+
|
|
155
|
+
# Presence ops are the ONLY ones where a missing field can be a match.
|
|
156
|
+
if op == "exists":
|
|
157
|
+
return value is not None
|
|
158
|
+
if op == "not_exists":
|
|
159
|
+
return value is None
|
|
160
|
+
|
|
161
|
+
# Every other op: a missing field is NO-MATCH (uniform, fail-closed).
|
|
162
|
+
if value is None:
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
if op in ("gt", "lt", "gte", "lte"):
|
|
166
|
+
fv, fo = _as_float(value), _as_float(operand)
|
|
167
|
+
if fv is None or fo is None:
|
|
168
|
+
return False # non-numeric operand/field -> NO-MATCH
|
|
169
|
+
if op == "gt":
|
|
170
|
+
return fv > fo
|
|
171
|
+
if op == "lt":
|
|
172
|
+
return fv < fo
|
|
173
|
+
if op == "gte":
|
|
174
|
+
return fv >= fo
|
|
175
|
+
return fv <= fo
|
|
176
|
+
|
|
177
|
+
if op in ("in", "not_in"):
|
|
178
|
+
items = [str(x) for x in _as_list(operand)]
|
|
179
|
+
if not items:
|
|
180
|
+
# Malformed / empty operand -> NO-MATCH for BOTH in and not_in
|
|
181
|
+
# (fail-closed; a deny does not over-fire, an allow does not
|
|
182
|
+
# over-allow). The validator rejects an empty operand at publish.
|
|
183
|
+
return False
|
|
184
|
+
if case_insensitive:
|
|
185
|
+
present = str(value).casefold() in {i.casefold() for i in items}
|
|
186
|
+
else:
|
|
187
|
+
present = str(value) in items
|
|
188
|
+
return present if op == "in" else not present
|
|
189
|
+
|
|
190
|
+
if op in ("in_range", "outside_range"):
|
|
191
|
+
bounds = _range_bounds(operand)
|
|
192
|
+
if bounds is None:
|
|
193
|
+
return False
|
|
194
|
+
inside = _in_range(value, bounds[0], bounds[1])
|
|
195
|
+
return inside if op == "in_range" else not inside
|
|
196
|
+
|
|
197
|
+
# String ops below.
|
|
198
|
+
sval, soperand = str(value), str(operand)
|
|
199
|
+
if case_insensitive:
|
|
200
|
+
sval, soperand = sval.casefold(), soperand.casefold()
|
|
201
|
+
|
|
202
|
+
if op == "equals":
|
|
203
|
+
# Exact equality; wildcards are LITERAL (unlike glob).
|
|
204
|
+
return sval == soperand
|
|
205
|
+
|
|
206
|
+
if op == "contains":
|
|
207
|
+
# Substring; the operand's own glob metachars are literal (not wildcards).
|
|
208
|
+
return soperand in sval
|
|
209
|
+
|
|
210
|
+
if op == "glob":
|
|
211
|
+
return fnmatch.fnmatchcase(sval, soperand)
|
|
212
|
+
|
|
213
|
+
if op == "matches":
|
|
214
|
+
# Regex (re.search). Author owns anchoring. BETA: bound input + pattern
|
|
215
|
+
# length to cap ReDoS backtracking (codex P1); uncompilable / runtime
|
|
216
|
+
# error -> NO-MATCH (fail-closed), never an exception that fails open.
|
|
217
|
+
if len(soperand) > _MATCHES_PATTERN_CAP:
|
|
218
|
+
return False
|
|
219
|
+
flags = re.IGNORECASE if case_insensitive else 0
|
|
220
|
+
try:
|
|
221
|
+
return re.search(soperand, sval[:_MATCHES_INPUT_CAP], flags) is not None
|
|
222
|
+
except re.error:
|
|
223
|
+
return False
|
|
224
|
+
|
|
225
|
+
# Unknown operator: fail-closed (the publish-time validator also rejects it).
|
|
226
|
+
return False
|
|
@@ -470,6 +470,30 @@ class PolicyEvaluator:
|
|
|
470
470
|
cls = sql_semantic_class(sql_text)
|
|
471
471
|
if cls:
|
|
472
472
|
semantic_action = f"{tool}:{cls}"
|
|
473
|
+
# Shell semantic-class layer (#437): the SAME additive pattern for
|
|
474
|
+
# shell-mediated file/network ops. When the tool is the canonical
|
|
475
|
+
# shell (`Bash`) and no class was provided, derive it from the
|
|
476
|
+
# command and promote it into the file_write/file_delete/file_read/
|
|
477
|
+
# network NAMESPACE (`<class>:<class>`) -- NOT `Bash:<class>` --
|
|
478
|
+
# so a rule `deny: file_write` (normalized `file_write:*`) ALSO
|
|
479
|
+
# blocks `Bash(touch x)`. Existing `Bash:*` rules are unaffected
|
|
480
|
+
# because the per-command `action` is always tried first. The
|
|
481
|
+
# hook-check CLI provides the class via
|
|
482
|
+
# ``context['action_semantic_class']``; this is the on-the-fly
|
|
483
|
+
# path for direct ``guard("Bash", args={"command": ...})`` callers.
|
|
484
|
+
if not semantic_action and tool == "Bash" and args:
|
|
485
|
+
from controlzero._internal.hook_extractors import shell_semantic_class
|
|
486
|
+
cmd_text = None
|
|
487
|
+
if isinstance(args, dict):
|
|
488
|
+
for _key in ("command", "CommandLine"):
|
|
489
|
+
_v = args.get(_key)
|
|
490
|
+
if isinstance(_v, str) and _v:
|
|
491
|
+
cmd_text = _v
|
|
492
|
+
break
|
|
493
|
+
if cmd_text:
|
|
494
|
+
cls = shell_semantic_class(cmd_text)
|
|
495
|
+
if cls:
|
|
496
|
+
semantic_action = f"{cls}:{cls}"
|
|
473
497
|
candidate_actions: list[str] = [action]
|
|
474
498
|
if semantic_action and semantic_action != action:
|
|
475
499
|
candidate_actions.append(semantic_action)
|
|
@@ -516,7 +540,9 @@ class PolicyEvaluator:
|
|
|
516
540
|
|
|
517
541
|
for rule in self._rules:
|
|
518
542
|
evaluated += 1
|
|
519
|
-
|
|
543
|
+
# Action gate: tool-case-insensitive so `bash:*` matches `Bash:rm`
|
|
544
|
+
# (#1011). Resource/client/project gates below stay case-sensitive.
|
|
545
|
+
if not any(_action_glob_any(rule.actions, a) for a in candidate_actions):
|
|
520
546
|
continue
|
|
521
547
|
# Client + project gates (gh#175). A rule whose selector
|
|
522
548
|
# excludes this request does NOT apply to this surface, so
|
|
@@ -676,10 +702,22 @@ class PolicyEvaluator:
|
|
|
676
702
|
All condition keys must match. Missing key = no match. Context keys
|
|
677
703
|
override args on collision. Nested ``context['tags']`` dict is flattened.
|
|
678
704
|
"""
|
|
679
|
-
|
|
705
|
+
# #1011 follow-up (codex P1): the non-dict guard MUST run before the
|
|
706
|
+
# falsy short-circuit, otherwise a falsy-but-malformed conditions ([],
|
|
707
|
+
# "", 0, False) would be read as "no conditions" and the rule would match
|
|
708
|
+
# unconditionally (an allow rule -> unconditional allow, fail-open). A
|
|
709
|
+
# corrupt/tampered or future-shaped signed rule is unverifiable, so the
|
|
710
|
+
# rule does NOT match: an allow rule then does not fire and the default
|
|
711
|
+
# posture governs (fail-closed); a deny rule simply does not contribute.
|
|
712
|
+
# `None` is the genuine "no conditions" case and matches.
|
|
713
|
+
if not isinstance(rule_conditions, dict):
|
|
714
|
+
return rule_conditions is None
|
|
715
|
+
if not rule_conditions: # empty dict -> no conditions to satisfy
|
|
680
716
|
return True
|
|
681
717
|
import fnmatch as _fn
|
|
682
718
|
|
|
719
|
+
from .io_capture_producer import _INPUT_TEXT_KEYS
|
|
720
|
+
|
|
683
721
|
merged: dict = {}
|
|
684
722
|
if args:
|
|
685
723
|
merged.update(args)
|
|
@@ -688,11 +726,71 @@ class PolicyEvaluator:
|
|
|
688
726
|
tags = context.get("tags") or {}
|
|
689
727
|
if isinstance(tags, dict):
|
|
690
728
|
merged.update(tags)
|
|
729
|
+
# #1011 follow-up: derive `input_text` (the key the dashboard/templates
|
|
730
|
+
# author, "command/input contains X") DETERMINISTICALLY from the payload
|
|
731
|
+
# rather than trusting a caller-supplied label. The raw command/content
|
|
732
|
+
# is the GROUND TRUTH of what runs, so it wins first: neither a benign
|
|
733
|
+
# `input_text` nor a benign pre-extracted `input_payload` can shadow a
|
|
734
|
+
# dangerous `command` (codex P1: a payload setting input_text="ls" or
|
|
735
|
+
# input_payload="ls" while command="rm -rf /" would otherwise bypass the
|
|
736
|
+
# deny under default allow). Precedence: the canonical _INPUT_TEXT_KEYS
|
|
737
|
+
# scan (the SAME order audit extraction uses, so enforcement and audit
|
|
738
|
+
# never drift), then the host's pre-extracted input_payload, then any
|
|
739
|
+
# existing input_text, each only as a fallback when nothing richer is
|
|
740
|
+
# present.
|
|
741
|
+
_derived = None
|
|
742
|
+
for _k in _INPUT_TEXT_KEYS:
|
|
743
|
+
_v = merged.get(_k)
|
|
744
|
+
if _v:
|
|
745
|
+
_derived = _v
|
|
746
|
+
break
|
|
747
|
+
if not _derived:
|
|
748
|
+
_derived = merged.get("input_payload")
|
|
749
|
+
if not _derived:
|
|
750
|
+
_derived = merged.get("input_text")
|
|
751
|
+
if _derived:
|
|
752
|
+
merged["input_text"] = _derived
|
|
753
|
+
from .condition_ops import (
|
|
754
|
+
RESERVED_STRUCTURED_KEYS,
|
|
755
|
+
is_envelope,
|
|
756
|
+
match_operator,
|
|
757
|
+
)
|
|
758
|
+
|
|
691
759
|
for key, pattern in rule_conditions.items():
|
|
760
|
+
# #1495: a TAGGED ENVELOPE {op, value} dispatches to the shared
|
|
761
|
+
# operator table (equals/contains/matches/gt/lt/in/not_in/range/
|
|
762
|
+
# exists/...). Everything else is a LEGACY SCALAR and keeps the exact
|
|
763
|
+
# pre-#1495 glob/CONTAINS path below, so existing policies and signed
|
|
764
|
+
# bundles evaluate byte-identically. exists/not_exists are the only
|
|
765
|
+
# ops where a missing field can match, so the envelope branch runs
|
|
766
|
+
# BEFORE the missing-key short-circuit. A RESERVED structured key is
|
|
767
|
+
# matched by key name, never as an envelope (the tags:{op:...} hole).
|
|
768
|
+
if key not in RESERVED_STRUCTURED_KEYS and is_envelope(pattern):
|
|
769
|
+
if not match_operator(
|
|
770
|
+
pattern["op"],
|
|
771
|
+
merged.get(key),
|
|
772
|
+
pattern.get("value"),
|
|
773
|
+
bool(pattern.get("case_insensitive")),
|
|
774
|
+
):
|
|
775
|
+
return False
|
|
776
|
+
continue
|
|
692
777
|
value = merged.get(key)
|
|
693
778
|
if value is None:
|
|
694
779
|
return False
|
|
695
|
-
|
|
780
|
+
sval, spat = str(value), str(pattern)
|
|
781
|
+
# Only `input_text` (the dashboard's authored text-content key) uses
|
|
782
|
+
# CONTAINS for a plain pattern, so `input_text: "rm -rf"` catches
|
|
783
|
+
# `rm -rf /tmp/foo`. A pattern with glob metacharacters still uses
|
|
784
|
+
# fnmatch; EVERY other key (including the raw `command`/`prompt`
|
|
785
|
+
# sources) keeps exact-glob semantics, so an existing exact-match
|
|
786
|
+
# allowlist is never silently broadened into a substring match
|
|
787
|
+
# (codex P1 fail-open regression).
|
|
788
|
+
if key.casefold() in _TEXT_CONTENT_CONDITION_KEYS and not any(
|
|
789
|
+
c in spat for c in "*?["
|
|
790
|
+
):
|
|
791
|
+
if spat not in sval:
|
|
792
|
+
return False
|
|
793
|
+
elif not _fn.fnmatchcase(sval, spat):
|
|
696
794
|
return False
|
|
697
795
|
return True
|
|
698
796
|
|
|
@@ -836,8 +934,58 @@ def _glob_any(patterns: list[str], value: str) -> bool:
|
|
|
836
934
|
|
|
837
935
|
fnmatchcase is case-sensitive (matches policy semantics) and supports the
|
|
838
936
|
same syntax users learn from .gitignore: *, ?, [seq], [!seq].
|
|
937
|
+
|
|
938
|
+
NOTE: this stays case-sensitive and is used for the resource / client /
|
|
939
|
+
project selector gates. Action matching uses :func:`_action_glob_any`,
|
|
940
|
+
which case-folds the tool segment (see below).
|
|
839
941
|
"""
|
|
840
942
|
for p in patterns:
|
|
841
943
|
if fnmatch.fnmatchcase(value, p):
|
|
842
944
|
return True
|
|
843
945
|
return False
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
def _norm_action_tool(s: str) -> str:
|
|
949
|
+
"""Case-fold ONLY the tool segment of an action token (the part before the
|
|
950
|
+
first ``:``); leave the method/arg glob untouched.
|
|
951
|
+
|
|
952
|
+
The canonical action the SDK emits is capital-tool (e.g. ``Bash:rm``, the
|
|
953
|
+
canonical TOOL NAME from the extractor spec), but a user or a policy
|
|
954
|
+
template may author the tool in any case (``bash:*``). The tool name is a
|
|
955
|
+
fixed, known vocabulary, so case is never meaningful for it; folding it lets
|
|
956
|
+
a user-authored rule reconcile with the call the agent actually makes. The
|
|
957
|
+
METHOD stays case-sensitive so method-cased semantics (e.g. ``database:DROP``
|
|
958
|
+
vs ``database:drop``) are preserved. A token with no ``:`` (e.g. the
|
|
959
|
+
universal ``*``) is folded whole, a no-op for glob metacharacters.
|
|
960
|
+
"""
|
|
961
|
+
tool, sep, method = s.partition(":")
|
|
962
|
+
return (tool.casefold() + sep + method) if sep else s.casefold()
|
|
963
|
+
|
|
964
|
+
|
|
965
|
+
def _action_glob_any(patterns: list[str], action: str) -> bool:
|
|
966
|
+
"""Action-gate matcher: like :func:`_glob_any` but case-INSENSITIVE on the
|
|
967
|
+
tool segment of both the rule pattern and the action (see
|
|
968
|
+
:func:`_norm_action_tool`).
|
|
969
|
+
|
|
970
|
+
This is what makes a deny rule authored as ``bash:*`` actually match the
|
|
971
|
+
canonical ``Bash:rm`` (#1011 follow-up: a customer's deny rule silently
|
|
972
|
+
never fired because the matcher compared the tool case-sensitively).
|
|
973
|
+
"""
|
|
974
|
+
a = _norm_action_tool(action)
|
|
975
|
+
for p in patterns:
|
|
976
|
+
if fnmatch.fnmatchcase(a, _norm_action_tool(p)):
|
|
977
|
+
return True
|
|
978
|
+
return False
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
# The ONE condition key whose plain (metachar-free) pattern is matched by
|
|
982
|
+
# CONTAINS (substring): `input_text`. It is the key the dashboard/templates
|
|
983
|
+
# author for "the command/input contains X", and a user typing `rm -rf` means
|
|
984
|
+
# "contains rm -rf", not "IS exactly rm -rf". Patterns WITH glob metachars still
|
|
985
|
+
# use fnmatch. This set is deliberately NARROW: the raw payload keys (command,
|
|
986
|
+
# input_payload, prompt, ...) are seeding SOURCES for input_text, not author-
|
|
987
|
+
# facing condition keys. Briefly including them here broadened any existing
|
|
988
|
+
# exact-match allowlist keyed on `command` into a substring match, a fail-open
|
|
989
|
+
# regression (codex P1). Every key except `input_text` keeps exact-glob
|
|
990
|
+
# semantics. (#1011 follow-up.)
|
|
991
|
+
_TEXT_CONTENT_CONDITION_KEYS = frozenset({"input_text"})
|