agentmetry 0.4.0__tar.gz → 0.5.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.
- {agentmetry-0.4.0 → agentmetry-0.5.0}/.env.example +7 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/PKG-INFO +2 -2
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/api/main.py +5 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/api/routes/audit.py +5 -1
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/cli/__init__.py +329 -1
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/adapters/agt.py +4 -1
- agentmetry-0.5.0/agentmetry/core/audit/adapters/chronicle.py +260 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/adapters/cloudevents.py +2 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/adapters/ecs.py +71 -0
- agentmetry-0.5.0/agentmetry/core/audit/atlas.py +274 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/canonical.py +11 -1
- agentmetry-0.5.0/agentmetry/core/audit/detection/corpus/attack_secret_manager_then_egress.jsonl +2 -0
- agentmetry-0.5.0/agentmetry/core/audit/detection/corpus/attack_single_quoted_interpreter_egress.jsonl +2 -0
- agentmetry-0.5.0/agentmetry/core/audit/detection/corpus/benign_commit_message_names_cloud_clis.jsonl +3 -0
- agentmetry-0.5.0/agentmetry/core/audit/detection/corpus/benign_secret_names_and_docs.jsonl +5 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/corpus.yaml +49 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/disposition.py +2 -1
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/live.py +17 -2
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/rules.py +20 -4
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/traits.py +69 -1
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/yaml_rules.py +14 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/dogfood.py +27 -2
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/external.py +6 -0
- agentmetry-0.5.0/agentmetry/core/audit/heartbeat.py +313 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/hook_bootstrap.py +243 -27
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/ingest.py +132 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/mitre.py +11 -6
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/sinks.py +124 -1
- agentmetry-0.5.0/agentmetry/core/audit/trail_anchor.py +441 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/trail_chain.py +177 -11
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/bus/audit_exporter.py +5 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/config.py +30 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/diagnostics/doctor.py +153 -20
- agentmetry-0.5.0/agentmetry/core/diagnostics/hook_coverage.py +240 -0
- agentmetry-0.5.0/agentmetry/core/diagnostics/mcp_inventory.py +256 -0
- agentmetry-0.5.0/agentmetry/core/diagnostics/mcp_schema.py +268 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/version.py +1 -1
- agentmetry-0.5.0/agentmetry/hooks/__init__.py +1 -0
- agentmetry-0.5.0/agentmetry/hooks/ingest.py +1567 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/pyproject.toml +14 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_agentmetry_audit.py +3 -1
- agentmetry-0.5.0/tests/test_atlas_detection_enrichment.py +332 -0
- agentmetry-0.5.0/tests/test_atlas_mapping.py +164 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_audit_sinks.py +41 -1
- agentmetry-0.5.0/tests/test_chronicle_adapter.py +241 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_cloudevents_adapter.py +2 -0
- agentmetry-0.5.0/tests/test_detection_duplicate_emission.py +184 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_detection_evasion.py +91 -0
- agentmetry-0.5.0/tests/test_detection_quoted_command_words.py +156 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_doctor.py +135 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_dogfood_freeze.py +48 -0
- agentmetry-0.5.0/tests/test_ecs_threat.py +132 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_external_ingest.py +2 -1
- agentmetry-0.5.0/tests/test_heartbeat.py +249 -0
- agentmetry-0.5.0/tests/test_hook_bootstrap.py +348 -0
- agentmetry-0.5.0/tests/test_hook_coverage.py +283 -0
- agentmetry-0.5.0/tests/test_mcp_audit_proxy.py +134 -0
- agentmetry-0.5.0/tests/test_mcp_inventory.py +205 -0
- agentmetry-0.5.0/tests/test_mcp_schema.py +233 -0
- agentmetry-0.5.0/tests/test_trail_anchor.py +268 -0
- agentmetry-0.5.0/tests/test_trail_concurrency.py +182 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tools/mcp_audit_proxy.py +53 -6
- agentmetry-0.4.0/tests/test_hook_bootstrap.py +0 -144
- agentmetry-0.4.0/tests/test_mcp_audit_proxy.py +0 -57
- {agentmetry-0.4.0 → agentmetry-0.5.0}/.dockerignore +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/.env.agentmetry-demo +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/.gitignore +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/Dockerfile +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/README.md +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/__init__.py +0 -0
- {agentmetry-0.4.0/agentmetry/core/diagnostics → agentmetry-0.5.0/agentmetry/api}/__init__.py +0 -0
- {agentmetry-0.4.0/agentmetry/core/audit/adapters → agentmetry-0.5.0/agentmetry/api/routes}/__init__.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/api/websocket.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/api/ws_bridge.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/cli/__main__.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/__init__.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/__init__.py +0 -0
- {agentmetry-0.4.0/agentmetry/api/routes → agentmetry-0.5.0/agentmetry/core/audit/adapters}/__init__.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/adapters/splunk.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/alerts.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/compliance_digest.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/__init__.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/benchmark.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/attack_approval_denied_then_executed.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/attack_arbitrary_host_stage_execute.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/attack_autonomous_unapproved_write.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/attack_credential_exfil.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/attack_credential_then_cloud_api.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/attack_destructive_delete_burst.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/attack_discovery_then_collect.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/attack_dotfile_then_git_push.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/attack_encoded_command_download.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/attack_env_credential_exfil.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/attack_hashed_only_no_command.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/attack_interpreter_egress.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/attack_pr_merged_without_review.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/attack_proc_substitution_cradle.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/attack_remote_pipe_to_shell.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/attack_remote_staging_then_execute.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/attack_session_tool_burst.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/attack_single_command_exfil.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/attack_ssh_directory_exfil.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/attack_subagent_swarm.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/attack_timestamp_collision.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/attack_untrusted_input_then_action.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/benign_authoring_merge_fixtures.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/benign_autonomous_after_approval.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/benign_build_artifact_cleanup.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/benign_ci_artifact_download.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/benign_database_migration.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/benign_dependency_install_and_build.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/benign_download_release_archive.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/benign_fetch_data_then_run_repo_script.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/benign_fetch_dataset_then_analyse.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/benign_fetch_lockfile_then_install.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/benign_git_review_and_push.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/benign_human_driven_deletes.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/benign_local_api_probing.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/benign_long_but_calm_session.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/benign_loopback_is_not_egress.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/benign_loopback_pipe_to_interpreter.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/benign_ordinary_development.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/benign_package_manager_after_fetch.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/benign_reading_config_that_is_not_secret.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/benign_remote_api_call_no_credentials.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/benign_research_then_docs.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/benign_reversed_order_is_not_exfil.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/benign_test_and_fix_loop.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/corpus/benign_writing_about_credentials.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/engine.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/live_store.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/models.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/detection/yaml_config.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/dlp/__init__.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/dlp/loader.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/dlp/models.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/dlp/scanner.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/evidence_pack.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/hashing.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/identity.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/migrate.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/policy.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/redaction.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/replay.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/run_context.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/spool.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/tool_policy/__init__.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/tool_policy/evaluator.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/tool_policy/loader.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/tool_policy/models.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/trail_db.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/audit/trail_merkle.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/auth.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/bus/__init__.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/bus/bridges.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/bus/bus.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/bus/events.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/bus/outbox.py +0 -0
- {agentmetry-0.4.0/agentmetry/api → agentmetry-0.5.0/agentmetry/core/diagnostics}/__init__.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/diagnostics/autostart.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/diagnostics/driver_paths.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/diagnostics/env_file.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/drivers/__init__.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/drivers/host.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/drivers/permissions.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/drivers/spec.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/extensions.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/core/health.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/policies/detection/manifest.yaml +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/policies/dlp/manifest.yaml +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/policies/opa/agent_rules.rego +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/agentmetry/policies/tool/manifest.yaml +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/conftest.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/fixtures/agt_filesink_exfil.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/fixtures/agt_filesink_mixed.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/fixtures/fake_mcp_server.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_agentmetry_ingest_client.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_agt_adapter.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_audit_stats.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_audit_tail.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_auth.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_autostart.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_boot_sequence.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_burst_windows.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_chinese_agent_hooks.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_chinese_agent_sprint_b.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_chinese_agent_sprint_c.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_cli_backup.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_compliance_digest.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_crewai_adapter.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_detection_adi.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_detection_benchmark.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_detection_default_config.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_detection_disposition.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_detection_disposition_api.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_detection_emit_durability.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_detection_engine.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_detection_hf_incident.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_detection_rule_identity.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_detection_rules_v2.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_dlp_agent_env_override.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_dlp_invisible_unicode.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_dlp_markdown_exfil.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_dlp_scanner.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_dogfood.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_driver_paths.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_event_bus.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_evidence_pack.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_extensions.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_forwarding_shape.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_hook_enforcement_timing.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_hook_spool.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_isolation.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_launch_targets.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_live_detection.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_live_detection_store.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_mitre_dlp.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_opensre_adapter.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_readme_claims.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_redaction.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_replay.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_static_serving.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_subagent_lifecycle.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_tool_policy.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_tool_policy_agent_cli.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_tool_policy_git_hooks.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_trail_chain.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_trail_merkle.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_unattended_agent_policy.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_version.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tests/test_yaml_detection_rules.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.5.0}/tools/vault_fs_server.py +0 -0
|
@@ -17,4 +17,11 @@ AGENTMETRY_DLP_MODE=log
|
|
|
17
17
|
AGENTMETRY_AUDIT_EXPORT_ENABLED=1
|
|
18
18
|
AGENTMETRY_AUDIT_SINK=file
|
|
19
19
|
|
|
20
|
+
# Anchor log, when it is not the file sitting beside the trail. Point this at a
|
|
21
|
+
# working copy of a remote this machine cannot rewrite and `verify --trail` and
|
|
22
|
+
# `doctor` will check against it without being handed --anchors. Unset is fine:
|
|
23
|
+
# the chain still verifies, it just cannot survive an attacker who owns this
|
|
24
|
+
# host. See docs/anchoring.md.
|
|
25
|
+
# AGENTMETRY_ANCHOR_LOG=../agentmetry-anchors/home-lab/agentmetry-trail.anchors.jsonl
|
|
26
|
+
|
|
20
27
|
# More tuning: see repo root .env.example and docs/agentmetry-dogfood-checklist.md
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: agentmetry
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
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
|
|
@@ -78,6 +78,7 @@ async def lifespan(app: FastAPI):
|
|
|
78
78
|
# time — so every hook that fires during the drain is refused and spooled,
|
|
79
79
|
# which makes the next drain larger. The recorder has to be reachable first
|
|
80
80
|
# and catch up second.
|
|
81
|
+
from agentmetry.core.audit.heartbeat import heartbeat_forever
|
|
81
82
|
from agentmetry.core.audit.spool import drain_forever, drain_spool
|
|
82
83
|
|
|
83
84
|
async def _boot_drain() -> None:
|
|
@@ -108,6 +109,10 @@ async def lifespan(app: FastAPI):
|
|
|
108
109
|
# backlog growing unnoticed whenever ingest is unreachable while the
|
|
109
110
|
# process itself stays up.
|
|
110
111
|
asyncio.create_task(drain_forever(), name="spool-drain"),
|
|
112
|
+
# Attest on an interval so a machine that goes quiet is distinguishable
|
|
113
|
+
# from a machine whose hooks were removed. Absence only means something
|
|
114
|
+
# if presence was being asserted.
|
|
115
|
+
asyncio.create_task(heartbeat_forever(), name="heartbeat"),
|
|
111
116
|
]
|
|
112
117
|
|
|
113
118
|
# Drivers mount in the background: a slow npx download must not delay boot.
|
|
@@ -51,7 +51,7 @@ class ExternalIngestBody(BaseModel):
|
|
|
51
51
|
)
|
|
52
52
|
event_type: str = Field(
|
|
53
53
|
...,
|
|
54
|
-
description="session_start | session_end | tool_called | tool_denied | tool_failed | approval_request | approval_response",
|
|
54
|
+
description="session_start | session_end | tool_called | tool_denied | tool_failed | approval_request | approval_response | mcp_schema",
|
|
55
55
|
)
|
|
56
56
|
correlation_id: str = ""
|
|
57
57
|
session_id: str = ""
|
|
@@ -71,6 +71,10 @@ class ExternalIngestBody(BaseModel):
|
|
|
71
71
|
# pydantic drops it and a `log`-mode match is silently lost.
|
|
72
72
|
dlp: dict[str, Any] | None = None
|
|
73
73
|
tool_policy: dict[str, Any] | None = None
|
|
74
|
+
# Compact `tools/list` fingerprint from mcp_audit_proxy. Hash only: the
|
|
75
|
+
# description never leaves the proxy process.
|
|
76
|
+
schema_fingerprint: str = ""
|
|
77
|
+
schema_tool_count: int = 0
|
|
74
78
|
|
|
75
79
|
|
|
76
80
|
def _parse_event_ts(event: dict[str, Any]) -> datetime | None:
|
|
@@ -395,6 +395,130 @@ def cmd_install(args: argparse.Namespace) -> int:
|
|
|
395
395
|
return 0
|
|
396
396
|
|
|
397
397
|
|
|
398
|
+
#: Agents this CLI can install for. Antigravity is absent on purpose: its hooks
|
|
399
|
+
#: live in ~/.gemini and are written by scripts/install_antigravity_hooks.ps1,
|
|
400
|
+
#: which has no Python equivalent. Listing it here and quietly doing nothing
|
|
401
|
+
#: would be the reporting failure this codebase keeps finding in itself.
|
|
402
|
+
_HOOK_INSTALLERS = ("cursor", "claude", "codex", "qwen", "kimi", "qoder", "codebuddy")
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _hooks_status() -> int:
|
|
406
|
+
"""Coverage as an exit code, for a deployment tool rather than a person.
|
|
407
|
+
|
|
408
|
+
Intune Remediations, and every scheduled-check mechanism like it, want a
|
|
409
|
+
detection script whose exit code is the answer. Parsing `doctor` output
|
|
410
|
+
would work until somebody reworded a line, and a coverage check that breaks
|
|
411
|
+
silently on a wording change is worse than no check.
|
|
412
|
+
|
|
413
|
+
The contract:
|
|
414
|
+
0 every agent present here is recorded, or none is installed
|
|
415
|
+
1 at least one agent is present and not recorded, and installing helps
|
|
416
|
+
2 coverage cannot be determined, and installing would not help
|
|
417
|
+
|
|
418
|
+
2 is separate from 1 on purpose. A service profile sees no developer's
|
|
419
|
+
configuration at all, so remediation there would run forever and fix
|
|
420
|
+
nothing. Reporting it as compliant would be the lie this whole subsystem
|
|
421
|
+
exists to prevent, so it is neither.
|
|
422
|
+
"""
|
|
423
|
+
from agentmetry.core.diagnostics import hook_coverage
|
|
424
|
+
|
|
425
|
+
if hook_coverage.is_service_profile():
|
|
426
|
+
print(
|
|
427
|
+
"UNDETERMINABLE: running under a service profile. Hook configuration "
|
|
428
|
+
"lives in each developer's profile and none is visible from here. "
|
|
429
|
+
"Run this in the user's context.",
|
|
430
|
+
file=sys.stderr,
|
|
431
|
+
)
|
|
432
|
+
return 2
|
|
433
|
+
|
|
434
|
+
states = hook_coverage.coverage()
|
|
435
|
+
for line in hook_coverage.summary_lines(states):
|
|
436
|
+
print(line.strip())
|
|
437
|
+
|
|
438
|
+
uncovered = hook_coverage.uncovered(states)
|
|
439
|
+
if uncovered:
|
|
440
|
+
print(f"NEEDS REMEDIATION: {', '.join(uncovered)}")
|
|
441
|
+
return 1
|
|
442
|
+
print("COMPLIANT")
|
|
443
|
+
return 0
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def cmd_hooks(args: argparse.Namespace) -> int:
|
|
447
|
+
"""Install hook configs, defaulting to the agents actually on this machine.
|
|
448
|
+
|
|
449
|
+
The default matters more than it looks. This is what a per-user deployment
|
|
450
|
+
step runs at first logon, where nobody knows in advance which IDEs that
|
|
451
|
+
developer uses. Installing for all seven would write configs for agents that
|
|
452
|
+
are not there; installing for two would miss the ones they do use.
|
|
453
|
+
"""
|
|
454
|
+
from agentmetry.core.audit import hook_bootstrap
|
|
455
|
+
from agentmetry.core.diagnostics import hook_coverage
|
|
456
|
+
|
|
457
|
+
if args.action == "status":
|
|
458
|
+
return _hooks_status()
|
|
459
|
+
|
|
460
|
+
if hook_bootstrap.hook_target() == "none":
|
|
461
|
+
print(
|
|
462
|
+
"No way to reach ingest from here: no checkout, no frozen binary, no "
|
|
463
|
+
"agentmetry-hook script. Installing hook configs now would write "
|
|
464
|
+
"commands that cannot run.",
|
|
465
|
+
file=sys.stderr,
|
|
466
|
+
)
|
|
467
|
+
return 1
|
|
468
|
+
|
|
469
|
+
if args.agent:
|
|
470
|
+
wanted = [a for a in args.agent if a in _HOOK_INSTALLERS]
|
|
471
|
+
unknown = sorted(set(args.agent) - set(wanted))
|
|
472
|
+
if unknown:
|
|
473
|
+
print(f"No installer for: {', '.join(unknown)}", file=sys.stderr)
|
|
474
|
+
return 2
|
|
475
|
+
elif args.all:
|
|
476
|
+
wanted = list(_HOOK_INSTALLERS)
|
|
477
|
+
else:
|
|
478
|
+
states = hook_coverage.coverage()
|
|
479
|
+
wanted = [a for a in _HOOK_INSTALLERS if states.get(a) != hook_coverage.ABSENT]
|
|
480
|
+
if not wanted:
|
|
481
|
+
print("No supported agent found on this machine; nothing to install.")
|
|
482
|
+
return 0
|
|
483
|
+
|
|
484
|
+
failures = 0
|
|
485
|
+
for agent in wanted:
|
|
486
|
+
install = getattr(hook_bootstrap, f"install_{agent}_global_hooks", None)
|
|
487
|
+
if install is None:
|
|
488
|
+
print(f" {agent}: no installer", file=sys.stderr)
|
|
489
|
+
failures += 1
|
|
490
|
+
continue
|
|
491
|
+
try:
|
|
492
|
+
path = install()
|
|
493
|
+
except OSError as exc:
|
|
494
|
+
print(f" {agent}: FAILED ({exc})", file=sys.stderr)
|
|
495
|
+
failures += 1
|
|
496
|
+
continue
|
|
497
|
+
if path is None:
|
|
498
|
+
print(f" {agent}: skipped", file=sys.stderr)
|
|
499
|
+
failures += 1
|
|
500
|
+
else:
|
|
501
|
+
print(f" {agent}: {path}")
|
|
502
|
+
|
|
503
|
+
if "codex" in wanted and not failures:
|
|
504
|
+
print("")
|
|
505
|
+
print("codex: open Codex, run /hooks and approve the entries, or it")
|
|
506
|
+
print("skips them silently and records nothing.")
|
|
507
|
+
return 1 if failures else 0
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def cmd_hook(args: argparse.Namespace) -> int:
|
|
511
|
+
"""Forward one hook event. Present here for discoverability and docs.
|
|
512
|
+
|
|
513
|
+
Hook configs should name the `agentmetry-hook` console script instead. This
|
|
514
|
+
path imports the whole CLI module, including httpx, on a code path that runs
|
|
515
|
+
once per tool call, and a hook has no reason to pay for that.
|
|
516
|
+
"""
|
|
517
|
+
from agentmetry.hooks.ingest import main as ingest_main
|
|
518
|
+
|
|
519
|
+
return ingest_main([args.app, "hook", args.event])
|
|
520
|
+
|
|
521
|
+
|
|
398
522
|
def cmd_uninstall(args: argparse.Namespace) -> int:
|
|
399
523
|
from agentmetry.core.diagnostics import autostart
|
|
400
524
|
|
|
@@ -655,6 +779,110 @@ def cmd_prove(args: argparse.Namespace) -> int:
|
|
|
655
779
|
return 0
|
|
656
780
|
|
|
657
781
|
|
|
782
|
+
def cmd_mcp(args: argparse.Namespace) -> int:
|
|
783
|
+
"""List the MCP servers the agents on this machine are wired to.
|
|
784
|
+
|
|
785
|
+
The trail answers what an agent did. This answers what it was allowed to
|
|
786
|
+
reach, which is the question asked first in every security review and the one
|
|
787
|
+
a behavioural record cannot reconstruct after the fact.
|
|
788
|
+
"""
|
|
789
|
+
from pathlib import Path as _Path
|
|
790
|
+
|
|
791
|
+
from agentmetry.core.diagnostics.mcp_inventory import collect, summary_lines
|
|
792
|
+
|
|
793
|
+
inv = collect(_Path(args.project).resolve() if args.project else _Path.cwd())
|
|
794
|
+
for line in summary_lines(inv):
|
|
795
|
+
print(line)
|
|
796
|
+
from agentmetry.core.diagnostics.mcp_schema import schema_summary_lines
|
|
797
|
+
|
|
798
|
+
for line in schema_summary_lines():
|
|
799
|
+
print(line)
|
|
800
|
+
|
|
801
|
+
if args.digest:
|
|
802
|
+
# Safe to publish beside a trail anchor: it commits to the configured
|
|
803
|
+
# surface without naming a single server.
|
|
804
|
+
print(inv.digest())
|
|
805
|
+
# Findings describe a risk posture, not a broken install, so they are
|
|
806
|
+
# reported rather than turned into a non-zero exit that would break a
|
|
807
|
+
# health check somebody wired into MDM.
|
|
808
|
+
return 0
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
def cmd_anchor(args: argparse.Namespace) -> int:
|
|
812
|
+
"""Publish, list, or check the checkpoints that commit the trail externally.
|
|
813
|
+
|
|
814
|
+
The chain and the Merkle root are both computed on the audited machine from
|
|
815
|
+
the audited file, so an attacker who can rewrite one can rewrite all of them.
|
|
816
|
+
A checkpoint is the escape: publish `(tree_size, root)` off-host and any later
|
|
817
|
+
edit below that size stops matching. This command produces the commitment;
|
|
818
|
+
where it goes is the operator's trust decision, not ours.
|
|
819
|
+
"""
|
|
820
|
+
import json
|
|
821
|
+
|
|
822
|
+
from agentmetry.core.audit.trail_anchor import (
|
|
823
|
+
FileAnchorSink,
|
|
824
|
+
anchor_path,
|
|
825
|
+
build_checkpoint,
|
|
826
|
+
coverage_lines,
|
|
827
|
+
read_checkpoints,
|
|
828
|
+
verify_anchors,
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
path = Path(args.path)
|
|
832
|
+
if not path.is_file():
|
|
833
|
+
print(f"No such file: {path}")
|
|
834
|
+
return 1
|
|
835
|
+
anchors = Path(args.anchors) if args.anchors else anchor_path(path)
|
|
836
|
+
|
|
837
|
+
if args.show:
|
|
838
|
+
checkpoints = read_checkpoints(anchors)
|
|
839
|
+
if not checkpoints:
|
|
840
|
+
print(f"No checkpoints in {anchors}")
|
|
841
|
+
return 0
|
|
842
|
+
print(f"{len(checkpoints)} checkpoint(s) in {anchors}")
|
|
843
|
+
for cp in checkpoints:
|
|
844
|
+
print(f" {cp.timestamp} size {cp.tree_size:>7} root {cp.root_sha256[:16]}…")
|
|
845
|
+
return 0
|
|
846
|
+
|
|
847
|
+
if args.verify:
|
|
848
|
+
coverage = verify_anchors(path, anchors)
|
|
849
|
+
for line in coverage_lines(coverage):
|
|
850
|
+
print(line.strip())
|
|
851
|
+
return 0 if coverage.ok else 1
|
|
852
|
+
|
|
853
|
+
try:
|
|
854
|
+
checkpoint = build_checkpoint(path, host_id=args.host_id or "")
|
|
855
|
+
except ValueError as exc:
|
|
856
|
+
print(str(exc))
|
|
857
|
+
return 1
|
|
858
|
+
|
|
859
|
+
# --print emits without recording. The point is to hand the statement to a
|
|
860
|
+
# sink we do not implement (a timestamp authority, a git commit, a log)
|
|
861
|
+
# without also implying it was anchored here.
|
|
862
|
+
if args.print_only:
|
|
863
|
+
print(checkpoint.statement() if not args.json else json.dumps(checkpoint.to_dict(), indent=2))
|
|
864
|
+
return 0
|
|
865
|
+
|
|
866
|
+
try:
|
|
867
|
+
receipt = FileAnchorSink(anchors).publish(checkpoint)
|
|
868
|
+
except (ValueError, OSError) as exc:
|
|
869
|
+
print(f"FAILED — {exc}")
|
|
870
|
+
return 1
|
|
871
|
+
|
|
872
|
+
print(f"Anchored {checkpoint.tree_size} record(s) at root {checkpoint.root_sha256}")
|
|
873
|
+
print(f" checkpoint: {receipt.ref} in {anchors}")
|
|
874
|
+
print(f" statement: {checkpoint.statement()}")
|
|
875
|
+
# Phrased as a condition, not an assertion. With --anchors the log may
|
|
876
|
+
# already be in a working copy of a protected remote, and telling an
|
|
877
|
+
# operator who has done the work that they have not is how a correct
|
|
878
|
+
# warning teaches people to ignore warnings.
|
|
879
|
+
print(
|
|
880
|
+
" Until a copy of this log lives where this machine cannot write it, "
|
|
881
|
+
"the commitment can be rewritten along with the trail."
|
|
882
|
+
)
|
|
883
|
+
return 0
|
|
884
|
+
|
|
885
|
+
|
|
658
886
|
def cmd_verify(args: argparse.Namespace) -> int:
|
|
659
887
|
import json
|
|
660
888
|
|
|
@@ -689,6 +917,36 @@ def cmd_verify(args: argparse.Namespace) -> int:
|
|
|
689
917
|
# the file, so it is the more useful of the two to publish.
|
|
690
918
|
print(f" merkle root: {root}")
|
|
691
919
|
print(f" tree size: {size} (rfc6962-sha256)")
|
|
920
|
+
|
|
921
|
+
# Everything above was computed from the file being checked, which is
|
|
922
|
+
# the file an attacker with host access would have rewritten. Anchor
|
|
923
|
+
# coverage is the only part of this output that can contradict it, so
|
|
924
|
+
# it prints even when there are no anchors — an operator who never
|
|
925
|
+
# sees the sentence never learns the guarantee has a ceiling.
|
|
926
|
+
from agentmetry.core.audit.trail_anchor import coverage_lines, verify_anchors
|
|
927
|
+
|
|
928
|
+
# Which log this checks is what decides whether the check means
|
|
929
|
+
# anything. Beside the trail, an attacker who rewrote one rewrote
|
|
930
|
+
# the other. On a protected remote, the same comparison is the only
|
|
931
|
+
# thing here they could not forge.
|
|
932
|
+
from agentmetry.core.audit.trail_anchor import resolve_anchor_log
|
|
933
|
+
|
|
934
|
+
anchor_log, source = resolve_anchor_log(path, getattr(args, "anchors", None))
|
|
935
|
+
coverage = verify_anchors(path, anchor_log, local_only=(source == "default"))
|
|
936
|
+
for line in coverage_lines(coverage):
|
|
937
|
+
print(line)
|
|
938
|
+
if source == "config" and not anchor_log.is_file():
|
|
939
|
+
print(f" AGENTMETRY_ANCHOR_LOG points at {anchor_log}, which does not exist")
|
|
940
|
+
if not coverage.ok:
|
|
941
|
+
# The chain printed OK several lines ago and it was telling the
|
|
942
|
+
# truth: the file is internally consistent, because whoever
|
|
943
|
+
# rewrote it made it so. Leaving that as the last word would let
|
|
944
|
+
# a skimmer take the wrong verdict away, so restate it.
|
|
945
|
+
print(
|
|
946
|
+
"FAILED — the chain is internally consistent but contradicts a "
|
|
947
|
+
"published anchor. The file was rewritten."
|
|
948
|
+
)
|
|
949
|
+
return 1
|
|
692
950
|
return 0
|
|
693
951
|
print(f"FAILED — {result.message}")
|
|
694
952
|
if result.first_bad_line:
|
|
@@ -817,6 +1075,37 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
817
1075
|
)
|
|
818
1076
|
serve.add_argument("--host", default="127.0.0.1")
|
|
819
1077
|
serve.add_argument("--port", type=int, default=8000)
|
|
1078
|
+
hooks = sub.add_parser(
|
|
1079
|
+
"hooks",
|
|
1080
|
+
help="install IDE hook configs for the agents on this machine",
|
|
1081
|
+
)
|
|
1082
|
+
hooks.add_argument(
|
|
1083
|
+
"action",
|
|
1084
|
+
choices=("install", "status"),
|
|
1085
|
+
help=(
|
|
1086
|
+
"install writes hook configs; status reports coverage and exits "
|
|
1087
|
+
"0 compliant, 1 needs remediation, 2 undeterminable"
|
|
1088
|
+
),
|
|
1089
|
+
)
|
|
1090
|
+
hooks.add_argument(
|
|
1091
|
+
"--agent",
|
|
1092
|
+
action="append",
|
|
1093
|
+
default=[],
|
|
1094
|
+
help="install only this agent (repeatable). Default: every supported agent present here",
|
|
1095
|
+
)
|
|
1096
|
+
hooks.add_argument(
|
|
1097
|
+
"--all",
|
|
1098
|
+
action="store_true",
|
|
1099
|
+
help="install for every supported agent, present on this machine or not",
|
|
1100
|
+
)
|
|
1101
|
+
|
|
1102
|
+
hook = sub.add_parser(
|
|
1103
|
+
"hook",
|
|
1104
|
+
help="forward one IDE hook event to ingest (see also the agentmetry-hook script)",
|
|
1105
|
+
)
|
|
1106
|
+
hook.add_argument("app", help="cursor | claude | codex | antigravity | qwen | kimi | qoder | codebuddy")
|
|
1107
|
+
hook.add_argument("event", help="the IDE's event name, e.g. PreToolUse")
|
|
1108
|
+
|
|
820
1109
|
sub.add_parser("status", help="orchestrator health and audit export status")
|
|
821
1110
|
stats = sub.add_parser("stats", help="audit trail metrics for dogfood (events, detections)")
|
|
822
1111
|
stats.add_argument("--days", type=int, default=7)
|
|
@@ -879,6 +1168,34 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
879
1168
|
"--record-root", dest="record_root", action="store_true",
|
|
880
1169
|
help="recompute the root and store it in the chain sidecar",
|
|
881
1170
|
)
|
|
1171
|
+
anchor = sub.add_parser(
|
|
1172
|
+
"anchor",
|
|
1173
|
+
help="publish a checkpoint committing the trail somewhere the host cannot rewrite",
|
|
1174
|
+
)
|
|
1175
|
+
anchor.add_argument("path", help="JSONL trail file")
|
|
1176
|
+
anchor.add_argument(
|
|
1177
|
+
"--anchors", default=None,
|
|
1178
|
+
help="anchor log to append to (default: <trail>.anchors.jsonl)",
|
|
1179
|
+
)
|
|
1180
|
+
anchor.add_argument(
|
|
1181
|
+
"--print", dest="print_only", action="store_true",
|
|
1182
|
+
help="print the checkpoint statement without recording it, to hand to an external sink",
|
|
1183
|
+
)
|
|
1184
|
+
anchor.add_argument("--json", action="store_true", help="with --print: emit JSON, not one line")
|
|
1185
|
+
anchor.add_argument("--show", action="store_true", help="list recorded checkpoints")
|
|
1186
|
+
anchor.add_argument(
|
|
1187
|
+
"--verify", action="store_true",
|
|
1188
|
+
help="recompute each checkpoint's root from the trail and compare",
|
|
1189
|
+
)
|
|
1190
|
+
anchor.add_argument("--host-id", dest="host_id", default="", help="identity to stamp on the checkpoint")
|
|
1191
|
+
mcp = sub.add_parser(
|
|
1192
|
+
"mcp", help="list MCP servers configured for the agents on this machine"
|
|
1193
|
+
)
|
|
1194
|
+
mcp.add_argument("--project", default=None, help="project root to include (default: cwd)")
|
|
1195
|
+
mcp.add_argument(
|
|
1196
|
+
"--digest", action="store_true",
|
|
1197
|
+
help="also print the config digest, which names no server and is safe to publish",
|
|
1198
|
+
)
|
|
882
1199
|
verify = sub.add_parser("verify", help="verify evidence pack or JSONL trail chain")
|
|
883
1200
|
verify.add_argument(
|
|
884
1201
|
"path",
|
|
@@ -887,7 +1204,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
887
1204
|
verify.add_argument(
|
|
888
1205
|
"--trail",
|
|
889
1206
|
action="store_true",
|
|
890
|
-
help="verify
|
|
1207
|
+
help="verify the hash chain on an audit JSONL file and report anchor coverage",
|
|
1208
|
+
)
|
|
1209
|
+
verify.add_argument(
|
|
1210
|
+
"--anchors",
|
|
1211
|
+
default=None,
|
|
1212
|
+
help="with --trail: check against this anchor log instead of the one beside "
|
|
1213
|
+
"the trail. Point it at an off-host copy; a local one can be rewritten "
|
|
1214
|
+
"by whoever rewrote the trail.",
|
|
891
1215
|
)
|
|
892
1216
|
doctor = sub.add_parser(
|
|
893
1217
|
"doctor", help="SIEM preflight (manifests, trail chain, health, hooks)"
|
|
@@ -922,6 +1246,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
922
1246
|
"start": cmd_start,
|
|
923
1247
|
"serve": cmd_serve,
|
|
924
1248
|
"stop": cmd_stop,
|
|
1249
|
+
"hooks": cmd_hooks,
|
|
1250
|
+
"hook": cmd_hook,
|
|
925
1251
|
"status": cmd_status,
|
|
926
1252
|
"stats": cmd_stats,
|
|
927
1253
|
"logs": cmd_logs,
|
|
@@ -932,6 +1258,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
932
1258
|
"export": cmd_export,
|
|
933
1259
|
"verify": cmd_verify,
|
|
934
1260
|
"prove": cmd_prove,
|
|
1261
|
+
"anchor": cmd_anchor,
|
|
1262
|
+
"mcp": cmd_mcp,
|
|
935
1263
|
"import-agt": cmd_import_agt,
|
|
936
1264
|
"doctor": cmd_doctor,
|
|
937
1265
|
"benchmark": cmd_benchmark,
|
|
@@ -220,6 +220,8 @@ def agt_to_canonical(
|
|
|
220
220
|
adds up to.
|
|
221
221
|
"""
|
|
222
222
|
from agentmetry.core.audit.detection.traits import classify_command
|
|
223
|
+
from agentmetry.core.audit.atlas import attach_atlas
|
|
224
|
+
from agentmetry.core.audit.canonical import SCHEMA_VERSION
|
|
223
225
|
from agentmetry.core.audit.mitre import get_mitre_mapping
|
|
224
226
|
|
|
225
227
|
data = entry.get("data") if isinstance(entry.get("data"), dict) else {}
|
|
@@ -248,11 +250,12 @@ def agt_to_canonical(
|
|
|
248
250
|
mapping = get_mitre_mapping(resource or "run", data or command)
|
|
249
251
|
if mapping:
|
|
250
252
|
tool["mitre"] = mapping
|
|
253
|
+
attach_atlas(tool)
|
|
251
254
|
|
|
252
255
|
agent_did = str(entry.get("agent_did") or "")
|
|
253
256
|
|
|
254
257
|
return {
|
|
255
|
-
"schema_version":
|
|
258
|
+
"schema_version": SCHEMA_VERSION,
|
|
256
259
|
# AGT's entry_id is `audit_<16 hex>`, not a UUID, so it cannot be used
|
|
257
260
|
# as an event_id directly. Derived deterministically so re-importing the
|
|
258
261
|
# same file does not mint new identities for the same events.
|