agentmetry 0.9.8__tar.gz → 0.10.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (327) hide show
  1. {agentmetry-0.9.8 → agentmetry-0.10.0}/PKG-INFO +39 -13
  2. agentmetry-0.10.0/README.md +71 -0
  3. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/api/main.py +49 -2
  4. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/api/routes/audit.py +59 -4
  5. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/cli/__init__.py +129 -5
  6. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/api_token.py +76 -0
  7. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/external.py +18 -0
  8. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/heartbeat.py +60 -2
  9. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/hook_bootstrap.py +101 -20
  10. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/ingest.py +13 -1
  11. agentmetry-0.10.0/agentmetry/core/audit/ingest_quota.py +138 -0
  12. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/trail_db.py +13 -0
  13. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/auth.py +20 -0
  14. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/config.py +28 -0
  15. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/diagnostics/doctor.py +220 -2
  16. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/diagnostics/hook_coverage.py +62 -3
  17. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/diagnostics/machine_view.py +2 -2
  18. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/diagnostics/mcp_inventory.py +3 -2
  19. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/diagnostics/posture.py +17 -6
  20. agentmetry-0.10.0/agentmetry/core/managed_policy.py +818 -0
  21. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/paths.py +18 -0
  22. agentmetry-0.10.0/agentmetry/core/scanner/__init__.py +12 -0
  23. agentmetry-0.10.0/agentmetry/core/scanner/injection.py +209 -0
  24. agentmetry-0.10.0/agentmetry/core/scanner/known_packages.py +120 -0
  25. agentmetry-0.10.0/agentmetry/core/scanner/scan.py +255 -0
  26. agentmetry-0.10.0/agentmetry/core/scanner/typosquat.py +115 -0
  27. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/version.py +1 -1
  28. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/hooks/ingest.py +19 -1
  29. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/hooks/mcp_proxy.py +6 -0
  30. {agentmetry-0.9.8 → agentmetry-0.10.0}/pyproject.toml +2 -2
  31. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/conftest.py +25 -0
  32. agentmetry-0.10.0/tests/fixtures/scanner/benign_skill/SKILL.md +13 -0
  33. agentmetry-0.10.0/tests/fixtures/scanner/benign_tools.json +29 -0
  34. agentmetry-0.10.0/tests/fixtures/scanner/poisoned_skill/SKILL.md +11 -0
  35. agentmetry-0.10.0/tests/fixtures/scanner/poisoned_tools.json +26 -0
  36. agentmetry-0.10.0/tests/test_codex_windows_command.py +160 -0
  37. agentmetry-0.10.0/tests/test_coverage_observed.py +178 -0
  38. agentmetry-0.10.0/tests/test_demo_isolation.py +74 -0
  39. agentmetry-0.10.0/tests/test_demo_mode_flag.py +60 -0
  40. agentmetry-0.10.0/tests/test_doctor_json_and_remediation.py +154 -0
  41. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_heartbeat.py +6 -0
  42. agentmetry-0.10.0/tests/test_ingest_rate_limit.py +165 -0
  43. agentmetry-0.10.0/tests/test_ingest_token.py +284 -0
  44. agentmetry-0.10.0/tests/test_managed_policy.py +518 -0
  45. agentmetry-0.10.0/tests/test_scanner.py +220 -0
  46. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_static_serving.py +13 -0
  47. agentmetry-0.10.0/tests/test_user_profile_dir.py +138 -0
  48. agentmetry-0.9.8/README.md +0 -45
  49. {agentmetry-0.9.8 → agentmetry-0.10.0}/.dockerignore +0 -0
  50. {agentmetry-0.9.8 → agentmetry-0.10.0}/.env.agentmetry-demo +0 -0
  51. {agentmetry-0.9.8 → agentmetry-0.10.0}/.env.example +0 -0
  52. {agentmetry-0.9.8 → agentmetry-0.10.0}/.gitignore +0 -0
  53. {agentmetry-0.9.8 → agentmetry-0.10.0}/Dockerfile +0 -0
  54. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/__init__.py +0 -0
  55. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/api/__init__.py +0 -0
  56. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/api/routes/__init__.py +0 -0
  57. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/api/routes/auth.py +0 -0
  58. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/api/trusted_host.py +0 -0
  59. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/api/websocket.py +0 -0
  60. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/api/ws_bridge.py +0 -0
  61. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/cli/__main__.py +0 -0
  62. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/__init__.py +0 -0
  63. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/__init__.py +0 -0
  64. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/adapters/__init__.py +0 -0
  65. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/adapters/agt.py +0 -0
  66. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/adapters/chronicle.py +0 -0
  67. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/adapters/cloudevents.py +0 -0
  68. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/adapters/ecs.py +0 -0
  69. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/adapters/sentinel.py +0 -0
  70. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/adapters/splunk.py +0 -0
  71. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/alerts.py +0 -0
  72. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/antigravity_trajectory.py +0 -0
  73. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/atlas.py +0 -0
  74. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/canonical.py +0 -0
  75. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/cloud_bridge.py +0 -0
  76. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/compliance_digest.py +0 -0
  77. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/__init__.py +0 -0
  78. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/benchmark.py +0 -0
  79. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/attack_approval_denied_then_executed.jsonl +0 -0
  80. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/attack_arbitrary_host_stage_execute.jsonl +0 -0
  81. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/attack_autonomous_unapproved_write.jsonl +0 -0
  82. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/attack_credential_exfil.jsonl +0 -0
  83. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/attack_credential_then_cloud_api.jsonl +0 -0
  84. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/attack_destructive_delete_burst.jsonl +0 -0
  85. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/attack_discovery_then_collect.jsonl +0 -0
  86. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/attack_dotfile_then_git_push.jsonl +0 -0
  87. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/attack_encoded_command_download.jsonl +0 -0
  88. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/attack_env_credential_exfil.jsonl +0 -0
  89. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/attack_hashed_only_no_command.jsonl +0 -0
  90. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/attack_interpreter_egress.jsonl +0 -0
  91. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/attack_pr_merged_without_review.jsonl +0 -0
  92. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/attack_proc_substitution_cradle.jsonl +0 -0
  93. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/attack_remote_pipe_to_shell.jsonl +0 -0
  94. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/attack_remote_staging_then_execute.jsonl +0 -0
  95. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/attack_secret_manager_then_egress.jsonl +0 -0
  96. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/attack_session_tool_burst.jsonl +0 -0
  97. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/attack_single_command_exfil.jsonl +0 -0
  98. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/attack_single_quoted_interpreter_egress.jsonl +0 -0
  99. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/attack_ssh_directory_exfil.jsonl +0 -0
  100. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/attack_subagent_swarm.jsonl +0 -0
  101. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/attack_timestamp_collision.jsonl +0 -0
  102. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/attack_untrusted_input_then_action.jsonl +0 -0
  103. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/benign_authoring_merge_fixtures.jsonl +0 -0
  104. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/benign_autonomous_after_approval.jsonl +0 -0
  105. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/benign_build_artifact_cleanup.jsonl +0 -0
  106. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/benign_ci_artifact_download.jsonl +0 -0
  107. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/benign_commit_message_names_cloud_clis.jsonl +0 -0
  108. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/benign_container_registry_publish.jsonl +0 -0
  109. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/benign_database_migration.jsonl +0 -0
  110. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/benign_dependency_audit_network_burst.jsonl +0 -0
  111. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/benign_dependency_install_and_build.jsonl +0 -0
  112. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/benign_download_release_archive.jsonl +0 -0
  113. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/benign_env_example_read.jsonl +0 -0
  114. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/benign_fetch_data_then_run_repo_script.jsonl +0 -0
  115. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/benign_fetch_dataset_then_analyse.jsonl +0 -0
  116. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/benign_fetch_lockfile_then_install.jsonl +0 -0
  117. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/benign_fetch_piped_into_own_script.jsonl +0 -0
  118. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/benign_git_review_and_push.jsonl +0 -0
  119. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/benign_github_raw_docs_then_script.jsonl +0 -0
  120. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/benign_human_driven_deletes.jsonl +0 -0
  121. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/benign_local_api_probing.jsonl +0 -0
  122. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/benign_long_but_calm_session.jsonl +0 -0
  123. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/benign_loopback_is_not_egress.jsonl +0 -0
  124. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/benign_loopback_pipe_to_interpreter.jsonl +0 -0
  125. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/benign_mcp_text_mentions_credential_path.jsonl +0 -0
  126. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/benign_ordinary_development.jsonl +0 -0
  127. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/benign_package_manager_after_fetch.jsonl +0 -0
  128. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/benign_reading_config_that_is_not_secret.jsonl +0 -0
  129. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/benign_remote_api_call_no_credentials.jsonl +0 -0
  130. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/benign_research_then_docs.jsonl +0 -0
  131. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/benign_reversed_order_is_not_exfil.jsonl +0 -0
  132. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/benign_secret_names_and_docs.jsonl +0 -0
  133. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/benign_terraform_apply_with_profile.jsonl +0 -0
  134. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/benign_test_and_fix_loop.jsonl +0 -0
  135. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/benign_writing_about_credentials.jsonl +0 -0
  136. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/corpus/corpus.yaml +0 -0
  137. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/disposition.py +0 -0
  138. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/engine.py +0 -0
  139. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/live.py +0 -0
  140. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/live_store.py +0 -0
  141. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/models.py +0 -0
  142. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/rules.py +0 -0
  143. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/traits.py +0 -0
  144. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/yaml_config.py +0 -0
  145. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/detection/yaml_rules.py +0 -0
  146. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/dlp/__init__.py +0 -0
  147. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/dlp/loader.py +0 -0
  148. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/dlp/models.py +0 -0
  149. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/dlp/scanner.py +0 -0
  150. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/dogfood.py +0 -0
  151. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/evidence_pack.py +0 -0
  152. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/forwarder.py +0 -0
  153. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/grok_bot_transcript.py +0 -0
  154. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/hashing.py +0 -0
  155. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/identity.py +0 -0
  156. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/managed_hooks.py +0 -0
  157. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/mcp_inventory_event.py +0 -0
  158. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/migrate.py +0 -0
  159. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/mitre.py +0 -0
  160. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/otel_ingest.py +0 -0
  161. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/policy.py +0 -0
  162. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/policy_settings.py +0 -0
  163. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/posture_event.py +0 -0
  164. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/redaction.py +0 -0
  165. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/replay.py +0 -0
  166. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/run_context.py +0 -0
  167. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/session_tree.py +0 -0
  168. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/signing.py +0 -0
  169. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/sinks.py +0 -0
  170. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/spool.py +0 -0
  171. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/tool_policy/__init__.py +0 -0
  172. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/tool_policy/capability.py +0 -0
  173. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/tool_policy/evaluator.py +0 -0
  174. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/tool_policy/loader.py +0 -0
  175. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/tool_policy/models.py +0 -0
  176. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/trail_anchor.py +0 -0
  177. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/trail_chain.py +0 -0
  178. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/trail_merkle.py +0 -0
  179. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/audit/trail_rotation.py +0 -0
  180. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/bus/__init__.py +0 -0
  181. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/bus/audit_exporter.py +0 -0
  182. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/bus/bridges.py +0 -0
  183. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/bus/bus.py +0 -0
  184. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/bus/events.py +0 -0
  185. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/bus/outbox.py +0 -0
  186. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/dashboard_session.py +0 -0
  187. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/diagnostics/__init__.py +0 -0
  188. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/diagnostics/autostart.py +0 -0
  189. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/diagnostics/driver_paths.py +0 -0
  190. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/diagnostics/env_file.py +0 -0
  191. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/diagnostics/mcp_schema.py +0 -0
  192. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/drivers/__init__.py +0 -0
  193. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/drivers/host.py +0 -0
  194. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/drivers/permissions.py +0 -0
  195. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/drivers/spec.py +0 -0
  196. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/extensions.py +0 -0
  197. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/health.py +0 -0
  198. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/core/operator_identity.py +0 -0
  199. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/demo.py +0 -0
  200. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/hooks/__init__.py +0 -0
  201. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/policies/detection/manifest.yaml +0 -0
  202. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/policies/dlp/manifest.yaml +0 -0
  203. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/policies/opa/agent_rules.rego +0 -0
  204. {agentmetry-0.9.8 → agentmetry-0.10.0}/agentmetry/policies/tool/manifest.yaml +0 -0
  205. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/fixtures/agt_filesink_exfil.jsonl +0 -0
  206. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/fixtures/agt_filesink_mixed.jsonl +0 -0
  207. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/fixtures/codex_hooks/README.md +0 -0
  208. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/fixtures/codex_hooks/permission-request.command.output.schema.json +0 -0
  209. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/fixtures/codex_hooks/pre-tool-use.command.output.schema.json +0 -0
  210. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/fixtures/fake_mcp_server.py +0 -0
  211. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/fixtures/otel_claude_code_session.json +0 -0
  212. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_agentmetry_audit.py +0 -0
  213. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_agentmetry_ingest_client.py +0 -0
  214. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_agt_adapter.py +0 -0
  215. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_antigravity_trajectory.py +0 -0
  216. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_ask_gate_phase_a.py +0 -0
  217. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_atlas_detection_enrichment.py +0 -0
  218. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_atlas_mapping.py +0 -0
  219. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_audit_sinks.py +0 -0
  220. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_audit_stats.py +0 -0
  221. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_audit_tail.py +0 -0
  222. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_auth.py +0 -0
  223. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_auth_default.py +0 -0
  224. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_autostart.py +0 -0
  225. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_boot_sequence.py +0 -0
  226. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_burst_windows.py +0 -0
  227. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_chinese_agent_hooks.py +0 -0
  228. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_chinese_agent_sprint_b.py +0 -0
  229. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_chinese_agent_sprint_c.py +0 -0
  230. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_chronicle_adapter.py +0 -0
  231. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_cli_backup.py +0 -0
  232. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_cli_commands.py +0 -0
  233. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_cli_env_var_names.py +0 -0
  234. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_cloud_bridge.py +0 -0
  235. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_cloudevents_adapter.py +0 -0
  236. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_compliance_digest.py +0 -0
  237. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_container_and_legacy_runtime.py +0 -0
  238. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_copilot_hooks.py +0 -0
  239. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_crewai_adapter.py +0 -0
  240. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_demo.py +0 -0
  241. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_detection_adi.py +0 -0
  242. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_detection_benchmark.py +0 -0
  243. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_detection_default_config.py +0 -0
  244. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_detection_disposition.py +0 -0
  245. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_detection_disposition_api.py +0 -0
  246. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_detection_duplicate_emission.py +0 -0
  247. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_detection_emit_durability.py +0 -0
  248. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_detection_engine.py +0 -0
  249. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_detection_evasion.py +0 -0
  250. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_detection_hf_incident.py +0 -0
  251. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_detection_quoted_command_words.py +0 -0
  252. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_detection_rule_identity.py +0 -0
  253. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_detection_rules_v2.py +0 -0
  254. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_dlp_agent_env_override.py +0 -0
  255. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_dlp_invisible_unicode.py +0 -0
  256. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_dlp_markdown_exfil.py +0 -0
  257. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_dlp_scanner.py +0 -0
  258. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_doctor.py +0 -0
  259. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_dogfood.py +0 -0
  260. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_dogfood_freeze.py +0 -0
  261. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_driver_paths.py +0 -0
  262. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_ecs_threat.py +0 -0
  263. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_event_bus.py +0 -0
  264. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_event_signing.py +0 -0
  265. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_evidence_pack.py +0 -0
  266. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_extensions.py +0 -0
  267. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_external_ingest.py +0 -0
  268. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_forwarder.py +0 -0
  269. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_forwarding_shape.py +0 -0
  270. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_funding_manifest.py +0 -0
  271. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_grok_bot_transcript.py +0 -0
  272. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_hash_key.py +0 -0
  273. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_hook_bootstrap.py +0 -0
  274. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_hook_coverage.py +0 -0
  275. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_hook_decision_format.py +0 -0
  276. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_hook_enforcement_timing.py +0 -0
  277. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_hook_import_cost.py +0 -0
  278. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_hook_real_process.py +0 -0
  279. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_hook_spool.py +0 -0
  280. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_identity_enrich.py +0 -0
  281. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_identity_provider.py +0 -0
  282. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_ingest_roundtrip.py +0 -0
  283. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_isolation.py +0 -0
  284. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_launch_targets.py +0 -0
  285. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_live_detection.py +0 -0
  286. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_live_detection_store.py +0 -0
  287. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_machine_view.py +0 -0
  288. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_managed_hooks.py +0 -0
  289. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_mcp_audit_proxy.py +0 -0
  290. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_mcp_concealed_text.py +0 -0
  291. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_mcp_inventory.py +0 -0
  292. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_mcp_inventory_event.py +0 -0
  293. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_mcp_meta_fingerprint.py +0 -0
  294. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_mcp_proxy_packaged.py +0 -0
  295. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_mcp_schema.py +0 -0
  296. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_mitre_dlp.py +0 -0
  297. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_opensre_adapter.py +0 -0
  298. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_operator_attribution.py +0 -0
  299. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_otel_ingest.py +0 -0
  300. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_paths.py +0 -0
  301. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_posture.py +0 -0
  302. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_powershell_encoding.py +0 -0
  303. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_readme_claims.py +0 -0
  304. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_redaction.py +0 -0
  305. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_replay.py +0 -0
  306. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_sentinel.py +0 -0
  307. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_serve_script_binds_loopback.py +0 -0
  308. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_session_tree.py +0 -0
  309. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_shared_install.py +0 -0
  310. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_sigma_pack.py +0 -0
  311. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_subagent_lifecycle.py +0 -0
  312. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_suite_leaves_real_home_alone.py +0 -0
  313. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_tool_policy.py +0 -0
  314. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_tool_policy_agent_cli.py +0 -0
  315. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_tool_policy_git_hooks.py +0 -0
  316. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_trail_anchor.py +0 -0
  317. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_trail_chain.py +0 -0
  318. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_trail_concurrency.py +0 -0
  319. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_trail_merkle.py +0 -0
  320. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_transcript_watcher.py +0 -0
  321. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_trusted_host.py +0 -0
  322. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_unattended_agent_policy.py +0 -0
  323. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_version.py +0 -0
  324. {agentmetry-0.9.8 → agentmetry-0.10.0}/tests/test_yaml_detection_rules.py +0 -0
  325. {agentmetry-0.9.8 → agentmetry-0.10.0}/tools/generate_sigma_pack.py +0 -0
  326. {agentmetry-0.9.8 → agentmetry-0.10.0}/tools/mcp_audit_proxy.py +0 -0
  327. {agentmetry-0.9.8 → agentmetry-0.10.0}/tools/vault_fs_server.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentmetry
