agentmetry 0.9.0__tar.gz → 0.9.2__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 (259) hide show
  1. {agentmetry-0.9.0 → agentmetry-0.9.2}/.env.example +9 -1
  2. {agentmetry-0.9.0 → agentmetry-0.9.2}/PKG-INFO +2 -2
  3. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/api/routes/audit.py +15 -0
  4. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/cli/__init__.py +8 -1
  5. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/disposition.py +10 -4
  6. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/dlp/loader.py +8 -1
  7. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/dlp/scanner.py +7 -4
  8. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/evidence_pack.py +2 -1
  9. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/external.py +9 -7
  10. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/heartbeat.py +9 -0
  11. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/ingest.py +1 -0
  12. agentmetry-0.9.2/agentmetry/core/audit/mcp_inventory_event.py +119 -0
  13. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/otel_ingest.py +1 -1
  14. agentmetry-0.9.2/agentmetry/core/audit/policy_settings.py +162 -0
  15. agentmetry-0.9.2/agentmetry/core/audit/run_context.py +197 -0
  16. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/sinks.py +13 -2
  17. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/tool_policy/evaluator.py +4 -3
  18. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/tool_policy/loader.py +7 -1
  19. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/bus/audit_exporter.py +1 -0
  20. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/config.py +15 -0
  21. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/diagnostics/mcp_inventory.py +43 -0
  22. agentmetry-0.9.2/agentmetry/core/operator_identity.py +87 -0
  23. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/version.py +1 -1
  24. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/hooks/ingest.py +67 -24
  25. {agentmetry-0.9.0 → agentmetry-0.9.2}/pyproject.toml +1 -1
  26. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/conftest.py +8 -0
  27. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_audit_sinks.py +65 -0
  28. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_cli_commands.py +6 -1
  29. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_dlp_scanner.py +10 -7
  30. agentmetry-0.9.2/tests/test_hook_import_cost.py +179 -0
  31. agentmetry-0.9.2/tests/test_hook_real_process.py +100 -0
  32. agentmetry-0.9.2/tests/test_mcp_inventory_event.py +189 -0
  33. agentmetry-0.9.2/tests/test_operator_attribution.py +280 -0
  34. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_readme_claims.py +22 -3
  35. agentmetry-0.9.0/agentmetry/core/audit/run_context.py +0 -129
  36. {agentmetry-0.9.0 → agentmetry-0.9.2}/.dockerignore +0 -0
  37. {agentmetry-0.9.0 → agentmetry-0.9.2}/.env.agentmetry-demo +0 -0
  38. {agentmetry-0.9.0 → agentmetry-0.9.2}/.gitignore +0 -0
  39. {agentmetry-0.9.0 → agentmetry-0.9.2}/Dockerfile +0 -0
  40. {agentmetry-0.9.0 → agentmetry-0.9.2}/README.md +0 -0
  41. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/__init__.py +0 -0
  42. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/api/__init__.py +0 -0
  43. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/api/main.py +0 -0
  44. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/api/routes/__init__.py +0 -0
  45. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/api/websocket.py +0 -0
  46. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/api/ws_bridge.py +0 -0
  47. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/cli/__main__.py +0 -0
  48. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/__init__.py +0 -0
  49. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/__init__.py +0 -0
  50. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/adapters/__init__.py +0 -0
  51. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/adapters/agt.py +0 -0
  52. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/adapters/chronicle.py +0 -0
  53. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/adapters/cloudevents.py +0 -0
  54. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/adapters/ecs.py +0 -0
  55. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/adapters/splunk.py +0 -0
  56. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/alerts.py +0 -0
  57. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/atlas.py +0 -0
  58. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/canonical.py +0 -0
  59. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/compliance_digest.py +0 -0
  60. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/__init__.py +0 -0
  61. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/benchmark.py +0 -0
  62. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/attack_approval_denied_then_executed.jsonl +0 -0
  63. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/attack_arbitrary_host_stage_execute.jsonl +0 -0
  64. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/attack_autonomous_unapproved_write.jsonl +0 -0
  65. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/attack_credential_exfil.jsonl +0 -0
  66. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/attack_credential_then_cloud_api.jsonl +0 -0
  67. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/attack_destructive_delete_burst.jsonl +0 -0
  68. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/attack_discovery_then_collect.jsonl +0 -0
  69. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/attack_dotfile_then_git_push.jsonl +0 -0
  70. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/attack_encoded_command_download.jsonl +0 -0
  71. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/attack_env_credential_exfil.jsonl +0 -0
  72. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/attack_hashed_only_no_command.jsonl +0 -0
  73. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/attack_interpreter_egress.jsonl +0 -0
  74. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/attack_pr_merged_without_review.jsonl +0 -0
  75. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/attack_proc_substitution_cradle.jsonl +0 -0
  76. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/attack_remote_pipe_to_shell.jsonl +0 -0
  77. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/attack_remote_staging_then_execute.jsonl +0 -0
  78. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/attack_secret_manager_then_egress.jsonl +0 -0
  79. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/attack_session_tool_burst.jsonl +0 -0
  80. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/attack_single_command_exfil.jsonl +0 -0
  81. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/attack_single_quoted_interpreter_egress.jsonl +0 -0
  82. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/attack_ssh_directory_exfil.jsonl +0 -0
  83. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/attack_subagent_swarm.jsonl +0 -0
  84. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/attack_timestamp_collision.jsonl +0 -0
  85. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/attack_untrusted_input_then_action.jsonl +0 -0
  86. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/benign_authoring_merge_fixtures.jsonl +0 -0
  87. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/benign_autonomous_after_approval.jsonl +0 -0
  88. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/benign_build_artifact_cleanup.jsonl +0 -0
  89. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/benign_ci_artifact_download.jsonl +0 -0
  90. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/benign_commit_message_names_cloud_clis.jsonl +0 -0
  91. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/benign_database_migration.jsonl +0 -0
  92. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/benign_dependency_install_and_build.jsonl +0 -0
  93. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/benign_download_release_archive.jsonl +0 -0
  94. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/benign_env_example_read.jsonl +0 -0
  95. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/benign_fetch_data_then_run_repo_script.jsonl +0 -0
  96. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/benign_fetch_dataset_then_analyse.jsonl +0 -0
  97. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/benign_fetch_lockfile_then_install.jsonl +0 -0
  98. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/benign_fetch_piped_into_own_script.jsonl +0 -0
  99. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/benign_git_review_and_push.jsonl +0 -0
  100. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/benign_github_raw_docs_then_script.jsonl +0 -0
  101. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/benign_human_driven_deletes.jsonl +0 -0
  102. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/benign_local_api_probing.jsonl +0 -0
  103. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/benign_long_but_calm_session.jsonl +0 -0
  104. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/benign_loopback_is_not_egress.jsonl +0 -0
  105. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/benign_loopback_pipe_to_interpreter.jsonl +0 -0
  106. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/benign_mcp_text_mentions_credential_path.jsonl +0 -0
  107. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/benign_ordinary_development.jsonl +0 -0
  108. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/benign_package_manager_after_fetch.jsonl +0 -0
  109. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/benign_reading_config_that_is_not_secret.jsonl +0 -0
  110. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/benign_remote_api_call_no_credentials.jsonl +0 -0
  111. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/benign_research_then_docs.jsonl +0 -0
  112. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/benign_reversed_order_is_not_exfil.jsonl +0 -0
  113. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/benign_secret_names_and_docs.jsonl +0 -0
  114. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/benign_test_and_fix_loop.jsonl +0 -0
  115. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/benign_writing_about_credentials.jsonl +0 -0
  116. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/corpus/corpus.yaml +0 -0
  117. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/engine.py +0 -0
  118. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/live.py +0 -0
  119. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/live_store.py +0 -0
  120. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/models.py +0 -0
  121. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/rules.py +0 -0
  122. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/traits.py +0 -0
  123. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/yaml_config.py +0 -0
  124. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/detection/yaml_rules.py +0 -0
  125. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/dlp/__init__.py +0 -0
  126. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/dlp/models.py +0 -0
  127. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/dogfood.py +0 -0
  128. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/hashing.py +0 -0
  129. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/hook_bootstrap.py +0 -0
  130. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/identity.py +0 -0
  131. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/migrate.py +0 -0
  132. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/mitre.py +0 -0
  133. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/policy.py +0 -0
  134. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/redaction.py +0 -0
  135. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/replay.py +0 -0
  136. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/spool.py +0 -0
  137. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/tool_policy/__init__.py +0 -0
  138. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/tool_policy/capability.py +0 -0
  139. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/tool_policy/models.py +0 -0
  140. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/trail_anchor.py +0 -0
  141. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/trail_chain.py +0 -0
  142. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/trail_db.py +0 -0
  143. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/audit/trail_merkle.py +0 -0
  144. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/auth.py +0 -0
  145. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/bus/__init__.py +0 -0
  146. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/bus/bridges.py +0 -0
  147. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/bus/bus.py +0 -0
  148. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/bus/events.py +0 -0
  149. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/bus/outbox.py +0 -0
  150. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/diagnostics/__init__.py +0 -0
  151. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/diagnostics/autostart.py +0 -0
  152. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/diagnostics/doctor.py +0 -0
  153. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/diagnostics/driver_paths.py +0 -0
  154. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/diagnostics/env_file.py +0 -0
  155. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/diagnostics/hook_coverage.py +0 -0
  156. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/diagnostics/mcp_schema.py +0 -0
  157. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/drivers/__init__.py +0 -0
  158. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/drivers/host.py +0 -0
  159. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/drivers/permissions.py +0 -0
  160. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/drivers/spec.py +0 -0
  161. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/extensions.py +0 -0
  162. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/core/health.py +0 -0
  163. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/hooks/__init__.py +0 -0
  164. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/policies/detection/manifest.yaml +0 -0
  165. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/policies/dlp/manifest.yaml +0 -0
  166. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/policies/opa/agent_rules.rego +0 -0
  167. {agentmetry-0.9.0 → agentmetry-0.9.2}/agentmetry/policies/tool/manifest.yaml +0 -0
  168. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/fixtures/agt_filesink_exfil.jsonl +0 -0
  169. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/fixtures/agt_filesink_mixed.jsonl +0 -0
  170. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/fixtures/fake_mcp_server.py +0 -0
  171. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/fixtures/otel_claude_code_session.json +0 -0
  172. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_agentmetry_audit.py +0 -0
  173. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_agentmetry_ingest_client.py +0 -0
  174. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_agt_adapter.py +0 -0
  175. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_ask_gate_phase_a.py +0 -0
  176. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_atlas_detection_enrichment.py +0 -0
  177. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_atlas_mapping.py +0 -0
  178. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_audit_stats.py +0 -0
  179. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_audit_tail.py +0 -0
  180. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_auth.py +0 -0
  181. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_autostart.py +0 -0
  182. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_boot_sequence.py +0 -0
  183. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_burst_windows.py +0 -0
  184. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_chinese_agent_hooks.py +0 -0
  185. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_chinese_agent_sprint_b.py +0 -0
  186. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_chinese_agent_sprint_c.py +0 -0
  187. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_chronicle_adapter.py +0 -0
  188. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_cli_backup.py +0 -0
  189. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_cli_env_var_names.py +0 -0
  190. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_cloudevents_adapter.py +0 -0
  191. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_compliance_digest.py +0 -0
  192. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_crewai_adapter.py +0 -0
  193. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_detection_adi.py +0 -0
  194. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_detection_benchmark.py +0 -0
  195. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_detection_default_config.py +0 -0
  196. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_detection_disposition.py +0 -0
  197. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_detection_disposition_api.py +0 -0
  198. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_detection_duplicate_emission.py +0 -0
  199. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_detection_emit_durability.py +0 -0
  200. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_detection_engine.py +0 -0
  201. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_detection_evasion.py +0 -0
  202. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_detection_hf_incident.py +0 -0
  203. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_detection_quoted_command_words.py +0 -0
  204. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_detection_rule_identity.py +0 -0
  205. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_detection_rules_v2.py +0 -0
  206. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_dlp_agent_env_override.py +0 -0
  207. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_dlp_invisible_unicode.py +0 -0
  208. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_dlp_markdown_exfil.py +0 -0
  209. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_doctor.py +0 -0
  210. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_dogfood.py +0 -0
  211. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_dogfood_freeze.py +0 -0
  212. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_driver_paths.py +0 -0
  213. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_ecs_threat.py +0 -0
  214. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_event_bus.py +0 -0
  215. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_evidence_pack.py +0 -0
  216. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_extensions.py +0 -0
  217. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_external_ingest.py +0 -0
  218. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_forwarding_shape.py +0 -0
  219. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_funding_manifest.py +0 -0
  220. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_heartbeat.py +0 -0
  221. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_hook_bootstrap.py +0 -0
  222. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_hook_coverage.py +0 -0
  223. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_hook_decision_format.py +0 -0
  224. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_hook_enforcement_timing.py +0 -0
  225. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_hook_spool.py +0 -0
  226. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_identity_enrich.py +0 -0
  227. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_ingest_roundtrip.py +0 -0
  228. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_isolation.py +0 -0
  229. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_launch_targets.py +0 -0
  230. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_live_detection.py +0 -0
  231. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_live_detection_store.py +0 -0
  232. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_mcp_audit_proxy.py +0 -0
  233. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_mcp_concealed_text.py +0 -0
  234. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_mcp_inventory.py +0 -0
  235. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_mcp_meta_fingerprint.py +0 -0
  236. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_mcp_schema.py +0 -0
  237. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_mitre_dlp.py +0 -0
  238. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_opensre_adapter.py +0 -0
  239. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_otel_ingest.py +0 -0
  240. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_powershell_encoding.py +0 -0
  241. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_redaction.py +0 -0
  242. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_replay.py +0 -0
  243. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_sigma_pack.py +0 -0
  244. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_static_serving.py +0 -0
  245. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_subagent_lifecycle.py +0 -0
  246. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_suite_leaves_real_home_alone.py +0 -0
  247. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_tool_policy.py +0 -0
  248. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_tool_policy_agent_cli.py +0 -0
  249. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_tool_policy_git_hooks.py +0 -0
  250. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_trail_anchor.py +0 -0
  251. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_trail_chain.py +0 -0
  252. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_trail_concurrency.py +0 -0
  253. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_trail_merkle.py +0 -0
  254. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_unattended_agent_policy.py +0 -0
  255. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_version.py +0 -0
  256. {agentmetry-0.9.0 → agentmetry-0.9.2}/tests/test_yaml_detection_rules.py +0 -0
  257. {agentmetry-0.9.0 → agentmetry-0.9.2}/tools/generate_sigma_pack.py +0 -0
  258. {agentmetry-0.9.0 → agentmetry-0.9.2}/tools/mcp_audit_proxy.py +0 -0
  259. {agentmetry-0.9.0 → agentmetry-0.9.2}/tools/vault_fs_server.py +0 -0
