controlzero 1.11.9__tar.gz → 1.12.0__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.9 → controlzero-1.12.0}/CHANGELOG.md +32 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/PKG-INFO +1 -1
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/__init__.py +1 -1
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/agent_config.py +50 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/debug_bundle.py +66 -13
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/main.py +139 -20
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/hosted_policy.py +133 -14
- {controlzero-1.11.9 → controlzero-1.12.0}/pyproject.toml +1 -1
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_cli_debug_bundle.py +2 -1
- controlzero-1.12.0/tests/test_per_client_isolation_1011.py +251 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_pr4_status.py +21 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/.gitignore +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/Dockerfile.test +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/LICENSE +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/README.md +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/__main__.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/_internal/__init__.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/_internal/_messages.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/_internal/action_aliases.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/_internal/action_validator.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/_internal/bundle.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/_internal/credential_hook.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/_internal/credential_scanner.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/_internal/credentials_data/__init__.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/_internal/credentials_data/built_in.yaml +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/_internal/dlp_scanner.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/_internal/enforcer.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/_internal/hook_extractors.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/_internal/io_capture_producer.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/_internal/source_labels.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/_internal/tool_extractors.json +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/_internal/types.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/audit_local.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/audit_remote.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/canonical.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/__init__.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/__main__.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/_secrets.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/console.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/doctor.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/exec_cmd.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/hook_entry.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/hosts/__init__.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/hosts/antigravity.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/hosts/base.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/hosts/claude_code.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/hosts/codex_cli.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/hosts/cursor.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/hosts/cursor_cli.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/hosts/gemini_cli.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/hosts/kiro.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/hosts/unknown.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/kiro_adapter.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/migrate.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/output.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/spool_cmd.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/telemetry_consent.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/antigravity/hooks.json +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/antigravity.yaml +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/autogen.yaml +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/claude-code.yaml +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/codex-cli.yaml +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/cost-cap.yaml +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/crewai.yaml +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/cursor.yaml +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/gemini-cli.yaml +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/generic.yaml +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/kiro/ide-file-save.kiro.hook +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/kiro/ide-pre-tool-use.kiro.hook +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/kiro/ide-prompt-submit.kiro.hook +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/kiro/kiro.yaml +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/langchain.yaml +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/mcp.yaml +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/templates/rag.yaml +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/cli/update_cmd.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/client.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/device.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/enrollment.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/error_codes.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/error_codes.yaml +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/errors.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/hitl/__init__.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/hitl/grant_protocol.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/hitl/mock.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/hitl/pending_approval.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/hitl/secret_leak_guard.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/hitl/status.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/hook_integrity.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/hooks/__init__.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/hooks/tool_output_handler.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/__init__.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/anthropic.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/autogen.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/braintrust.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/crewai/__init__.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/crewai/agent.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/crewai/crew.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/crewai/task.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/crewai/tool.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/google.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/google_adk/__init__.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/google_adk/agent.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/google_adk/tool.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/langchain/__init__.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/langchain/agent.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/langchain/callbacks.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/langchain/chain.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/langchain/graph.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/langchain/modern.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/langchain/tool.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/langfuse.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/litellm.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/openai.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/pydantic_ai.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/integrations/vercel_ai.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/layout_migration.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/policy_loader.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/secrets/__init__.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/secrets/reference.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/secrets/resolver.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/secrets/secretstr.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/spool/__init__.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/spool/_compress.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/spool/_constants.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/spool/_crc32c.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/spool/_crypto.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/spool/_frame.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/spool/_keyring.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/spool/_metrics.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/spool/_spool.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/spool/_state.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/spool/_uploader.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/spool/cz-audit-v1.dict +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/tamper.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/controlzero/tracecontext.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/examples/hello_world.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/_fixtures/antigravity-envelope-expected.json +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/_fixtures/jcs_args_hash_vectors.json +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/conftest.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/integrations/__init__.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/integrations/test_google.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/parity/action_aliases.json +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/spool/__init__.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/spool/conftest.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/spool/test_spool_cli.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/spool/test_spool_concurrency.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/spool/test_spool_conformance.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/spool/test_spool_core.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/spool/test_spool_crash.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/spool/test_spool_diskfull.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/spool/test_spool_durable_default_tamper.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/spool/test_spool_keychain_dek.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/spool/test_spool_sink_wiring.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/spool/test_spool_transcript_localack.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/spool/test_spool_uploader.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_action_aliases.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_action_canonicalization.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_action_validator_t86.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_adapter_interface.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_agent_name_env.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_agy_docs_no_exit2_1373.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_antigravity_adapter.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_antigravity_envelope_parity.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_antigravity_ga_blockers_1248.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_antigravity_hook_check.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_antigravity_install.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_antigravity_posttooluse_observe_58.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_antigravity_tool_vocab_1303.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_api_key_mask.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_audit_project_id_attribution_pr2.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_audit_remote.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_audit_remote_io_capture.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_audit_remote_sdk_version.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_audit_sink_isolation.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_bundle_cache_freshness_1303.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_bundle_parser.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_bundle_translate.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_canonical_phase1a.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_capability_matrix_doc.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_cli_carve_out.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_cli_extractor_integration.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_cli_hook.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_cli_hook_entry.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_cli_hosted_refresh.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_cli_init.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_cli_init_templates.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_cli_tail.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_cli_test.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_cli_validate.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_coding_agent_hooks.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_conditions.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_config_format_parity_1303.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_conformance.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_console.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_credential_hook.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_cursor_adapter.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_default_action.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_device.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_dlp_mask.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_dlp_scanner.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_doctor.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_engine_version_consistency.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_enrollment.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_env_dump_438.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_epic_1247_bryan_acceptance.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_error_codes.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_errors_e_codes.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_fail_closed_eval.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_failopen_1303.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_glob_matching.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hitl_5d_email_install.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hitl_6a_cli_flag.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hitl_6a_exceptions.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hitl_6a_get_secret_hitl.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hitl_6a_mock_backend.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hitl_6a_pending_approval.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hitl_6a_request_approval.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hitl_6a_secret_leak_guard.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hitl_6a_wait.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hitl_conformance.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hitl_phase2b_protocol.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hitl_reason_codes.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hitl_validator_keys.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hook_extractors.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hook_integrity.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hook_mask_failclosed.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hosted_local_audit_1247.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hosted_local_shadow_warn_1265.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hosted_policy_e2e.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hosts_adapter.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hybrid_mode_strict.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_hybrid_mode_warn.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_install_hook_command.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_install_hooks.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_io_capture_producer.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_kiro_adapter.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_kiro_cli_e2e.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_kiro_cli_hook_pin_1265.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_kiro_hook_templates.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_kiro_install.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_layout_migration_t101.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_layout_parity_t102.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_local_mode_dict.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_local_mode_file_json.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_local_mode_file_yaml.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_localized_reason.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_log_fallback_stderr.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_log_options_ignored_hosted.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_log_rotation.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_migrate.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_min_sdk_version_gate.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_multi_client_per_project_175.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_no_policy_no_key.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_observe_mode_1247.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_package_rename_shim.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_part3_active_policy_count_1303.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_policy_engine_version_phase1b.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_policy_freshness.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_policy_load_failclosed_f3.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_policy_settings.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_policy_source_audit.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_pr4_install_messaging.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_pr4_output.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_probe_audit_isolation.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_quarantine.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_reason_code.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_refresh.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_secrets.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_secrets_reference_resolver.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_source_labels.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_sql_semantic_class.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_synthetic_policy_id_t79.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_t103_precedence.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_t104_cache_gc.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_t108_local_override_audit.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_t96_single_audit_log.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_t99_install_prefetch_bundle.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_tamper.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_tamper_behavior.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_tamper_hook.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_telemetry_consent.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_tracecontext.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_unsafe_int_boundary.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_update_cmd.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tests/test_upgrade_nudge.py +0 -0
- {controlzero-1.11.9 → controlzero-1.12.0}/tools/cz-kiro-adapter +0 -0
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.12.0 -- 2026-06-25 (per-agent key authority + per-agent bundle cache + whoami)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`controlzero whoami`.** Prints which API key the SDK will actually use for a
|
|
8
|
+
surface (per-agent / env / global), its prefix only (never the full key), and
|
|
9
|
+
the project + org it resolves to. Diagnoses the most common "my audit logs are
|
|
10
|
+
missing" cause: a `CONTROLZERO_API_KEY` exported in the shell that maps to a
|
|
11
|
+
different project than the integration you installed, so events land in the
|
|
12
|
+
wrong tenant and the dashboard shows nothing.
|
|
13
|
+
- **Per-agent bundle cache.** Each governed surface now caches its policy bundle
|
|
14
|
+
under `~/.controlzero/integrations/<agent>/cache/` (next to the per-agent
|
|
15
|
+
`config.yaml`), so two surfaces on one machine each bound to their own project
|
|
16
|
+
no longer share a bundle. A read fallback keeps existing installs working on
|
|
17
|
+
upgrade: an empty per-agent cache transparently reads the legacy shared cache
|
|
18
|
+
until the first fetch re-caches it (no fail-closed on upgrade).
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **Per-agent key is now AUTHORITATIVE in the enforcement hook.** Hook-check
|
|
23
|
+
precedence becomes per-agent (for the active surface) > env > global. A key
|
|
24
|
+
installed via `install <agent> --api-key` / `kiro init --api-key` now wins over
|
|
25
|
+
a stale ambient `CONTROLZERO_API_KEY`, with a loud one-line warning when they
|
|
26
|
+
differ. This closes the silent wrong-tenant bug where a hook spooled + uploaded
|
|
27
|
+
+ ingested correctly but attributed the events to whatever key was exported in
|
|
28
|
+
the shell, so the dashboard for the installed project showed "no audit entries".
|
|
29
|
+
Non-hook resolution (`status`, install-time display) is unchanged
|
|
30
|
+
(env > per-agent > global).
|
|
31
|
+
- **Cache bundle extension is now `.czpolicy`** (was `.bin`), matching the UI
|
|
32
|
+
policy export (both are the same signed bundle format). Legacy `.bin` caches
|
|
33
|
+
are still read and garbage-collected, so nothing breaks on upgrade.
|
|
34
|
+
|
|
3
35
|
## 1.11.9 -- 2026-06-25 (policy-posture remediation: honest hosted/local CLI, per-agent keys, key-validity diagnostics)
|
|
4
36
|
|
|
5
37
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: controlzero
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.12.0
|
|
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
|
|
@@ -169,3 +169,53 @@ def resolve_api_key(
|
|
|
169
169
|
return global_key, "global"
|
|
170
170
|
|
|
171
171
|
return None, "none"
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def per_agent_api_key(
|
|
175
|
+
state_dir: Path, canonical_source: Optional[str]
|
|
176
|
+
) -> Optional[str]:
|
|
177
|
+
"""The api_key installed for THIS agent surface only -- no global fallback.
|
|
178
|
+
|
|
179
|
+
Unlike :func:`resolve_api_key` (env > per-agent > global), this returns
|
|
180
|
+
ONLY the per-agent ``integrations/<canonical_source>/config.yaml`` key, or
|
|
181
|
+
None. hook-check uses it to make a surface's installed key AUTHORITATIVE
|
|
182
|
+
over a stale ambient ``CONTROLZERO_API_KEY`` (#1011 follow-up): a surface
|
|
183
|
+
wired by ``install <agent> --api-key`` / ``kiro init --api-key`` must
|
|
184
|
+
attribute its audit to the project it was installed for, never to whatever
|
|
185
|
+
key happens to be exported in the shell. Never raises.
|
|
186
|
+
"""
|
|
187
|
+
if not canonical_source:
|
|
188
|
+
return None
|
|
189
|
+
return _read_api_key_from_yaml(
|
|
190
|
+
per_agent_config_path(state_dir, canonical_source)
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def resolve_hook_api_key(
|
|
195
|
+
state_dir: Path,
|
|
196
|
+
canonical_source: Optional[str],
|
|
197
|
+
*,
|
|
198
|
+
env: Optional[dict] = None,
|
|
199
|
+
) -> tuple[Optional[str], str, bool]:
|
|
200
|
+
"""Resolve the hook-check api_key with PER-AGENT-AUTHORITATIVE precedence.
|
|
201
|
+
|
|
202
|
+
Unlike :func:`resolve_api_key` (env > per-agent > global), the enforcement
|
|
203
|
+
hook makes a surface's installed key win over a stale ambient env var
|
|
204
|
+
(#1011 follow-up): per-agent > env > global. This is what fixes the silent
|
|
205
|
+
wrong-tenant bug. Returns ``(api_key_or_None, source_label, overrode_env)``
|
|
206
|
+
where ``source_label`` is ``"per_agent"`` / ``"env"`` / ``"global"`` /
|
|
207
|
+
``"none"`` and ``overrode_env`` is True when a per-agent key replaced a
|
|
208
|
+
DIFFERENT ambient ``CONTROLZERO_API_KEY`` (the case the hook warns about).
|
|
209
|
+
Never raises.
|
|
210
|
+
"""
|
|
211
|
+
env = env if env is not None else os.environ
|
|
212
|
+
ambient = env.get("CONTROLZERO_API_KEY")
|
|
213
|
+
per_agent = per_agent_api_key(state_dir, canonical_source)
|
|
214
|
+
if per_agent:
|
|
215
|
+
return per_agent, "per_agent", bool(ambient and ambient != per_agent)
|
|
216
|
+
if ambient:
|
|
217
|
+
return ambient, "env", False
|
|
218
|
+
global_key = _read_api_key_from_yaml(state_dir / "config.yaml")
|
|
219
|
+
if global_key:
|
|
220
|
+
return global_key, "global", False
|
|
221
|
+
return None, "none", False
|
|
@@ -10,7 +10,8 @@ What it does
|
|
|
10
10
|
|
|
11
11
|
1. Reads ``~/.controlzero/cache/bootstrap-<prefix>.json`` to get the
|
|
12
12
|
project encryption key + signing public key.
|
|
13
|
-
2. Reads
|
|
13
|
+
2. Reads ``bundle-<prefix>.czpolicy`` (legacy ``.bin``) for the raw blob,
|
|
14
|
+
from the shared cache or a per-client ``integrations/<slug>/cache/`` dir.
|
|
14
15
|
3. Calls :func:`controlzero._internal.bundle.parse_bundle` to verify
|
|
15
16
|
the signature, decrypt the AES-GCM payload, and decode the JSON.
|
|
16
17
|
4. Prints a human-readable summary (bundle id / created_at / expires_at,
|
|
@@ -133,30 +134,79 @@ def _api_key_prefix(api_key: str) -> str:
|
|
|
133
134
|
return api_key[:_KEY_SCOPE_LEN].replace("/", "_").replace(".", "_")
|
|
134
135
|
|
|
135
136
|
|
|
137
|
+
def _all_cache_dirs() -> list[Path]:
|
|
138
|
+
"""Per-client integration cache dirs FIRST, then the shared cache dir.
|
|
139
|
+
|
|
140
|
+
#1011 follow-up: a surface's bundle now lives under
|
|
141
|
+
~/.controlzero/integrations/<slug>/cache/. Integration dirs are listed
|
|
142
|
+
before the legacy shared ~/.controlzero/cache/ so the debug tool prefers
|
|
143
|
+
the bundle the hook actually enforces over a stale shared-cache entry.
|
|
144
|
+
"""
|
|
145
|
+
dirs: list[Path] = []
|
|
146
|
+
integ = Path.home() / ".controlzero" / "integrations"
|
|
147
|
+
if integ.exists():
|
|
148
|
+
for sub in sorted(integ.iterdir()):
|
|
149
|
+
c = sub / "cache"
|
|
150
|
+
if c.is_dir():
|
|
151
|
+
dirs.append(c)
|
|
152
|
+
dirs.append(CACHE_DIR)
|
|
153
|
+
return dirs
|
|
154
|
+
|
|
155
|
+
|
|
136
156
|
def _resolve_cache_for_prefix(prefix: str) -> CacheLocation:
|
|
157
|
+
# The bundle for a prefix may live in the shared cache or under a per-client
|
|
158
|
+
# integration cache. Prefer a dir that actually holds the BUNDLE (the policy
|
|
159
|
+
# the hook enforces), integration caches before the shared cache, and the
|
|
160
|
+
# current .czpolicy over a legacy .bin. A migrated install can have a stale
|
|
161
|
+
# bootstrap/old bundle in the shared cache plus the live bundle under
|
|
162
|
+
# integrations/<slug>/cache; preferring the bundle avoids inspecting the
|
|
163
|
+
# stale shared copy.
|
|
164
|
+
dirs = _all_cache_dirs()
|
|
165
|
+
for d in dirs:
|
|
166
|
+
czp = d / f"bundle-{prefix}.czpolicy"
|
|
167
|
+
legacy = d / f"bundle-{prefix}.bin"
|
|
168
|
+
if czp.exists() or legacy.exists():
|
|
169
|
+
bundle_path = czp if czp.exists() else legacy
|
|
170
|
+
return CacheLocation(
|
|
171
|
+
prefix=prefix,
|
|
172
|
+
bootstrap_path=d / f"bootstrap-{prefix}.json",
|
|
173
|
+
bundle_path=bundle_path,
|
|
174
|
+
)
|
|
175
|
+
# No bundle anywhere -- fall back to a dir that at least has the bootstrap
|
|
176
|
+
# (so the not-found message points at a real bootstrap), else the shared
|
|
177
|
+
# cache with the current extension.
|
|
178
|
+
for d in dirs:
|
|
179
|
+
boot = d / f"bootstrap-{prefix}.json"
|
|
180
|
+
if boot.exists():
|
|
181
|
+
return CacheLocation(
|
|
182
|
+
prefix=prefix,
|
|
183
|
+
bootstrap_path=boot,
|
|
184
|
+
bundle_path=d / f"bundle-{prefix}.czpolicy",
|
|
185
|
+
)
|
|
137
186
|
return CacheLocation(
|
|
138
187
|
prefix=prefix,
|
|
139
188
|
bootstrap_path=CACHE_DIR / f"bootstrap-{prefix}.json",
|
|
140
|
-
bundle_path=CACHE_DIR / f"bundle-{prefix}.
|
|
189
|
+
bundle_path=CACHE_DIR / f"bundle-{prefix}.czpolicy",
|
|
141
190
|
)
|
|
142
191
|
|
|
143
192
|
|
|
144
193
|
def _autodetect_cache() -> Optional[CacheLocation]:
|
|
145
|
-
"""Pick a single bootstrap+bundle pair
|
|
194
|
+
"""Pick a single bootstrap+bundle pair across all cache dirs.
|
|
146
195
|
|
|
147
196
|
Used when the operator did not specify ``--api-key-prefix`` and we
|
|
148
197
|
cannot find an API key in env / config. Returns the unique pair
|
|
149
|
-
when there is exactly one
|
|
150
|
-
several (caller surfaces a
|
|
198
|
+
when there is exactly one (across the shared + per-client caches);
|
|
199
|
+
returns None when there are zero or several (caller surfaces a
|
|
200
|
+
helpful error in either case).
|
|
151
201
|
"""
|
|
152
|
-
|
|
202
|
+
prefixes: list[str] = []
|
|
203
|
+
for d in _all_cache_dirs():
|
|
204
|
+
for b in sorted(d.glob("bootstrap-*.json")):
|
|
205
|
+
prefixes.append(b.name[len("bootstrap-") : -len(".json")])
|
|
206
|
+
prefixes = sorted(set(prefixes))
|
|
207
|
+
if len(prefixes) != 1:
|
|
153
208
|
return None
|
|
154
|
-
|
|
155
|
-
if len(bootstraps) != 1:
|
|
156
|
-
return None
|
|
157
|
-
name = bootstraps[0].name # bootstrap-<prefix>.json
|
|
158
|
-
prefix = name[len("bootstrap-") : -len(".json")]
|
|
159
|
-
return _resolve_cache_for_prefix(prefix)
|
|
209
|
+
return _resolve_cache_for_prefix(prefixes[0])
|
|
160
210
|
|
|
161
211
|
|
|
162
212
|
# --- Bundle load ----------------------------------------------------------
|
|
@@ -478,7 +528,10 @@ def debug_bundle_cmd(api_key_prefix: Optional[str], simulate: Optional[str]):
|
|
|
478
528
|
" Set CONTROLZERO_API_KEY=cz_live_... or pass --api-key-prefix.",
|
|
479
529
|
err=True,
|
|
480
530
|
)
|
|
481
|
-
click.echo(
|
|
531
|
+
click.echo(
|
|
532
|
+
f" Looked under: {', '.join(str(d) for d in _all_cache_dirs())}",
|
|
533
|
+
err=True,
|
|
534
|
+
)
|
|
482
535
|
sys.exit(1)
|
|
483
536
|
|
|
484
537
|
# 2. Load bootstrap (keys + safe metadata).
|
|
@@ -541,6 +541,94 @@ def heartbeat_cmd():
|
|
|
541
541
|
click.echo(" tamper : none (hooks intact)")
|
|
542
542
|
|
|
543
543
|
|
|
544
|
+
@cli.command("whoami")
|
|
545
|
+
@click.option(
|
|
546
|
+
"--client",
|
|
547
|
+
default=None,
|
|
548
|
+
help="Integration surface to inspect (e.g. kiro_cli, kiro_ide). "
|
|
549
|
+
"Defaults to CONTROLZERO_CLIENT.",
|
|
550
|
+
)
|
|
551
|
+
def whoami_cmd(client: Optional[str]) -> None:
|
|
552
|
+
"""Show which API key the SDK will use and the project/org it maps to.
|
|
553
|
+
|
|
554
|
+
Diagnoses the most common "my audit logs are missing" cause: a
|
|
555
|
+
CONTROLZERO_API_KEY exported in the shell that maps to a DIFFERENT project
|
|
556
|
+
than the integration you installed, so events land in the wrong tenant and
|
|
557
|
+
the dashboard shows nothing. The key is never printed in full -- only its
|
|
558
|
+
prefix, its source, and the project/org it resolves to.
|
|
559
|
+
"""
|
|
560
|
+
from controlzero._internal.source_labels import canonicalize_source
|
|
561
|
+
|
|
562
|
+
client = client or os.environ.get("CONTROLZERO_CLIENT")
|
|
563
|
+
canon = canonicalize_source(client) if client else None
|
|
564
|
+
canon = canon if canon and canon != "unknown" else None
|
|
565
|
+
|
|
566
|
+
# Use the SAME precedence the hook uses (per-agent authoritative > env >
|
|
567
|
+
# global) so whoami reports exactly what enforcement will do.
|
|
568
|
+
env_key = os.environ.get("CONTROLZERO_API_KEY")
|
|
569
|
+
effective, label, _overrode = agent_config.resolve_hook_api_key(
|
|
570
|
+
GLOBAL_POLICY_DIR, canon
|
|
571
|
+
)
|
|
572
|
+
if not effective:
|
|
573
|
+
click.echo(
|
|
574
|
+
"No Control Zero API key configured (no integration config, no "
|
|
575
|
+
"CONTROLZERO_API_KEY, no ~/.controlzero/config.yaml). Local-file "
|
|
576
|
+
"policy mode."
|
|
577
|
+
)
|
|
578
|
+
click.echo(
|
|
579
|
+
"Run `controlzero install <agent> --api-key cz_...` or "
|
|
580
|
+
"`controlzero kiro init --api-key cz_...` to enable hosted "
|
|
581
|
+
"policy + audit sync."
|
|
582
|
+
)
|
|
583
|
+
return
|
|
584
|
+
source = {
|
|
585
|
+
"per_agent": f"integration config (integrations/{canon}/config.yaml)",
|
|
586
|
+
"env": "CONTROLZERO_API_KEY environment variable",
|
|
587
|
+
"global": "~/.controlzero/config.yaml",
|
|
588
|
+
}.get(label, label)
|
|
589
|
+
|
|
590
|
+
click.echo(f"Client surface: {canon or '(none)'}")
|
|
591
|
+
click.echo(f"Effective API key: {effective[:12]}... (source: {source})")
|
|
592
|
+
if env_key and effective != env_key:
|
|
593
|
+
click.echo(
|
|
594
|
+
"WARNING: a DIFFERENT CONTROLZERO_API_KEY is set in your shell. The "
|
|
595
|
+
"key above takes precedence for this surface; the shell key would "
|
|
596
|
+
"attribute audit to its own project. Unset it to avoid confusion."
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
# Resolve project/org via the per-surface cache, then a live call.
|
|
600
|
+
_prev = os.environ.get("CONTROLZERO_CLIENT")
|
|
601
|
+
if canon:
|
|
602
|
+
os.environ["CONTROLZERO_CLIENT"] = canon
|
|
603
|
+
try:
|
|
604
|
+
from controlzero.hosted_policy import (
|
|
605
|
+
get_or_fetch_bootstrap,
|
|
606
|
+
load_cached_bootstrap,
|
|
607
|
+
)
|
|
608
|
+
keys = load_cached_bootstrap(effective)
|
|
609
|
+
origin = "cache"
|
|
610
|
+
if keys is None:
|
|
611
|
+
keys = get_or_fetch_bootstrap(effective)
|
|
612
|
+
origin = "backend"
|
|
613
|
+
click.echo(f"Project: {keys.project_id}")
|
|
614
|
+
click.echo(f"Org: {keys.org_id or '(unknown)'} (from {origin})")
|
|
615
|
+
click.echo(
|
|
616
|
+
"Audit events from this surface are attributed to that project."
|
|
617
|
+
)
|
|
618
|
+
except Exception as exc: # noqa: BLE001
|
|
619
|
+
click.echo(f"Could not resolve the project/org for this key: {exc}")
|
|
620
|
+
click.echo(
|
|
621
|
+
"Check the key validity and network reachability to "
|
|
622
|
+
"https://api.controlzero.ai."
|
|
623
|
+
)
|
|
624
|
+
finally:
|
|
625
|
+
if canon:
|
|
626
|
+
if _prev is None:
|
|
627
|
+
os.environ.pop("CONTROLZERO_CLIENT", None)
|
|
628
|
+
else:
|
|
629
|
+
os.environ["CONTROLZERO_CLIENT"] = _prev
|
|
630
|
+
|
|
631
|
+
|
|
544
632
|
@cli.command("policy-pull")
|
|
545
633
|
def policy_pull_cmd():
|
|
546
634
|
"""Pull the latest policy bundle from the backend.
|
|
@@ -930,10 +1018,27 @@ def status_cmd(check_key: bool):
|
|
|
930
1018
|
hosted = bool(resolved_key)
|
|
931
1019
|
unenrolled_first_run = not (api_key_env or api_key_config or enrolled)
|
|
932
1020
|
|
|
1021
|
+
# If the env/global default is local but individual agents are bound to
|
|
1022
|
+
# their own hosted keys, the machine is in a MIXED posture. Report that
|
|
1023
|
+
# instead of a flat LOCAL, which would understate that some agents are
|
|
1024
|
+
# actually enforcing a hosted bundle (the bindings table below names them).
|
|
1025
|
+
any_per_agent_key = any(
|
|
1026
|
+
agent_config._read_api_key_from_yaml(
|
|
1027
|
+
agent_config.per_agent_config_path(GLOBAL_POLICY_DIR, slug)
|
|
1028
|
+
)
|
|
1029
|
+
for slug in _KNOWN_AGENT_SLUGS
|
|
1030
|
+
)
|
|
1031
|
+
if hosted:
|
|
1032
|
+
mode = "HOSTED"
|
|
1033
|
+
elif any_per_agent_key:
|
|
1034
|
+
mode = "MIXED (local default; some agents hosted -- see bindings)"
|
|
1035
|
+
else:
|
|
1036
|
+
mode = "LOCAL"
|
|
1037
|
+
|
|
933
1038
|
cli_output.emit(Severity.INFO, "Control Zero status")
|
|
934
1039
|
facts = {
|
|
935
1040
|
"state dir": str(GLOBAL_POLICY_DIR),
|
|
936
|
-
"mode":
|
|
1041
|
+
"mode": mode,
|
|
937
1042
|
"config source": resolved_source,
|
|
938
1043
|
"key (masked, env/global default)": _status_mask_key(resolved_key),
|
|
939
1044
|
"enrolled": enrolled,
|
|
@@ -1567,18 +1672,35 @@ def hook_check(policy: Optional[str], strict_flag: Optional[bool]):
|
|
|
1567
1672
|
# key in config.yaml but not in env fell through to the local-file
|
|
1568
1673
|
# path on every hook-check.
|
|
1569
1674
|
#
|
|
1570
|
-
#
|
|
1571
|
-
# integrations/<canonical_source>/config.yaml
|
|
1572
|
-
#
|
|
1573
|
-
#
|
|
1574
|
-
#
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1675
|
+
# Precedence (#1011 follow-up, per-client isolation): the key installed
|
|
1676
|
+
# FOR THIS agent surface (integrations/<canonical_source>/config.yaml) is
|
|
1677
|
+
# AUTHORITATIVE -- it wins over a stale ambient CONTROLZERO_API_KEY so the
|
|
1678
|
+
# surface's audit lands in the project it was installed for, not whatever
|
|
1679
|
+
# key happens to be exported in the shell. That stale-env-hijack was the
|
|
1680
|
+
# silent wrong-tenant bug: a Kiro hook spooled + uploaded + ingested fine,
|
|
1681
|
+
# but the events were attributed to the env key's org, so the dashboard for
|
|
1682
|
+
# the installed project showed "no audit entries". Only when there is NO
|
|
1683
|
+
# per-agent key do we keep the env > global precedence (the PR4-F default).
|
|
1684
|
+
# The slug helper is SHARED with the install-write path (agent_config), so
|
|
1685
|
+
# a key written by `install <agent> --api-key` is found here under the same
|
|
1686
|
+
# adapter. A malformed per-agent file degrades to env/global (never crashes).
|
|
1687
|
+
_canon_source = getattr(_adapter, "canonical_source", None)
|
|
1688
|
+
_eff_key, _eff_src, _overrode_env = agent_config.resolve_hook_api_key(
|
|
1689
|
+
GLOBAL_POLICY_DIR, _canon_source
|
|
1690
|
+
)
|
|
1691
|
+
if _overrode_env:
|
|
1692
|
+
# Make the override LOUD (mirrors the hosted/local shadow warning): a
|
|
1693
|
+
# stale shell key silently stealing a surface's audit is exactly what
|
|
1694
|
+
# we are closing. Never print the key itself.
|
|
1695
|
+
click.echo(
|
|
1696
|
+
"[Control Zero] Using the API key installed for this integration; "
|
|
1697
|
+
"ignoring a different CONTROLZERO_API_KEY found in the environment "
|
|
1698
|
+
"(audit is attributed to the installed key's project). Run "
|
|
1699
|
+
"`controlzero whoami` to confirm.",
|
|
1700
|
+
err=True,
|
|
1579
1701
|
)
|
|
1580
|
-
|
|
1581
|
-
|
|
1702
|
+
if _eff_key:
|
|
1703
|
+
os.environ["CONTROLZERO_API_KEY"] = _eff_key
|
|
1582
1704
|
|
|
1583
1705
|
# T103 review (B1, B2): the CLI hook-check is a non-interactive
|
|
1584
1706
|
# subprocess that Claude Code / Gemini CLI / Codex CLI spawn fresh
|
|
@@ -2836,17 +2958,14 @@ def _warn_no_deny_rules(agent_name: str, template_path: Path) -> None:
|
|
|
2836
2958
|
|
|
2837
2959
|
Also shows what deny rules the template WOULD have added.
|
|
2838
2960
|
"""
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
bold=True,
|
|
2843
|
-
err=True,
|
|
2961
|
+
cli_output.emit(
|
|
2962
|
+
Severity.WARNING,
|
|
2963
|
+
f"Your existing policy at {GLOBAL_POLICY_PATH} has NO deny rules.",
|
|
2844
2964
|
)
|
|
2845
|
-
|
|
2965
|
+
cli_output.emit(
|
|
2966
|
+
Severity.WARNING,
|
|
2846
2967
|
f"The {agent_name} hook will ALLOW ALL tool calls. "
|
|
2847
2968
|
"This may not be what you want.",
|
|
2848
|
-
fg="yellow",
|
|
2849
|
-
err=True,
|
|
2850
2969
|
)
|
|
2851
2970
|
click.echo("", err=True)
|
|
2852
2971
|
click.echo(
|
|
@@ -138,13 +138,71 @@ def get_api_url() -> str:
|
|
|
138
138
|
return os.environ.get("CONTROLZERO_API_URL", DEFAULT_API_URL).rstrip("/")
|
|
139
139
|
|
|
140
140
|
|
|
141
|
-
def
|
|
142
|
-
"""
|
|
143
|
-
|
|
141
|
+
def _integration_slug(client: Optional[str] = None) -> Optional[str]:
|
|
142
|
+
"""Canonical integration slug for per-client cache isolation, or None.
|
|
143
|
+
|
|
144
|
+
Per-client isolation (#1011 follow-up): when a hook-check runs for a
|
|
145
|
+
specific client surface, ``cli.main`` pins ``CONTROLZERO_CLIENT`` to the
|
|
146
|
+
adapter's canonical source BEFORE the policy load, so each surface's cache
|
|
147
|
+
lives under ``~/.controlzero/integrations/<slug>/cache/`` instead of one
|
|
148
|
+
shared directory. Two surfaces on one machine (each installed with its own
|
|
149
|
+
project's key) then never share a bundle cache. Returns ``None`` for a
|
|
150
|
+
library caller with no client -- the legacy shared ``~/.controlzero/cache/``
|
|
151
|
+
layout.
|
|
152
|
+
"""
|
|
153
|
+
raw = client if client is not None else os.environ.get("CONTROLZERO_CLIENT")
|
|
154
|
+
if not raw:
|
|
155
|
+
return None
|
|
156
|
+
# Lazy import: source_labels is pure data, but importing it lazily keeps
|
|
157
|
+
# the hosted_policy import graph flat (same pattern as __version__ below).
|
|
158
|
+
from controlzero._internal.source_labels import canonicalize_source
|
|
159
|
+
slug = canonicalize_source(raw)
|
|
160
|
+
return slug if slug != "unknown" else None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _cache_dir(client: Optional[str] = None) -> Path:
|
|
164
|
+
"""Resolve the cache directory, creating it if missing.
|
|
165
|
+
|
|
166
|
+
When a client integration is active (see :func:`_integration_slug`) the
|
|
167
|
+
cache is namespaced under ``~/.controlzero/integrations/<slug>/cache/`` so
|
|
168
|
+
each governed surface keeps its own bundle; otherwise the shared
|
|
169
|
+
``~/.controlzero/cache/`` layout is used.
|
|
170
|
+
"""
|
|
171
|
+
slug = _integration_slug(client)
|
|
172
|
+
if slug:
|
|
173
|
+
p = Path.home() / ".controlzero" / "integrations" / slug / "cache"
|
|
174
|
+
else:
|
|
175
|
+
p = Path.home() / ".controlzero" / "cache"
|
|
144
176
|
p.mkdir(parents=True, exist_ok=True)
|
|
145
177
|
return p
|
|
146
178
|
|
|
147
179
|
|
|
180
|
+
def _cache_read_dirs(client: Optional[str] = None) -> list[Path]:
|
|
181
|
+
"""Cache dirs to READ from, in priority order.
|
|
182
|
+
|
|
183
|
+
Writes always go to the active (per-client when set) dir via
|
|
184
|
+
:func:`_cache_dir`. Reads additionally fall back to the legacy shared
|
|
185
|
+
~/.controlzero/cache/ so an EXISTING install keeps serving its
|
|
186
|
+
last-known-good bundle on first run after upgrading to per-client caches
|
|
187
|
+
(#1011 follow-up) -- it would otherwise see an empty per-client cache and,
|
|
188
|
+
if the backend were briefly unreachable, fail closed. The bundle is still
|
|
189
|
+
signature-verified on parse, so the fallback never weakens enforcement.
|
|
190
|
+
|
|
191
|
+
The legacy dir is derived from the active dir's <root>/integrations/<slug>/
|
|
192
|
+
cache shape, NOT from Path.home() independently, so a test that points
|
|
193
|
+
_cache_dir at an arbitrary throwaway dir does not leak onto the real home.
|
|
194
|
+
"""
|
|
195
|
+
# Call _cache_dir() with no positional arg on the common (env-driven) path
|
|
196
|
+
# so a test that monkeypatches _cache_dir with a zero-arg stub still works.
|
|
197
|
+
primary = _cache_dir() if client is None else _cache_dir(client)
|
|
198
|
+
parts = primary.parts
|
|
199
|
+
if len(parts) >= 4 and parts[-1] == "cache" and parts[-3] == "integrations":
|
|
200
|
+
legacy = primary.parent.parent.parent / "cache"
|
|
201
|
+
if legacy != primary:
|
|
202
|
+
return [primary, legacy]
|
|
203
|
+
return [primary]
|
|
204
|
+
|
|
205
|
+
|
|
148
206
|
def _key_scope(api_key: str) -> str:
|
|
149
207
|
"""Return a filename-safe scope identifier for the given API key."""
|
|
150
208
|
if not api_key:
|
|
@@ -171,9 +229,29 @@ def _bootstrap_cache_path(api_key: str) -> Path:
|
|
|
171
229
|
|
|
172
230
|
|
|
173
231
|
def load_cached_bootstrap(api_key: str) -> Optional[BootstrapKeys]:
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
232
|
+
# Read from the active (per-client) dir first, then the legacy shared dir
|
|
233
|
+
# as a migration fallback (#1011 follow-up). When served from the fallback,
|
|
234
|
+
# copy into the active dir so the per-agent cache stops depending on the
|
|
235
|
+
# shared one (see load_cached_bundle for the rationale).
|
|
236
|
+
scope = _key_scope(api_key)
|
|
237
|
+
read_dirs = _cache_read_dirs()
|
|
238
|
+
primary = read_dirs[0]
|
|
239
|
+
for d in read_dirs:
|
|
240
|
+
path = d / f"bootstrap-{scope}.json"
|
|
241
|
+
if not path.exists():
|
|
242
|
+
continue
|
|
243
|
+
keys = _read_bootstrap_file(path)
|
|
244
|
+
if keys is not None:
|
|
245
|
+
if d != primary:
|
|
246
|
+
try:
|
|
247
|
+
save_cached_bootstrap(api_key, keys)
|
|
248
|
+
except OSError:
|
|
249
|
+
pass
|
|
250
|
+
return keys
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _read_bootstrap_file(path: Path) -> Optional[BootstrapKeys]:
|
|
177
255
|
try:
|
|
178
256
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
179
257
|
return BootstrapKeys(
|
|
@@ -252,11 +330,14 @@ def _extract_scope_from_cache_filename(name: str) -> Optional[str]:
|
|
|
252
330
|
|
|
253
331
|
Matches:
|
|
254
332
|
bootstrap-<scope>.json
|
|
255
|
-
bundle-<scope>.
|
|
333
|
+
bundle-<scope>.czpolicy (current)
|
|
334
|
+
bundle-<scope>.bin (legacy -- still recognised so a key
|
|
335
|
+
rotation GCs pre-.czpolicy caches)
|
|
256
336
|
bundle-<scope>.meta
|
|
257
337
|
"""
|
|
258
338
|
for prefix, suffix in (
|
|
259
339
|
("bootstrap-", ".json"),
|
|
340
|
+
("bundle-", ".czpolicy"),
|
|
260
341
|
("bundle-", ".bin"),
|
|
261
342
|
("bundle-", ".meta"),
|
|
262
343
|
):
|
|
@@ -363,20 +444,58 @@ class CachedBundle:
|
|
|
363
444
|
|
|
364
445
|
def _bundle_cache_paths(api_key: str) -> tuple[Path, Path]:
|
|
365
446
|
scope = _key_scope(api_key)
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
447
|
+
d = _cache_dir()
|
|
448
|
+
# #1011 follow-up: the cached bundle uses the .czpolicy extension to match
|
|
449
|
+
# the UI policy export, so the auto-downloaded and hand-exported bundles
|
|
450
|
+
# share one recognizable suffix (both are the same signed CZ01 format).
|
|
451
|
+
bundle_path = d / f"bundle-{scope}.czpolicy"
|
|
452
|
+
meta_path = d / f"bundle-{scope}.meta"
|
|
453
|
+
return bundle_path, meta_path
|
|
369
454
|
|
|
370
455
|
|
|
371
456
|
def load_cached_bundle(api_key: str) -> Optional[CachedBundle]:
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
457
|
+
# Read from the active (per-client) dir first, then the legacy shared dir
|
|
458
|
+
# (#1011 follow-up); within each dir prefer the current .czpolicy bundle and
|
|
459
|
+
# fall back to a legacy .bin so a pre-#1011 cache stays readable until the
|
|
460
|
+
# next successful fetch re-caches it under the new name/dir.
|
|
461
|
+
scope = _key_scope(api_key)
|
|
462
|
+
read_dirs = _cache_read_dirs()
|
|
463
|
+
primary = read_dirs[0]
|
|
464
|
+
for d in read_dirs:
|
|
465
|
+
meta_path = d / f"bundle-{scope}.meta"
|
|
466
|
+
if not meta_path.exists():
|
|
467
|
+
continue
|
|
468
|
+
bin_path = d / f"bundle-{scope}.czpolicy"
|
|
469
|
+
if not bin_path.exists():
|
|
470
|
+
bin_path = d / f"bundle-{scope}.bin"
|
|
471
|
+
if not bin_path.exists():
|
|
472
|
+
continue
|
|
473
|
+
loaded = _read_cached_bundle_pair(bin_path, meta_path)
|
|
474
|
+
if loaded is not None:
|
|
475
|
+
if d != primary:
|
|
476
|
+
# Migrate-on-read: copy the last-known-good bundle from the
|
|
477
|
+
# legacy shared cache into the active per-agent cache so the
|
|
478
|
+
# surface stops depending on the shared dir. Without this, the
|
|
479
|
+
# common upgrade path (next pull is a 304, so load_hosted_policy
|
|
480
|
+
# serves this cached bundle and never calls save_cached_bundle)
|
|
481
|
+
# would leave the per-agent cache empty indefinitely -- and if
|
|
482
|
+
# the shared cache were later GC'd, the surface would lose its
|
|
483
|
+
# last-known-good. Best-effort: a failed copy still serves the
|
|
484
|
+
# read. The bundle is re-written under the .czpolicy name.
|
|
485
|
+
try:
|
|
486
|
+
save_cached_bundle(api_key, loaded.bytes_, loaded.etag)
|
|
487
|
+
except OSError:
|
|
488
|
+
pass
|
|
489
|
+
return loaded
|
|
490
|
+
return None
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _read_cached_bundle_pair(bin_path: Path, meta_path: Path) -> Optional[CachedBundle]:
|
|
375
494
|
try:
|
|
376
495
|
blob = bin_path.read_bytes()
|
|
377
496
|
meta = json.loads(meta_path.read_text(encoding="utf-8"))
|
|
378
497
|
stored_checksum = str(meta.get("checksum", ""))
|
|
379
|
-
# #1303 residual B (cache integrity): verify the
|
|
498
|
+
# #1303 residual B (cache integrity): verify the cached blob still
|
|
380
499
|
# matches the sha256 checksum recorded in .meta. A mismatch means the
|
|
381
500
|
# cached bundle on disk was tampered with, truncated, or corrupted
|
|
382
501
|
# since it was written. We NEVER silently trust such a blob -- a
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "controlzero"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.12.0"
|
|
8
8
|
description = "AI agent governance: policies, audit, and observability for tool calls. Works locally with no signup."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "Apache-2.0"}
|
|
@@ -345,7 +345,8 @@ def test_missing_bundle_file_emits_helpful_error(tmp_path, monkeypatch):
|
|
|
345
345
|
)
|
|
346
346
|
assert result.exit_code == 2, result.output
|
|
347
347
|
assert "bundle file not found" in result.output
|
|
348
|
-
|
|
348
|
+
# #1011 follow-up: the default cache bundle extension is now .czpolicy.
|
|
349
|
+
assert f"bundle-{prefix}.czpolicy" in result.output
|
|
349
350
|
|
|
350
351
|
|
|
351
352
|
def test_output_does_not_leak_encryption_key_or_signing_pubkey(cache_with_bundle):
|