agentmetry 0.9.2__tar.gz → 0.9.3__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 (289) hide show
  1. {agentmetry-0.9.2 → agentmetry-0.9.3}/.env.example +22 -0
  2. agentmetry-0.9.3/Dockerfile +42 -0
  3. {agentmetry-0.9.2 → agentmetry-0.9.3}/PKG-INFO +1 -1
  4. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/api/main.py +66 -14
  5. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/api/routes/audit.py +29 -3
  6. agentmetry-0.9.3/agentmetry/api/routes/auth.py +84 -0
  7. agentmetry-0.9.3/agentmetry/api/trusted_host.py +72 -0
  8. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/cli/__init__.py +144 -6
  9. agentmetry-0.9.3/agentmetry/core/api_token.py +164 -0
  10. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/adapters/agt.py +4 -2
  11. agentmetry-0.9.3/agentmetry/core/audit/adapters/sentinel.py +79 -0
  12. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/canonical.py +4 -3
  13. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/dogfood.py +3 -1
  14. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/external.py +10 -5
  15. agentmetry-0.9.3/agentmetry/core/audit/forwarder.py +379 -0
  16. agentmetry-0.9.3/agentmetry/core/audit/hashing.py +47 -0
  17. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/heartbeat.py +3 -1
  18. agentmetry-0.9.3/agentmetry/core/audit/identity.py +84 -0
  19. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/ingest.py +3 -24
  20. agentmetry-0.9.3/agentmetry/core/audit/managed_hooks.py +212 -0
  21. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/migrate.py +21 -19
  22. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/policy_settings.py +8 -2
  23. agentmetry-0.9.3/agentmetry/core/audit/replay.py +95 -0
  24. agentmetry-0.9.3/agentmetry/core/audit/signing.py +63 -0
  25. agentmetry-0.9.3/agentmetry/core/audit/sinks.py +675 -0
  26. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/spool.py +3 -1
  27. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/trail_chain.py +98 -76
  28. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/trail_merkle.py +23 -21
  29. agentmetry-0.9.3/agentmetry/core/audit/trail_rotation.py +157 -0
  30. agentmetry-0.9.3/agentmetry/core/auth.py +117 -0
  31. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/bus/audit_exporter.py +4 -23
  32. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/bus/outbox.py +2 -1
  33. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/config.py +89 -5
  34. agentmetry-0.9.3/agentmetry/core/dashboard_session.py +84 -0
  35. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/diagnostics/doctor.py +87 -25
  36. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/diagnostics/mcp_schema.py +2 -2
  37. agentmetry-0.9.3/agentmetry/core/paths.py +97 -0
  38. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/version.py +1 -1
  39. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/hooks/ingest.py +77 -11
  40. agentmetry-0.9.2/tools/mcp_audit_proxy.py → agentmetry-0.9.3/agentmetry/hooks/mcp_proxy.py +42 -21
  41. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/conftest.py +22 -0
  42. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_audit_sinks.py +1 -1
  43. agentmetry-0.9.3/tests/test_auth_default.py +368 -0
  44. agentmetry-0.9.3/tests/test_container_and_legacy_runtime.py +66 -0
  45. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_doctor.py +9 -8
  46. agentmetry-0.9.3/tests/test_event_signing.py +66 -0
  47. agentmetry-0.9.3/tests/test_forwarder.py +356 -0
  48. agentmetry-0.9.3/tests/test_hash_key.py +128 -0
  49. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_hook_import_cost.py +86 -1
  50. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_hook_spool.py +25 -2
  51. agentmetry-0.9.3/tests/test_identity_provider.py +68 -0
  52. agentmetry-0.9.3/tests/test_managed_hooks.py +168 -0
  53. agentmetry-0.9.3/tests/test_mcp_proxy_packaged.py +133 -0
  54. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_operator_attribution.py +4 -4
  55. agentmetry-0.9.3/tests/test_paths.py +98 -0
  56. agentmetry-0.9.3/tests/test_sentinel.py +158 -0
  57. agentmetry-0.9.3/tests/test_serve_script_binds_loopback.py +23 -0
  58. agentmetry-0.9.3/tests/test_shared_install.py +115 -0
  59. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_suite_leaves_real_home_alone.py +22 -4
  60. agentmetry-0.9.3/tests/test_trail_chain.py +344 -0
  61. agentmetry-0.9.3/tests/test_trusted_host.py +62 -0
  62. agentmetry-0.9.3/tools/mcp_audit_proxy.py +28 -0
  63. agentmetry-0.9.2/Dockerfile +0 -18
  64. agentmetry-0.9.2/agentmetry/core/audit/hashing.py +0 -21
  65. agentmetry-0.9.2/agentmetry/core/audit/identity.py +0 -39
  66. agentmetry-0.9.2/agentmetry/core/audit/replay.py +0 -54
  67. agentmetry-0.9.2/agentmetry/core/audit/sinks.py +0 -369
  68. agentmetry-0.9.2/agentmetry/core/auth.py +0 -54
  69. agentmetry-0.9.2/tests/test_trail_chain.py +0 -145
  70. {agentmetry-0.9.2 → agentmetry-0.9.3}/.dockerignore +0 -0
  71. {agentmetry-0.9.2 → agentmetry-0.9.3}/.env.agentmetry-demo +0 -0
  72. {agentmetry-0.9.2 → agentmetry-0.9.3}/.gitignore +0 -0
  73. {agentmetry-0.9.2 → agentmetry-0.9.3}/README.md +0 -0
  74. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/__init__.py +0 -0
  75. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/api/__init__.py +0 -0
  76. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/api/routes/__init__.py +0 -0
  77. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/api/websocket.py +0 -0
  78. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/api/ws_bridge.py +0 -0
  79. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/cli/__main__.py +0 -0
  80. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/__init__.py +0 -0
  81. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/__init__.py +0 -0
  82. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/adapters/__init__.py +0 -0
  83. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/adapters/chronicle.py +0 -0
  84. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/adapters/cloudevents.py +0 -0
  85. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/adapters/ecs.py +0 -0
  86. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/adapters/splunk.py +0 -0
  87. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/alerts.py +0 -0
  88. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/atlas.py +0 -0
  89. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/compliance_digest.py +0 -0
  90. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/__init__.py +0 -0
  91. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/benchmark.py +0 -0
  92. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/attack_approval_denied_then_executed.jsonl +0 -0
  93. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/attack_arbitrary_host_stage_execute.jsonl +0 -0
  94. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/attack_autonomous_unapproved_write.jsonl +0 -0
  95. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/attack_credential_exfil.jsonl +0 -0
  96. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/attack_credential_then_cloud_api.jsonl +0 -0
  97. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/attack_destructive_delete_burst.jsonl +0 -0
  98. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/attack_discovery_then_collect.jsonl +0 -0
  99. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/attack_dotfile_then_git_push.jsonl +0 -0
  100. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/attack_encoded_command_download.jsonl +0 -0
  101. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/attack_env_credential_exfil.jsonl +0 -0
  102. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/attack_hashed_only_no_command.jsonl +0 -0
  103. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/attack_interpreter_egress.jsonl +0 -0
  104. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/attack_pr_merged_without_review.jsonl +0 -0
  105. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/attack_proc_substitution_cradle.jsonl +0 -0
  106. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/attack_remote_pipe_to_shell.jsonl +0 -0
  107. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/attack_remote_staging_then_execute.jsonl +0 -0
  108. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/attack_secret_manager_then_egress.jsonl +0 -0
  109. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/attack_session_tool_burst.jsonl +0 -0
  110. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/attack_single_command_exfil.jsonl +0 -0
  111. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/attack_single_quoted_interpreter_egress.jsonl +0 -0
  112. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/attack_ssh_directory_exfil.jsonl +0 -0
  113. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/attack_subagent_swarm.jsonl +0 -0
  114. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/attack_timestamp_collision.jsonl +0 -0
  115. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/attack_untrusted_input_then_action.jsonl +0 -0
  116. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/benign_authoring_merge_fixtures.jsonl +0 -0
  117. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/benign_autonomous_after_approval.jsonl +0 -0
  118. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/benign_build_artifact_cleanup.jsonl +0 -0
  119. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/benign_ci_artifact_download.jsonl +0 -0
  120. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/benign_commit_message_names_cloud_clis.jsonl +0 -0
  121. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/benign_database_migration.jsonl +0 -0
  122. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/benign_dependency_install_and_build.jsonl +0 -0
  123. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/benign_download_release_archive.jsonl +0 -0
  124. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/benign_env_example_read.jsonl +0 -0
  125. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/benign_fetch_data_then_run_repo_script.jsonl +0 -0
  126. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/benign_fetch_dataset_then_analyse.jsonl +0 -0
  127. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/benign_fetch_lockfile_then_install.jsonl +0 -0
  128. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/benign_fetch_piped_into_own_script.jsonl +0 -0
  129. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/benign_git_review_and_push.jsonl +0 -0
  130. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/benign_github_raw_docs_then_script.jsonl +0 -0
  131. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/benign_human_driven_deletes.jsonl +0 -0
  132. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/benign_local_api_probing.jsonl +0 -0
  133. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/benign_long_but_calm_session.jsonl +0 -0
  134. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/benign_loopback_is_not_egress.jsonl +0 -0
  135. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/benign_loopback_pipe_to_interpreter.jsonl +0 -0
  136. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/benign_mcp_text_mentions_credential_path.jsonl +0 -0
  137. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/benign_ordinary_development.jsonl +0 -0
  138. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/benign_package_manager_after_fetch.jsonl +0 -0
  139. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/benign_reading_config_that_is_not_secret.jsonl +0 -0
  140. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/benign_remote_api_call_no_credentials.jsonl +0 -0
  141. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/benign_research_then_docs.jsonl +0 -0
  142. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/benign_reversed_order_is_not_exfil.jsonl +0 -0
  143. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/benign_secret_names_and_docs.jsonl +0 -0
  144. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/benign_test_and_fix_loop.jsonl +0 -0
  145. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/benign_writing_about_credentials.jsonl +0 -0
  146. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/corpus/corpus.yaml +0 -0
  147. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/disposition.py +0 -0
  148. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/engine.py +0 -0
  149. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/live.py +0 -0
  150. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/live_store.py +0 -0
  151. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/models.py +0 -0
  152. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/rules.py +0 -0
  153. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/traits.py +0 -0
  154. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/yaml_config.py +0 -0
  155. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/detection/yaml_rules.py +0 -0
  156. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/dlp/__init__.py +0 -0
  157. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/dlp/loader.py +0 -0
  158. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/dlp/models.py +0 -0
  159. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/dlp/scanner.py +0 -0
  160. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/evidence_pack.py +0 -0
  161. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/hook_bootstrap.py +0 -0
  162. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/mcp_inventory_event.py +0 -0
  163. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/mitre.py +0 -0
  164. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/otel_ingest.py +0 -0
  165. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/policy.py +0 -0
  166. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/redaction.py +0 -0
  167. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/run_context.py +0 -0
  168. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/tool_policy/__init__.py +0 -0
  169. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/tool_policy/capability.py +0 -0
  170. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/tool_policy/evaluator.py +0 -0
  171. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/tool_policy/loader.py +0 -0
  172. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/tool_policy/models.py +0 -0
  173. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/trail_anchor.py +0 -0
  174. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/audit/trail_db.py +0 -0
  175. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/bus/__init__.py +0 -0
  176. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/bus/bridges.py +0 -0
  177. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/bus/bus.py +0 -0
  178. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/bus/events.py +0 -0
  179. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/diagnostics/__init__.py +0 -0
  180. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/diagnostics/autostart.py +0 -0
  181. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/diagnostics/driver_paths.py +0 -0
  182. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/diagnostics/env_file.py +0 -0
  183. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/diagnostics/hook_coverage.py +0 -0
  184. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/diagnostics/mcp_inventory.py +0 -0
  185. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/drivers/__init__.py +0 -0
  186. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/drivers/host.py +0 -0
  187. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/drivers/permissions.py +0 -0
  188. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/drivers/spec.py +0 -0
  189. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/extensions.py +0 -0
  190. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/health.py +0 -0
  191. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/core/operator_identity.py +0 -0
  192. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/hooks/__init__.py +0 -0
  193. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/policies/detection/manifest.yaml +0 -0
  194. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/policies/dlp/manifest.yaml +0 -0
  195. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/policies/opa/agent_rules.rego +0 -0
  196. {agentmetry-0.9.2 → agentmetry-0.9.3}/agentmetry/policies/tool/manifest.yaml +0 -0
  197. {agentmetry-0.9.2 → agentmetry-0.9.3}/pyproject.toml +0 -0
  198. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/fixtures/agt_filesink_exfil.jsonl +0 -0
  199. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/fixtures/agt_filesink_mixed.jsonl +0 -0
  200. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/fixtures/fake_mcp_server.py +0 -0
  201. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/fixtures/otel_claude_code_session.json +0 -0
  202. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_agentmetry_audit.py +0 -0
  203. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_agentmetry_ingest_client.py +0 -0
  204. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_agt_adapter.py +0 -0
  205. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_ask_gate_phase_a.py +0 -0
  206. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_atlas_detection_enrichment.py +0 -0
  207. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_atlas_mapping.py +0 -0
  208. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_audit_stats.py +0 -0
  209. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_audit_tail.py +0 -0
  210. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_auth.py +0 -0
  211. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_autostart.py +0 -0
  212. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_boot_sequence.py +0 -0
  213. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_burst_windows.py +0 -0
  214. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_chinese_agent_hooks.py +0 -0
  215. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_chinese_agent_sprint_b.py +0 -0
  216. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_chinese_agent_sprint_c.py +0 -0
  217. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_chronicle_adapter.py +0 -0
  218. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_cli_backup.py +0 -0
  219. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_cli_commands.py +0 -0
  220. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_cli_env_var_names.py +0 -0
  221. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_cloudevents_adapter.py +0 -0
  222. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_compliance_digest.py +0 -0
  223. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_crewai_adapter.py +0 -0
  224. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_detection_adi.py +0 -0
  225. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_detection_benchmark.py +0 -0
  226. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_detection_default_config.py +0 -0
  227. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_detection_disposition.py +0 -0
  228. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_detection_disposition_api.py +0 -0
  229. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_detection_duplicate_emission.py +0 -0
  230. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_detection_emit_durability.py +0 -0
  231. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_detection_engine.py +0 -0
  232. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_detection_evasion.py +0 -0
  233. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_detection_hf_incident.py +0 -0
  234. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_detection_quoted_command_words.py +0 -0
  235. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_detection_rule_identity.py +0 -0
  236. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_detection_rules_v2.py +0 -0
  237. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_dlp_agent_env_override.py +0 -0
  238. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_dlp_invisible_unicode.py +0 -0
  239. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_dlp_markdown_exfil.py +0 -0
  240. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_dlp_scanner.py +0 -0
  241. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_dogfood.py +0 -0
  242. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_dogfood_freeze.py +0 -0
  243. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_driver_paths.py +0 -0
  244. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_ecs_threat.py +0 -0
  245. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_event_bus.py +0 -0
  246. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_evidence_pack.py +0 -0
  247. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_extensions.py +0 -0
  248. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_external_ingest.py +0 -0
  249. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_forwarding_shape.py +0 -0
  250. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_funding_manifest.py +0 -0
  251. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_heartbeat.py +0 -0
  252. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_hook_bootstrap.py +0 -0
  253. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_hook_coverage.py +0 -0
  254. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_hook_decision_format.py +0 -0
  255. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_hook_enforcement_timing.py +0 -0
  256. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_hook_real_process.py +0 -0
  257. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_identity_enrich.py +0 -0
  258. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_ingest_roundtrip.py +0 -0
  259. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_isolation.py +0 -0
  260. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_launch_targets.py +0 -0
  261. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_live_detection.py +0 -0
  262. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_live_detection_store.py +0 -0
  263. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_mcp_audit_proxy.py +0 -0
  264. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_mcp_concealed_text.py +0 -0
  265. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_mcp_inventory.py +0 -0
  266. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_mcp_inventory_event.py +0 -0
  267. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_mcp_meta_fingerprint.py +0 -0
  268. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_mcp_schema.py +0 -0
  269. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_mitre_dlp.py +0 -0
  270. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_opensre_adapter.py +0 -0
  271. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_otel_ingest.py +0 -0
  272. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_powershell_encoding.py +0 -0
  273. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_readme_claims.py +0 -0
  274. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_redaction.py +0 -0
  275. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_replay.py +0 -0
  276. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_sigma_pack.py +0 -0
  277. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_static_serving.py +0 -0
  278. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_subagent_lifecycle.py +0 -0
  279. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_tool_policy.py +0 -0
  280. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_tool_policy_agent_cli.py +0 -0
  281. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_tool_policy_git_hooks.py +0 -0
  282. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_trail_anchor.py +0 -0
  283. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_trail_concurrency.py +0 -0
  284. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_trail_merkle.py +0 -0
  285. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_unattended_agent_policy.py +0 -0
  286. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_version.py +0 -0
  287. {agentmetry-0.9.2 → agentmetry-0.9.3}/tests/test_yaml_detection_rules.py +0 -0
  288. {agentmetry-0.9.2 → agentmetry-0.9.3}/tools/generate_sigma_pack.py +0 -0
  289. {agentmetry-0.9.2 → agentmetry-0.9.3}/tools/vault_fs_server.py +0 -0