@@ -5,7 +5,10 @@
5
5
  # Demo MCP vault path (relative to this directory)
6
6
  AGENTMETRY_VAULT_PATH=../../vault
7
7
 
8
- AGENTMETRY_OPERATOR_ID=local
8
+ # Who ran the agent. Leave empty and every event records the OS account the
9
+ # agent ran as (DOMAIN\user on a domain or Entra Windows account). Set it to
10
+ # override: an email, or a pseudonym to keep account names out of a SIEM.
11
+ AGENTMETRY_OPERATOR_ID=
9
12
  AGENTMETRY_FLEET_ID=
10
13
  AGENTMETRY_API_KEY=
11
14
 
@@ -24,4 +27,9 @@ AGENTMETRY_AUDIT_SINK=file
24
27
  # host. See docs/anchoring.md.
25
28
  # AGENTMETRY_ANCHOR_LOG=../agentmetry-anchors/home-lab/agentmetry-trail.anchors.jsonl
26
29
 
30
+ # The configured MCP servers as an `mcp_inventory` event, for a fleet that wants
31
+ # them in the SIEM: names, launchers, package names and hosts, never arguments or
32
+ # env values. Off by default, because the heartbeat names no server.
33
+ # AGENTMETRY_MCP_INVENTORY=1
34
+
27
35
  # More tuning: see repo root .env.example and docs/agentmetry-dogfood-checklist.md
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentmetry
3
- Version: 0.9.0
3
+ Version: 0.9.2
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.6; extra == 'dev'
40
+ Requires-Dist: ruff==0.16.9; extra == 'dev'
41
41
  Description-Content-Type: text/markdown