3
- Version: 0.9.8
4
- Summary: Local-first flight recorder for AI coding agents: hash-chained audit trail, MITRE-mapped sequence detection, DLP
3
+ Version: 0.10.0
4
+ Summary: Local-first endpoint sensor 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
7
7
  Project-URL: Issues, https://github.com/blitzcrieg1/agentmetry/issues
@@ -37,26 +37,45 @@ 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.10; extra == 'dev'
40
+ Requires-Dist: ruff==0.17.1; extra == 'dev'
41
41
  Description-Content-Type: text/markdown
42
42
 
43
43
  # Agentmetry
44
44
 
45
- A local-first flight recorder for AI coding agents.
45
+ A local-first endpoint sensor for AI coding agents.
46
46
 
47
- Agentmetry hooks the tool lifecycle of Claude Code, Cursor, Codex and Antigravity,
48
- writes every tool call, approval and denial to a hash-chained JSONL trail on your
49
- machine, and runs sequence detection over the session. A credential read followed
50
- by network egress becomes one finding rather than two unremarkable log lines.
47
+ Agentmetry hooks the tool lifecycle of Claude Code, Cursor, Codex, GitHub Copilot,
48
+ Antigravity, Qwen, Kimi, Qoder and CodeBuddy, writes every tool call, approval and
49
+ denial to a hash-chained JSONL trail on your machine, and runs sequence detection
50
+ over the session. A credential read followed by network egress becomes one finding
51
+ rather than two unremarkable log lines. Findings carry MITRE ATT&CK labels, and
52
+ MITRE ATLAS labels where ATLAS says something ATT&CK cannot.
51
53
 