@@ -10,6 +10,22 @@ AGENTMETRY_VAULT_PATH=../../vault
10
10
  # override: an email, or a pseudonym to keep account names out of a SIEM.
11
11
  AGENTMETRY_OPERATOR_ID=
12
12
  AGENTMETRY_FLEET_ID=
13
+
14
+ # Per-fleet secret for tool-argument fingerprints. Unset: plain SHA-256, which
15
+ # anyone with the trail or SIEM can recompute for a guessable argument. Set:
16
+ # HMAC-SHA256 under this key, labelled input_redaction "hmac". Use the same
17
+ # value on every host in a fleet so fingerprints still correlate.
18
+ AGENTMETRY_HASH_KEY=
19
+
20
+ # Host names the API answers to besides localhost / 127.0.0.1 / ::1. Anything
21
+ # else gets 400 (DNS-rebinding guard). Set this when the API is reached by
22
+ # another name: a container service name, a LAN address, a reverse proxy.
23
+ # AGENTMETRY_TRUSTED_HOSTS=orchestrator,devbox.corp.example
24
+
25
+ # The API needs a token by default: the first start writes one, owner-only,
26
+ # to the data directory, and the hooks and CLI read it from there. Set this
27
+ # only to distribute your own key across a fleet; it then replaces the file.
28
+ # AGENTMETRY_AUTH_DISABLED=1 turns authentication off (development only).
13
29
  AGENTMETRY_API_KEY=