42
42
 
43
43
  # Agentmetry
@@ -70,6 +70,19 @@ class IngestInitiatorBody(BaseModel):
70
70
  return v if v in _ACTOR_TYPES else "agent"
71
71
 
72
72
 
73
+ class IngestOperatorBody(BaseModel):
74
+ """Which account the capture process ran as, and how it knew.
75
+
76
+ Separate from `initiator`, which says what kind of actor acted. A claim,
77
+ like everything a client sends: `run_context.resolve_operator` decides what
78
+ is recorded, and an orchestrator-side AGENTMETRY_OPERATOR_ID overrides an
79
+ id the hook only read from the operating system.
80
+ """
81
+
82
+ id: str = ""
83
+ source: str = ""
84
+
85
+
73
86
  class ExternalIngestBody(BaseModel):
74
87
  """Adapter payload — normalized to canonical v1.2 on ingest.
75
88
 
@@ -113,6 +126,8 @@ class ExternalIngestBody(BaseModel):
113
126
  tool_policy: dict[str, Any] | None = None
114
127
  # Who the capture surface says triggered this. Honoured by external.py.
115
128
  initiator: IngestInitiatorBody | None = None
129
+ # Which account ran the agent, resolved by the hook as the developer.
130
+ operator: IngestOperatorBody | None = None
116
131
  # Compact `tools/list` fingerprint from mcp_audit_proxy. Hash only: the
117
132
  # description never leaves the proxy process.
118
133
  schema_fingerprint: str = ""
@@ -284,6 +284,12 @@ def cmd_detections(args: argparse.Namespace) -> int:
284
284
  return 0
285
285
 
286
286
 
287
+ def _triage_operator() -> str:
288
+ from agentmetry.core.operator_identity import os_operator, stated
289
+
290
+ return stated(os.environ.get("AGENTMETRY_OPERATOR_ID", "")) or os_operator()
291
+
292
+
287
293
  def cmd_disposition(args: argparse.Namespace) -> int:
288
294
  """Close a detection from the shell, using the same API as the dashboard."""
289
295
  note = args.note.strip()
@@ -299,7 +305,8 @@ def cmd_disposition(args: argparse.Namespace) -> int:
299
305
  "rule_id": args.rule_id,
300
306
  "status": args.status,
301
307
  "note": note,
302
- "decided_by": args.decided_by,
308
+ # The CLI runs as the person triaging; the orchestrator may not.
309
+ "decided_by": args.decided_by or _triage_operator(),
303
310
  },
304
311
  headers=_api_headers(),
305
312
  timeout=10.0,
@@ -431,8 +431,14 @@ def build_disposition_event(
431
431
  `action.type:detection_disposition AND action.outcome:risk_accepted`
432
432
  without understanding Agentmetry's vocabulary. The note is operator-written
433
433
  text, never captured command content, so it is safe to forward.
434
+
435
+ Who decided falls back to the resolved operator, not to the raw setting:
436
+ with AGENTMETRY_OPERATOR_ID unset that was an empty string, so a triage
437
+ record could not say who triaged.
434
438
  """