52
- Everything runs locally. There are no cloud calls and no telemetry. Forwarding to
53
- Elastic ECS, Splunk HEC or a webhook exists and is off unless you configure it.
54
+ Nothing leaves the machine unless you configure it, and there is no telemetry.
55
+ Forwarding to Splunk HEC, Elastic ECS, Google SecOps, Microsoft Sentinel or a
56
+ webhook (canonical JSON or CloudEvents) is off by default.
57
+
58
+ ## Try it with nothing installed
59
+
60
+ ```bash
61
+ uvx agentmetry demo
62
+ ```
63
+
64
+ It replays a session into a temporary directory it deletes: a private-key read,
65
+ then an upload. That raises one critical finding. It then edits the trail and
66
+ shows `agentmetry verify` fail.
67
+
68
+ ## Record a real agent
54
69
 
55
70
  ```bash
56
71
  pip install agentmetry
72
+ agentmetry hooks install
73
+ agentmetry start
57
74
  agentmetry doctor
58
75
  ```
59
76
 
77
+ `agentmetry dashboard` opens the local console with a one-time sign-in link.
78
+
60
79
  ## Check the detection claims yourself
61
80
 
62
81
  The corpus ships inside the package, so this works from a clean install:
@@ -72,12 +91,19 @@ matters: any tool can fire on an attack, and a feed that cries wolf gets muted.
72
91
  ## What it does not do