14
30
 
15
31
  # Hook boundary — log | block | disable
@@ -19,6 +35,12 @@ AGENTMETRY_DLP_MODE=log
19
35
  # Audit export (JSONL + SQLite index)
20
36
  AGENTMETRY_AUDIT_EXPORT_ENABLED=1
21
37
  AGENTMETRY_AUDIT_SINK=file
38
+ # Network sinks forward from the trail with a cursor, batching and retrying
39
+ # while the SIEM is down. 0 = the old inline one-request-per-event sinks.
40
+ # AGENTMETRY_AUDIT_FORWARDER=1
41
+ # Rotate the trail into <trail>.archive/ at this size; the chain continues
42
+ # across segments and nothing is deleted (docs/trail-retention.md). 0 = never.
43
+ # AGENTMETRY_TRAIL_ROTATE_BYTES=268435456
22
44
 
23
45
  # Anchor log, when it is not the file sitting beside the trail. Point this at a
24
46
  # working copy of a remote this machine cannot rewrite and `verify --trail` and
@@ -0,0 +1,42 @@
1
+ # Agentmetry recorder in one image: the orchestrator API, serving the dashboard's
2
+ # static export. Build from the repository root:
3
+ #
4
+ # docker build -f apps/orchestrator/Dockerfile -t agentmetry .
5
+ #
6
+ # It used to start `uvicorn api.main:app`, a module path that has not existed
7
+ # since the package became `agentmetry`, so the image could not boot. It also
8
+ # installed gcc and libpq for a Postgres the recorder no longer uses.
9
+
10
+ FROM node:20-alpine AS dashboard
11
+ WORKDIR /dashboard
12
+ COPY apps/dashboard/package.json apps/dashboard/package-lock.json ./
13
+ RUN npm ci
14
+ COPY apps/dashboard/ ./
15
+ # Same-origin build: the dashboard talks to the API that serves it. No API key
16
+ # is baked into the bundle.
17
+ ENV NEXT_PUBLIC_SAME_ORIGIN=true
18
+ RUN npm run build
19
+
20
+ FROM python:3.12-slim
21
+ WORKDIR /app
22
+
23
+ COPY apps/orchestrator/ ./
24
+ RUN pip install --no-cache-dir . \
25
+ && useradd --system --uid 10001 --home-dir /data agentmetry \
26
+ && mkdir -p /data && chown agentmetry /data
27
+
28
+ # agentmetry/api/main.py serves <three levels up>/dashboard/out.
29
+ COPY --from=dashboard /dashboard/out /dashboard/out
30
+
31
+ ENV AGENTMETRY_AUDIT_EXPORT_PATH=/data/audit-forward.jsonl \
32
+ AGENTMETRY_AUDIT_DB_PATH=/data/audit.db \
33
+ AGENTMETRY_DETECTION_LIVE_DB_PATH=/data/detection_live.db \
34
+ AGENTMETRY_DETECTION_DISPOSITION_DB_PATH=/data/detection_dispositions.db
35
+
36
+ USER agentmetry
37
+ VOLUME ["/data"]
38
+ EXPOSE 8000
39
+
40
+ # 0.0.0.0 inside the container is required for port publishing to work; the
41
+ # compose file publishes it on the host's loopback only.
42
+ CMD ["uvicorn", "agentmetry.api.main:app", "--host", "0.0.0.0", "--port", "8000"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentmetry
3
- Version: 0.9.2
3
+ Version: 0.9.3
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
@@ -9,6 +9,9 @@ from pathlib import Path
9
9
  from fastapi import FastAPI, Query, WebSocket, WebSocketDisconnect
10
10
  from fastapi.middleware.cors import CORSMiddleware
11
11
 
12
+ from agentmetry.api.routes.auth import router as auth_router
13
+ from agentmetry.api.trusted_host import TrustedHostMiddleware
14
+
12
15
  from agentmetry.api.routes.audit import router as audit_router
13
16
  from agentmetry.api.websocket import ws_manager
14
17
  from agentmetry.api.ws_bridge import ws_event_bridge
@@ -18,13 +21,14 @@ from agentmetry.core.bus.bridges import outbox_persister
18
21
  from agentmetry.core.bus.bus import bus
19
22
  from agentmetry.core.bus.outbox import get_outbox
20
23
  from agentmetry.core.config import settings
24
+ from agentmetry.core.paths import data_dir
21
25
  from agentmetry.core.extensions import load_extensions
22
26
  from agentmetry.core.health import get_system_health
23
27
  from agentmetry.core.version import __version__
24
28
 
25
29
  logger = logging.getLogger(__name__)
26
30
 
27
- _LOG_DIR = Path(__file__).resolve().parents[2] / "data" / "logs"
31
+ _LOG_DIR = data_dir() / "logs"
28
32
 
29
33
 
30
34
  def _setup_logging() -> None:
@@ -57,6 +61,21 @@ _setup_logging()
57
61
 
58
62
 
59
63
 
64
+ def _start_forwarders() -> list[asyncio.Task]:
65
+ """One task per network sink, tailing the trail (pilot hardening item 19)."""
66
+ if not (settings.audit_export_enabled and settings.audit_forwarder):
67
+ return []
68
+ from agentmetry.core.audit.forwarder import Forwarder
69
+ from agentmetry.core.audit.sinks import forward_destinations
70
+
71
+ tasks = []
72
+ for destination in forward_destinations(settings):
73
+ forwarder = Forwarder(destination, settings.audit_export_path)
74
+ tasks.append(asyncio.create_task(forwarder.run(), name=f"forward-{destination.name}"))
75
+ logger.info("Forwarding the trail to %s from its cursor", destination.name)
76
+ return tasks
77
+
78
+
60
79
  @asynccontextmanager
61
80
  async def lifespan(app: FastAPI):
62
81
  from agentmetry.core.audit.migrate import backfill_db_from_jsonl
@@ -114,22 +133,48 @@ async def lifespan(app: FastAPI):
114
133
  # if presence was being asserted.
115
134
  asyncio.create_task(heartbeat_forever(), name="heartbeat"),
116
135
  ]