435
- from agentmetry.core.config import settings
439
+ from agentmetry.core.audit.run_context import resolve_operator
440
+
441
+ decided_by = decided_by or resolve_operator()[0]
436
442
 
437
443
  return {
438
444
  "schema_version": SCHEMA_VERSION,
@@ -443,8 +449,8 @@ def build_disposition_event(
443
449
  **identity_fields(),
444
450
  "source_topic": f"disposition/{rule_id}",
445
451
  "source": {"tier": "detection", "app": "agentmetry"},
446
- "actor": {"id": decided_by or settings.operator_id, "type": "human"},
447
- "initiator": {"type": "human", "id": decided_by or settings.operator_id},
452
+ "actor": {"id": decided_by, "type": "human"},
453
+ "initiator": {"type": "human", "id": decided_by},
448
454
  "action": {
449
455
  "type": DISPOSITION_EVENT_TYPE,
450
456
  "outcome": status,
@@ -459,7 +465,7 @@ def build_disposition_event(
459
465
  "previous_status": previous_status,
460
466
  "assignee": assignee,
461
467
  "note": note,
462
- "decided_by": decided_by or settings.operator_id,
468
+ "decided_by": decided_by,
463
469
  },
464
470
  }
465
471
 
@@ -4,6 +4,13 @@ from typing import List
4
4
 
5
5
  from .models import RuleMeta
6
6
 
7
+ #: libyaml where it is installed. The pure-Python parser was most of the hook's
8
+ #: runtime: the DLP and tool manifests are re-read by every hook process, and
9
+ #: parsing them took ~55 ms of an ~85 ms tool call (#171). Same SafeConstructor,
10
+ #: so the same Python objects, which `test_hook_import_cost.py` checks.
11
+ SAFE_LOADER = getattr(yaml, "CSafeLoader", yaml.SafeLoader)
12
+
13
+
7
14
  def load_dlp_rules(manifest_path: Path | str) -> List[RuleMeta]:
8
15
  """Load DLP rules from a YAML manifest file."""
9
16
  path = Path(manifest_path)
@@ -11,7 +18,7 @@ def load_dlp_rules(manifest_path: Path | str) -> List[RuleMeta]:
11
18
  return []
12
19
 
13
20
  with open(path, "r", encoding="utf-8") as f:
14
- data = yaml.safe_load(f)
21
+ data = yaml.load(f, Loader=SAFE_LOADER) # noqa: S506 - a safe loader
15
22
 
16
23
  if not data or "rules" not in data:
17
24
  return []
@@ -5,7 +5,9 @@ from typing import Any, Dict
5
5
 
6
6
  from .models import DlpVerdict, DlpMatch
7
7
  from .loader import load_dlp_rules
8
- from ...config import settings
8
+ # Not core.config: that import is pydantic, and the hook paid ~220 ms for it
9
+ # on every tool call to read three values (#171).
10
+ from ..policy_settings import policy_settings
9
11
 
10
12
  logger = logging.getLogger(__name__)
11
13
 
@@ -21,9 +23,10 @@ def reset_rules() -> None:
21
23
  def _init_rules() -> None:
22
24
  if _COMPILED_RULES:
23
25
  return
24
- rules = load_dlp_rules(settings.dlp_rules_path)
26
+ cfg = policy_settings()
27
+ rules = load_dlp_rules(cfg.dlp_rules_path)
25
28
  for r in rules:
26
- if not settings.dlp_pii and r.category == "pii":
29
+ if not cfg.dlp_pii and r.category == "pii":
27
30
  continue
28
31
  try:
29
32
  _COMPILED_RULES.append((re.compile(r.pattern), r))
@@ -59,7 +62,7 @@ def scan(tool_qualified: str, arguments: Dict[str, Any] | str, mode: str | None
59
62
  metadata is returned — never the matched value.
60
63
  """
61
64
  if mode is None:
62
- mode = settings.dlp_mode
65
+ mode = policy_settings().dlp_mode
63
66
  if mode == "disable":
64
67
  return DlpVerdict(matched=False, mode=mode)
65
68
 
@@ -23,6 +23,7 @@ from pathlib import Path
23
23
  from typing import Any
24
24
 
25
25
  from agentmetry.core.audit.detection.disposition import extract_dispositions
26
+ from agentmetry.core.audit.run_context import resolve_operator
26
27
  from agentmetry.core.config import settings
27
28
  from agentmetry.core.version import __version__
28
29
 
@@ -290,7 +291,7 @@ def _controls_snapshot() -> dict[str, Any]:
290
291
  "detection": {
291
292
  "off_hours_enabled": settings.detect_off_hours,
292
293
  },
293
- "operator_id": settings.operator_id.strip() or "local",
294
+ "operator_id": resolve_operator()[0],
294
295
  }
295
296
 
296
297
 
@@ -10,7 +10,11 @@ from agentmetry.core.audit.canonical import SCHEMA_VERSION
10
10
  from agentmetry.core.audit.identity import identity_fields
11
11
  from agentmetry.core.audit.hashing import arguments_sha256
12
12
  from agentmetry.core.audit.redaction import scrub_arg_values, scrub_secrets
13
- from agentmetry.core.audit.run_context import actor_from_initiator, build_initiator
13
+ from agentmetry.core.audit.run_context import (
14
+ actor_from_initiator,
15
+ build_initiator,
16
+ operator_from_payload,
17
+ )
14
18
  from agentmetry.core.config import settings
15
19
 
16
20
  ExternalApp = Literal[
@@ -34,10 +38,6 @@ _ACTION_MAP = {
34
38
  }
35
39
 
36
40
 
37
- def _operator_id() -> str:
38
- return settings.operator_id.strip() or "local"
39
-
40
-
41
41
  def _utc_now() -> str:
42
42
  return datetime.now(timezone.utc).isoformat()
43
43
 
@@ -63,16 +63,18 @@ def build_external_canonical(payload: dict[str, Any]) -> dict[str, Any]:
63
63
  correlation_id = str(payload.get("correlation_id") or payload.get("thread_id") or "")
64
64
  session_id = str(payload.get("session_id") or "")
65
65
 
66
+ operator = operator_from_payload(payload)
66
67
  initiator_raw = payload.get("initiator")
67
68
  if isinstance(initiator_raw, dict) and initiator_raw.get("actor_type"):
68
69
  initiator = {
69
70
  "actor_type": str(initiator_raw.get("actor_type") or "human"),
70
71
  "trigger": str(initiator_raw.get("trigger") or "manual"),
71
- "operator_id": str(initiator_raw.get("operator_id") or _operator_id()),
72
+ "operator_id": operator[0],
73
+ "operator_source": operator[1],
72
74
  }
73
75
  else:
74
76
  trigger = str(payload.get("triggered_by") or "manual")
75
- initiator = build_initiator(trigger)
77
+ initiator = build_initiator(trigger, operator)
76
78
 
77
79
  tool_block = payload.get("tool") if isinstance(payload.get("tool"), dict) else {}
78
80
  tool_qualified = str(
@@ -297,11 +297,14 @@ async def heartbeat_forever(interval: float | None = None) -> None:
297
297
  """
298
298
  import asyncio
299
299
 
300
+ from agentmetry.core.audit.mcp_inventory_event import InventoryState, maybe_emit_inventory
301
+
300
302
  period = interval if interval is not None else interval_seconds()
301
303
  if not period:
302
304
  logger.info("Heartbeat disabled (AGENTMETRY_HEARTBEAT_SECONDS=0)")
303
305
  return
304
306
 
307
+ inventory_state = InventoryState()
305
308
  while True:
306
309
  try:
307
310
  event = await emit_heartbeat()
@@ -310,4 +313,10 @@ async def heartbeat_forever(interval: float | None = None) -> None:
310
313
  except Exception:
311
314
  # Never let attestation kill the recorder it attests for.
312
315
  logger.exception("Heartbeat failed")
316
+ try:
317
+ # Opt-in (#169). Separate from the beat, so the beat stays
318
+ # name-free, and in its own try, so it cannot stop the beat.
319
+ await maybe_emit_inventory(inventory_state)
320
+ except Exception:
321
+ logger.exception("MCP inventory event failed")
313
322
  await asyncio.sleep(period)
@@ -144,6 +144,7 @@ def _get_sink():
144
144
  webhook_url=settings.audit_webhook_url,
145
145
  webhook_timeout_seconds=settings.audit_webhook_timeout_seconds,
146
146
  webhook_format=settings.audit_webhook_format,
147
+ webhook_token=settings.audit_webhook_token,
147
148
  elastic_url=settings.audit_elastic_url,
148
149
  elastic_index=settings.audit_elastic_index,
149
150
  elastic_api_key=settings.audit_elastic_api_key,
@@ -0,0 +1,119 @@
1
+ """The configured MCP servers as an event a SIEM can see, when an operator asks (#169).
2
+
3
+ `agentmetry mcp` answers "what are the agents on this machine wired to", but
4
+ only for whoever runs it, on that machine. Nothing reached the trail, so no SIEM
5
+ could answer "which machines have this server" or "when did it first appear".
6
+
7
+ This is deliberately not the heartbeat. The heartbeat goes to every sink on
8
+ every install and commits to the MCP surface by digest alone, so that recording
9
+ your own laptop never ships your tool inventory to a SOC. That property stays.
10
+ The inventory is a separate event, off unless `AGENTMETRY_MCP_INVENTORY=1`, for
11
+ the fleet that wants it.
12
+
13
+ When on, it is emitted when the recorder starts, whenever the configured
14
+ surface changes, and once a day regardless, so a dashboard over the last 24
15
+ hours sees every host. Each server is reduced to `McpServer.wire_entry()`: no
16
+ arguments, no environment, no URL beyond its host.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import logging
22
+ import time
23
+ import uuid
24
+ from dataclasses import dataclass
25
+ from typing import Any
26
+
27
+ from agentmetry.core.audit.canonical import SCHEMA_VERSION
28
+ from agentmetry.core.audit.identity import identity_fields
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ #: Re-emit an unchanged inventory this often, so a fleet view over a day is whole.
33
+ SNAPSHOT_SECONDS = 24 * 3600
34
+
35
+ #: The event goes to every sink. A machine with more servers than this is
36
+ #: reported by count, with the list cut and `truncated` set.
37
+ LIMIT = 100
38
+
39
+
40
+ def enabled() -> bool:
41
+ from agentmetry.core.config import settings
42
+
43
+ return bool(getattr(settings, "mcp_inventory_enabled", False))
44
+
45
+
46
+ @dataclass
47
+ class InventoryState:
48
+ """What was last emitted, so an unchanged surface is not re-sent every beat."""
49
+
50
+ digest: str | None = None
51
+ emitted_at: float = 0.0
52
+
53
+
54
+ def build_inventory_event(now_utc: str, inventory: Any, *, outcome: str) -> dict[str, Any]:
55
+ servers = list(inventory.servers)
56
+ entries = [s.wire_entry() for s in servers[:LIMIT]]
57
+ flagged = sum(1 for e in entries if e["findings"])
58
+ return {
59
+ "schema_version": SCHEMA_VERSION,
60
+ "event_id": str(uuid.uuid4()),
61
+ "session_id": "",
62
+ "correlation_id": "",
63
+ "timestamp_utc": now_utc,
64
+ **identity_fields(),
65
+ "source_topic": "agentmetry/mcp_inventory",
66
+ "source": {"tier": "agentmetry", "app": "agentmetry", "adapter": "mcp_inventory"},
67
+ "initiator": {"actor_type": "system", "trigger": "scheduled", "operator_id": ""},
68
+ "actor": {"type": "system", "id": "agentmetry", "role": "recorder"},
69
+ "action": {
70
+ "type": "mcp_inventory",
71
+ "outcome": outcome,
72
+ "reason": f"{len(servers)} MCP server(s) configured, {flagged} flagged",
73
+ },
74
+ "agent": {"name": "agentmetry", "skill_id": ""},
75
+ "mcp_inventory": {
76
+ "servers": entries,
77
+ "server_count": len(servers),
78
+ "truncated": len(servers) > LIMIT,
79
+ "config_digest": inventory.digest()[:16] if servers else "",
80
+ "files_read": len(inventory.files_read),
81
+ "files_unreadable": len(inventory.unreadable),
82
+ },
83
+ }
84
+
85
+
86
+ def due(state: InventoryState, digest: str, now_monotonic: float) -> str | None:
87
+ """`snapshot`, `changed`, or None when there is nothing to say."""
88
+ if state.digest is None:
89
+ return "snapshot"
90
+ if digest != state.digest:
91
+ return "changed"
92
+ if now_monotonic - state.emitted_at >= SNAPSHOT_SECONDS:
93
+ return "snapshot"
94
+ return None
95
+
96
+
97
+ async def maybe_emit_inventory(state: InventoryState) -> dict[str, Any] | None:
98
+ """Emit the inventory if the operator turned it on and there is news."""
99
+ if not enabled():
100
+ return None
101
+ from datetime import datetime, timezone
102
+
103
+ from agentmetry.core.audit.ingest import _get_sink
104
+ from agentmetry.core.audit.trail_db import get_trail_db
105
+ from agentmetry.core.diagnostics.mcp_inventory import collect
106
+
107
+ inventory = collect()
108
+ digest = inventory.digest() if inventory.servers else ""
109
+ now = time.monotonic()
110
+ outcome = due(state, digest, now)
111
+ if outcome is None:
112
+ return None
113
+ event = build_inventory_event(datetime.now(timezone.utc).isoformat(), inventory, outcome=outcome)
114
+ # The heartbeat's durability contract: the local insert is the guarantee,
115
+ # network sinks swallow their own failures.
116
+ get_trail_db().insert(event)
117
+ await _get_sink().emit(event)
118
+ state.digest, state.emitted_at = digest, now
119
+ return event
@@ -276,7 +276,7 @@ def _map_tool_decision(attrs: dict[str, Any], ts_iso: str) -> tuple[dict[str, An
276
276
  "outcome": "success",
277
277
  "reason": f"otel_decision:{source}",
278
278
  "tool_qualified": str(attrs.get("tool_name") or ""),
279
- "initiator": {"actor_type": "human", "trigger": "manual", "operator_id": "local"},
279
+ "initiator": {"actor_type": "human", "trigger": "manual", "operator_id": ""},
280
280
  })
281
281
  return payload, ""
282
282
  if decision == "reject" and source in _HUMAN_REJECT:
@@ -0,0 +1,162 @@
1
+ """The five settings DLP and tool policy read, without importing pydantic (#171).
2
+
3
+ The hook runs in front of every tool call, and on a blocking hook the agent waits
4
+ for it. The DLP scanner and the tool-policy evaluator used to import
5
+ `core.config` to learn a mode and a manifest path each, and that one import
6
+ pulled in pydantic and pydantic-settings: about 220 ms of a 626 ms hook, paid on
7
+ every tool call to read five values.
8
+
9
+ So there are two ways to get them, and one answer:
10
+
11
+ * Where `core.config` is already loaded, the orchestrator and the test suite,
12
+ the real `settings` object is read. A test that monkeypatches
13
+ `settings.dlp_mode` still changes what the scanner sees.
14
+ * Where it is not, the hook, they are resolved here the way `Settings` resolves
15
+ them: environment first, matched case-insensitively, then the orchestrator's
16
+ `.env`, then the same defaults. `test_hook_import_cost.py` checks the two
17
+ agree.
18
+
19
+ Standard library only. That is the whole point of the module.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import os
25
+ import sys
26
+ from dataclasses import dataclass
27
+ from pathlib import Path
28
+
29
+ _HERE = Path(__file__).resolve()
30
+ _PACKAGE_ROOT = _HERE.parents[2] # agentmetry/, as config._PACKAGE_ROOT
31
+ _ORCHESTRATOR_ROOT = _HERE.parents[3] # where Settings reads .env, as config._ORCHESTRATOR_ROOT
32
+
33
+ #: pydantic's own boolean vocabulary, so `AGENTMETRY_DLP_PII=off` means the same
34
+ #: thing to the hook as to the orchestrator.
35
+ _TRUE = frozenset({"1", "on", "t", "true", "y", "yes"})
36
+ _FALSE = frozenset({"0", "off", "f", "false", "n", "no"})
37
+
38
+ _DOTENV_CACHE: tuple[tuple[str, float, int] | None, dict[str, str]] = (None, {})
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class PolicySettings:
43
+ dlp_mode: str
44
+ dlp_rules_path: Path
45
+ dlp_pii: bool
46
+ tool_policy_mode: str
47
+ tool_policy_path: Path
48
+
49
+
50
+ def _defaults() -> PolicySettings:
51
+ policies = _PACKAGE_ROOT / "policies"
52
+ return PolicySettings(
53
+ dlp_mode="log",
54
+ dlp_rules_path=policies / "dlp" / "manifest.yaml",
55
+ dlp_pii=True,
56
+ tool_policy_mode="log",
57
+ tool_policy_path=policies / "tool" / "manifest.yaml",
58
+ )
59
+
60
+
61
+ def _parse_dotenv(text: str) -> dict[str, str]:
62
+ """The subset of python-dotenv that a `.env` for this project uses.
63
+
64
+ `export` prefixes, single or double quotes, and a ` #` comment after an
65
+ unquoted value. Keys are lowercased, because Settings matches them
66
+ case-insensitively.
67
+ """
68
+ out: dict[str, str] = {}
69
+ for raw in text.splitlines():
70
+ line = raw.strip()
71
+ if not line or line.startswith("#") or "=" not in line:
72
+ continue
73
+ if line.startswith("export "):
74
+ line = line[len("export "):].lstrip()
75
+ key, _, value = line.partition("=")
76
+ value = value.strip()
77
+ if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
78
+ value = value[1:-1]
79
+ elif " #" in value:
80
+ value = value.split(" #", 1)[0].rstrip()
81
+ out[key.strip().lower()] = value
82
+ return out
83
+
84
+
85
+ def _dotenv() -> dict[str, str]:
86
+ """The orchestrator's `.env`, parsed once per change to the file."""
87
+ global _DOTENV_CACHE
88
+ path = _ORCHESTRATOR_ROOT / ".env"
89
+ try:
90
+ stat = path.stat()
91
+ except OSError:
92
+ return {}
93
+ stamp = (str(path), stat.st_mtime, stat.st_size)
94
+ if _DOTENV_CACHE[0] == stamp:
95
+ return _DOTENV_CACHE[1]
96
+ try:
97
+ parsed = _parse_dotenv(path.read_text(encoding="utf-8"))
98
+ except (OSError, UnicodeDecodeError):
99
+ return {}
100
+ _DOTENV_CACHE = (stamp, parsed)
101
+ return parsed
102
+
103
+
104
+ def _lookup(name: str, environ: dict[str, str], dotenv: dict[str, str]) -> str | None:
105
+ # Present-but-empty counts as set, as it does for Settings (env_ignore_empty
106
+ # is off): an empty mode is not "use the default".
107
+ key = name.lower()
108
+ if key in environ:
109
+ return environ[key]
110
+ return dotenv.get(key)
111
+
112
+
113
+ def _bool(raw: str | None, default: bool) -> bool:
114
+ if raw is None:
115
+ return default
116
+ text = raw.strip().lower()
117
+ if text in _TRUE:
118
+ return True
119
+ if text in _FALSE:
120
+ return False
121
+ # Settings refuses to start on this. The hook must not fail a tool call over
122
+ # a typo, so it keeps the default and the orchestrator's error stays the
123
+ # place the operator finds out.
124
+ return default
125
+
126
+
127
+ def resolve() -> PolicySettings:
128
+ """Resolve without pydantic, as the hook does."""
129
+ environ = {k.lower(): v for k, v in os.environ.items()}
130
+ dotenv = _dotenv()
131
+ base = _defaults()
132
+
133
+ def text(name: str, default: str) -> str:
134
+ value = _lookup(name, environ, dotenv)
135
+ return default if value is None else value
136
+
137
+ def path(name: str, default: Path) -> Path:
138
+ value = _lookup(name, environ, dotenv)
139
+ return default if value is None else Path(value)
140
+
141
+ return PolicySettings(
142
+ dlp_mode=text("AGENTMETRY_DLP_MODE", base.dlp_mode),
143
+ dlp_rules_path=path("AGENTMETRY_DLP_RULES_PATH", base.dlp_rules_path),
144
+ dlp_pii=_bool(_lookup("AGENTMETRY_DLP_PII", environ, dotenv), base.dlp_pii),
145
+ tool_policy_mode=text("AGENTMETRY_TOOL_POLICY_MODE", base.tool_policy_mode),
146
+ tool_policy_path=path("AGENTMETRY_TOOL_POLICY_PATH", base.tool_policy_path),
147
+ )
148
+
149
+
150
+ def policy_settings() -> PolicySettings:
151
+ """The settings, from the loaded `Settings` object when there is one."""
152
+ config = sys.modules.get("agentmetry.core.config")
153
+ settings = getattr(config, "settings", None) if config is not None else None
154
+ if settings is None:
155
+ return resolve()
156
+ return PolicySettings(
157
+ dlp_mode=settings.dlp_mode,
158
+ dlp_rules_path=Path(settings.dlp_rules_path),
159
+ dlp_pii=bool(settings.dlp_pii),
160
+ tool_policy_mode=settings.tool_policy_mode,
161
+ tool_policy_path=Path(settings.tool_policy_path),
162
+ )