73
92
 
74
93
  - **It is not a CASB.** It records the agents you wire in. An unmanaged browser
75
- assistant is invisible to it.
76
- - **It is a recorder, not a sandbox.** The only enforcement path is pre-execution
77
- DLP blocking in the hook.
94
+ assistant, or an IDE with its hooks off, is invisible to it.
95
+ - **It is a recorder, not a sandbox.** By default it records. DLP and tool policy
96
+ can deny a call in the hook, before it runs, when you set them to block.
78
97
  - **The DLP is regex, not ML.** A starting pack you extend in YAML.
98
+ - **macOS is in preview.** CI runs the full suite on Windows, Linux and macOS, but
99
+ only Windows and Linux are required checks, and no hook has been exercised
100
+ against a live agent on a Mac.
79
101
  - **It is a public alpha.** Integration surfaces may still change.
80
102
 
103
+ For a fleet, a managed policy file can lock settings such as the DLP and tool
104
+ policy modes so a developer cannot change them. See
105
+ [docs/managed-policy.md](https://github.com/blitzcrieg1/agentmetry/blob/master/docs/managed-policy.md).
106
+
81
107
  Full documentation, the event schema, and the SIEM integration guides are in the
82
108
  repository.
83
109
 
@@ -0,0 +1,71 @@
1
+ # Agentmetry
2
+
3
+ A local-first endpoint sensor for AI coding agents.
4
+
5
+ Agentmetry hooks the tool lifecycle of Claude Code, Cursor, Codex, GitHub Copilot,
6
+ Antigravity, Qwen, Kimi, Qoder and CodeBuddy, writes every tool call, approval and
7
+ denial to a hash-chained JSONL trail on your machine, and runs sequence detection
8
+ over the session. A credential read followed by network egress becomes one finding
9
+ rather than two unremarkable log lines. Findings carry MITRE ATT&CK labels, and
10
+ MITRE ATLAS labels where ATLAS says something ATT&CK cannot.
11
+
12
+ Nothing leaves the machine unless you configure it, and there is no telemetry.
13
+ Forwarding to Splunk HEC, Elastic ECS, Google SecOps, Microsoft Sentinel or a
14
+ webhook (canonical JSON or CloudEvents) is off by default.
15
+
16
+ ## Try it with nothing installed
17
+
18
+ ```bash
19
+ uvx agentmetry demo
20
+ ```
21
+
22
+ It replays a session into a temporary directory it deletes: a private-key read,
23
+ then an upload. That raises one critical finding. It then edits the trail and
24
+ shows `agentmetry verify` fail.
25
+
26
+ ## Record a real agent
27
+
28
+ ```bash
29
+ pip install agentmetry
30
+ agentmetry hooks install
31
+ agentmetry start
32
+ agentmetry doctor
33
+ ```
34
+
35
+ `agentmetry dashboard` opens the local console with a one-time sign-in link.
36
+
37
+ ## Check the detection claims yourself
38
+
39
+ The corpus ships inside the package, so this works from a clean install:
40
+
41
+ ```bash
42
+ agentmetry benchmark
43
+ ```
44
+
45
+ It replays recorded sessions through the real rule engine and exits non-zero on
46
+ any missed rule or any false positive. The benign half is the number that
47
+ matters: any tool can fire on an attack, and a feed that cries wolf gets muted.
48
+
49
+ ## What it does not do
50
+
51
+ - **It is not a CASB.** It records the agents you wire in. An unmanaged browser
52
+ assistant, or an IDE with its hooks off, is invisible to it.
53
+ - **It is a recorder, not a sandbox.** By default it records. DLP and tool policy
54
+ can deny a call in the hook, before it runs, when you set them to block.
55
+ - **The DLP is regex, not ML.** A starting pack you extend in YAML.
56
+ - **macOS is in preview.** CI runs the full suite on Windows, Linux and macOS, but
57
+ only Windows and Linux are required checks, and no hook has been exercised
58
+ against a live agent on a Mac.
59
+ - **It is a public alpha.** Integration surfaces may still change.
60
+
61
+ For a fleet, a managed policy file can lock settings such as the DLP and tool
62
+ policy modes so a developer cannot change them. See
63
+ [docs/managed-policy.md](https://github.com/blitzcrieg1/agentmetry/blob/master/docs/managed-policy.md).
64
+
65
+ Full documentation, the event schema, and the SIEM integration guides are in the
66
+ repository.
67
+
68
+ - Source and issues: https://github.com/blitzcrieg1/agentmetry
69
+ - Website: https://agentmetry.ai
70
+
71
+ Apache-2.0.
@@ -79,6 +79,22 @@ def _start_forwarders() -> list[asyncio.Task]:
79
79
  return tasks
80
80
 
81
81
 
82
+ def antigravity_watcher_wanted() -> bool:
83
+ """Whether this recorder should tail the developer's Antigravity transcripts.
84
+
85
+ It used to start whenever the script existed, so every recorder did it,
86
+ including the dashboard demo, whose trail is supposed to hold seeded events
87
+ only. The watcher reads `~/.gemini/antigravity/brain`, the real one, so a
88
+ demo shown to someone could carry the presenter's actual agent activity, and
89
+ a second recorder doubled every Antigravity call. It now runs only where the
90
+ recorder accepts ingest, and `AGENTMETRY_ANTIGRAVITY_WATCHER=0` turns it off.
91
+ """
92
+ if not settings.audit_ingest_enabled:
93
+ return False
94
+ flag = os.environ.get("AGENTMETRY_ANTIGRAVITY_WATCHER", "").strip().lower()
95
+ return flag not in ("0", "false", "no", "off")
96
+
97
+
82
98
  def start_transcript_watcher(watcher_path: Path) -> subprocess.Popen:
83
99
  """Start the Antigravity transcript watcher, tied to this process's life.
84
100
 
@@ -111,6 +127,20 @@ def stop_transcript_watcher(process: subprocess.Popen) -> None:
111
127
  async def lifespan(app: FastAPI):
112
128
  from agentmetry.core.audit.migrate import backfill_db_from_jsonl
113
129
 
130
+ # The ingest-only token, which hooks prefer and the ingest route accepts.
131
+ # Without it a hook running as another account (Codex's Windows sandbox)
132
+ # could not authenticate and every event it sent was lost (#302). Owner-
133
+ # only like the full token; `hooks install` widens it to Codex's sandbox
134
+ # accounts where they exist. A failure here costs that, never the boot.
135
+ from agentmetry.core import api_token
136
+ from agentmetry.core.auth import auth_is_disabled
137
+
138
+ if not auth_is_disabled():
139
+ try:
140
+ api_token.ensure_ingest_token()
141
+ except OSError:
142
+ logger.exception("Could not write the ingest-only token; hooks fall back to the full token")
143
+
114
144
  # Backfill SQLite from the existing JSONL trail. A broken or locked audit DB
115
145
  # must degrade the dashboard, never stop the daemon from booting: the
116
146
  # recorder's job is to keep recording.
@@ -211,7 +241,7 @@ async def lifespan(app: FastAPI):
211
241
  watcher_process = None
212
242
  try:
213
243
  watcher_path = Path(__file__).resolve().parents[4] / "scripts" / "antigravity_transcript_watcher.py"
214
- if watcher_path.exists():
244
+ if watcher_path.exists() and antigravity_watcher_wanted():
215
245
  watcher_process = start_transcript_watcher(watcher_path)
216
246
  logger.info("Started Antigravity transcript watcher (PID: %s)", watcher_process.pid)
217
247
  except Exception as exc:
@@ -306,7 +336,24 @@ def mount_dashboard(target: FastAPI, directory: Path = _DASHBOARD_DIR) -> bool:
306
336
 
307
337
  from fastapi.staticfiles import StaticFiles
308
338
 
309
- target.mount("/", StaticFiles(directory=str(directory), html=True), name="dashboard")
339
+ class DashboardFiles(StaticFiles):
340
+ """The export, with its HTML pages marked `no-cache`.
341
+
342
+ Served with no cache header at all, a browser may reuse a stale page on
343
+ an ordinary reload, and the page names the old scripts, so an upgraded
344
+ recorder kept showing the previous dashboard until a hard reload.
345
+ `no-cache` still lets the browser keep a copy; it only has to ask
346
+ whether the page changed. The scripts are not touched: their names
347
+ change with every build.
348
+ """
349
+
350
+ async def get_response(self, path: str, scope): # type: ignore[override]
351
+ response = await super().get_response(path, scope)
352
+ if response.headers.get("content-type", "").startswith("text/html"):
353
+ response.headers["Cache-Control"] = "no-cache"
354
+ return response
355
+
356
+ target.mount("/", DashboardFiles(directory=str(directory), html=True), name="dashboard")
310
357
  logger.info("Serving dashboard from %s", directory)
311
358
  return True
312
359
 
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
+ import logging
6
7
  from datetime import datetime, timedelta, timezone
7
8
  from pathlib import Path
8
9
  from typing import Any, Literal
@@ -11,12 +12,13 @@ from fastapi import APIRouter, Depends, HTTPException, Query, Request
11
12
  from fastapi.responses import FileResponse
12
13
  from pydantic import BaseModel, Field, field_validator
13
14
 
14
- from agentmetry.core.auth import require_api_key
15
+ from agentmetry.core.auth import require_api_key, require_ingest_key
15
16
  from agentmetry.core.audit.detection.disposition import STATUSES, get_disposition_store
16
17
  from agentmetry.core.audit.ingest import ingest_external_event
17
18
  from agentmetry.core.config import settings
18
19
 
19
20
  router = APIRouter(prefix="/audit", tags=["audit"])
21
+ logger = logging.getLogger(__name__)
20
22
 
21
23
  _RUN_ACTION_TYPES = frozenset({
22
24
  "session_start",
@@ -105,7 +107,7 @@ class ExternalIngestBody(BaseModel):
105
107
  )
106
108
  event_type: str = Field(
107
109
  ...,
108
- description="session_start | session_end | tool_called | tool_denied | tool_failed | approval_request | approval_response | mcp_schema",
110
+ description="session_start | session_end | tool_called | tool_denied | tool_failed | approval_request | approval_response | mcp_schema | hook_remediation",
109
111
  )
110
112
  correlation_id: str = ""
111
113
  session_id: str = ""
@@ -142,6 +144,8 @@ class ExternalIngestBody(BaseModel):
142
144
  # drops what it does not know, silently, which has already cost this file
143
145
  # two shipped bugs.
144
146
  schema_concealed: dict[str, int] = Field(default_factory=dict)
147
+ # Model-steering text in a tool description (#188), counts per category.
148
+ schema_injection: dict[str, int] = Field(default_factory=dict)
145
149
  server_version: str = ""
146
150
  list_changed: bool | None = None
147
151
 
@@ -191,7 +195,24 @@ def _event_source_app(event: dict[str, Any]) -> str:
191
195
  return "agentmetry"
192
196
 
193
197
 
194
- @router.post("/ingest", dependencies=[Depends(require_api_key)])
198
+ _THROTTLE_WARNED: dict[str, float] = {}
199
+
200
+
201
+ def _warn_throttled(source_app: str) -> None:
202
+ """Log throttling once a minute per source, so a flood does not flood the log too."""
203
+ import time
204
+
205
+ now = time.monotonic()
206
+ if now - _THROTTLE_WARNED.get(source_app, -60.0) >= 60.0:
207
+ _THROTTLE_WARNED[source_app] = now
208
+ # The caller passes a resolved, allowlisted name, so this changes
209
+ # nothing today. It keeps a future caller that passes client text from
210
+ # forging log lines, and it is the guard code scanning can see.
211
+ safe = (source_app or "unknown").replace("\r", "").replace("\n", "")
212
+ logger.warning("Ingest rate limit reached for source %s; events are spooled for replay", safe)
213
+
214
+
215
+ @router.post("/ingest", dependencies=[Depends(require_ingest_key)])
195
216
  async def audit_ingest(body: ExternalIngestBody):
196
217
  """Accept canonical adapter events from Cursor, Claude, MCP proxy, etc."""
197
218
  if not settings.audit_ingest_enabled:
@@ -199,6 +220,26 @@ async def audit_ingest(body: ExternalIngestBody):
199
220
  if not settings.audit_export_enabled:
200
221
  raise HTTPException(status_code=503, detail="Audit export is disabled")
201
222
 
223
+ # Rate limit per source app and per session (#312). The hook spools a 429
224
+ # and the recorder's drain replays it with the original timestamps, so a
225
+ # burst past the cap is late, not lost.
226
+ from agentmetry.core.audit.external import _resolve_source_app
227
+ from agentmetry.core.audit.ingest_quota import get_limiter
228
+
229
+ # Keyed and logged by the name the trail will record, not the one the
230
+ # client sent: an unlisted name becomes `unknown`. Keyed on raw text, a
231
+ # flood inventing a name per event got a fresh bucket each time, and the
232
+ # warning wrote client text into the log.
233
+ source = _resolve_source_app(body.source_app)[0]
234
+ limiter = get_limiter()
235
+ if not limiter.allow(source, body.session_id or body.correlation_id):
236
+ _warn_throttled(source)
237
+ raise HTTPException(
238
+ status_code=429,
239
+ detail="Ingest rate limit reached; the hook spools this event and the recorder replays it",
240
+ headers={"Retry-After": str(limiter.retry_after())},
241
+ )
242
+
202
243
  payload = body.model_dump(exclude_none=True)
203
244
  try:
204
245
  canonical = await ingest_external_event(payload)
@@ -495,11 +536,19 @@ async def audit_status():
495
536
  Powers the freshness badge and `selftest` — makes silent hook failure visible
496
537
  instead of the operator falsely believing they are being audited.
497
538
  """
539
+ from agentmetry.core.audit.ingest_quota import get_limiter
498
540
  from agentmetry.core.audit.spool import spool_depth, spool_oldest_age_seconds
499
541
  from agentmetry.core.audit.trail_db import get_trail_db
500
542
  path = Path(settings.audit_export_path)
501
543
  if not settings.audit_export_enabled:
502
- return {"enabled": False, "last_event_utc": None, "recent": 0, "by_source": {}, "path": str(path)}
544
+ return {
545
+ "enabled": False,
546
+ "last_event_utc": None,
547
+ "recent": 0,
548
+ "by_source": {},
549
+ "path": str(path),
550
+ "demo": settings.demo_mode,
551
+ }
503
552
 
504
553
  status_data = get_trail_db().status()
505
554
  # Pending spool depth belongs in the same payload as freshness. A feed that
@@ -516,6 +565,12 @@ async def audit_status():
516
565
  # Per SIEM: the last seq it accepted, and how long it has been failing.
517
566
  "forwarding": _forward_status(),
518
567
  "spool_oldest_age_seconds": spool_oldest_age_seconds(),
568
+ # Events refused by the ingest rate limit since the recorder started.
569
+ # They went to the hook spool, so a non-zero count is late work, and a
570
+ # growing one is an agent sending far faster than agents do (#312).
571
+ "ingest_throttled": get_limiter().snapshot(),
572
+ # A demo server's events are synthetic. The dashboard says so from here.
573
+ "demo": settings.demo_mode,
519
574
  }
520
575
 
521
576
 
@@ -48,6 +48,70 @@ def _api_base_url(port: int) -> str:
48
48
  return os.environ.get("AGENTMETRY_URL", "").strip().rstrip("/") or _base_url(port)
49
49
 
50
50
 
51
+ def _add_dir_flags(parser: argparse.ArgumentParser) -> None:
52
+ """The path split. Omit the flags and nothing moves.
53
+
54
+ `SUPPRESS`, so the same flag on `agentmetry` and on the subcommand do not
55
+ overwrite each other with a default of None.
56
+ """
57
+ parser.add_argument(
58
+ "--data-dir", default=argparse.SUPPRESS,
59
+ help="trail, spool, token and logs (env AGENTMETRY_DATA_DIR)",
60
+ )
61
+ parser.add_argument(
62
+ "--user-profile-dir", default=argparse.SUPPRESS,
63
+ help="the developer home whose IDE configs to install into and check "
64
+ "(env AGENTMETRY_USER_PROFILE_DIR); for a recorder run as root or LocalSystem",
65
+ )
66
+ parser.add_argument(
67
+ "--policy-dir", default=argparse.SUPPRESS,
68
+ help="an extra managed policy location, checked like the platform one "
69
+ "(env AGENTMETRY_POLICY_DIR)",
70
+ )
71
+
72
+
73
+ def _apply_dir_flags(args: argparse.Namespace) -> None:
74
+ """Export the path flags before a command reads data_dir() or the profile.
75
+
76
+ Exported, not passed: `start` spawns `serve`, and the hook commands it
77
+ writes run in other processes, all of which read the environment.
78
+ """
79
+ global _DATA_DIR, _PID_FILE
80
+ if getattr(args, "user_profile_dir", None):
81
+ os.environ["AGENTMETRY_USER_PROFILE_DIR"] = args.user_profile_dir
82
+ if getattr(args, "policy_dir", None):
83
+ os.environ["AGENTMETRY_POLICY_DIR"] = args.policy_dir
84
+ if getattr(args, "data_dir", None):
85
+ os.environ["AGENTMETRY_DATA_DIR"] = args.data_dir
86
+ # Only when the flag was given: a caller that already pointed these at
87
+ # a temp tree must not have them recomputed under it.
88
+ _DATA_DIR = data_dir()
89
+ _PID_FILE = _DATA_DIR / "agentmetry.pid"
90
+
91
+
92
+ def _apply_managed_policy(args: argparse.Namespace) -> None:
93
+ """Locks over flags and environment, for this process and everything it starts.
94
+
95
+ After `_apply_dir_flags`, so `--data-dir` cannot move a trail a fleet
96
+ locked. Says so when it overrides a flag: a silently ignored flag reads as
97
+ a bug.
98
+ """
99
+ global _DATA_DIR, _PID_FILE
100
+ from agentmetry.core import managed_policy
101
+
102
+ try:
103
+ policy = managed_policy.apply_locks()
104
+ except Exception as exc:
105
+ print(f"Managed policy was not applied: {exc}", file=sys.stderr)
106
+ return
107
+ if "data_dir" not in policy.locked:
108
+ return
109
+ if getattr(args, "data_dir", None):
110
+ print("--data-dir ignored: managed policy locks data_dir.", file=sys.stderr)
111
+ _DATA_DIR = data_dir()
112
+ _PID_FILE = _DATA_DIR / "agentmetry.pid"
113
+
114
+
51
115
  def _api_headers() -> dict[str, str]:
52
116
  from agentmetry.core.api_token import client_token
53
117
 
@@ -564,7 +628,14 @@ def cmd_cloud(args: argparse.Namespace) -> int:
564
628
  headers = {"Content-Type": "application/json"}
565
629
  if key:
566
630
  headers["Authorization"] = f"Bearer {key}"
567
- resp = httpx.post(f"{url}/api/v1/audit/ingest", json=payload, headers=headers, timeout=15)
631
+ # A 429 is the ingest rate limit (#312). Unlike a hook, this importer
632
+ # has no spool, so it waits as told and tries again rather than drop
633
+ # the event.
634
+ for _ in range(6):
635
+ resp = httpx.post(f"{url}/api/v1/audit/ingest", json=payload, headers=headers, timeout=15)
636
+ if resp.status_code != 429:
637
+ break
638
+ time.sleep(min(30.0, float(resp.headers.get("Retry-After", "1") or 1)))
568
639
  ok = resp.status_code < 300
569
640
  if ok:
570
641
  posted += 1
@@ -636,11 +707,12 @@ def _hooks_status() -> int:
636
707
  """
637
708
  from agentmetry.core.diagnostics import hook_coverage
638
709
 
639
- if hook_coverage.is_service_profile():
710
+ if hook_coverage.profile_blind():
640
711
  print(
641
712
  "UNDETERMINABLE: running under a service profile. Hook configuration "
642
713
  "lives in each developer's profile and none is visible from here. "
643
- "Run this in the user's context.",
714
+ "Run this in the user's context, or name the developer's home with "
715
+ "--user-profile-dir.",
644
716
  file=sys.stderr,
645
717
  )
646
718
  return 2
@@ -1074,6 +1146,32 @@ def cmd_mcp(args: argparse.Namespace) -> int:
1074
1146
  return 0
1075
1147
 
1076
1148
 
1149
+ def cmd_scan(args: argparse.Namespace) -> int:
1150
+ """Check what an agent is about to depend on: MCP servers, skills, packages.
1151
+
1152
+ Exits 1 when anything is found and 2 when nothing could be read. Unlike
1153
+ `mcp` and `posture`, which a health check runs in the background, this is
1154
+ run on purpose before something is installed, so a finding should stop the
1155
+ script that ran it. See core/scanner/scan.py.
1156
+ """
1157
+ import json as _json
1158
+ from pathlib import Path as _Path
1159
+
1160
+ from agentmetry.core.scanner.scan import run
1161
+
1162
+ report = run(
1163
+ args.target,
1164
+ project=_Path(args.project).resolve() if args.project else _Path.cwd(),
1165
+ known_file=args.known,
1166
+ )
1167
+ if args.json:
1168
+ print(_json.dumps(report.to_json(), indent=2))
1169
+ else:
1170
+ for line in report.lines():
1171
+ print(line)
1172
+ return report.exit_code()
1173
+
1174
+
1077
1175
  def cmd_posture(args: argparse.Namespace) -> int:
1078
1176
  """Report agent settings that take a human or a hook out of the loop.
1079
1177
 
@@ -1414,10 +1512,10 @@ def cmd_demo(args: argparse.Namespace) -> int:
1414
1512
  def cmd_doctor(args: argparse.Namespace) -> int:
1415
1513
  """SIEM preflight: manifests, trail chain, orchestrator health, hooks."""
1416
1514
  sys.path.insert(0, str(_ORCH_ROOT))
1417
- from agentmetry.core.diagnostics.doctor import format_report, run_doctor
1515
+ from agentmetry.core.diagnostics.doctor import print_report, run_doctor
1418
1516
 
1419
1517
  report = run_doctor(fix_drivers=getattr(args, "fix", False))
1420
- print("Agentmetry doctor\n" + format_report(report))
1518
+ print_report(report, as_json=getattr(args, "json", False))
1421
1519
  return report.exit_code
1422
1520
 
1423
1521
 
@@ -1451,6 +1549,7 @@ def main(argv: list[str] | None = None) -> int:
1451
1549
  prog="agentmetry",
1452
1550
  description="Agentmetry local ops. New here? Start with `agentmetry demo`.",
1453
1551
  )