136
+ bridge_tasks += _start_forwarders()
117
137
 
118
- # Drivers mount in the background: a slow npx download must not delay boot.
119
- from agentmetry.core.drivers.host import get_mcp_host
138
+ # The removed agent runtime's MCP driver host. Off unless
139
+ # AGENTMETRY_LEGACY_DRIVERS=1 (#209): it spawned tools/vault_fs_server.py on
140
+ # every boot of a recorder that has no use for it.
141
+ mount_task = None
142
+ if settings.legacy_drivers:
143
+ from agentmetry.core.drivers.host import get_mcp_host
120
144
 
121
- mount_task = asyncio.create_task(get_mcp_host().mount_all(), name="driver-mounts")
145
+ # Mount in the background: a slow npx download must not delay boot.
146
+ mount_task = asyncio.create_task(get_mcp_host().mount_all(), name="driver-mounts")
122
147
 
123
- from agentmetry.core.audit.hook_bootstrap import bootstrap_tier_b_hooks
148
+ # Create the per-install API token now, before the first hook asks for it.
149
+ from agentmetry.core.auth import effective_key
124
150
 
125
151
  try:
126
- hook_paths = bootstrap_tier_b_hooks()
127
- if hook_paths.get("cursor"):
128
- logger.info("Global Cursor hooks ready: %s", hook_paths["cursor"])
129
- if hook_paths.get("claude"):
130
- logger.info("Global Claude hooks ready: %s", hook_paths["claude"])
131
- except Exception as exc:
132
- logger.warning("Tier B hook bootstrap failed: %s", exc)
152
+ if effective_key():
153
+ logger.info("API authentication on; local clients read the token from the data directory")
154
+ else:
155
+ logger.warning("API authentication is OFF (AGENTMETRY_AUTH_DISABLED); development only")
156
+ except OSError as exc:
157
+ logger.error("Could not create the API token (%s); every request will be refused", exc)
158
+
159
+ # Opt-in (AGENTMETRY_AUTO_INSTALL_HOOKS=1). Unconditional, this rewrote the
160
+ # developer's global IDE hook configs to point at whichever checkout booted,
161
+ # which is how a test clone or demo instance took over live hooks.
162
+ if settings.auto_install_hooks:
163
+ from agentmetry.core.audit.hook_bootstrap import bootstrap_tier_b_hooks
164
+
165
+ try:
166
+ hook_paths = bootstrap_tier_b_hooks()
167
+ if hook_paths.get("cursor"):
168
+ logger.info("Global Cursor hooks ready: %s", hook_paths["cursor"])
169
+ if hook_paths.get("claude"):
170
+ logger.info("Global Claude hooks ready: %s", hook_paths["claude"])
171
+ except Exception as exc:
172
+ logger.warning("Tier B hook bootstrap failed: %s", exc)
173
+ else:
174
+ logger.info(
175
+ "IDE hooks are not rewritten at boot. Install or repair them with "
176
+ "`agentmetry hooks install`, or set AGENTMETRY_AUTO_INSTALL_HOOKS=1."
177
+ )
133
178
 
