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.
Files changed (236) hide show
  1. {agentmetry-0.4.0 → agentmetry-0.6.0}/.env.example +7 -0
  2. {agentmetry-0.4.0 → agentmetry-0.6.0}/.gitignore +4 -0
  3. {agentmetry-0.4.0 → agentmetry-0.6.0}/PKG-INFO +4 -3
  4. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/api/main.py +5 -0
  5. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/api/routes/audit.py +5 -1
  6. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/cli/__init__.py +358 -7
  7. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/adapters/agt.py +4 -1
  8. agentmetry-0.6.0/agentmetry/core/audit/adapters/chronicle.py +260 -0
  9. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/adapters/cloudevents.py +2 -0
  10. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/adapters/ecs.py +71 -0
  11. agentmetry-0.6.0/agentmetry/core/audit/atlas.py +274 -0
  12. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/canonical.py +11 -1
  13. agentmetry-0.6.0/agentmetry/core/audit/detection/corpus/attack_secret_manager_then_egress.jsonl +2 -0
  14. agentmetry-0.6.0/agentmetry/core/audit/detection/corpus/attack_single_quoted_interpreter_egress.jsonl +2 -0
  15. agentmetry-0.6.0/agentmetry/core/audit/detection/corpus/benign_commit_message_names_cloud_clis.jsonl +3 -0
  16. agentmetry-0.6.0/agentmetry/core/audit/detection/corpus/benign_secret_names_and_docs.jsonl +5 -0
  17. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/corpus.yaml +49 -0
  18. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/disposition.py +54 -7
  19. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/live.py +17 -2
  20. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/rules.py +20 -4
  21. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/traits.py +69 -1
  22. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/yaml_config.py +8 -2
  23. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/yaml_rules.py +14 -0
  24. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/dogfood.py +27 -2
  25. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/external.py +6 -0
  26. agentmetry-0.6.0/agentmetry/core/audit/heartbeat.py +313 -0
  27. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/hook_bootstrap.py +243 -27
  28. agentmetry-0.6.0/agentmetry/core/audit/ingest.py +508 -0
  29. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/mitre.py +11 -6
  30. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/sinks.py +124 -1
  31. agentmetry-0.6.0/agentmetry/core/audit/trail_anchor.py +490 -0
  32. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/trail_chain.py +177 -11
  33. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/trail_db.py +10 -3
  34. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/bus/audit_exporter.py +5 -0
  35. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/bus/events.py +1 -1
  36. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/config.py +30 -0
  37. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/diagnostics/doctor.py +154 -21
  38. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/diagnostics/driver_paths.py +6 -4
  39. agentmetry-0.6.0/agentmetry/core/diagnostics/hook_coverage.py +240 -0
  40. agentmetry-0.6.0/agentmetry/core/diagnostics/mcp_inventory.py +256 -0
  41. agentmetry-0.6.0/agentmetry/core/diagnostics/mcp_schema.py +433 -0
  42. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/version.py +1 -1
  43. agentmetry-0.6.0/agentmetry/hooks/__init__.py +1 -0
  44. agentmetry-0.6.0/agentmetry/hooks/ingest.py +1599 -0
  45. {agentmetry-0.4.0 → agentmetry-0.6.0}/pyproject.toml +48 -2
  46. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_agentmetry_audit.py +3 -1
  47. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_agentmetry_ingest_client.py +47 -0
  48. agentmetry-0.6.0/tests/test_atlas_detection_enrichment.py +332 -0
  49. agentmetry-0.6.0/tests/test_atlas_mapping.py +164 -0
  50. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_audit_sinks.py +41 -1
  51. agentmetry-0.6.0/tests/test_chronicle_adapter.py +241 -0
  52. agentmetry-0.6.0/tests/test_cli_commands.py +467 -0
  53. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_cloudevents_adapter.py +2 -0
  54. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_detection_disposition.py +47 -0
  55. agentmetry-0.6.0/tests/test_detection_duplicate_emission.py +184 -0
  56. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_detection_evasion.py +91 -0
  57. agentmetry-0.6.0/tests/test_detection_quoted_command_words.py +156 -0
  58. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_doctor.py +135 -0
  59. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_dogfood_freeze.py +48 -0
  60. agentmetry-0.6.0/tests/test_ecs_threat.py +132 -0
  61. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_external_ingest.py +2 -1
  62. agentmetry-0.6.0/tests/test_heartbeat.py +249 -0
  63. agentmetry-0.6.0/tests/test_hook_bootstrap.py +348 -0
  64. agentmetry-0.6.0/tests/test_hook_coverage.py +283 -0
  65. agentmetry-0.6.0/tests/test_mcp_audit_proxy.py +188 -0
  66. agentmetry-0.6.0/tests/test_mcp_inventory.py +205 -0
  67. agentmetry-0.6.0/tests/test_mcp_schema.py +490 -0
  68. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_readme_claims.py +53 -0
  69. agentmetry-0.6.0/tests/test_sigma_pack.py +126 -0
  70. agentmetry-0.6.0/tests/test_trail_anchor.py +268 -0
  71. agentmetry-0.6.0/tests/test_trail_concurrency.py +182 -0
  72. agentmetry-0.6.0/tools/generate_sigma_pack.py +198 -0
  73. {agentmetry-0.4.0 → agentmetry-0.6.0}/tools/mcp_audit_proxy.py +128 -7
  74. agentmetry-0.4.0/agentmetry/core/audit/ingest.py +0 -242
  75. agentmetry-0.4.0/tests/test_hook_bootstrap.py +0 -144
  76. agentmetry-0.4.0/tests/test_mcp_audit_proxy.py +0 -57
  77. {agentmetry-0.4.0 → agentmetry-0.6.0}/.dockerignore +0 -0
  78. {agentmetry-0.4.0 → agentmetry-0.6.0}/.env.agentmetry-demo +0 -0
  79. {agentmetry-0.4.0 → agentmetry-0.6.0}/Dockerfile +0 -0
  80. {agentmetry-0.4.0 → agentmetry-0.6.0}/README.md +0 -0
  81. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/__init__.py +0 -0
  82. {agentmetry-0.4.0/agentmetry/core/diagnostics → agentmetry-0.6.0/agentmetry/api}/__init__.py +0 -0
  83. {agentmetry-0.4.0/agentmetry/core/audit/adapters → agentmetry-0.6.0/agentmetry/api/routes}/__init__.py +0 -0
  84. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/api/websocket.py +0 -0
  85. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/api/ws_bridge.py +0 -0
  86. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/cli/__main__.py +0 -0
  87. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/__init__.py +0 -0
  88. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/__init__.py +0 -0
  89. {agentmetry-0.4.0/agentmetry/api/routes → agentmetry-0.6.0/agentmetry/core/audit/adapters}/__init__.py +0 -0
  90. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/adapters/splunk.py +0 -0
  91. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/alerts.py +0 -0
  92. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/compliance_digest.py +0 -0
  93. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/__init__.py +0 -0
  94. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/benchmark.py +0 -0
  95. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_approval_denied_then_executed.jsonl +0 -0
  96. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_arbitrary_host_stage_execute.jsonl +0 -0
  97. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_autonomous_unapproved_write.jsonl +0 -0
  98. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_credential_exfil.jsonl +0 -0
  99. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_credential_then_cloud_api.jsonl +0 -0
  100. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_destructive_delete_burst.jsonl +0 -0
  101. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_discovery_then_collect.jsonl +0 -0
  102. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_dotfile_then_git_push.jsonl +0 -0
  103. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_encoded_command_download.jsonl +0 -0
  104. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_env_credential_exfil.jsonl +0 -0
  105. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_hashed_only_no_command.jsonl +0 -0
  106. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_interpreter_egress.jsonl +0 -0
  107. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_pr_merged_without_review.jsonl +0 -0
  108. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_proc_substitution_cradle.jsonl +0 -0
  109. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_remote_pipe_to_shell.jsonl +0 -0
  110. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_remote_staging_then_execute.jsonl +0 -0
  111. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_session_tool_burst.jsonl +0 -0
  112. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_single_command_exfil.jsonl +0 -0
  113. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_ssh_directory_exfil.jsonl +0 -0
  114. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_subagent_swarm.jsonl +0 -0
  115. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_timestamp_collision.jsonl +0 -0
  116. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/attack_untrusted_input_then_action.jsonl +0 -0
  117. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_authoring_merge_fixtures.jsonl +0 -0
  118. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_autonomous_after_approval.jsonl +0 -0
  119. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_build_artifact_cleanup.jsonl +0 -0
  120. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_ci_artifact_download.jsonl +0 -0
  121. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_database_migration.jsonl +0 -0
  122. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_dependency_install_and_build.jsonl +0 -0
  123. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_download_release_archive.jsonl +0 -0
  124. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_fetch_data_then_run_repo_script.jsonl +0 -0
  125. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_fetch_dataset_then_analyse.jsonl +0 -0
  126. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_fetch_lockfile_then_install.jsonl +0 -0
  127. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_git_review_and_push.jsonl +0 -0
  128. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_human_driven_deletes.jsonl +0 -0
  129. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_local_api_probing.jsonl +0 -0
  130. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_long_but_calm_session.jsonl +0 -0
  131. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_loopback_is_not_egress.jsonl +0 -0
  132. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_loopback_pipe_to_interpreter.jsonl +0 -0
  133. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_ordinary_development.jsonl +0 -0
  134. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_package_manager_after_fetch.jsonl +0 -0
  135. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_reading_config_that_is_not_secret.jsonl +0 -0
  136. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_remote_api_call_no_credentials.jsonl +0 -0
  137. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_research_then_docs.jsonl +0 -0
  138. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_reversed_order_is_not_exfil.jsonl +0 -0
  139. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_test_and_fix_loop.jsonl +0 -0
  140. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/corpus/benign_writing_about_credentials.jsonl +0 -0
  141. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/engine.py +0 -0
  142. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/live_store.py +0 -0
  143. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/detection/models.py +0 -0
  144. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/dlp/__init__.py +0 -0
  145. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/dlp/loader.py +0 -0
  146. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/dlp/models.py +0 -0
  147. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/dlp/scanner.py +0 -0
  148. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/evidence_pack.py +0 -0
  149. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/hashing.py +0 -0
  150. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/identity.py +0 -0
  151. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/migrate.py +0 -0
  152. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/policy.py +0 -0
  153. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/redaction.py +0 -0
  154. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/replay.py +0 -0
  155. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/run_context.py +0 -0
  156. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/spool.py +0 -0
  157. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/tool_policy/__init__.py +0 -0
  158. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/tool_policy/evaluator.py +0 -0
  159. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/tool_policy/loader.py +0 -0
  160. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/tool_policy/models.py +0 -0
  161. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/audit/trail_merkle.py +0 -0
  162. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/auth.py +0 -0
  163. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/bus/__init__.py +0 -0
  164. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/bus/bridges.py +0 -0
  165. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/bus/bus.py +0 -0
  166. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/bus/outbox.py +0 -0
  167. {agentmetry-0.4.0/agentmetry/api → agentmetry-0.6.0/agentmetry/core/diagnostics}/__init__.py +0 -0
  168. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/diagnostics/autostart.py +0 -0
  169. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/diagnostics/env_file.py +0 -0
  170. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/drivers/__init__.py +0 -0
  171. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/drivers/host.py +0 -0
  172. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/drivers/permissions.py +0 -0
  173. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/drivers/spec.py +0 -0
  174. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/extensions.py +0 -0
  175. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/core/health.py +0 -0
  176. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/policies/detection/manifest.yaml +0 -0
  177. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/policies/dlp/manifest.yaml +0 -0
  178. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/policies/opa/agent_rules.rego +0 -0
  179. {agentmetry-0.4.0 → agentmetry-0.6.0}/agentmetry/policies/tool/manifest.yaml +0 -0
  180. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/conftest.py +0 -0
  181. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/fixtures/agt_filesink_exfil.jsonl +0 -0
  182. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/fixtures/agt_filesink_mixed.jsonl +0 -0
  183. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/fixtures/fake_mcp_server.py +0 -0
  184. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_agt_adapter.py +0 -0
  185. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_audit_stats.py +0 -0
  186. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_audit_tail.py +0 -0
  187. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_auth.py +0 -0
  188. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_autostart.py +0 -0
  189. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_boot_sequence.py +0 -0
  190. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_burst_windows.py +0 -0
  191. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_chinese_agent_hooks.py +0 -0
  192. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_chinese_agent_sprint_b.py +0 -0
  193. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_chinese_agent_sprint_c.py +0 -0
  194. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_cli_backup.py +0 -0
  195. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_compliance_digest.py +0 -0
  196. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_crewai_adapter.py +0 -0
  197. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_detection_adi.py +0 -0
  198. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_detection_benchmark.py +0 -0
  199. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_detection_default_config.py +0 -0
  200. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_detection_disposition_api.py +0 -0
  201. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_detection_emit_durability.py +0 -0
  202. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_detection_engine.py +0 -0
  203. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_detection_hf_incident.py +0 -0
  204. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_detection_rule_identity.py +0 -0
  205. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_detection_rules_v2.py +0 -0
  206. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_dlp_agent_env_override.py +0 -0
  207. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_dlp_invisible_unicode.py +0 -0
  208. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_dlp_markdown_exfil.py +0 -0
  209. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_dlp_scanner.py +0 -0
  210. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_dogfood.py +0 -0
  211. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_driver_paths.py +0 -0
  212. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_event_bus.py +0 -0
  213. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_evidence_pack.py +0 -0
  214. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_extensions.py +0 -0
  215. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_forwarding_shape.py +0 -0
  216. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_hook_enforcement_timing.py +0 -0
  217. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_hook_spool.py +0 -0
  218. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_isolation.py +0 -0
  219. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_launch_targets.py +0 -0
  220. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_live_detection.py +0 -0
  221. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_live_detection_store.py +0 -0
  222. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_mitre_dlp.py +0 -0
  223. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_opensre_adapter.py +0 -0
  224. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_redaction.py +0 -0
  225. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_replay.py +0 -0
  226. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_static_serving.py +0 -0
  227. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_subagent_lifecycle.py +0 -0
  228. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_tool_policy.py +0 -0
  229. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_tool_policy_agent_cli.py +0 -0
  230. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_tool_policy_git_hooks.py +0 -0
  231. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_trail_chain.py +0 -0
  232. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_trail_merkle.py +0 -0
  233. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_unattended_agent_policy.py +0 -0
  234. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_version.py +0 -0
  235. {agentmetry-0.4.0 → agentmetry-0.6.0}/tests/test_yaml_detection_rules.py +0 -0
  236. {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
@@ -96,3 +96,7 @@ docs/*-review-*.md
96
96
  docs/*audit*prompt*.md
97
97
  docs/audit-*.md
98
98
  docs/*-audit-*.md
99
+
100
+ # pytest-cov data file
101
+ .coverage
102
+ .coverage.*
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: agentmetry
3
- Version: 0.4.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.0; extra == 'dev'
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
- pass
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 - depends on the host console
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 tamper-evident hash chain on an audit JSONL file",
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": "1.1.0",
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.