1552
+ _add_dir_flags(parser)
1454
1553
  parser.add_argument("--port", type=int, default=8000)
1455
1554
  sub = parser.add_subparsers(dest="command", required=True)
1456
1555
  # First, so it heads the help: it is the one command that needs nothing set up.
@@ -1465,6 +1564,7 @@ def main(argv: list[str] | None = None) -> int:
1465
1564
 
1466
1565
  sub.add_parser("stop", help="stop the orchestrator")
1467
1566
  start = sub.add_parser("start", help="start the orchestrator (detached)")
1567
+ _add_dir_flags(start)
1468
1568
  start.add_argument(
1469
1569
  "--host",
1470
1570
  default="127.0.0.1",
@@ -1474,12 +1574,14 @@ def main(argv: list[str] | None = None) -> int:
1474
1574
  "serve",
1475
1575
  help="run the orchestrator in the foreground (what autostart registers)",
1476
1576
  )
1577
+ _add_dir_flags(serve)
1477
1578
  serve.add_argument("--host", default="127.0.0.1")
1478
1579
  serve.add_argument("--port", type=int, default=8000)
1479
1580
  hooks = sub.add_parser(
1480
1581
  "hooks",
1481
1582
  help="install IDE hook configs for the agents on this machine",
1482
1583
  )