134
179
  # Launch transcript watcher for Antigravity in the background
135
180
  import subprocess
@@ -148,8 +193,11 @@ async def lifespan(app: FastAPI):
148
193
  logger.warning("Failed to start Antigravity transcript watcher: %s", exc)
149
194
 
150
195
  yield
151
- mount_task.cancel()
152
- await get_mcp_host().unmount_all()
196
+ if mount_task is not None:
197
+ from agentmetry.core.drivers.host import get_mcp_host
198
+
199
+ mount_task.cancel()
200
+ await get_mcp_host().unmount_all()
153
201
  for task in bridge_tasks:
154
202
  task.cancel()
155
203
  if watcher_process:
@@ -177,8 +225,12 @@ app.add_middleware(
177
225
  allow_methods=["*"],
178
226
  allow_headers=["*"],
179
227
  )
228
+ # Outermost, so a request with a foreign Host header is refused before CORS or
229
+ # any route sees it (DNS rebinding; pilot hardening item 9).
230
+ app.add_middleware(TrustedHostMiddleware)
180
231
 
181
232
  app.include_router(audit_router, prefix="/api/v1")
233
+ app.include_router(auth_router, prefix="/api/v1")
182
234
 
183
235
  load_extensions(app, settings=settings)
184
236
 
@@ -6,7 +6,7 @@ from datetime import datetime, timedelta, timezone
6
6
  from pathlib import Path
