agentmetry 0.4.0__tar.gz → 0.6.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.6.0}/.env.example +7 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/.gitignore +4 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/PKG-INFO +4 -3
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/api/main.py +5 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/api/routes/audit.py +5 -1
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/cli/__init__.py +358 -7
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/adapters/agt.py +4 -1
- agentmetry-0.6.0/agentmetry/core/audit/adapters/chronicle.py +260 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/adapters/cloudevents.py +2 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/adapters/ecs.py +71 -0
- agentmetry-0.6.0/agentmetry/core/audit/atlas.py +274 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/canonical.py +11 -1
- agentmetry-0.6.0/agentmetry/core/audit/detection/corpus/attack_secret_manager_then_egress.jsonl +2 -0
- agentmetry-0.6.0/agentmetry/core/audit/detection/corpus/attack_single_quoted_interpreter_egress.jsonl +2 -0
- agentmetry-0.6.0/agentmetry/core/audit/detection/corpus/benign_commit_message_names_cloud_clis.jsonl +3 -0
- agentmetry-0.6.0/agentmetry/core/audit/detection/corpus/benign_secret_names_and_docs.jsonl +5 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/corpus.yaml +49 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/disposition.py +54 -7
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/live.py +17 -2
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/rules.py +20 -4
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/traits.py +69 -1
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/yaml_config.py +8 -2
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/yaml_rules.py +14 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/dogfood.py +27 -2
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/external.py +6 -0
- agentmetry-0.6.0/agentmetry/core/audit/heartbeat.py +313 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/hook_bootstrap.py +243 -27
- agentmetry-0.6.0/agentmetry/core/audit/ingest.py +508 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/mitre.py +11 -6
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/sinks.py +124 -1
- agentmetry-0.6.0/agentmetry/core/audit/trail_anchor.py +490 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/trail_chain.py +177 -11
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/trail_db.py +10 -3
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/bus/audit_exporter.py +5 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/bus/events.py +1 -1
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/config.py +30 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/diagnostics/doctor.py +154 -21
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/diagnostics/driver_paths.py +6 -4
- agentmetry-0.6.0/agentmetry/core/diagnostics/hook_coverage.py +240 -0
- agentmetry-0.6.0/agentmetry/core/diagnostics/mcp_inventory.py +256 -0
- agentmetry-0.6.0/agentmetry/core/diagnostics/mcp_schema.py +433 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/version.py +1 -1
- agentmetry-0.6.0/agentmetry/hooks/__init__.py +1 -0
- agentmetry-0.6.0/agentmetry/hooks/ingest.py +1599 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/pyproject.toml +48 -2
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_agentmetry_audit.py +3 -1
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_agentmetry_ingest_client.py +47 -0
- agentmetry-0.6.0/tests/test_atlas_detection_enrichment.py +332 -0
- agentmetry-0.6.0/tests/test_atlas_mapping.py +164 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_audit_sinks.py +41 -1
- agentmetry-0.6.0/tests/test_chronicle_adapter.py +241 -0
- agentmetry-0.6.0/tests/test_cli_commands.py +467 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_cloudevents_adapter.py +2 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_detection_disposition.py +47 -0
- agentmetry-0.6.0/tests/test_detection_duplicate_emission.py +184 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_detection_evasion.py +91 -0
- agentmetry-0.6.0/tests/test_detection_quoted_command_words.py +156 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_doctor.py +135 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_dogfood_freeze.py +48 -0
- agentmetry-0.6.0/tests/test_ecs_threat.py +132 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_external_ingest.py +2 -1
- agentmetry-0.6.0/tests/test_heartbeat.py +249 -0
- agentmetry-0.6.0/tests/test_hook_bootstrap.py +348 -0
- agentmetry-0.6.0/tests/test_hook_coverage.py +283 -0
- agentmetry-0.6.0/tests/test_mcp_audit_proxy.py +188 -0
- agentmetry-0.6.0/tests/test_mcp_inventory.py +205 -0
- agentmetry-0.6.0/tests/test_mcp_schema.py +490 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_readme_claims.py +53 -0
- agentmetry-0.6.0/tests/test_sigma_pack.py +126 -0
- agentmetry-0.6.0/tests/test_trail_anchor.py +268 -0
- agentmetry-0.6.0/tests/test_trail_concurrency.py +182 -0
- agentmetry-0.6.0/tools/generate_sigma_pack.py +198 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tools/mcp_audit_proxy.py +128 -7
- agentmetry-0.4.0/agentmetry/core/audit/ingest.py +0 -242
- 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.6.0}/.dockerignore +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/.env.agentmetry-demo +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/Dockerfile +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/README.md +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/__init__.py +0 -0
- {agentmetry-0.4.0/agentmetry/core/diagnostics → agentmetry-0.6.0/agentmetry/api}/__init__.py +0 -0
- {agentmetry-0.4.0/agentmetry/core/audit/adapters → agentmetry-0.6.0/agentmetry/api/routes}/__init__.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/api/websocket.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/api/ws_bridge.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/cli/__main__.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/__init__.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/__init__.py +0 -0
- {agentmetry-0.4.0/agentmetry/api/routes → agentmetry-0.6.0/agentmetry/core/audit/adapters}/__init__.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/adapters/splunk.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/alerts.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/compliance_digest.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/__init__.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/benchmark.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_approval_denied_then_executed.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_arbitrary_host_stage_execute.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_autonomous_unapproved_write.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_credential_exfil.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_credential_then_cloud_api.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_destructive_delete_burst.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_discovery_then_collect.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_dotfile_then_git_push.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_encoded_command_download.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_env_credential_exfil.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_hashed_only_no_command.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_interpreter_egress.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_pr_merged_without_review.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_proc_substitution_cradle.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_remote_pipe_to_shell.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_remote_staging_then_execute.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_session_tool_burst.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_single_command_exfil.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_ssh_directory_exfil.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_subagent_swarm.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_timestamp_collision.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_untrusted_input_then_action.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_authoring_merge_fixtures.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_autonomous_after_approval.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_build_artifact_cleanup.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_ci_artifact_download.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_database_migration.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_dependency_install_and_build.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_download_release_archive.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_fetch_data_then_run_repo_script.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_fetch_dataset_then_analyse.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_fetch_lockfile_then_install.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_git_review_and_push.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_human_driven_deletes.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_local_api_probing.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_long_but_calm_session.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_loopback_is_not_egress.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_loopback_pipe_to_interpreter.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_ordinary_development.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_package_manager_after_fetch.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_reading_config_that_is_not_secret.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_remote_api_call_no_credentials.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_research_then_docs.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_reversed_order_is_not_exfil.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_test_and_fix_loop.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_writing_about_credentials.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/engine.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/live_store.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/models.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/dlp/__init__.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/dlp/loader.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/dlp/models.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/dlp/scanner.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/evidence_pack.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/hashing.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/identity.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/migrate.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/policy.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/redaction.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/replay.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/run_context.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/spool.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/tool_policy/__init__.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/tool_policy/evaluator.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/tool_policy/loader.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/tool_policy/models.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/trail_merkle.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/auth.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/bus/__init__.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/bus/bridges.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/bus/bus.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/bus/outbox.py +0 -0
- {agentmetry-0.4.0/agentmetry/api → agentmetry-0.6.0/agentmetry/core/diagnostics}/__init__.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/diagnostics/autostart.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/diagnostics/env_file.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/drivers/__init__.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/drivers/host.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/drivers/permissions.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/drivers/spec.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/extensions.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/health.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/policies/detection/manifest.yaml +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/policies/dlp/manifest.yaml +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/policies/opa/agent_rules.rego +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/policies/tool/manifest.yaml +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/conftest.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/fixtures/agt_filesink_exfil.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/fixtures/agt_filesink_mixed.jsonl +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/fixtures/fake_mcp_server.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_agt_adapter.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_audit_stats.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_audit_tail.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_auth.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_autostart.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_boot_sequence.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_burst_windows.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_chinese_agent_hooks.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_chinese_agent_sprint_b.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_chinese_agent_sprint_c.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_cli_backup.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_compliance_digest.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_crewai_adapter.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_detection_adi.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_detection_benchmark.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_detection_default_config.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_detection_disposition_api.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_detection_emit_durability.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_detection_engine.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_detection_hf_incident.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_detection_rule_identity.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_detection_rules_v2.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_dlp_agent_env_override.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_dlp_invisible_unicode.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_dlp_markdown_exfil.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_dlp_scanner.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_dogfood.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_driver_paths.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_event_bus.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_evidence_pack.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_extensions.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_forwarding_shape.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_hook_enforcement_timing.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_hook_spool.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_isolation.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_launch_targets.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_live_detection.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_live_detection_store.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_mitre_dlp.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_opensre_adapter.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_redaction.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_replay.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_static_serving.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_subagent_lifecycle.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_tool_policy.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_tool_policy_agent_cli.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_tool_policy_git_hooks.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_trail_chain.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_trail_merkle.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_unattended_agent_policy.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_version.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_yaml_detection_rules.py +0 -0
- {agentmetry-0.4.0 → agentmetry-0.6.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.6.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
|
|
@@ -35,8 +35,9 @@ Requires-Dist: uvicorn[standard]>=0.32.0
|
|
|
35
35
|
Requires-Dist: websockets>=14.0
|
|
36
36
|
Provides-Extra: dev
|
|
37
37
|
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
38
|
+
Requires-Dist: pytest-cov>=6.0; extra == 'dev'
|
|
38
39
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
39
|
-
Requires-Dist: ruff==0.16.
|
|
40
|
+
Requires-Dist: ruff==0.16.4; extra == 'dev'
|
|
40
41
|
Description-Content-Type: text/markdown
|
|
41
42
|
|
|
42
43
|
# 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:
|
|
@@ -7,6 +7,7 @@ Pure stdlib + httpx; never imports the FastAPI app (fast startup, no side effect
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import argparse
|
|
10
|
+
import logging
|
|
10
11
|
import os
|
|
11
12
|
import shutil
|
|
12
13
|
import socket
|
|
@@ -30,11 +31,13 @@ _TASK_NAME = "Agentmetry Orchestrator"
|
|
|
30
31
|
# Paths bundled by backup, relative to the repo root.
|
|
31
32
|
_BACKUP_PREFIXES = ("vault/", "apps/orchestrator/data/")
|
|
32
33
|
_BACKUP_EXCLUDE_DIRS = {"logs"}
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
33
36
|
_BACKUP_EXCLUDE_SUFFIXES = {".pid"}
|
|
34
37
|
|
|
35
38
|
|
|
36
39
|
def _base_url(port: int, host: str = "127.0.0.1") -> str:
|
|
37
|
-
display = host if host != "0.0.0.0" else "127.0.0.1"
|
|
40
|
+
display = host if host != "0.0.0.0" else "127.0.0.1" # noqa: S104
|
|
38
41
|
return f"http://{display}:{port}"
|
|
39
42
|
|
|
40
43
|
|
|
@@ -65,8 +68,12 @@ def _fetch_health(port: int) -> dict | None:
|
|
|
65
68
|
resp = httpx.get(f"{_base_url(port)}/api/v1/health", timeout=10.0)
|
|
66
69
|
if resp.status_code == 200:
|
|
67
70
|
return resp.json()
|
|
68
|
-
except Exception:
|
|
69
|
-
|
|
71
|
+
except Exception as exc:
|
|
72
|
+
# Not running is the expected answer here and `None` says so. Logged at
|
|
73
|
+
# debug anyway: a probe that fails for a reason other than "nothing is
|
|
74
|
+
# listening" (TLS, a proxy, a permission error) otherwise looks identical
|
|
75
|
+
# to a stopped orchestrator, and that is an hour of somebody's afternoon.
|
|
76
|
+
logger.debug("health probe on port %s failed: %s", port, exc)
|
|
70
77
|
return None
|
|
71
78
|
|
|
72
79
|
|
|
@@ -77,7 +84,7 @@ def cmd_start(args: argparse.Namespace) -> int:
|
|
|
77
84
|
host = getattr(args, "host", "127.0.0.1")
|
|
78
85
|
if _fetch_health(args.port):
|
|
79
86
|
print(f"Already running on {_base_url(args.port, host)}")
|
|
80
|
-
if host == "0.0.0.0":
|
|
87
|
+
if host == "0.0.0.0": # noqa: S104
|
|
81
88
|
_print_lan_hint(args.port)
|
|
82
89
|
return 0
|
|
83
90
|
|
|
@@ -112,7 +119,7 @@ def cmd_start(args: argparse.Namespace) -> int:
|
|
|
112
119
|
while time.monotonic() < deadline:
|
|
113
120
|
if _fetch_health(args.port):
|
|
114
121
|
print(f"Agentmetry running on {_base_url(args.port, host)} (pid {proc.pid})")
|
|
115
|
-
if host == "0.0.0.0":
|
|
122
|
+
if host == "0.0.0.0": # noqa: S104
|
|
116
123
|
_print_lan_hint(args.port)
|
|
117
124
|
return 0
|
|
118
125
|
if proc.poll() is not None:
|
|
@@ -395,6 +402,130 @@ def cmd_install(args: argparse.Namespace) -> int:
|
|
|
395
402
|
return 0
|
|
396
403
|
|
|
397
404
|
|
|
405
|
+
#: Agents this CLI can install for. Antigravity is absent on purpose: its hooks
|
|
406
|
+
#: live in ~/.gemini and are written by scripts/install_antigravity_hooks.ps1,
|
|
407
|
+
#: which has no Python equivalent. Listing it here and quietly doing nothing
|
|
408
|
+
#: would be the reporting failure this codebase keeps finding in itself.
|
|
409
|
+
_HOOK_INSTALLERS = ("cursor", "claude", "codex", "qwen", "kimi", "qoder", "codebuddy")
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _hooks_status() -> int:
|
|
413
|
+
"""Coverage as an exit code, for a deployment tool rather than a person.
|
|
414
|
+
|
|
415
|
+
Intune Remediations, and every scheduled-check mechanism like it, want a
|
|
416
|
+
detection script whose exit code is the answer. Parsing `doctor` output
|
|
417
|
+
would work until somebody reworded a line, and a coverage check that breaks
|
|
418
|
+
silently on a wording change is worse than no check.
|
|
419
|
+
|
|
420
|
+
The contract:
|
|
421
|
+
0 every agent present here is recorded, or none is installed
|
|
422
|
+
1 at least one agent is present and not recorded, and installing helps
|
|
423
|
+
2 coverage cannot be determined, and installing would not help
|
|
424
|
+
|
|
425
|
+
2 is separate from 1 on purpose. A service profile sees no developer's
|
|
426
|
+
configuration at all, so remediation there would run forever and fix
|
|
427
|
+
nothing. Reporting it as compliant would be the lie this whole subsystem
|
|
428
|
+
exists to prevent, so it is neither.
|
|
429
|
+
"""
|
|
430
|
+
from agentmetry.core.diagnostics import hook_coverage
|
|
431
|
+
|
|
432
|
+
if hook_coverage.is_service_profile():
|
|
433
|
+
print(
|
|
434
|
+
"UNDETERMINABLE: running under a service profile. Hook configuration "
|
|
435
|
+
"lives in each developer's profile and none is visible from here. "
|
|
436
|
+
"Run this in the user's context.",
|
|
437
|
+
file=sys.stderr,
|
|
438
|
+
)
|
|
439
|
+
return 2
|
|
440
|
+
|
|
441
|
+
states = hook_coverage.coverage()
|
|
442
|
+
for line in hook_coverage.summary_lines(states):
|
|
443
|
+
print(line.strip())
|
|
444
|
+
|
|
445
|
+
uncovered = hook_coverage.uncovered(states)
|
|
446
|
+
if uncovered:
|
|
447
|
+
print(f"NEEDS REMEDIATION: {', '.join(uncovered)}")
|
|
448
|
+
return 1
|
|
449
|
+
print("COMPLIANT")
|
|
450
|
+
return 0
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def cmd_hooks(args: argparse.Namespace) -> int:
|
|
454
|
+
"""Install hook configs, defaulting to the agents actually on this machine.
|
|
455
|
+
|
|
456
|
+
The default matters more than it looks. This is what a per-user deployment
|
|
457
|
+
step runs at first logon, where nobody knows in advance which IDEs that
|
|
458
|
+
developer uses. Installing for all seven would write configs for agents that
|
|
459
|
+
are not there; installing for two would miss the ones they do use.
|
|
460
|
+
"""
|
|
461
|
+
from agentmetry.core.audit import hook_bootstrap
|
|
462
|
+
from agentmetry.core.diagnostics import hook_coverage
|
|
463
|
+
|
|
464
|
+
if args.action == "status":
|
|
465
|
+
return _hooks_status()
|
|
466
|
+
|
|
467
|
+
if hook_bootstrap.hook_target() == "none":
|
|
468
|
+
print(
|
|
469
|
+
"No way to reach ingest from here: no checkout, no frozen binary, no "
|
|
470
|
+
"agentmetry-hook script. Installing hook configs now would write "
|
|
471
|
+
"commands that cannot run.",
|
|
472
|
+
file=sys.stderr,
|
|
473
|
+
)
|
|
474
|
+
return 1
|
|
475
|
+
|
|
476
|
+
if args.agent:
|
|
477
|
+
wanted = [a for a in args.agent if a in _HOOK_INSTALLERS]
|
|
478
|
+
unknown = sorted(set(args.agent) - set(wanted))
|
|
479
|
+
if unknown:
|
|
480
|
+
print(f"No installer for: {', '.join(unknown)}", file=sys.stderr)
|
|
481
|
+
return 2
|
|
482
|
+
elif args.all:
|
|
483
|
+
wanted = list(_HOOK_INSTALLERS)
|
|
484
|
+
else:
|
|
485
|
+
states = hook_coverage.coverage()
|
|
486
|
+
wanted = [a for a in _HOOK_INSTALLERS if states.get(a) != hook_coverage.ABSENT]
|
|
487
|
+
if not wanted:
|
|
488
|
+
print("No supported agent found on this machine; nothing to install.")
|
|
489
|
+
return 0
|
|
490
|
+
|
|
491
|
+
failures = 0
|
|
492
|
+
for agent in wanted:
|
|
493
|
+
install = getattr(hook_bootstrap, f"install_{agent}_global_hooks", None)
|
|
494
|
+
if install is None:
|
|
495
|
+
print(f" {agent}: no installer", file=sys.stderr)
|
|
496
|
+
failures += 1
|
|
497
|
+
continue
|
|
498
|
+
try:
|
|
499
|
+
path = install()
|
|
500
|
+
except OSError as exc:
|
|
501
|
+
print(f" {agent}: FAILED ({exc})", file=sys.stderr)
|
|
502
|
+
failures += 1
|
|
503
|
+
continue
|
|
504
|
+
if path is None:
|
|
505
|
+
print(f" {agent}: skipped", file=sys.stderr)
|
|
506
|
+
failures += 1
|
|
507
|
+
else:
|
|
508
|
+
print(f" {agent}: {path}")
|
|
509
|
+
|
|
510
|
+
if "codex" in wanted and not failures:
|
|
511
|
+
print("")
|
|
512
|
+
print("codex: open Codex, run /hooks and approve the entries, or it")
|
|
513
|
+
print("skips them silently and records nothing.")
|
|
514
|
+
return 1 if failures else 0
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def cmd_hook(args: argparse.Namespace) -> int:
|
|
518
|
+
"""Forward one hook event. Present here for discoverability and docs.
|
|
519
|
+
|
|
520
|
+
Hook configs should name the `agentmetry-hook` console script instead. This
|
|
521
|
+
path imports the whole CLI module, including httpx, on a code path that runs
|
|
522
|
+
once per tool call, and a hook has no reason to pay for that.
|
|
523
|
+
"""
|
|
524
|
+
from agentmetry.hooks.ingest import main as ingest_main
|
|
525
|
+
|
|
526
|
+
return ingest_main([args.app, "hook", args.event])
|
|
527
|
+
|
|
528
|
+
|
|
398
529
|
def cmd_uninstall(args: argparse.Namespace) -> int:
|
|
399
530
|
from agentmetry.core.diagnostics import autostart
|
|
400
531
|
|
|
@@ -655,6 +786,110 @@ def cmd_prove(args: argparse.Namespace) -> int:
|
|
|
655
786
|
return 0
|
|
656
787
|
|
|
657
788
|
|
|
789
|
+
def cmd_mcp(args: argparse.Namespace) -> int:
|
|
790
|
+
"""List the MCP servers the agents on this machine are wired to.
|
|
791
|
+
|
|
792
|
+
The trail answers what an agent did. This answers what it was allowed to
|
|
793
|
+
reach, which is the question asked first in every security review and the one
|
|
794
|
+
a behavioural record cannot reconstruct after the fact.
|
|
795
|
+
"""
|
|
796
|
+
from pathlib import Path as _Path
|
|
797
|
+
|
|
798
|
+
from agentmetry.core.diagnostics.mcp_inventory import collect, summary_lines
|
|
799
|
+
|
|
800
|
+
inv = collect(_Path(args.project).resolve() if args.project else _Path.cwd())
|
|
801
|
+
for line in summary_lines(inv):
|
|
802
|
+
print(line)
|
|
803
|
+
from agentmetry.core.diagnostics.mcp_schema import schema_summary_lines
|
|
804
|
+
|
|
805
|
+
for line in schema_summary_lines():
|
|
806
|
+
print(line)
|
|
807
|
+
|
|
808
|
+
if args.digest:
|
|
809
|
+
# Safe to publish beside a trail anchor: it commits to the configured
|
|
810
|
+
# surface without naming a single server.
|
|
811
|
+
print(inv.digest())
|
|
812
|
+
# Findings describe a risk posture, not a broken install, so they are
|
|
813
|
+
# reported rather than turned into a non-zero exit that would break a
|
|
814
|
+
# health check somebody wired into MDM.
|
|
815
|
+
return 0
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
def cmd_anchor(args: argparse.Namespace) -> int:
|
|
819
|
+
"""Publish, list, or check the checkpoints that commit the trail externally.
|
|
820
|
+
|
|
821
|
+
The chain and the Merkle root are both computed on the audited machine from
|
|
822
|
+
the audited file, so an attacker who can rewrite one can rewrite all of them.
|
|
823
|
+
A checkpoint is the escape: publish `(tree_size, root)` off-host and any later
|
|
824
|
+
edit below that size stops matching. This command produces the commitment;
|
|
825
|
+
where it goes is the operator's trust decision, not ours.
|
|
826
|
+
"""
|
|
827
|
+
import json
|
|
828
|
+
|
|
829
|
+
from agentmetry.core.audit.trail_anchor import (
|
|
830
|
+
FileAnchorSink,
|
|
831
|
+
anchor_path,
|
|
832
|
+
build_checkpoint,
|
|
833
|
+
coverage_lines,
|
|
834
|
+
read_checkpoints,
|
|
835
|
+
verify_anchors,
|
|
836
|
+
)
|
|
837
|
+
|
|
838
|
+
path = Path(args.path)
|
|
839
|
+
if not path.is_file():
|
|
840
|
+
print(f"No such file: {path}")
|
|
841
|
+
return 1
|
|
842
|
+
anchors = Path(args.anchors) if args.anchors else anchor_path(path)
|
|
843
|
+
|
|
844
|
+
if args.show:
|
|
845
|
+
checkpoints = read_checkpoints(anchors)
|
|
846
|
+
if not checkpoints:
|
|
847
|
+
print(f"No checkpoints in {anchors}")
|
|
848
|
+
return 0
|
|
849
|
+
print(f"{len(checkpoints)} checkpoint(s) in {anchors}")
|
|
850
|
+
for cp in checkpoints:
|
|
851
|
+
print(f" {cp.timestamp} size {cp.tree_size:>7} root {cp.root_sha256[:16]}…")
|
|
852
|
+
return 0
|
|
853
|
+
|
|
854
|
+
if args.verify:
|
|
855
|
+
coverage = verify_anchors(path, anchors)
|
|
856
|
+
for line in coverage_lines(coverage):
|
|
857
|
+
print(line.strip())
|
|
858
|
+
return 0 if coverage.ok else 1
|
|
859
|
+
|
|
860
|
+
try:
|
|
861
|
+
checkpoint = build_checkpoint(path, host_id=args.host_id or "")
|
|
862
|
+
except ValueError as exc:
|
|
863
|
+
print(str(exc))
|
|
864
|
+
return 1
|
|
865
|
+
|
|
866
|
+
# --print emits without recording. The point is to hand the statement to a
|
|
867
|
+
# sink we do not implement (a timestamp authority, a git commit, a log)
|
|
868
|
+
# without also implying it was anchored here.
|
|
869
|
+
if args.print_only:
|
|
870
|
+
print(checkpoint.statement() if not args.json else json.dumps(checkpoint.to_dict(), indent=2))
|
|
871
|
+
return 0
|
|
872
|
+
|
|
873
|
+
try:
|
|
874
|
+
receipt = FileAnchorSink(anchors).publish(checkpoint)
|
|
875
|
+
except (ValueError, OSError) as exc:
|
|
876
|
+
print(f"FAILED — {exc}")
|
|
877
|
+
return 1
|
|
878
|
+
|
|
879
|
+
print(f"Anchored {checkpoint.tree_size} record(s) at root {checkpoint.root_sha256}")
|
|
880
|
+
print(f" checkpoint: {receipt.ref} in {anchors}")
|
|
881
|
+
print(f" statement: {checkpoint.statement()}")
|
|
882
|
+
# Phrased as a condition, not an assertion. With --anchors the log may
|
|
883
|
+
# already be in a working copy of a protected remote, and telling an
|
|
884
|
+
# operator who has done the work that they have not is how a correct
|
|
885
|
+
# warning teaches people to ignore warnings.
|
|
886
|
+
print(
|
|
887
|
+
" Until a copy of this log lives where this machine cannot write it, "
|
|
888
|
+
"the commitment can be rewritten along with the trail."
|
|
889
|
+
)
|
|
890
|
+
return 0
|
|
891
|
+
|
|
892
|
+
|
|
658
893
|
def cmd_verify(args: argparse.Namespace) -> int:
|
|
659
894
|
import json
|
|
660
895
|
|
|
@@ -689,6 +924,47 @@ def cmd_verify(args: argparse.Namespace) -> int:
|
|
|
689
924
|
# the file, so it is the more useful of the two to publish.
|
|
690
925
|
print(f" merkle root: {root}")
|
|
691
926
|
print(f" tree size: {size} (rfc6962-sha256)")
|
|
927
|
+
|
|
928
|
+
# Everything above was computed from the file being checked, which is
|
|
929
|
+
# the file an attacker with host access would have rewritten. Anchor
|
|
930
|
+
# coverage is the only part of this output that can contradict it, so
|
|
931
|
+
# it prints even when there are no anchors — an operator who never
|
|
932
|
+
# sees the sentence never learns the guarantee has a ceiling.
|
|
933
|
+
from agentmetry.core.audit.trail_anchor import coverage_lines, verify_anchors
|
|
934
|
+
|
|
935
|
+
# Which log this checks is what decides whether the check means
|
|
936
|
+
# anything. Beside the trail, an attacker who rewrote one rewrote
|
|
937
|
+
# the other. On a protected remote, the same comparison is the only
|
|
938
|
+
# thing here they could not forge.
|
|
939
|
+
from agentmetry.core.audit.trail_anchor import resolve_anchor_log
|
|
940
|
+
|
|
941
|
+
anchor_log, source = resolve_anchor_log(path, getattr(args, "anchors", None))
|
|
942
|
+
coverage = verify_anchors(path, anchor_log, local_only=(source == "default"))
|
|
943
|
+
for line in coverage_lines(coverage):
|
|
944
|
+
print(line)
|
|
945
|
+
if source == "config" and not anchor_log.is_file():
|
|
946
|
+
print(f" AGENTMETRY_ANCHOR_LOG points at {anchor_log}, which does not exist")
|
|
947
|
+
if coverage.tampering:
|
|
948
|
+
# The chain printed OK several lines ago and it was telling the
|
|
949
|
+
# truth: the file is internally consistent, because whoever
|
|
950
|
+
# rewrote it made it so. Leaving that as the last word would let
|
|
951
|
+
# a skimmer take the wrong verdict away, so restate it.
|
|
952
|
+
print(
|
|
953
|
+
"FAILED — the chain is internally consistent but contradicts a "
|
|
954
|
+
"published anchor. The file was rewritten."
|
|
955
|
+
)
|
|
956
|
+
return 1
|
|
957
|
+
if not coverage.ok:
|
|
958
|
+
# Checkpoints exist and none of them cover this file. Worth
|
|
959
|
+
# failing, because an operator who configured an anchor log
|
|
960
|
+
# believes they are anchored. Not worth calling tampering:
|
|
961
|
+
# nothing here contradicts the trail, and a trust command that
|
|
962
|
+
# says "rewritten" about a clean file gets muted.
|
|
963
|
+
print(
|
|
964
|
+
"FAILED — no checkpoint in this anchor log covers this trail. "
|
|
965
|
+
"Check AGENTMETRY_ANCHOR_LOG, or anchor this file."
|
|
966
|
+
)
|
|
967
|
+
return 1
|
|
692
968
|
return 0
|
|
693
969
|
print(f"FAILED — {result.message}")
|
|
694
970
|
if result.first_bad_line:
|
|
@@ -797,7 +1073,12 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
797
1073
|
# platform. Same guard as scripts/demo.py.
|
|
798
1074
|
try:
|
|
799
1075
|
sys.stdout.reconfigure(encoding="utf-8") # type: ignore[union-attr]
|
|
800
|
-
except Exception: # pragma: no cover -
|
|
1076
|
+
except Exception: # noqa: S110 # pragma: no cover - host console dependent
|
|
1077
|
+
# The one swallow with nowhere to swallow into: this runs before logging
|
|
1078
|
+
# is configured, and the failure means the console cannot render UTF-8,
|
|
1079
|
+
# which is also why a log line about it would not render. The fallback is
|
|
1080
|
+
# the console's own encoding, which is what would have happened without
|
|
1081
|
+
# the call.
|
|
801
1082
|
pass
|
|
802
1083
|
|
|
803
1084
|
parser = argparse.ArgumentParser(prog="agentmetry", description="Agentmetry local ops")
|
|
@@ -817,6 +1098,37 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
817
1098
|
)
|
|
818
1099
|
serve.add_argument("--host", default="127.0.0.1")
|
|
819
1100
|
serve.add_argument("--port", type=int, default=8000)
|
|
1101
|
+
hooks = sub.add_parser(
|
|
1102
|
+
"hooks",
|
|
1103
|
+
help="install IDE hook configs for the agents on this machine",
|
|
1104
|
+
)
|
|
1105
|
+
hooks.add_argument(
|
|
1106
|
+
"action",
|
|
1107
|
+
choices=("install", "status"),
|
|
1108
|
+
help=(
|
|
1109
|
+
"install writes hook configs; status reports coverage and exits "
|
|
1110
|
+
"0 compliant, 1 needs remediation, 2 undeterminable"
|
|
1111
|
+
),
|
|
1112
|
+
)
|
|
1113
|
+
hooks.add_argument(
|
|
1114
|
+
"--agent",
|
|
1115
|
+
action="append",
|
|
1116
|
+
default=[],
|
|
1117
|
+
help="install only this agent (repeatable). Default: every supported agent present here",
|
|
1118
|
+
)
|
|
1119
|
+
hooks.add_argument(
|
|
1120
|
+
"--all",
|
|
1121
|
+
action="store_true",
|
|
1122
|
+
help="install for every supported agent, present on this machine or not",
|
|
1123
|
+
)
|
|
1124
|
+
|
|
1125
|
+
hook = sub.add_parser(
|
|
1126
|
+
"hook",
|
|
1127
|
+
help="forward one IDE hook event to ingest (see also the agentmetry-hook script)",
|
|
1128
|
+
)
|
|
1129
|
+
hook.add_argument("app", help="cursor | claude | codex | antigravity | qwen | kimi | qoder | codebuddy")
|
|
1130
|
+
hook.add_argument("event", help="the IDE's event name, e.g. PreToolUse")
|
|
1131
|
+
|
|
820
1132
|
sub.add_parser("status", help="orchestrator health and audit export status")
|
|
821
1133
|
stats = sub.add_parser("stats", help="audit trail metrics for dogfood (events, detections)")
|
|
822
1134
|
stats.add_argument("--days", type=int, default=7)
|
|
@@ -879,6 +1191,34 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
879
1191
|
"--record-root", dest="record_root", action="store_true",
|
|
880
1192
|
help="recompute the root and store it in the chain sidecar",
|
|
881
1193
|
)
|
|
1194
|
+
anchor = sub.add_parser(
|
|
1195
|
+
"anchor",
|
|
1196
|
+
help="publish a checkpoint committing the trail somewhere the host cannot rewrite",
|
|
1197
|
+
)
|
|
1198
|
+
anchor.add_argument("path", help="JSONL trail file")
|
|
1199
|
+
anchor.add_argument(
|
|
1200
|
+
"--anchors", default=None,
|
|
1201
|
+
help="anchor log to append to (default: <trail>.anchors.jsonl)",
|
|
1202
|
+
)
|
|
1203
|
+
anchor.add_argument(
|
|
1204
|
+
"--print", dest="print_only", action="store_true",
|
|
1205
|
+
help="print the checkpoint statement without recording it, to hand to an external sink",
|
|
1206
|
+
)
|
|
1207
|
+
anchor.add_argument("--json", action="store_true", help="with --print: emit JSON, not one line")
|
|
1208
|
+
anchor.add_argument("--show", action="store_true", help="list recorded checkpoints")
|
|
1209
|
+
anchor.add_argument(
|
|
1210
|
+
"--verify", action="store_true",
|
|
1211
|
+
help="recompute each checkpoint's root from the trail and compare",
|
|
1212
|
+
)
|
|
1213
|
+
anchor.add_argument("--host-id", dest="host_id", default="", help="identity to stamp on the checkpoint")
|
|
1214
|
+
mcp = sub.add_parser(
|
|
1215
|
+
"mcp", help="list MCP servers configured for the agents on this machine"
|
|
1216
|
+
)
|
|
1217
|
+
mcp.add_argument("--project", default=None, help="project root to include (default: cwd)")
|
|
1218
|
+
mcp.add_argument(
|
|
1219
|
+
"--digest", action="store_true",
|
|
1220
|
+
help="also print the config digest, which names no server and is safe to publish",
|
|
1221
|
+
)
|
|
882
1222
|
verify = sub.add_parser("verify", help="verify evidence pack or JSONL trail chain")
|
|
883
1223
|
verify.add_argument(
|
|
884
1224
|
"path",
|
|
@@ -887,7 +1227,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
887
1227
|
verify.add_argument(
|
|
888
1228
|
"--trail",
|
|
889
1229
|
action="store_true",
|
|
890
|
-
help="verify
|
|
1230
|
+
help="verify the hash chain on an audit JSONL file and report anchor coverage",
|
|
1231
|
+
)
|
|
1232
|
+
verify.add_argument(
|
|
1233
|
+
"--anchors",
|
|
1234
|
+
default=None,
|
|
1235
|
+
help="with --trail: check against this anchor log instead of the one beside "
|
|
1236
|
+
"the trail. Point it at an off-host copy; a local one can be rewritten "
|
|
1237
|
+
"by whoever rewrote the trail.",
|
|
891
1238
|
)
|
|
892
1239
|
doctor = sub.add_parser(
|
|
893
1240
|
"doctor", help="SIEM preflight (manifests, trail chain, health, hooks)"
|
|
@@ -922,6 +1269,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
922
1269
|
"start": cmd_start,
|
|
923
1270
|
"serve": cmd_serve,
|
|
924
1271
|
"stop": cmd_stop,
|
|
1272
|
+
"hooks": cmd_hooks,
|
|
1273
|
+
"hook": cmd_hook,
|
|
925
1274
|
"status": cmd_status,
|
|
926
1275
|
"stats": cmd_stats,
|
|
927
1276
|
"logs": cmd_logs,
|
|
@@ -932,6 +1281,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
932
1281
|
"export": cmd_export,
|
|
933
1282
|
"verify": cmd_verify,
|
|
934
1283
|
"prove": cmd_prove,
|
|
1284
|
+
"anchor": cmd_anchor,
|
|
1285
|
+
"mcp": cmd_mcp,
|
|
935
1286
|
"import-agt": cmd_import_agt,
|
|
936
1287
|
"doctor": cmd_doctor,
|
|
937
1288
|
"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.
|