1584
+ _add_dir_flags(hooks)
1483
1585
  hooks.add_argument(
1484
1586
  "action",
1485
1587
  choices=("install", "status"),
@@ -1629,6 +1731,18 @@ def main(argv: list[str] | None = None) -> int:
1629
1731
  posture.add_argument(
1630
1732
  "--strict", action="store_true", help="exit 1 when any high-severity finding is present",
1631
1733
  )
1734
+ scan = sub.add_parser(
1735
+ "scan",
1736
+ help="check MCP servers, skills or a package name before an agent depends on them",
1737
+ )
1738
+ scan.add_argument(
1739
+ "target", nargs="?", default=None,
1740
+ help="a file or folder (skills, rules, a saved tools/list), npm:NAME or pypi:NAME; "
1741
+ "omit to scan the MCP servers configured on this machine",
1742
+ )
1743
+ scan.add_argument("--project", default=None, help="project root whose MCP config to include (default: cwd)")
1744
+ scan.add_argument("--known", default=None, help="file of extra known package names, one per line (npm:/pypi: or bare)")
1745
+ scan.add_argument("--json", action="store_true", help="print the report as JSON")
1632
1746
  otel = sub.add_parser(
1633
1747
  "otel", help="receive Claude Code's native OpenTelemetry stream (no hooks needed)"
1634
1748
  )
@@ -1681,6 +1795,13 @@ def main(argv: list[str] | None = None) -> int:
1681
1795
  doctor = sub.add_parser(
1682
1796
  "doctor", help="SIEM preflight (manifests, trail chain, health, hooks)"
1683
1797
  )
1798
+ _add_dir_flags(doctor)
1799
+ doctor.add_argument(
1800
+ "--json",
1801
+ action="store_true",
1802
+ help="the report as JSON, for a deployment script: exit_code, "
1803
+ "orchestrator_reachable, hooks, policy and findings",
1804
+ )
1684
1805
  doctor.add_argument(
1685
1806
  "--fix",
1686
1807
  action="store_true",
@@ -1708,6 +1829,8 @@ def main(argv: list[str] | None = None) -> int:
1708
1829
  replay.add_argument("--trail", default="", help="trail to read (default: the configured trail)")
1709
1830
 
1710
1831
  args = parser.parse_args(argv)
1832
+ _apply_dir_flags(args)
1833
+ _apply_managed_policy(args)
1711
1834
  handlers = {
1712
1835
  "demo": cmd_demo,
1713
1836
  "start": cmd_start,
@@ -1732,6 +1855,7 @@ def main(argv: list[str] | None = None) -> int:
1732
1855
  "anchor": cmd_anchor,
1733
1856
  "mcp": cmd_mcp,
1734
1857
  "posture": cmd_posture,
1858
+ "scan": cmd_scan,
1735
1859
  "dashboard": cmd_dashboard,
1736
1860
  "otel": cmd_otel,
1737
1861
  "import-agt": cmd_import_agt,
@@ -155,6 +155,82 @@ def write_ingest_token(token: str, path: Path | None = None) -> Path:
155
155
  return target
156
156
 
157
157
 
158
+ def ensure_ingest_token(path: Path | None = None) -> str:
159
+ """The ingest-only token, creating it once if it does not exist yet.
160
+
161
+ The core used to leave this to Agentmetry Enterprise, which left the open
162
+ source install with one credential that could do everything. A hook that
163
+ runs as another account (Codex's Windows sandbox runs hooks as its own local
164
+ user) then had two choices: be handed the full key, which also reads the
165
+ trail and closes detections, or record nothing. It recorded nothing, and
166
+ nothing said so (#302). An existing token, Enterprise's included, is kept.
167
+ """
168
+ target = path or ingest_token_path()
169
+ existing = read_token(target)
170
+ if existing:
171
+ return existing
172
+ return read_token(write_ingest_token(secrets.token_urlsafe(32), target))
173
+
174
+
175
+ #: The local accounts Codex's Windows sandbox runs commands and hooks as.
176
+ CODEX_SANDBOX_ACCOUNTS = ("CodexSandboxOffline", "CodexSandboxOnline")
177
+
178
+
179
+ def codex_sandbox_accounts() -> list[str]:
180
+ """Which of Codex's sandbox accounts exist on this Windows machine."""
181
+ if os.name != "nt":
182
+ return []
183
+ import subprocess # account lookup only; the hook never calls this
184
+ found = []
185
+ for name in CODEX_SANDBOX_ACCOUNTS:
186
+ try:
187
+ probe = subprocess.run(["net", "user", name], capture_output=True, check=False, timeout=15)
188
+ except (OSError, subprocess.SubprocessError):
189
+ continue
190
+ if probe.returncode == 0:
191
+ found.append(name)
192
+ return found
193
+
194
+
195
+ def grant_read(path: Path, accounts: list[str]) -> list[str]:
196
+ """Add a read entry for each account to the file's ACL. Windows only.
197
+
198
+ Only ever called on the ingest-only token: what an account gains is the
199
+ ability to send events, never to read the trail or close a detection.
200
+ """
201
+ if os.name != "nt" or not accounts:
202
+ return []
203
+ import subprocess # ACL work only; the hook reads tokens on every call
204
+ granted = []
205
+ for name in accounts:
206
+ try:
207
+ done = subprocess.run(
208
+ ["icacls", str(path), "/grant", f"{name}:R"],
209
+ capture_output=True, check=False, timeout=15,
210
+ )
211
+ except (OSError, subprocess.SubprocessError):
212
+ continue
213
+ if done.returncode == 0:
214
+ granted.append(name)
215
+ return granted
216
+
217
+
218
+ def readable_by(path: Path, accounts: list[str]) -> list[str] | None:
219
+ """Which of these accounts hold an entry on the file's ACL, or None if it cannot be told."""
220
+ if os.name != "nt":
221
+ return None
222
+ import subprocess # ACL work only; the hook reads tokens on every call
223
+ try:
224
+ out = subprocess.run(
225
+ ["icacls", str(path)], capture_output=True, text=True, check=False, timeout=15
226
+ ).stdout.lower()
227
+ except (OSError, subprocess.SubprocessError):
228
+ return None
229
+ if not out:
230
+ return None
231
+ return [name for name in accounts if f"\\{name.lower()}:" in out or f" {name.lower()}:" in out]
232
+
233
+
158
234
  def auth_disabled() -> bool:
159
235
  return os.environ.get("AGENTMETRY_AUTH_DISABLED", "").strip().lower() in _TRUTHY
160
236