7
7
  from typing import Any, Literal
8
8
 
9
- from fastapi import APIRouter, Depends, HTTPException, Query
9
+ from fastapi import APIRouter, Depends, HTTPException, Query, Request
10
10
  from fastapi.responses import FileResponse
11
11
  from pydantic import BaseModel, Field, field_validator
12
12
 
@@ -350,8 +350,32 @@ class DispositionBody(BaseModel):
350
350
  severity: str = ""
351
351
 
352
352
 
353
+ def _forward_status() -> dict:
354
+ from agentmetry.core.audit.forwarder import forward_status
355
+
356
+ return forward_status()
357
+
358
+
359
+ def _decider(request: Request, claimed: str) -> str:
360
+ """Who made this decision, as far as the server can tell.
361
+
362
+ A dashboard session and an extension principal are server-side facts, so
363
+ they win over anything the body claims. A token-authenticated caller (the
364
+ CLI, on this machine) states who it is; with nothing stated, the resolved
365
+ operator is recorded rather than an empty string.
366
+ """
367
+ kind, who = getattr(request.state, "auth", ("", ""))
368
+ if kind in ("session", "extension") and who:
369
+ return who
370
+ if claimed.strip():
371
+ return claimed.strip()
372
+ from agentmetry.core.audit.run_context import resolve_operator
373
+
374
+ return resolve_operator()[0]
375
+
376
+
353
377
  @router.post("/detections/disposition", dependencies=[Depends(require_api_key)])
