agentmetry 0.9.4__tar.gz → 0.9.6__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.
- {agentmetry-0.9.4 → agentmetry-0.9.6}/.gitignore +8 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/PKG-INFO +2 -2
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/cli/__init__.py +40 -2
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/atlas.py +1 -1
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/canonical.py +1 -1
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/external.py +31 -5
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/heartbeat.py +7 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/hook_bootstrap.py +79 -1
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/managed_hooks.py +22 -1
- agentmetry-0.9.6/agentmetry/core/audit/posture_event.py +104 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/tool_policy/evaluator.py +3 -2
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/config.py +8 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/diagnostics/hook_coverage.py +34 -1
- agentmetry-0.9.6/agentmetry/core/diagnostics/posture.py +414 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/version.py +1 -1
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/hooks/ingest.py +211 -16
- {agentmetry-0.9.4 → agentmetry-0.9.6}/pyproject.toml +1 -1
- agentmetry-0.9.6/tests/fixtures/codex_hooks/README.md +10 -0
- agentmetry-0.9.6/tests/fixtures/codex_hooks/permission-request.command.output.schema.json +91 -0
- agentmetry-0.9.6/tests/fixtures/codex_hooks/pre-tool-use.command.output.schema.json +93 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_atlas_detection_enrichment.py +1 -1
- agentmetry-0.9.6/tests/test_copilot_hooks.py +211 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_external_ingest.py +37 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_hook_coverage.py +29 -1
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_hook_decision_format.py +95 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_managed_hooks.py +26 -0
- agentmetry-0.9.6/tests/test_posture.py +199 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_readme_claims.py +40 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tools/generate_sigma_pack.py +3 -3
- {agentmetry-0.9.4 → agentmetry-0.9.6}/.dockerignore +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/.env.agentmetry-demo +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/.env.example +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/Dockerfile +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/README.md +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/__init__.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/api/__init__.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/api/main.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/api/routes/__init__.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/api/routes/audit.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/api/routes/auth.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/api/trusted_host.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/api/websocket.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/api/ws_bridge.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/cli/__main__.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/__init__.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/api_token.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/__init__.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/adapters/__init__.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/adapters/agt.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/adapters/chronicle.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/adapters/cloudevents.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/adapters/ecs.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/adapters/sentinel.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/adapters/splunk.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/alerts.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/compliance_digest.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/__init__.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/benchmark.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_approval_denied_then_executed.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_arbitrary_host_stage_execute.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_autonomous_unapproved_write.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_credential_exfil.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_credential_then_cloud_api.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_destructive_delete_burst.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_discovery_then_collect.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_dotfile_then_git_push.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_encoded_command_download.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_env_credential_exfil.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_hashed_only_no_command.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_interpreter_egress.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_pr_merged_without_review.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_proc_substitution_cradle.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_remote_pipe_to_shell.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_remote_staging_then_execute.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_secret_manager_then_egress.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_session_tool_burst.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_single_command_exfil.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_single_quoted_interpreter_egress.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_ssh_directory_exfil.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_subagent_swarm.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_timestamp_collision.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_untrusted_input_then_action.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_authoring_merge_fixtures.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_autonomous_after_approval.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_build_artifact_cleanup.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_ci_artifact_download.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_commit_message_names_cloud_clis.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_database_migration.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_dependency_install_and_build.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_download_release_archive.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_env_example_read.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_fetch_data_then_run_repo_script.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_fetch_dataset_then_analyse.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_fetch_lockfile_then_install.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_fetch_piped_into_own_script.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_git_review_and_push.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_github_raw_docs_then_script.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_human_driven_deletes.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_local_api_probing.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_long_but_calm_session.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_loopback_is_not_egress.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_loopback_pipe_to_interpreter.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_mcp_text_mentions_credential_path.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_ordinary_development.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_package_manager_after_fetch.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_reading_config_that_is_not_secret.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_remote_api_call_no_credentials.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_research_then_docs.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_reversed_order_is_not_exfil.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_secret_names_and_docs.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_test_and_fix_loop.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_writing_about_credentials.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/corpus.yaml +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/disposition.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/engine.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/live.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/live_store.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/models.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/rules.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/traits.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/yaml_config.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/yaml_rules.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/dlp/__init__.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/dlp/loader.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/dlp/models.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/dlp/scanner.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/dogfood.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/evidence_pack.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/forwarder.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/hashing.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/identity.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/ingest.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/mcp_inventory_event.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/migrate.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/mitre.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/otel_ingest.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/policy.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/policy_settings.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/redaction.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/replay.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/run_context.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/signing.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/sinks.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/spool.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/tool_policy/__init__.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/tool_policy/capability.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/tool_policy/loader.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/tool_policy/models.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/trail_anchor.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/trail_chain.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/trail_db.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/trail_merkle.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/trail_rotation.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/auth.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/bus/__init__.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/bus/audit_exporter.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/bus/bridges.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/bus/bus.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/bus/events.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/bus/outbox.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/dashboard_session.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/diagnostics/__init__.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/diagnostics/autostart.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/diagnostics/doctor.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/diagnostics/driver_paths.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/diagnostics/env_file.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/diagnostics/mcp_inventory.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/diagnostics/mcp_schema.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/drivers/__init__.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/drivers/host.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/drivers/permissions.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/drivers/spec.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/extensions.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/health.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/operator_identity.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/paths.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/demo.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/hooks/__init__.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/hooks/mcp_proxy.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/policies/detection/manifest.yaml +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/policies/dlp/manifest.yaml +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/policies/opa/agent_rules.rego +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/policies/tool/manifest.yaml +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/conftest.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/fixtures/agt_filesink_exfil.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/fixtures/agt_filesink_mixed.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/fixtures/fake_mcp_server.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/fixtures/otel_claude_code_session.json +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_agentmetry_audit.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_agentmetry_ingest_client.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_agt_adapter.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_ask_gate_phase_a.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_atlas_mapping.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_audit_sinks.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_audit_stats.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_audit_tail.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_auth.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_auth_default.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_autostart.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_boot_sequence.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_burst_windows.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_chinese_agent_hooks.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_chinese_agent_sprint_b.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_chinese_agent_sprint_c.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_chronicle_adapter.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_cli_backup.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_cli_commands.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_cli_env_var_names.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_cloudevents_adapter.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_compliance_digest.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_container_and_legacy_runtime.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_crewai_adapter.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_demo.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_detection_adi.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_detection_benchmark.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_detection_default_config.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_detection_disposition.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_detection_disposition_api.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_detection_duplicate_emission.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_detection_emit_durability.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_detection_engine.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_detection_evasion.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_detection_hf_incident.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_detection_quoted_command_words.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_detection_rule_identity.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_detection_rules_v2.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_dlp_agent_env_override.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_dlp_invisible_unicode.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_dlp_markdown_exfil.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_dlp_scanner.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_doctor.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_dogfood.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_dogfood_freeze.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_driver_paths.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_ecs_threat.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_event_bus.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_event_signing.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_evidence_pack.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_extensions.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_forwarder.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_forwarding_shape.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_funding_manifest.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_hash_key.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_heartbeat.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_hook_bootstrap.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_hook_enforcement_timing.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_hook_import_cost.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_hook_real_process.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_hook_spool.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_identity_enrich.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_identity_provider.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_ingest_roundtrip.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_isolation.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_launch_targets.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_live_detection.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_live_detection_store.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_mcp_audit_proxy.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_mcp_concealed_text.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_mcp_inventory.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_mcp_inventory_event.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_mcp_meta_fingerprint.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_mcp_proxy_packaged.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_mcp_schema.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_mitre_dlp.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_opensre_adapter.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_operator_attribution.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_otel_ingest.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_paths.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_powershell_encoding.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_redaction.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_replay.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_sentinel.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_serve_script_binds_loopback.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_shared_install.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_sigma_pack.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_static_serving.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_subagent_lifecycle.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_suite_leaves_real_home_alone.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_tool_policy.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_tool_policy_agent_cli.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_tool_policy_git_hooks.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_trail_anchor.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_trail_chain.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_trail_concurrency.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_trail_merkle.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_trusted_host.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_unattended_agent_policy.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_version.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_yaml_detection_rules.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tools/mcp_audit_proxy.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.6}/tools/vault_fs_server.py +0 -0
|
@@ -100,3 +100,11 @@ docs/*-audit-*.md
|
|
|
100
100
|
# pytest-cov data file
|
|
101
101
|
.coverage
|
|
102
102
|
.coverage.*
|
|
103
|
+
|
|
104
|
+
# Demo video renders and their working files (scripts/make_demo_video.py).
|
|
105
|
+
# The site serves its own copy from ai-audit-watch/public/video/.
|
|
106
|
+
docs/assets/*.mp4
|
|
107
|
+
docs/assets/agentmetry-demo-poster.png
|
|
108
|
+
docs/assets/agentmetry-linkedin-*-poster.png
|
|
109
|
+
docs/assets/voice/
|
|
110
|
+
docs/assets/demo-stills/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agentmetry
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.6
|
|
4
4
|
Summary: Local-first flight recorder for AI coding agents: hash-chained audit trail, MITRE-mapped sequence detection, DLP
|
|
5
5
|
Project-URL: Homepage, https://agentmetry.ai
|
|
6
6
|
Project-URL: Source, https://github.com/blitzcrieg1/agentmetry
|
|
@@ -37,7 +37,7 @@ Provides-Extra: dev
|
|
|
37
37
|
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
38
38
|
Requires-Dist: pytest-cov>=6.0; extra == 'dev'
|
|
39
39
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
40
|
-
Requires-Dist: ruff==0.16.
|
|
40
|
+
Requires-Dist: ruff==0.16.10; extra == 'dev'
|
|
41
41
|
Description-Content-Type: text/markdown
|
|
42
42
|
|
|
43
43
|
# Agentmetry
|
|
@@ -526,7 +526,7 @@ def cmd_install(args: argparse.Namespace) -> int:
|
|
|
526
526
|
#: live in ~/.gemini and are written by scripts/install_antigravity_hooks.ps1,
|
|
527
527
|
#: which has no Python equivalent. Listing it here and quietly doing nothing
|
|
528
528
|
#: would be the reporting failure this codebase keeps finding in itself.
|
|
529
|
-
_HOOK_INSTALLERS = ("cursor", "claude", "codex", "qwen", "kimi", "qoder", "codebuddy")
|
|
529
|
+
_HOOK_INSTALLERS = ("cursor", "claude", "codex", "qwen", "kimi", "qoder", "codebuddy", "copilot")
|
|
530
530
|
|
|
531
531
|
|
|
532
532
|
def _hooks_status() -> int:
|
|
@@ -972,6 +972,35 @@ def cmd_mcp(args: argparse.Namespace) -> int:
|
|
|
972
972
|
return 0
|
|
973
973
|
|
|
974
974
|
|
|
975
|
+
def cmd_posture(args: argparse.Namespace) -> int:
|
|
976
|
+
"""Report agent settings that take a human or a hook out of the loop.
|
|
977
|
+
|
|
978
|
+
Exits 0 with findings by default, like `mcp`: a posture is a risk report, and
|
|
979
|
+
a health check somebody wired into MDM should not fail on one. `--strict`
|
|
980
|
+
turns any high finding into exit 1 for a pipeline that wants exactly that.
|
|
981
|
+
"""
|
|
982
|
+
import json as _json
|
|
983
|
+
from pathlib import Path as _Path
|
|
984
|
+
|
|
985
|
+
from agentmetry.core.diagnostics.posture import HIGH, NOT_CHECKED, collect, summary_lines
|
|
986
|
+
|
|
987
|
+
posture = collect(_Path(args.project).resolve() if args.project else _Path.cwd())
|
|
988
|
+
if args.json:
|
|
989
|
+
print(_json.dumps({
|
|
990
|
+
"findings": [{**f.wire(), "why": f.why} for f in posture.sorted()],
|
|
991
|
+
"counts": posture.counts(),
|
|
992
|
+
"files_read": len(posture.files_read),
|
|
993
|
+
"files_unreadable": len(posture.unreadable),
|
|
994
|
+
"not_checked": sorted(NOT_CHECKED),
|
|
995
|
+
}, indent=2))
|
|
996
|
+
else:
|
|
997
|
+
for line in summary_lines(posture):
|
|
998
|
+
print(line)
|
|
999
|
+
if args.strict and posture.counts()[HIGH]:
|
|
1000
|
+
return 1
|
|
1001
|
+
return 0
|
|
1002
|
+
|
|
1003
|
+
|
|
975
1004
|
def cmd_otel(args: argparse.Namespace) -> int:
|
|
976
1005
|
"""Receive Claude Code's native OpenTelemetry stream and record it.
|
|
977
1006
|
|
|
@@ -1360,7 +1389,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1360
1389
|
hooks.add_argument(
|
|
1361
1390
|
"--managed",
|
|
1362
1391
|
action="store_true",
|
|
1363
|
-
help="use each vendor's admin-managed, machine-wide location (claude, cursor, codex); needs admin",
|
|
1392
|
+
help="use each vendor's admin-managed, machine-wide location (claude, cursor, codex, copilot); needs admin",
|
|
1364
1393
|
)
|
|
1365
1394
|
hooks.add_argument(
|
|
1366
1395
|
"--lock",
|
|
@@ -1485,6 +1514,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1485
1514
|
"--digest", action="store_true",
|
|
1486
1515
|
help="also print the config digest, which names no server and is safe to publish",
|
|
1487
1516
|
)
|
|
1517
|
+
posture = sub.add_parser(
|
|
1518
|
+
"posture", help="report agent settings that skip approval prompts or turn hooks off"
|
|
1519
|
+
)
|
|
1520
|
+
posture.add_argument("--project", default=None, help="project root to include (default: cwd)")
|
|
1521
|
+
posture.add_argument("--json", action="store_true", help="print findings as JSON")
|
|
1522
|
+
posture.add_argument(
|
|
1523
|
+
"--strict", action="store_true", help="exit 1 when any high-severity finding is present",
|
|
1524
|
+
)
|
|
1488
1525
|
otel = sub.add_parser(
|
|
1489
1526
|
"otel", help="receive Claude Code's native OpenTelemetry stream (no hooks needed)"
|
|
1490
1527
|
)
|
|
@@ -1586,6 +1623,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1586
1623
|
"prove": cmd_prove,
|
|
1587
1624
|
"anchor": cmd_anchor,
|
|
1588
1625
|
"mcp": cmd_mcp,
|
|
1626
|
+
"posture": cmd_posture,
|
|
1589
1627
|
"dashboard": cmd_dashboard,
|
|
1590
1628
|
"otel": cmd_otel,
|
|
1591
1629
|
"import-agt": cmd_import_agt,
|
|
@@ -170,7 +170,7 @@ def attach_atlas(tool: Any) -> None:
|
|
|
170
170
|
# adversary technique a whole sequence is evidence of, which is a claim only a
|
|
171
171
|
# rule can make and never a single call.
|
|
172
172
|
#
|
|
173
|
-
# Deliberately small. Of the
|
|
173
|
+
# Deliberately small. Of the fourteen published rules, one describes an ATLAS
|
|
174
174
|
# technique; the rest are host and operations behaviour that ATT&CK already
|
|
175
175
|
# covers, and tagging them would put an AI-threat label on a shell command.
|
|
176
176
|
# The empty entries are the design, not a backlog.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import re
|
|
5
6
|
import uuid
|
|
6
7
|
from datetime import datetime, timezone
|
|
7
8
|
from typing import Any, Literal
|
|
@@ -19,12 +20,12 @@ from agentmetry.core.config import settings
|
|
|
19
20
|
|
|
20
21
|
ExternalApp = Literal[
|
|
21
22
|
"cursor", "claude", "antigravity", "codex", "mcp_proxy",
|
|
22
|
-
"qwen", "kimi", "qoder", "codebuddy", "trae", "crewai", "opensre", "agentmetry",
|
|
23
|
+
"qwen", "kimi", "qoder", "codebuddy", "trae", "crewai", "opensre", "copilot", "agentmetry",
|
|
23
24
|
]
|
|
24
25
|
|
|
25
26
|
KNOWN_EXTERNAL_APPS = frozenset({
|
|
26
27
|
"cursor", "claude", "antigravity", "codex", "mcp_proxy",
|
|
27
|
-
"qwen", "kimi", "qoder", "codebuddy", "trae", "crewai", "opensre",
|
|
28
|
+
"qwen", "kimi", "qoder", "codebuddy", "trae", "crewai", "opensre", "copilot",
|
|
28
29
|
})
|
|
29
30
|
|
|
30
31
|
_ACTION_MAP = {
|
|
@@ -49,11 +50,34 @@ def _split_tool(qualified: str) -> tuple[str, str]:
|
|
|
49
50
|
return "", qualified
|
|
50
51
|
|
|
51
52
|
|
|
53
|
+
#: What an agent that is not on the allowlist is recorded as.
|
|
54
|
+
UNKNOWN_APP = "unknown"
|
|
55
|
+
|
|
56
|
+
_CLAIMED_APP_UNSAFE = re.compile(r"[^a-z0-9_.-]")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _resolve_source_app(raw: Any) -> tuple[str, str]:
|
|
60
|
+
"""The app to record, and the name the client claimed when that differs.
|
|
61
|
+
|
|
62
|
+
An unlisted name used to be recorded as `cursor`. So an OpenClaw plugin, a
|
|
63
|
+
LangGraph handler or anything else posting to ingest landed in the trail,
|
|
64
|
+
the SIEM and the detections attributed to Cursor: a confident wrong answer
|
|
65
|
+
in a record whose value is that it is right. It is now `unknown`, with the
|
|
66
|
+
claimed name kept beside it, reduced to a short identifier so a client
|
|
67
|
+
cannot write arbitrary text into a field SIEM rules group on.
|
|
68
|
+
|
|
69
|
+
A missing name still means Cursor. That default predates `source_app`, and
|
|
70
|
+
hooks installed before it may still post without one.
|
|
71
|
+
"""
|
|
72
|
+
source_app = str(raw or "cursor").strip().lower()
|
|
73
|
+
if source_app in KNOWN_EXTERNAL_APPS or source_app == "agentmetry":
|
|
74
|
+
return source_app, ""
|
|
75
|
+
return UNKNOWN_APP, _CLAIMED_APP_UNSAFE.sub("", source_app)[:64]
|
|
76
|
+
|
|
77
|
+
|
|
52
78
|
def build_external_canonical(payload: dict[str, Any]) -> dict[str, Any]:
|
|
53
79
|
"""Normalize an adapter payload into canonical v1.1 JSON."""
|
|
54
|
-
source_app =
|
|
55
|
-
if source_app not in KNOWN_EXTERNAL_APPS and source_app != "agentmetry":
|
|
56
|
-
source_app = "cursor"
|
|
80
|
+
source_app, claimed_app = _resolve_source_app(payload.get("source_app"))
|
|
57
81
|
|
|
58
82
|
event_type = str(payload.get("event_type") or "tool_called")
|
|
59
83
|
action_type, default_outcome = _ACTION_MAP.get(event_type, ("tool_called", "success"))
|
|
@@ -130,6 +154,8 @@ def build_external_canonical(payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
130
154
|
},
|
|
131
155
|
"model": {"id": model_id, "provider": model_provider},
|
|
132
156
|
}
|
|
157
|
+
if claimed_app:
|
|
158
|
+
event["source"]["claimed_app"] = claimed_app
|
|
133
159
|
|
|
134
160
|
if tool_qualified:
|
|
135
161
|
server = str(tool_block.get("server") or driver_name or source_app)
|
|
@@ -300,6 +300,7 @@ async def heartbeat_forever(interval: float | None = None) -> None:
|
|
|
300
300
|
import asyncio
|
|
301
301
|
|
|
302
302
|
from agentmetry.core.audit.mcp_inventory_event import InventoryState, maybe_emit_inventory
|
|
303
|
+
from agentmetry.core.audit.posture_event import PostureState, maybe_emit_posture
|
|
303
304
|
|
|
304
305
|
period = interval if interval is not None else interval_seconds()
|
|
305
306
|
if not period:
|
|
@@ -307,6 +308,7 @@ async def heartbeat_forever(interval: float | None = None) -> None:
|
|
|
307
308
|
return
|
|
308
309
|
|
|
309
310
|
inventory_state = InventoryState()
|
|
311
|
+
posture_state = PostureState()
|
|
310
312
|
while True:
|
|
311
313
|
try:
|
|
312
314
|
event = await emit_heartbeat()
|
|
@@ -321,4 +323,9 @@ async def heartbeat_forever(interval: float | None = None) -> None:
|
|
|
321
323
|
await maybe_emit_inventory(inventory_state)
|
|
322
324
|
except Exception:
|
|
323
325
|
logger.exception("MCP inventory event failed")
|
|
326
|
+
try:
|
|
327
|
+
# Opt-in, like the inventory, and in its own try for the same reason.
|
|
328
|
+
await maybe_emit_posture(posture_state)
|
|
329
|
+
except Exception:
|
|
330
|
+
logger.exception("Posture event failed")
|
|
324
331
|
await asyncio.sleep(period)
|
|
@@ -555,6 +555,83 @@ def install_codex_global_hooks(
|
|
|
555
555
|
return hooks_path
|
|
556
556
|
|
|
557
557
|
|
|
558
|
+
# --- GitHub Copilot (CLI and VS Code agent mode, ~/.copilot/hooks/) -----------
|
|
559
|
+
# One file serves both. Copilot CLI reads every *.json in ~/.copilot/hooks (or
|
|
560
|
+
# $COPILOT_HOME/hooks), and VS Code's Local harness reads the same directory and
|
|
561
|
+
# accepts the CLI's format, detected by its `version` field: lowerCamel event
|
|
562
|
+
# names, mapped to its own, and `bash` / `powershell` command fields, mapped to
|
|
563
|
+
# its per-OS commands (microsoft/vscode `hookSchema.ts`, `hookTypes.ts`).
|
|
564
|
+
#
|
|
565
|
+
# The file is ours alone, so it is written whole rather than merged: a user's own
|
|
566
|
+
# hooks live in their own files beside it, and Copilot loads them all.
|
|
567
|
+
#
|
|
568
|
+
# The PowerShell form needs `&`. VS Code on Windows runs a hook through
|
|
569
|
+
# `powershell -Command`, where a command that starts with a quoted path is a
|
|
570
|
+
# string expression, not a call. Without the call operator the hook would exit
|
|
571
|
+
# non-zero, which VS Code treats as a non-blocking error: nothing recorded and
|
|
572
|
+
# every deny an allow.
|
|
573
|
+
COPILOT_HOOK_FILE = "agentmetry.json"
|
|
574
|
+
|
|
575
|
+
#: (Copilot's event key, the PascalCase name our mapper and enforcement key on).
|
|
576
|
+
#: `permissionRequest` is CLI-only and VS Code does not map it; `preToolUse` is
|
|
577
|
+
#: the gate on both.
|
|
578
|
+
COPILOT_HOOK_EVENTS: tuple[tuple[str, str], ...] = (
|
|
579
|
+
("sessionStart", "SessionStart"),
|
|
580
|
+
("userPromptSubmitted", "UserPromptSubmit"),
|
|
581
|
+
("preToolUse", "PreToolUse"),
|
|
582
|
+
("postToolUse", "PostToolUse"),
|
|
583
|
+
("sessionEnd", "SessionEnd"),
|
|
584
|
+
("agentStop", "Stop"),
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def copilot_hooks_dir() -> Path:
|
|
589
|
+
home = os.environ.get("COPILOT_HOME", "").strip()
|
|
590
|
+
return (Path(home).expanduser() if home else Path.home() / ".copilot") / "hooks"
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def copilot_hooks_document(
|
|
594
|
+
*, python: str | None = None, ingest: Path | None = None, repo_root: Path | None = None
|
|
595
|
+
) -> dict[str, Any]:
|
|
596
|
+
"""The whole hooks file, in Copilot CLI's version-1 format."""
|
|
597
|
+
hooks: dict[str, Any] = {}
|
|
598
|
+
for key, event in COPILOT_HOOK_EVENTS:
|
|
599
|
+
command = hook_command("copilot", event, python=python, ingest=ingest, repo_root=repo_root)
|
|
600
|
+
hooks[key] = [
|
|
601
|
+
{
|
|
602
|
+
"type": "command",
|
|
603
|
+
"bash": command,
|
|
604
|
+
"powershell": f"& {command}",
|
|
605
|
+
"timeoutSec": 10,
|
|
606
|
+
}
|
|
607
|
+
]
|
|
608
|
+
return {"version": 1, "hooks": hooks}
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def install_copilot_global_hooks(
|
|
612
|
+
*, repo_root: Path | None = None, python: str | None = None
|
|
613
|
+
) -> Path | None:
|
|
614
|
+
"""Write ~/.copilot/hooks/agentmetry.json for Copilot CLI and VS Code agent mode.
|
|
615
|
+
|
|
616
|
+
Not called from `bootstrap_tier_b_hooks`. VS Code's hooks are a preview
|
|
617
|
+
feature an organisation can switch off, so a silent install could look like
|
|
618
|
+
coverage that is not there; installing is a decision `hooks install` makes.
|
|
619
|
+
"""
|
|
620
|
+
root = repo_root or _repo_root()
|
|
621
|
+
if hook_target(root) == "none":
|
|
622
|
+
logger.warning("Copilot hook bootstrap skipped: no way to reach ingest from %s", root)
|
|
623
|
+
return None
|
|
624
|
+
doc = copilot_hooks_document(
|
|
625
|
+
python=python or sys.executable, ingest=_ingest_script(root), repo_root=root
|
|
626
|
+
)
|
|
627
|
+
hooks_dir = copilot_hooks_dir()
|
|
628
|
+
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
629
|
+
path = hooks_dir / COPILOT_HOOK_FILE
|
|
630
|
+
path.write_text(json.dumps(doc, indent=2) + "\n", encoding="utf-8")
|
|
631
|
+
logger.info("Installed global Copilot audit hooks -> %s", path)
|
|
632
|
+
return path
|
|
633
|
+
|
|
634
|
+
|
|
558
635
|
def _kimi_config_dir() -> Path:
|
|
559
636
|
home = os.environ.get("KIMI_CODE_HOME", "").strip()
|
|
560
637
|
if home:
|
|
@@ -648,9 +725,10 @@ if __name__ == "__main__":
|
|
|
648
725
|
"qoder": install_qoder_global_hooks,
|
|
649
726
|
"codebuddy": install_codebuddy_global_hooks,
|
|
650
727
|
"codex": install_codex_global_hooks,
|
|
728
|
+
"copilot": install_copilot_global_hooks,
|
|
651
729
|
}
|
|
652
730
|
fn = installers.get(target, bootstrap_tier_b_hooks)
|
|
653
|
-
if target in ("qwen", "kimi", "qoder", "codebuddy", "codex"):
|
|
731
|
+
if target in ("qwen", "kimi", "qoder", "codebuddy", "codex", "copilot"):
|
|
654
732
|
path = fn()
|
|
655
733
|
if path:
|
|
656
734
|
print(f"Installed global {target} hooks -> {path}")
|
|
@@ -18,6 +18,14 @@ survive anything the user does to their own config:
|
|
|
18
18
|
- Codex: `requirements.toml` (Windows `%ProgramData%\\OpenAI\\Codex`, else
|
|
19
19
|
`/etc/codex`), `[[hooks.<Event>]]` entries; `allow_managed_hooks_only`
|
|
20
20
|
(opt-in) skips user, project and plugin hooks.
|
|
21
|
+
- GitHub Copilot CLI: a policy hook file in `policy.d` (Windows
|
|
22
|
+
`C:\\ProgramData\\GitHub\\Copilot\\policy.d`, else
|
|
23
|
+
`/etc/github-copilot/policy.d`). Policy hooks load before every other hook,
|
|
24
|
+
`disableAllHooks` cannot turn them off, and on POSIX Copilot ignores one that
|
|
25
|
+
is not root-owned or is group- or world-writable, so it is written 0644. There
|
|
26
|
+
is no lock to opt into: a policy hook already cannot be disabled. GitHub's
|
|
27
|
+
docs describe policy hooks for the CLI; whether VS Code agent mode reads them
|
|
28
|
+
is not documented, so the per-user install still matters there.
|
|
21
29
|
|
|
22
30
|
Paths and keys are from the vendors' documentation as of 2026-10. Two of these
|
|
23
31
|
files are shared with whatever else an organisation manages, so every write
|
|
@@ -38,7 +46,7 @@ from typing import Any
|
|
|
38
46
|
|
|
39
47
|
from agentmetry.core.audit import hook_bootstrap as hb
|
|
40
48
|
|
|
41
|
-
MANAGED_AGENTS = ("claude", "cursor", "codex")
|
|
49
|
+
MANAGED_AGENTS = ("claude", "cursor", "codex", "copilot")
|
|
42
50
|
CLAUDE_DROPIN = "50-agentmetry.json"
|
|
43
51
|
CODEX_BEGIN = "# agentmetry managed hooks begin (written by `agentmetry hooks install --managed`)"
|
|
44
52
|
CODEX_END = "# agentmetry managed hooks end"
|
|
@@ -70,11 +78,18 @@ def codex_requirements() -> Path:
|
|
|
70
78
|
return Path("/etc/codex/requirements.toml")
|
|
71
79
|
|
|
72
80
|
|
|
81
|
+
def copilot_policy_dir() -> Path:
|
|
82
|
+
if sys.platform == "win32":
|
|
83
|
+
return _program_data() / "GitHub" / "Copilot" / "policy.d"
|
|
84
|
+
return Path("/etc/github-copilot/policy.d")
|
|
85
|
+
|
|
86
|
+
|
|
73
87
|
def managed_paths() -> dict[str, Path]:
|
|
74
88
|
return {
|
|
75
89
|
"claude": claude_managed_dir() / "managed-settings.d" / CLAUDE_DROPIN,
|
|
76
90
|
"cursor": cursor_enterprise_hooks(),
|
|
77
91
|
"codex": codex_requirements(),
|
|
92
|
+
"copilot": copilot_policy_dir() / hb.COPILOT_HOOK_FILE,
|
|
78
93
|
}
|
|
79
94
|
|
|
80
95
|
|
|
@@ -190,9 +205,15 @@ def install(agent: str, *, lock: bool = False) -> Path:
|
|
|
190
205
|
elif agent == "codex":
|
|
191
206
|
current = path.read_text(encoding="utf-8") if path.is_file() else ""
|
|
192
207
|
text = merge_codex_requirements(current, lock=lock)
|
|
208
|
+
elif agent == "copilot":
|
|
209
|
+
# Ours alone, like the Claude drop-in: written, not merged.
|
|
210
|
+
text = json.dumps(hb.copilot_hooks_document(), indent=2) + "\n"
|
|
193
211
|
else:
|
|
194
212
|
raise ValueError(f"no managed location for {agent}")
|
|
195
213
|
_write(path, text)
|
|
214
|
+
if agent == "copilot" and sys.platform != "win32":
|
|
215
|
+
# Copilot skips a policy file that is group- or world-writable.
|
|
216
|
+
os.chmod(path, 0o644)
|
|
196
217
|
return path
|
|
197
218
|
|
|
198
219
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Agent posture as an event a SIEM can see, when an operator asks.
|
|
2
|
+
|
|
3
|
+
`agentmetry posture` answers "which agents here skip approval or have hooks
|
|
4
|
+
off", for whoever runs it on that machine. A fleet wants the same answer in the
|
|
5
|
+
SIEM, by host, and wants to know the day it changed.
|
|
6
|
+
|
|
7
|
+
Same contract as the MCP inventory event (`mcp_inventory_event`): off unless
|
|
8
|
+
`AGENTMETRY_POSTURE=1`, emitted when the recorder starts, when the findings
|
|
9
|
+
change and once a day regardless, written to the trail first and then to every
|
|
10
|
+
sink. Each finding is agent, scope label, setting, value and severity: no file
|
|
11
|
+
path, so no home directory or username leaves the machine.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import time
|
|
17
|
+
import uuid
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from agentmetry.core.audit.canonical import SCHEMA_VERSION
|
|
22
|
+
from agentmetry.core.audit.identity import identity_fields
|
|
23
|
+
|
|
24
|
+
#: Re-emit an unchanged posture this often, so a fleet view over a day is whole.
|
|
25
|
+
SNAPSHOT_SECONDS = 24 * 3600
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def enabled() -> bool:
|
|
29
|
+
from agentmetry.core.config import settings
|
|
30
|
+
|
|
31
|
+
return bool(getattr(settings, "posture_enabled", False))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class PostureState:
|
|
36
|
+
digest: str | None = None
|
|
37
|
+
emitted_at: float = 0.0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_posture_event(now_utc: str, posture: Any, *, outcome: str) -> dict[str, Any]:
|
|
41
|
+
from agentmetry.core.diagnostics.posture import NOT_CHECKED
|
|
42
|
+
|
|
43
|
+
findings = [f.wire() for f in posture.sorted()]
|
|
44
|
+
counts = posture.counts()
|
|
45
|
+
return {
|
|
46
|
+
"schema_version": SCHEMA_VERSION,
|
|
47
|
+
"event_id": str(uuid.uuid4()),
|
|
48
|
+
"session_id": "",
|
|
49
|
+
"correlation_id": "",
|
|
50
|
+
"timestamp_utc": now_utc,
|
|
51
|
+
**identity_fields(),
|
|
52
|
+
"source_topic": "agentmetry/posture",
|
|
53
|
+
"source": {"tier": "agentmetry", "app": "agentmetry", "adapter": "posture"},
|
|
54
|
+
"initiator": {"actor_type": "system", "trigger": "scheduled", "operator_id": ""},
|
|
55
|
+
"actor": {"type": "system", "id": "agentmetry", "role": "recorder"},
|
|
56
|
+
"action": {
|
|
57
|
+
"type": "posture",
|
|
58
|
+
"outcome": outcome,
|
|
59
|
+
"reason": f"{counts['high']} high, {counts['medium']} medium, {counts['low']} low",
|
|
60
|
+
},
|
|
61
|
+
"agent": {"name": "agentmetry", "skill_id": ""},
|
|
62
|
+
"posture": {
|
|
63
|
+
"findings": findings,
|
|
64
|
+
"counts": counts,
|
|
65
|
+
"digest": posture.digest()[:16],
|
|
66
|
+
"files_read": len(posture.files_read),
|
|
67
|
+
"files_unreadable": len(posture.unreadable),
|
|
68
|
+
"not_checked": sorted(NOT_CHECKED),
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def due(state: PostureState, digest: str, now_monotonic: float) -> str | None:
|
|
74
|
+
"""`snapshot`, `changed`, or None when there is nothing to say."""
|
|
75
|
+
if state.digest is None:
|
|
76
|
+
return "snapshot"
|
|
77
|
+
if digest != state.digest:
|
|
78
|
+
return "changed"
|
|
79
|
+
if now_monotonic - state.emitted_at >= SNAPSHOT_SECONDS:
|
|
80
|
+
return "snapshot"
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def maybe_emit_posture(state: PostureState) -> dict[str, Any] | None:
|
|
85
|
+
"""Emit the posture if the operator turned it on and there is news."""
|
|
86
|
+
if not enabled():
|
|
87
|
+
return None
|
|
88
|
+
from datetime import datetime, timezone
|
|
89
|
+
|
|
90
|
+
from agentmetry.core.audit.ingest import _get_sink
|
|
91
|
+
from agentmetry.core.audit.trail_db import get_trail_db
|
|
92
|
+
from agentmetry.core.diagnostics.posture import collect
|
|
93
|
+
|
|
94
|
+
posture = collect()
|
|
95
|
+
digest = posture.digest()
|
|
96
|
+
now = time.monotonic()
|
|
97
|
+
outcome = due(state, digest, now)
|
|
98
|
+
if outcome is None:
|
|
99
|
+
return None
|
|
100
|
+
event = build_posture_event(datetime.now(timezone.utc).isoformat(), posture, outcome=outcome)
|
|
101
|
+
get_trail_db().insert(event)
|
|
102
|
+
await _get_sink().emit(event)
|
|
103
|
+
state.digest, state.emitted_at = digest, now
|
|
104
|
+
return event
|
|
@@ -52,11 +52,12 @@ def _nested_arg_containers(hook_data: dict[str, Any]) -> tuple[list[dict[str, An
|
|
|
52
52
|
Cursor shell hooks put `command` at the top level, but Claude and Codex nest
|
|
53
53
|
it under `tool_input` and Antigravity under `toolCall.args`. Without those,
|
|
54
54
|
a `command_pattern` rule matched nothing on three of the four supported IDEs
|
|
55
|
-
— the shipped block_shell_rm rule was Cursor-only in practice.
|
|
55
|
+
— the shipped block_shell_rm rule was Cursor-only in practice. Copilot CLI
|
|
56
|
+
sends `toolArgs`, and the same gap would have opened again for it.
|
|
56
57
|
"""
|
|
57
58
|
containers: list[dict[str, Any]] = []
|
|
58
59
|
raw = ""
|
|
59
|
-
for key in ("arguments", "args", "input", "tool_input", "toolInput"):
|
|
60
|
+
for key in ("arguments", "args", "input", "tool_input", "toolInput", "toolArgs"):
|
|
60
61
|
val = hook_data.get(key)
|
|
61
62
|
if isinstance(val, str):
|
|
62
63
|
try:
|
|
@@ -220,6 +220,14 @@ class Settings(BaseSettings):
|
|
|
220
220
|
validation_alias=AliasChoices("AGENTMETRY_MCP_INVENTORY"),
|
|
221
221
|
)
|
|
222
222
|
|
|
223
|
+
# Agent settings that skip approval or turn hooks off, as a `posture` event
|
|
224
|
+
# (`agentmetry posture`). Off by default, for the same reason as the
|
|
225
|
+
# inventory: what reaches a SOC from a laptop is the operator's decision.
|
|
226
|
+
posture_enabled: bool = Field(
|
|
227
|
+
default=False,
|
|
228
|
+
validation_alias=AliasChoices("AGENTMETRY_POSTURE"),
|
|
229
|
+
)
|
|
230
|
+
|
|
223
231
|
# Rewrite ~/.claude/settings.json and ~/.cursor/hooks.json on every boot to
|
|
224
232
|
# point at this checkout. Off by default: it used to always run, so booting
|
|
225
233
|
# any second checkout (a test clone, a feature branch, a demo instance)
|
|
@@ -99,6 +99,11 @@ class Surface:
|
|
|
99
99
|
configs: tuple[str, ...]
|
|
100
100
|
#: Overridden where our command is not the ingest script itself.
|
|
101
101
|
markers: tuple[str, ...] = _DEFAULT_MARKERS
|
|
102
|
+
#: Paths under that directory that only this agent creates. Empty means the
|
|
103
|
+
#: directory is the agent's own and its existence is the answer. Set where
|
|
104
|
+
#: the directory is shared with another program, so that program's presence
|
|
105
|
+
#: does not read as this agent's.
|
|
106
|
+
presence: tuple[str, ...] = ()
|
|
102
107
|
|
|
103
108
|
def directory(self) -> Path:
|
|
104
109
|
override = os.environ.get(self.home_env, "").strip() if self.home_env else ""
|
|
@@ -106,6 +111,20 @@ class Surface:
|
|
|
106
111
|
return Path(override).expanduser()
|
|
107
112
|
return Path.home() / self.home_rel
|
|
108
113
|
|
|
114
|
+
def installed(self) -> bool:
|
|
115
|
+
"""Whether this agent is on the machine, as far as the disk can say.
|
|
116
|
+
|
|
117
|
+
A config we write counts as presence too: if our hook is in one of its
|
|
118
|
+
files, somebody installed it for this agent, and reporting the agent
|
|
119
|
+
absent would hide a hook that is there.
|
|
120
|
+
"""
|
|
121
|
+
root = self.directory()
|
|
122
|
+
if not root.is_dir():
|
|
123
|
+
return False
|
|
124
|
+
if not self.presence:
|
|
125
|
+
return True
|
|
126
|
+
return any((root / rel).exists() for rel in (*self.presence, *self.configs))
|
|
127
|
+
|
|
109
128
|
|
|
110
129
|
#: Mirrors hook_bootstrap's installers, plus the two surfaces that have none.
|
|
111
130
|
#: Adding an installer without adding a row here is the drift this module exists
|
|
@@ -124,16 +143,30 @@ SURFACES: tuple[Surface, ...] = (
|
|
|
124
143
|
# Codex's hash-based trust prompt, which skips untrusted hooks silently, so
|
|
125
144
|
# a covered Codex is configured rather than proven to be firing.
|
|
126
145
|
Surface("codex", "", ".codex", ("hooks.json",)),
|
|
146
|
+
# Copilot CLI and VS Code agent mode both read ~/.copilot/hooks/*.json, and
|
|
147
|
+
# the installer writes its own file there. The directory is Copilot CLI's,
|
|
148
|
+
# so a machine with only VS Code Copilot may report absent: VS Code does not
|
|
149
|
+
# necessarily create it. What a file check cannot see is VS Code's
|
|
150
|
+
# `chat.useHooks` switch or an organisation policy that turns hooks off,
|
|
151
|
+
# which `agentmetry posture` reports.
|
|
152
|
+
Surface("copilot", "COPILOT_HOME", ".copilot", ("hooks/agentmetry.json",)),
|
|
127
153
|
# Antigravity 2.0 usually runs from ~/.gemini/antigravity/scratch rather
|
|
128
154
|
# than the repo, so scripts/install_antigravity_hooks.ps1 writes both. Either
|
|
129
155
|
# one means the agent is recorded. Its command is a .cmd wrapper, not the
|
|
130
156
|
# ingest script, hence the marker override.
|
|
157
|
+
#
|
|
158
|
+
# ~/.gemini is not Antigravity's alone: Gemini CLI keeps its settings.json
|
|
159
|
+
# there too. Treating the directory as the answer reported every machine
|
|
160
|
+
# with only Gemini CLI as an uncovered Antigravity, which degraded its beat
|
|
161
|
+
# for an agent that was never installed. Antigravity's own data lives in
|
|
162
|
+
# ~/.gemini/antigravity, so that, or one of our hook files, is presence.
|
|
131
163
|
Surface(
|
|
132
164
|
"antigravity",
|
|
133
165
|
"",
|
|
134
166
|
".gemini",
|
|
135
167
|
("config/hooks.json", "antigravity/scratch/.agents/hooks.json"),
|
|
136
168
|
markers=("agentmetry_antigravity_hook", *_DEFAULT_MARKERS),
|
|
169
|
+
presence=("antigravity",),
|
|
137
170
|
),
|
|
138
171
|
)
|
|
139
172
|
|
|
@@ -175,7 +208,7 @@ def coverage() -> dict[str, str]:
|
|
|
175
208
|
result: dict[str, str] = {}
|
|
176
209
|
for surface in SURFACES:
|
|
177
210
|
try:
|
|
178
|
-
present = surface.
|
|
211
|
+
present = surface.installed()
|
|
179
212
|
except OSError:
|
|
180
213
|
present = False
|
|
181
214
|
if not present:
|