controlzero 1.9.0__tar.gz → 1.9.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {controlzero-1.9.0 → controlzero-1.9.2}/CHANGELOG.md +73 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/PKG-INFO +1 -1
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/__init__.py +1 -1
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/audit_remote.py +71 -11
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/spool_cmd.py +15 -2
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/client.py +108 -24
- controlzero-1.9.2/controlzero/spool/_keyring.py +311 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/spool/_spool.py +38 -3
- controlzero-1.9.2/controlzero/spool/_state.py +356 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/pyproject.toml +1 -1
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/conftest.py +24 -0
- controlzero-1.9.2/tests/spool/test_spool_durable_default_tamper.py +354 -0
- controlzero-1.9.2/tests/spool/test_spool_keychain_dek.py +309 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/spool/test_spool_sink_wiring.py +55 -6
- controlzero-1.9.2/tests/test_hosted_local_audit_1247.py +217 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hosted_policy_e2e.py +16 -3
- controlzero-1.9.2/tests/test_log_options_ignored_hosted.py +50 -0
- controlzero-1.9.0/controlzero/spool/_state.py +0 -154
- controlzero-1.9.0/tests/test_log_options_ignored_hosted.py +0 -35
- {controlzero-1.9.0 → controlzero-1.9.2}/.gitignore +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/Dockerfile.test +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/LICENSE +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/README.md +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/_internal/__init__.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/_internal/action_aliases.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/_internal/action_validator.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/_internal/bundle.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/_internal/credential_hook.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/_internal/credential_scanner.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/_internal/credentials_data/__init__.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/_internal/credentials_data/built_in.yaml +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/_internal/dlp_scanner.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/_internal/enforcer.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/_internal/hook_extractors.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/_internal/tool_extractors.json +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/_internal/types.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/audit_local.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/canonical.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/__init__.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/_secrets.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/console.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/debug_bundle.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/doctor.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/hosts/__init__.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/hosts/antigravity.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/hosts/base.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/hosts/claude_code.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/hosts/codex_cli.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/hosts/gemini_cli.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/hosts/kiro.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/hosts/unknown.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/kiro_adapter.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/main.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/migrate.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/telemetry_consent.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/antigravity/hooks.json +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/antigravity.yaml +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/autogen.yaml +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/claude-code.yaml +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/codex-cli.yaml +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/cost-cap.yaml +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/crewai.yaml +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/cursor.yaml +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/gemini-cli.yaml +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/generic.yaml +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/kiro/ide-pre-tool-use.kiro.hook +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/kiro/ide-prompt-submit.kiro.hook +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/kiro/kiro.yaml +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/langchain.yaml +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/mcp.yaml +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/cli/templates/rag.yaml +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/device.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/enrollment.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/error_codes.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/error_codes.yaml +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/errors.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/hitl/__init__.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/hitl/grant_protocol.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/hitl/mock.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/hitl/pending_approval.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/hitl/secret_leak_guard.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/hitl/status.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/hooks/__init__.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/hooks/tool_output_handler.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/hosted_policy.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/__init__.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/anthropic.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/autogen.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/braintrust.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/crewai/__init__.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/crewai/agent.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/crewai/crew.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/crewai/task.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/crewai/tool.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/google.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/google_adk/__init__.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/google_adk/agent.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/google_adk/tool.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/langchain/__init__.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/langchain/agent.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/langchain/callbacks.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/langchain/chain.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/langchain/graph.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/langchain/modern.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/langchain/tool.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/langfuse.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/litellm.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/openai.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/pydantic_ai.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/integrations/vercel_ai.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/layout_migration.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/policy_loader.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/spool/__init__.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/spool/_compress.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/spool/_constants.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/spool/_crc32c.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/spool/_crypto.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/spool/_frame.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/spool/_metrics.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/spool/_uploader.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/spool/cz-audit-v1.dict +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/tamper.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/controlzero/tracecontext.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/examples/hello_world.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/_fixtures/jcs_args_hash_vectors.json +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/integrations/__init__.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/integrations/test_google.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/parity/action_aliases.json +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/spool/__init__.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/spool/conftest.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/spool/test_spool_cli.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/spool/test_spool_concurrency.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/spool/test_spool_conformance.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/spool/test_spool_core.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/spool/test_spool_crash.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/spool/test_spool_diskfull.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/spool/test_spool_transcript_localack.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/spool/test_spool_uploader.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_action_aliases.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_action_canonicalization.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_action_validator_t86.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_agent_name_env.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_antigravity_adapter.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_antigravity_hook_check.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_antigravity_install.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_api_key_mask.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_audit_remote.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_audit_remote_sdk_version.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_audit_sink_isolation.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_bundle_parser.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_bundle_translate.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_canonical_phase1a.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_cli_carve_out.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_cli_debug_bundle.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_cli_extractor_integration.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_cli_hook.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_cli_hosted_refresh.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_cli_init.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_cli_init_templates.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_cli_tail.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_cli_test.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_cli_validate.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_coding_agent_hooks.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_conditions.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_conformance.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_console.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_credential_hook.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_default_action.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_device.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_dlp_scanner.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_doctor.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_engine_version_consistency.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_enrollment.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_env_dump_438.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_error_codes.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_errors_e_codes.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_fail_closed_eval.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_glob_matching.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hitl_5d_email_install.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hitl_6a_cli_flag.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hitl_6a_exceptions.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hitl_6a_get_secret_hitl.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hitl_6a_mock_backend.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hitl_6a_pending_approval.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hitl_6a_request_approval.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hitl_6a_secret_leak_guard.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hitl_6a_wait.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hitl_conformance.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hitl_phase2b_protocol.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hitl_reason_codes.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hitl_validator_keys.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hook_extractors.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hosts_adapter.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hybrid_mode_strict.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_hybrid_mode_warn.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_install_hook_command.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_install_hooks.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_kiro_adapter.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_kiro_hook_templates.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_kiro_install.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_layout_migration_t101.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_layout_parity_t102.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_local_mode_dict.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_local_mode_file_json.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_local_mode_file_yaml.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_log_fallback_stderr.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_log_rotation.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_migrate.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_min_sdk_version_gate.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_multi_client_per_project_175.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_no_policy_no_key.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_package_rename_shim.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_policy_engine_version_phase1b.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_policy_freshness.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_policy_settings.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_policy_source_audit.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_quarantine.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_reason_code.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_refresh.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_secrets.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_sql_semantic_class.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_synthetic_policy_id_t79.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_t103_precedence.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_t104_cache_gc.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_t108_local_override_audit.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_t96_single_audit_log.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_t99_install_prefetch_bundle.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_tamper.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_tamper_behavior.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_tamper_hook.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_telemetry_consent.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_tracecontext.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tests/test_unsafe_int_boundary.py +0 -0
- {controlzero-1.9.0 → controlzero-1.9.2}/tools/cz-kiro-adapter +0 -0
|
@@ -1,5 +1,78 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.1 -- 2026-06-16 (hosted-mode local audit log P0, epic gh#1247)
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **(P0) Hosted mode never wrote the local `~/.controlzero/audit.log`.** After
|
|
8
|
+
`controlzero install claude-code --api-key cz_live_...` (hosted mode), tool
|
|
9
|
+
calls reached the dashboard (remote audit) but the local audit log stayed
|
|
10
|
+
frozen indefinitely -- even though the claude-code template and the install
|
|
11
|
+
success message both promise that every tool call is logged to
|
|
12
|
+
`~/.controlzero/audit.log`. Root cause: `Client.__init__` constructed the
|
|
13
|
+
`LocalAuditLogger` ONLY when no API key was set, so hosted mode left the
|
|
14
|
+
local sink `None` and `_audit_decision()` skipped the local write (allow AND
|
|
15
|
+
deny). The local log is now written in EVERY mode; the remote sink is layered
|
|
16
|
+
on top, not instead. A blocked (deny) call is now locally recorded too -- the
|
|
17
|
+
exact case a customer hit (Claude Code `Bash` calls denied by no-rule-match).
|
|
18
|
+
This also restores `cz debug-bundle` and the tamper hash-chain, which read
|
|
19
|
+
the local log. Regression test: `tests/test_hosted_local_audit_1247.py`.
|
|
20
|
+
|
|
21
|
+
### Security
|
|
22
|
+
|
|
23
|
+
- **Local audit log redacts PII/financial DLP plaintext.** Now that hosted mode
|
|
24
|
+
writes the local plaintext audit log, a DLP finding's raw `matched_text` (which
|
|
25
|
+
is plaintext for the pii/financial categories; the secret category is already
|
|
26
|
+
SHA-256 hashed) is stripped from the LOCAL row -- it would otherwise expose
|
|
27
|
+
PII/financial data to anyone who can read the file, content hosted mode kept
|
|
28
|
+
server-side only. The finding metadata (rule_id/category/location) is
|
|
29
|
+
preserved so the local log still records THAT a rule fired, and the remote
|
|
30
|
+
sink keeps full fidelity.
|
|
31
|
+
|
|
32
|
+
## 1.9.2 -- 2026-06-16 (durable-by-default + keychain-DEK audit spool, epic gh#1247)
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- **Hosted-mode audit is now durable by default.** When a `Client` is
|
|
37
|
+
constructed with an API key (hosted mode) and `CONTROLZERO_SPOOL` is
|
|
38
|
+
unset, the audit sink now defaults to the **durable encrypted spool**:
|
|
39
|
+
every decision is serialized, encrypted, and fsynced to an append-only
|
|
40
|
+
on-disk WAL **before** any network send, then drained opportunistically
|
|
41
|
+
in a background thread. Audit is no longer lost on a backend outage. An
|
|
42
|
+
explicit `CONTROLZERO_SPOOL` value (including `off`) still wins, and the
|
|
43
|
+
enrolled-machine sink keeps its prior off-by-default. Sink init is
|
|
44
|
+
fail-soft: any open/IO error degrades to the prior in-memory path,
|
|
45
|
+
emits `spool_init_degraded_total`, and never crashes or blocks the
|
|
46
|
+
PreToolUse hook.
|
|
47
|
+
|
|
48
|
+
### Security
|
|
49
|
+
|
|
50
|
+
- **Spool encryption key (DEK) defaults to the OS keystore.** The DEK now
|
|
51
|
+
lives in the macOS Keychain (a `security` generic-password item) or the
|
|
52
|
+
Linux Secret Service (`secret-tool`/libsecret) by default when one is
|
|
53
|
+
available; the on-disk `spool.key` then holds only a sentinel, so a
|
|
54
|
+
file-read of the spool directory can neither **decrypt** nor **forge**
|
|
55
|
+
spooled audit records (the AES-GCM tag and hash chain are keyed on the
|
|
56
|
+
DEK). Keystore access is strictly **non-interactive** and
|
|
57
|
+
hard-timeout-bounded -- a prompt risk, locked keystore, or missing CLI
|
|
58
|
+
degrades to the legacy 0600 `spool.key` (key hardening never costs audit
|
|
59
|
+
durability and never blocks the hook). A pre-existing on-disk DEK is
|
|
60
|
+
migrated into the keystore on first keystore-enabled open. Force the
|
|
61
|
+
legacy file DEK with `CONTROLZERO_SPOOL_KEYCHAIN=0` or
|
|
62
|
+
`CONTROLZERO_SPOOL_KEYCHAIN_DISABLE=1`; require the keystore with
|
|
63
|
+
`CONTROLZERO_SPOOL_KEYCHAIN=1`.
|
|
64
|
+
|
|
65
|
+
### Tests
|
|
66
|
+
|
|
67
|
+
- New `tests/spool/test_spool_keychain_dek.py` (keystore round-trip,
|
|
68
|
+
sentinel-on-disk, prompt-risk fallback, file opt-out, file->keystore
|
|
69
|
+
migration) and `tests/spool/test_spool_durable_default_tamper.py`
|
|
70
|
+
(hosted durable default, encrypted-WAL-before-send, **tampered-frame
|
|
71
|
+
rejection surfaced via `spool_tamper_records_total{gcm_auth}`**,
|
|
72
|
+
graceful init-failure degrade, WAL append latency budget). Spool
|
|
73
|
+
isolation is enforced suite-wide so tests never touch the real home dir
|
|
74
|
+
or OS keychain.
|
|
75
|
+
|
|
3
76
|
## 1.9.0 -- 2026-06-15 (Antigravity install CLI, epic gh#925)
|
|
4
77
|
|
|
5
78
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: controlzero
|
|
3
|
-
Version: 1.9.
|
|
3
|
+
Version: 1.9.2
|
|
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
|
|
@@ -261,26 +261,74 @@ class _SpoolWiringMixin:
|
|
|
261
261
|
_spool = None
|
|
262
262
|
_spool_mode = "off"
|
|
263
263
|
|
|
264
|
-
def _init_spool(self, stream_key: str) -> None:
|
|
264
|
+
def _init_spool(self, stream_key: str, default_mode: str = "off") -> None:
|
|
265
|
+
"""Open the offline audit spool for this sink.
|
|
266
|
+
|
|
267
|
+
``default_mode`` is the mode used when ``CONTROLZERO_SPOOL`` is
|
|
268
|
+
UNSET. The hosted (API-key) sink passes ``"durable"`` so audit is
|
|
269
|
+
never lost on a backend outage even when the operator has not set
|
|
270
|
+
the env knob -- the founder requirement that hosted-mode audit
|
|
271
|
+
WALs to encrypted disk before send by default. The enrolled sink
|
|
272
|
+
keeps ``"off"`` (no behavior change). An explicit ``CONTROLZERO_SPOOL``
|
|
273
|
+
value ALWAYS wins (operators can force ``off``/``spool_only``).
|
|
274
|
+
|
|
275
|
+
Non-blocking guarantee (founder constraint): this runs inside the
|
|
276
|
+
PreToolUse hook hot path. Every failure mode -- spool import
|
|
277
|
+
error, keystore prompt risk, IO error, ENOSPC -- DEGRADES to the
|
|
278
|
+
prior in-memory behavior and emits ``spool_init_degraded_total``;
|
|
279
|
+
it never crashes or blocks the agent. Spool open itself is
|
|
280
|
+
bounded (one recovery scan + the 200 ms seq.lock budget); it does
|
|
281
|
+
no network IO.
|
|
282
|
+
"""
|
|
265
283
|
self._drain_state_lock = threading.Lock()
|
|
266
284
|
self._drain_inflight = False
|
|
267
285
|
self._drain_again = False
|
|
268
286
|
self._drain_auth_blocked = False
|
|
269
287
|
self._spool = None
|
|
270
288
|
self._spool_mode = "off"
|
|
271
|
-
# Fast path: flag off AND no spool
|
|
272
|
-
# spool import entirely so the
|
|
273
|
-
|
|
289
|
+
# Fast path: flag off/unset, NO hosted default, AND no spool
|
|
290
|
+
# directory on disk -- skip the spool import entirely so the
|
|
291
|
+
# legacy default path stays byte-identical. When default_mode is
|
|
292
|
+
# durable (hosted), we must NOT take this shortcut: the whole
|
|
293
|
+
# point is to open a durable spool with no env set.
|
|
294
|
+
env_present = os.environ.get("CONTROLZERO_SPOOL")
|
|
295
|
+
mode_raw = (env_present or "").strip().lower()
|
|
274
296
|
spool_dir = os.path.expanduser(
|
|
275
297
|
os.environ.get("CONTROLZERO_SPOOL_DIR") or _SPOOL_DEFAULT_DIR)
|
|
276
|
-
|
|
298
|
+
# "Unset" means the env var is absent or an empty/whitespace
|
|
299
|
+
# string. An EXPLICIT "off" is a deliberate operator choice and
|
|
300
|
+
# MUST win over the sink default -- it is NOT treated as unset.
|
|
301
|
+
env_unset = (env_present is None) or (mode_raw == "")
|
|
302
|
+
env_off = mode_raw == "off"
|
|
303
|
+
# Effective mode is "off" when: env explicitly off, OR env unset
|
|
304
|
+
# AND this sink's default is off. In that case, with no spool on
|
|
305
|
+
# disk, skip the spool import entirely (byte-identical legacy).
|
|
306
|
+
effective_off = env_off or (env_unset and default_mode in ("", "off"))
|
|
307
|
+
if effective_off and not os.path.isdir(spool_dir):
|
|
277
308
|
return
|
|
278
309
|
try:
|
|
279
|
-
from controlzero.spool import Spool, get_mode
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
310
|
+
from controlzero.spool import Spool, SpoolConfig, get_mode
|
|
311
|
+
|
|
312
|
+
# Resolve the effective mode: an explicit env value (incl.
|
|
313
|
+
# "off") wins; only a truly UNSET/blank env falls back to
|
|
314
|
+
# this sink's default_mode.
|
|
315
|
+
resolved = get_mode() # "off" when unset/blank or unknown
|
|
316
|
+
if env_unset and default_mode not in ("", "off"):
|
|
317
|
+
resolved = default_mode
|
|
318
|
+
cfg = SpoolConfig.from_env()
|
|
319
|
+
cfg.mode = resolved
|
|
320
|
+
self._spool_mode = resolved
|
|
321
|
+
self._spool = Spool.maybe_open(stream_key, config=cfg)
|
|
322
|
+
# If the spool opened but immediately degraded to memory-only
|
|
323
|
+
# (e.g. ENOSPC during the open-time recovery/cleanup), surface
|
|
324
|
+
# it as a metric. The sink still functions -- WAL just buffers
|
|
325
|
+
# in memory until disk frees up -- so this is a warning, not a
|
|
326
|
+
# crash (C15).
|
|
327
|
+
if (self._spool is not None
|
|
328
|
+
and getattr(self._spool, "in_memory_mode", False)):
|
|
329
|
+
self._spool_init_degraded("memory_mode")
|
|
283
330
|
except Exception as exc: # noqa: BLE001
|
|
331
|
+
self._spool_init_degraded("exception")
|
|
284
332
|
logger.warning(
|
|
285
333
|
"controlzero: audit spool unavailable (%s); "
|
|
286
334
|
"falling back to in-memory buffering",
|
|
@@ -289,6 +337,15 @@ class _SpoolWiringMixin:
|
|
|
289
337
|
self._spool = None
|
|
290
338
|
self._spool_mode = "off"
|
|
291
339
|
|
|
340
|
+
@staticmethod
|
|
341
|
+
def _spool_init_degraded(reason: str) -> None:
|
|
342
|
+
"""Emit the spool-init degrade metric. Never raises."""
|
|
343
|
+
try:
|
|
344
|
+
from controlzero.spool import metrics as _m
|
|
345
|
+
_m.incr("spool_init_degraded_total", reason)
|
|
346
|
+
except Exception: # noqa: BLE001
|
|
347
|
+
pass
|
|
348
|
+
|
|
292
349
|
@property
|
|
293
350
|
def _spool_wal(self) -> bool:
|
|
294
351
|
"""True when log() must take the spool-first WAL path."""
|
|
@@ -660,8 +717,11 @@ class BearerAuditSink(_SpoolWiringMixin):
|
|
|
660
717
|
self._closed = False
|
|
661
718
|
|
|
662
719
|
# Offline audit spool (Phase 2): the stream is keyed by the api
|
|
663
|
-
# key fingerprint, exactly the spec's stream identity.
|
|
664
|
-
|
|
720
|
+
# key fingerprint, exactly the spec's stream identity. HOSTED
|
|
721
|
+
# mode defaults to durable WAL-to-encrypted-disk so audit is
|
|
722
|
+
# never lost on a backend outage even with no env set (founder
|
|
723
|
+
# requirement). An explicit CONTROLZERO_SPOOL value still wins.
|
|
724
|
+
self._init_spool(api_key, default_mode=_SPOOL_MODE_DURABLE)
|
|
665
725
|
|
|
666
726
|
self._start_flush_timer()
|
|
667
727
|
# Drain-only rule (plan section 10): an existing non-empty spool
|
|
@@ -203,9 +203,22 @@ def spool_verify(as_json):
|
|
|
203
203
|
click.echo("error: spool.key missing; cannot verify", err=True)
|
|
204
204
|
sys.exit(2)
|
|
205
205
|
from controlzero.spool import assess_segment, derive_key
|
|
206
|
-
from controlzero.spool._state import
|
|
206
|
+
from controlzero.spool._state import (
|
|
207
|
+
SpoolKeyUnavailable,
|
|
208
|
+
load_or_create_dek,
|
|
209
|
+
secure_read,
|
|
210
|
+
)
|
|
207
211
|
|
|
208
|
-
|
|
212
|
+
try:
|
|
213
|
+
dek = load_or_create_dek(root)
|
|
214
|
+
except SpoolKeyUnavailable:
|
|
215
|
+
click.echo(
|
|
216
|
+
"error: spool DEK lives in the OS keystore but the keystore is "
|
|
217
|
+
"not readable right now (locked, or run on a different machine). "
|
|
218
|
+
"Unlock the keystore (or run on the originating host) and retry.",
|
|
219
|
+
err=True,
|
|
220
|
+
)
|
|
221
|
+
sys.exit(2)
|
|
209
222
|
|
|
210
223
|
def key_provider(header):
|
|
211
224
|
return derive_key(dek, header.device_id, header.stream_fp,
|
|
@@ -36,7 +36,6 @@ import sys
|
|
|
36
36
|
import threading
|
|
37
37
|
import time
|
|
38
38
|
import uuid
|
|
39
|
-
import warnings
|
|
40
39
|
import asyncio
|
|
41
40
|
from datetime import datetime, timezone
|
|
42
41
|
from pathlib import Path
|
|
@@ -120,6 +119,53 @@ def _mask_api_key(api_key: Optional[str]) -> str:
|
|
|
120
119
|
return "***"
|
|
121
120
|
|
|
122
121
|
|
|
122
|
+
# DLP categories whose ``matched_text`` is PLAINTEXT in a finding (the secret
|
|
123
|
+
# category is already SHA-256 hashed by the scanner, so it is safe to persist
|
|
124
|
+
# locally). Anything in this set must have its raw matched value stripped
|
|
125
|
+
# before the finding is written to the local plaintext audit log. See
|
|
126
|
+
# ``_redact_local_dlp`` and the #1247 review note in ``_audit_decision``.
|
|
127
|
+
_DLP_PLAINTEXT_CATEGORIES = ("pii", "financial")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _redact_local_dlp(entry: dict) -> dict:
|
|
131
|
+
"""Return a copy of an audit entry safe to write to the LOCAL plaintext log.
|
|
132
|
+
|
|
133
|
+
The only field that carries raw argument-derived sensitive content is
|
|
134
|
+
``dlp_findings[*].matched_text`` for pii/financial categories. We strip the
|
|
135
|
+
raw value (replacing it with a redaction marker) while preserving every
|
|
136
|
+
other field of the finding -- rule_id, category, location, count, etc. --
|
|
137
|
+
so the local log still records THAT a DLP rule fired, just not the matched
|
|
138
|
+
value. Entries without DLP findings are returned unchanged (a cheap
|
|
139
|
+
identity for the overwhelmingly common case).
|
|
140
|
+
"""
|
|
141
|
+
findings = entry.get("dlp_findings")
|
|
142
|
+
if not findings:
|
|
143
|
+
return entry
|
|
144
|
+
needs_redaction = any(
|
|
145
|
+
isinstance(f, dict)
|
|
146
|
+
and f.get("category") in _DLP_PLAINTEXT_CATEGORIES
|
|
147
|
+
and "matched_text" in f
|
|
148
|
+
for f in findings
|
|
149
|
+
)
|
|
150
|
+
if not needs_redaction:
|
|
151
|
+
return entry
|
|
152
|
+
safe = dict(entry)
|
|
153
|
+
redacted_findings = []
|
|
154
|
+
for f in findings:
|
|
155
|
+
if (
|
|
156
|
+
isinstance(f, dict)
|
|
157
|
+
and f.get("category") in _DLP_PLAINTEXT_CATEGORIES
|
|
158
|
+
and "matched_text" in f
|
|
159
|
+
):
|
|
160
|
+
rf = dict(f)
|
|
161
|
+
rf["matched_text"] = "[redacted-local]"
|
|
162
|
+
redacted_findings.append(rf)
|
|
163
|
+
else:
|
|
164
|
+
redacted_findings.append(f)
|
|
165
|
+
safe["dlp_findings"] = redacted_findings
|
|
166
|
+
return safe
|
|
167
|
+
|
|
168
|
+
|
|
123
169
|
class Client:
|
|
124
170
|
"""The ControlZero policy client.
|
|
125
171
|
|
|
@@ -302,35 +348,60 @@ class Client:
|
|
|
302
348
|
|
|
303
349
|
self._tamper_state_dir = Path.home() / ".controlzero"
|
|
304
350
|
|
|
305
|
-
# Set up local audit logger
|
|
306
|
-
#
|
|
307
|
-
#
|
|
351
|
+
# Set up the local audit logger in EVERY mode.
|
|
352
|
+
#
|
|
353
|
+
# P0 regression (epic #1247, 2026-06-15): a customer who ran
|
|
354
|
+
# `controlzero install claude-code --api-key cz_live_...` saw audit
|
|
355
|
+
# rows reach the dashboard (remote bearer sink) but their local
|
|
356
|
+
# ~/.controlzero/audit.log stayed frozen -- it was last appended weeks
|
|
357
|
+
# earlier. Root cause: this block used to create the LocalAuditLogger
|
|
358
|
+
# ONLY when `not self._has_api_key`, so hosted mode left ``self._audit``
|
|
359
|
+
# None and ``_audit_decision`` skipped the local write entirely.
|
|
360
|
+
#
|
|
361
|
+
# That silently broke the day-one value prop the claude-code template
|
|
362
|
+
# AND the install command both promise verbatim:
|
|
363
|
+
# "every Claude Code tool call is logged to ~/.controlzero/audit.log"
|
|
364
|
+
# "Audit log: ~/.controlzero/audit.log"
|
|
365
|
+
# The promise is mode-independent, so the local sink must be too. Local
|
|
366
|
+
# audit is now ALWAYS on (allow AND deny, local AND hosted); the remote
|
|
367
|
+
# bearer/enrolled sink is layered ON TOP in hosted mode, not instead of
|
|
368
|
+
# the local file. The local log is also what `cz debug-bundle` and the
|
|
369
|
+
# tamper hash-chain depend on, so a frozen file degraded those too.
|
|
308
370
|
self._audit: Optional[LocalAuditLogger] = None
|
|
309
|
-
|
|
310
|
-
|
|
371
|
+
# In hosted mode the caller (e.g. the hook-check CLI) may not pass a
|
|
372
|
+
# log_path; default it to the canonical global audit log so a hosted
|
|
373
|
+
# install still writes the file the template/install message advertise.
|
|
374
|
+
effective_log_path = log_path
|
|
375
|
+
if self._has_api_key and log_path == "./controlzero.log":
|
|
376
|
+
effective_log_path = str(Path.home() / ".controlzero" / "audit.log")
|
|
377
|
+
try:
|
|
311
378
|
self._audit = LocalAuditLogger(
|
|
312
|
-
log_path=
|
|
379
|
+
log_path=effective_log_path,
|
|
313
380
|
rotation=log_rotation,
|
|
314
381
|
retention=log_retention,
|
|
315
382
|
compression=log_compression,
|
|
316
383
|
log_format=log_format,
|
|
317
384
|
)
|
|
318
|
-
|
|
319
|
-
#
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
385
|
+
except (OSError, PermissionError) as exc:
|
|
386
|
+
# The local audit log must never crash the client when the FAILURE
|
|
387
|
+
# IS ENVIRONMENTAL: an unwritable log path (read-only HOME, sandbox,
|
|
388
|
+
# full disk) falls back to no local sink. In hosted mode the remote
|
|
389
|
+
# sink still carries the trail; in pure-local mode there may be no
|
|
390
|
+
# remote sink, but the alternative -- crashing every guard() call --
|
|
391
|
+
# is worse, and the warning surfaces the cause.
|
|
392
|
+
#
|
|
393
|
+
# #1247 review (codex): this catch is deliberately NARROW. A broad
|
|
394
|
+
# ``except Exception`` here would also swallow a genuine logger bug
|
|
395
|
+
# or bad config and silently set _audit=None, which in pure-local
|
|
396
|
+
# mode would make audit vanish with no signal. Programming errors
|
|
397
|
+
# must propagate so they are caught in tests/CI, not in production.
|
|
398
|
+
self._audit = None
|
|
399
|
+
import logging as _logging
|
|
400
|
+
_logging.getLogger("controlzero.client").warning(
|
|
401
|
+
"controlzero: local audit log path unavailable (%s); "
|
|
402
|
+
"audit will be recorded remotely only",
|
|
403
|
+
exc,
|
|
326
404
|
)
|
|
327
|
-
if user_set_log_opts:
|
|
328
|
-
warnings.warn(
|
|
329
|
-
"controlzero: log_* options are ignored when an API key is set "
|
|
330
|
-
"(audit is managed server-side).",
|
|
331
|
-
UserWarning,
|
|
332
|
-
stacklevel=2,
|
|
333
|
-
)
|
|
334
405
|
|
|
335
406
|
# --- Hosted policy periodic refresh state --------------------------
|
|
336
407
|
#
|
|
@@ -1746,9 +1817,22 @@ class Client:
|
|
|
1746
1817
|
if "host_tool_name" in context:
|
|
1747
1818
|
entry["host_tool_name"] = context["host_tool_name"]
|
|
1748
1819
|
|
|
1749
|
-
# Local file first (when local audit is enabled)
|
|
1820
|
+
# Local file first (when local audit is enabled).
|
|
1821
|
+
#
|
|
1822
|
+
# #1247 review (codex): now that the local audit log is written in
|
|
1823
|
+
# hosted mode too, we must NOT regress the data-exposure posture.
|
|
1824
|
+
# ``dlp_findings[*].matched_text`` is PLAINTEXT for pii/financial
|
|
1825
|
+
# categories (only the secret category is already SHA-256 hashed --
|
|
1826
|
+
# see dlp_scanner). Previously hosted mode kept that argument-derived
|
|
1827
|
+
# sensitive content server-side only; persisting it to the local
|
|
1828
|
+
# plaintext ~/.controlzero/audit.log would leak PII/financial data to
|
|
1829
|
+
# anyone who can read the file. So the LOCAL row carries DLP findings
|
|
1830
|
+
# with the raw plaintext stripped (rule_id / category / location /
|
|
1831
|
+
# count are preserved so the local log still shows THAT a match fired,
|
|
1832
|
+
# just not the matched value). The REMOTE sinks keep full fidelity --
|
|
1833
|
+
# they ship over TLS to server-side storage exactly as before.
|
|
1750
1834
|
if self._audit is not None:
|
|
1751
|
-
self._audit.log(entry)
|
|
1835
|
+
self._audit.log(_redact_local_dlp(entry))
|
|
1752
1836
|
|
|
1753
1837
|
# Remote sink (enrolled machines: signed-request auth).
|
|
1754
1838
|
if self._remote_sink is not None:
|