354
- async def audit_set_disposition(body: DispositionBody):
378
+ async def audit_set_disposition(body: DispositionBody, request: Request):
355
379
  """Record what a human decided about a detection.
356
380
 
357
381
  The decision is appended to the trail as a `detection_disposition` event
@@ -369,7 +393,7 @@ async def audit_set_disposition(body: DispositionBody):
369
393
  status=body.status,
370
394
  assignee=body.assignee,
371
395
  note=body.note,
372
- decided_by=body.decided_by,
396
+ decided_by=_decider(request, body.decided_by),
373
397
  severity=body.severity,
374
398
  )
375
399
  except DispositionError as exc:
@@ -453,6 +477,8 @@ async def audit_status():
453
477
  "by_source": status_data["by_source"],
454
478
  "path": str(path),
455
479
  "spool_pending": spool_depth(),
480
+ # Per SIEM: the last seq it accepted, and how long it has been failing.
481
+ "forwarding": _forward_status(),
456
482
  "spool_oldest_age_seconds": spool_oldest_age_seconds(),
457
483
  }
458
484
 
@@ -0,0 +1,84 @@
1
+ """Dashboard sign-in (pilot hardening items 8 and 13).
2
+
3
+ `agentmetry dashboard` calls POST /auth/dashboard-link with the API token and
4
+ opens the URL it returns. That URL exchanges a one-time code for an HttpOnly,
5
+ SameSite=Strict session cookie, so the browser is signed in without ever
6
+ holding the key.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+
12
+ from fastapi import APIRouter, Depends, HTTPException, Request
13
+ from fastapi.responses import RedirectResponse
14
+ from pydantic import BaseModel
15
+
16
+ from agentmetry.core.auth import require_api_key
17
+ from agentmetry.core.dashboard_session import (
18
+ CODE_SECONDS,
19
+ SESSION_COOKIE,
20
+ SESSION_SECONDS,
21
+ sessions,
22
+ )
23
+
24
+ router = APIRouter(prefix="/auth", tags=["auth"])
25
+
26
+
27
+
28
+ class DashboardLinkBody(BaseModel):
29
+ operator: str = ""
30
+
31
+
32
+ #: Where a sign-in may land. A lookup, not validation: the Location header is
33
+ #: always one of these constants, never anything taken from the request. The
34
+ #: dashboard is one page, so `/` is the only real destination; `dev` is the
35
+ #: Next dev server on its default port, for working on the dashboard itself.
36
+ #: (The previous "starts with one slash" check let `/\evil.example` through,
37
+ #: which browsers read as `//evil.example`.)
38
+ _LANDINGS = {
39
+ "": "/",
40
+ "/": "/",
41
+ "dev": "http://localhost:3000/",
42
+ }
43
+
44
+
45
+ def _safe_next(target: str) -> str:
46
+ """Where to land after sign-in. Anything not in the table lands on `/`."""
47
+ return _LANDINGS.get(target or "", "/")
48
+
49
+
50
+ @router.post("/dashboard-link")
51
+ async def dashboard_link(body: DashboardLinkBody, request: Request, _: None = Depends(require_api_key)):
52
+ """A one-time sign-in link. Needs the token itself, not a session."""
53
+ kind, _who = getattr(request.state, "auth", ("", ""))
54
+ if kind == "session":
55
+ raise HTTPException(status_code=403, detail="a dashboard session cannot mint sign-in links")
56
+ if not body.operator.strip():
57
+ from agentmetry.core.audit.run_context import resolve_operator
58
+
59
+ operator = resolve_operator()[0]
60
+ else:
61
+ operator = body.operator.strip()
62
+ code = sessions.issue_code(operator)
63
+ return {"url": f"/api/v1/auth/dashboard?code={code}", "expires_in": CODE_SECONDS}
64
+
65
+
66
+ @router.get("/dashboard")
67
+ async def dashboard_sign_in(request: Request, code: str = "", next: str = ""): # noqa: A002
68
+ session_id = sessions.redeem(code)
69
+ if session_id is None:
70
+ raise HTTPException(status_code=401, detail="sign-in link expired or already used; run `agentmetry dashboard`")
71
+ resp = RedirectResponse(_safe_next(next), status_code=303)
72
+ resp.set_cookie(
73
+ SESSION_COOKIE, session_id, max_age=SESSION_SECONDS, httponly=True,
74
+ samesite="strict", secure=request.url.scheme == "https", path="/",
75
+ )
76
+ return resp
77
+
78
+
79
+ @router.post("/logout")
80
+ async def dashboard_logout(request: Request):
81
+ sessions.revoke(request.cookies.get(SESSION_COOKIE))
82
+ resp = RedirectResponse("/", status_code=303)
83
+ resp.delete_cookie(SESSION_COOKIE, path="/")
84
+ return resp
@@ -0,0 +1,72 @@
1
+ """Refuse requests whose Host header is not this machine (pilot hardening item 9).
2
+
3
+ The orchestrator listens on loopback, and that is not enough on its own. A web
4
+ page the developer visits can make its own hostname resolve to 127.0.0.1 (DNS
5
+ rebinding) and then read the local API as same-origin, trail included, because
6
+ the browser believes it is talking to the attacker's site. The one thing that
7
+ gives it away is the Host header, which still names the attacker's domain.
8
+
9
+ So only loopback names and the names an operator configures are accepted:
10
+ `localhost`, `127.0.0.1`, `::1`, plus `AGENTMETRY_TRUSTED_HOSTS`
11
+ (comma-separated, `*` to disable the check). `mobile.bat` adds the LAN address
12
+ it detects.
13
+
14
+ Written as plain ASGI rather than Starlette's TrustedHostMiddleware because
15
+ that one fixes its list at startup, and this reads settings on each request so
16
+ a test or an edited `.env` takes effect.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import Any
22
+
23
+ LOOPBACK = frozenset({"localhost", "127.0.0.1", "::1"})
24
+
25
+
26
+ def allowed_hosts() -> frozenset[str]:
27
+ from agentmetry.core.config import settings
28
+
29
+ configured = {h.strip().lower() for h in str(getattr(settings, "trusted_hosts", "")).split(",") if h.strip()}
30
+ return LOOPBACK | frozenset(configured)
31
+
32
+
33
+ def host_of(header: str) -> str:
34
+ """The hostname in a Host header, without the port, lowercased."""
35
+ value = header.strip().lower()
36
+ if value.startswith("["): # [::1]:8000
37
+ end = value.find("]")
38
+ return value[1:end] if end > 0 else value
39
+ if value.count(":") == 1:
40
+ return value.split(":", 1)[0]
41
+ return value
42
+
43
+
44
+ class TrustedHostMiddleware:
45
+ def __init__(self, app: Any) -> None:
46
+ self.app = app
47
+
48
+ async def __call__(self, scope: dict, receive: Any, send: Any) -> None:
49
+ if scope.get("type") not in ("http", "websocket"):
50
+ return await self.app(scope, receive, send)
51
+ allowed = allowed_hosts()
52
+ if "*" in allowed:
53
+ return await self.app(scope, receive, send)
54
+ header = ""
55
+ for name, value in scope.get("headers") or []:
56
+ if name == b"host":
57
+ header = value.decode("latin-1")
58
+ break
59
+ if host_of(header) in allowed:
60
+ return await self.app(scope, receive, send)
61
+ if scope["type"] == "websocket":
62
+ message = await receive()
63
+ if message.get("type") == "websocket.connect":
64
+ await send({"type": "websocket.close", "code": 1008})
65
+ return None
66
+ body = b"Invalid host header"
67
+ await send({
68
+ "type": "http.response.start", "status": 400,
69
+ "headers": [(b"content-type", b"text/plain"), (b"content-length", str(len(body)).encode())],
70
+ })
71
+ await send({"type": "http.response.body", "body": body})
72
+ return None