agentmetry 0.9.4__tar.gz → 0.9.6__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 (291) hide show
  1. {agentmetry-0.9.4 → agentmetry-0.9.6}/.gitignore +8 -0
  2. {agentmetry-0.9.4 → agentmetry-0.9.6}/PKG-INFO +2 -2
  3. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/cli/__init__.py +40 -2
  4. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/atlas.py +1 -1
  5. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/canonical.py +1 -1
  6. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/external.py +31 -5
  7. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/heartbeat.py +7 -0
  8. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/hook_bootstrap.py +79 -1
  9. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/managed_hooks.py +22 -1
  10. agentmetry-0.9.6/agentmetry/core/audit/posture_event.py +104 -0
  11. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/tool_policy/evaluator.py +3 -2
  12. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/config.py +8 -0
  13. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/diagnostics/hook_coverage.py +34 -1
  14. agentmetry-0.9.6/agentmetry/core/diagnostics/posture.py +414 -0
  15. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/version.py +1 -1
  16. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/hooks/ingest.py +211 -16
  17. {agentmetry-0.9.4 → agentmetry-0.9.6}/pyproject.toml +1 -1
  18. agentmetry-0.9.6/tests/fixtures/codex_hooks/README.md +10 -0
  19. agentmetry-0.9.6/tests/fixtures/codex_hooks/permission-request.command.output.schema.json +91 -0
  20. agentmetry-0.9.6/tests/fixtures/codex_hooks/pre-tool-use.command.output.schema.json +93 -0
  21. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_atlas_detection_enrichment.py +1 -1
  22. agentmetry-0.9.6/tests/test_copilot_hooks.py +211 -0
  23. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_external_ingest.py +37 -0
  24. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_hook_coverage.py +29 -1
  25. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_hook_decision_format.py +95 -0
  26. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_managed_hooks.py +26 -0
  27. agentmetry-0.9.6/tests/test_posture.py +199 -0
  28. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_readme_claims.py +40 -0
  29. {agentmetry-0.9.4 → agentmetry-0.9.6}/tools/generate_sigma_pack.py +3 -3
  30. {agentmetry-0.9.4 → agentmetry-0.9.6}/.dockerignore +0 -0
  31. {agentmetry-0.9.4 → agentmetry-0.9.6}/.env.agentmetry-demo +0 -0
  32. {agentmetry-0.9.4 → agentmetry-0.9.6}/.env.example +0 -0
  33. {agentmetry-0.9.4 → agentmetry-0.9.6}/Dockerfile +0 -0
  34. {agentmetry-0.9.4 → agentmetry-0.9.6}/README.md +0 -0
  35. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/__init__.py +0 -0
  36. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/api/__init__.py +0 -0
  37. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/api/main.py +0 -0
  38. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/api/routes/__init__.py +0 -0
  39. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/api/routes/audit.py +0 -0
  40. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/api/routes/auth.py +0 -0
  41. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/api/trusted_host.py +0 -0
  42. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/api/websocket.py +0 -0
  43. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/api/ws_bridge.py +0 -0
  44. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/cli/__main__.py +0 -0
  45. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/__init__.py +0 -0
  46. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/api_token.py +0 -0
  47. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/__init__.py +0 -0
  48. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/adapters/__init__.py +0 -0
  49. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/adapters/agt.py +0 -0
  50. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/adapters/chronicle.py +0 -0
  51. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/adapters/cloudevents.py +0 -0
  52. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/adapters/ecs.py +0 -0
  53. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/adapters/sentinel.py +0 -0
  54. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/adapters/splunk.py +0 -0
  55. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/alerts.py +0 -0
  56. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/compliance_digest.py +0 -0
  57. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/__init__.py +0 -0
  58. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/benchmark.py +0 -0
  59. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_approval_denied_then_executed.jsonl +0 -0
  60. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_arbitrary_host_stage_execute.jsonl +0 -0
  61. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_autonomous_unapproved_write.jsonl +0 -0
  62. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_credential_exfil.jsonl +0 -0
  63. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_credential_then_cloud_api.jsonl +0 -0
  64. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_destructive_delete_burst.jsonl +0 -0
  65. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_discovery_then_collect.jsonl +0 -0
  66. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_dotfile_then_git_push.jsonl +0 -0
  67. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_encoded_command_download.jsonl +0 -0
  68. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_env_credential_exfil.jsonl +0 -0
  69. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_hashed_only_no_command.jsonl +0 -0
  70. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_interpreter_egress.jsonl +0 -0
  71. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_pr_merged_without_review.jsonl +0 -0
  72. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_proc_substitution_cradle.jsonl +0 -0
  73. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_remote_pipe_to_shell.jsonl +0 -0
  74. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_remote_staging_then_execute.jsonl +0 -0
  75. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_secret_manager_then_egress.jsonl +0 -0
  76. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_session_tool_burst.jsonl +0 -0
  77. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_single_command_exfil.jsonl +0 -0
  78. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_single_quoted_interpreter_egress.jsonl +0 -0
  79. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_ssh_directory_exfil.jsonl +0 -0
  80. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_subagent_swarm.jsonl +0 -0
  81. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_timestamp_collision.jsonl +0 -0
  82. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/attack_untrusted_input_then_action.jsonl +0 -0
  83. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_authoring_merge_fixtures.jsonl +0 -0
  84. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_autonomous_after_approval.jsonl +0 -0
  85. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_build_artifact_cleanup.jsonl +0 -0
  86. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_ci_artifact_download.jsonl +0 -0
  87. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_commit_message_names_cloud_clis.jsonl +0 -0
  88. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_database_migration.jsonl +0 -0
  89. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_dependency_install_and_build.jsonl +0 -0
  90. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_download_release_archive.jsonl +0 -0
  91. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_env_example_read.jsonl +0 -0
  92. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_fetch_data_then_run_repo_script.jsonl +0 -0
  93. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_fetch_dataset_then_analyse.jsonl +0 -0
  94. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_fetch_lockfile_then_install.jsonl +0 -0
  95. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_fetch_piped_into_own_script.jsonl +0 -0
  96. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_git_review_and_push.jsonl +0 -0
  97. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_github_raw_docs_then_script.jsonl +0 -0
  98. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_human_driven_deletes.jsonl +0 -0
  99. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_local_api_probing.jsonl +0 -0
  100. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_long_but_calm_session.jsonl +0 -0
  101. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_loopback_is_not_egress.jsonl +0 -0
  102. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_loopback_pipe_to_interpreter.jsonl +0 -0
  103. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_mcp_text_mentions_credential_path.jsonl +0 -0
  104. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_ordinary_development.jsonl +0 -0
  105. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_package_manager_after_fetch.jsonl +0 -0
  106. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_reading_config_that_is_not_secret.jsonl +0 -0
  107. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_remote_api_call_no_credentials.jsonl +0 -0
  108. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_research_then_docs.jsonl +0 -0
  109. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_reversed_order_is_not_exfil.jsonl +0 -0
  110. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_secret_names_and_docs.jsonl +0 -0
  111. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_test_and_fix_loop.jsonl +0 -0
  112. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/benign_writing_about_credentials.jsonl +0 -0
  113. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/corpus/corpus.yaml +0 -0
  114. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/disposition.py +0 -0
  115. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/engine.py +0 -0
  116. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/live.py +0 -0
  117. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/live_store.py +0 -0
  118. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/models.py +0 -0
  119. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/rules.py +0 -0
  120. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/traits.py +0 -0
  121. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/yaml_config.py +0 -0
  122. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/detection/yaml_rules.py +0 -0
  123. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/dlp/__init__.py +0 -0
  124. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/dlp/loader.py +0 -0
  125. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/dlp/models.py +0 -0
  126. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/dlp/scanner.py +0 -0
  127. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/dogfood.py +0 -0
  128. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/evidence_pack.py +0 -0
  129. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/forwarder.py +0 -0
  130. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/hashing.py +0 -0
  131. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/identity.py +0 -0
  132. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/ingest.py +0 -0
  133. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/mcp_inventory_event.py +0 -0
  134. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/migrate.py +0 -0
  135. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/mitre.py +0 -0
  136. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/otel_ingest.py +0 -0
  137. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/policy.py +0 -0
  138. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/policy_settings.py +0 -0
  139. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/redaction.py +0 -0
  140. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/replay.py +0 -0
  141. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/run_context.py +0 -0
  142. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/signing.py +0 -0
  143. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/sinks.py +0 -0
  144. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/spool.py +0 -0
  145. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/tool_policy/__init__.py +0 -0
  146. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/tool_policy/capability.py +0 -0
  147. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/tool_policy/loader.py +0 -0
  148. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/tool_policy/models.py +0 -0
  149. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/trail_anchor.py +0 -0
  150. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/trail_chain.py +0 -0
  151. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/trail_db.py +0 -0
  152. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/trail_merkle.py +0 -0
  153. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/audit/trail_rotation.py +0 -0
  154. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/auth.py +0 -0
  155. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/bus/__init__.py +0 -0
  156. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/bus/audit_exporter.py +0 -0
  157. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/bus/bridges.py +0 -0
  158. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/bus/bus.py +0 -0
  159. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/bus/events.py +0 -0
  160. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/bus/outbox.py +0 -0
  161. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/dashboard_session.py +0 -0
  162. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/diagnostics/__init__.py +0 -0
  163. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/diagnostics/autostart.py +0 -0
  164. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/diagnostics/doctor.py +0 -0
  165. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/diagnostics/driver_paths.py +0 -0
  166. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/diagnostics/env_file.py +0 -0
  167. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/diagnostics/mcp_inventory.py +0 -0
  168. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/diagnostics/mcp_schema.py +0 -0
  169. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/drivers/__init__.py +0 -0
  170. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/drivers/host.py +0 -0
  171. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/drivers/permissions.py +0 -0
  172. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/drivers/spec.py +0 -0
  173. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/extensions.py +0 -0
  174. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/health.py +0 -0
  175. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/operator_identity.py +0 -0
  176. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/core/paths.py +0 -0
  177. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/demo.py +0 -0
  178. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/hooks/__init__.py +0 -0
  179. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/hooks/mcp_proxy.py +0 -0
  180. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/policies/detection/manifest.yaml +0 -0
  181. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/policies/dlp/manifest.yaml +0 -0
  182. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/policies/opa/agent_rules.rego +0 -0
  183. {agentmetry-0.9.4 → agentmetry-0.9.6}/agentmetry/policies/tool/manifest.yaml +0 -0
  184. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/conftest.py +0 -0
  185. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/fixtures/agt_filesink_exfil.jsonl +0 -0
  186. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/fixtures/agt_filesink_mixed.jsonl +0 -0
  187. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/fixtures/fake_mcp_server.py +0 -0
  188. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/fixtures/otel_claude_code_session.json +0 -0
  189. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_agentmetry_audit.py +0 -0
  190. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_agentmetry_ingest_client.py +0 -0
  191. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_agt_adapter.py +0 -0
  192. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_ask_gate_phase_a.py +0 -0
  193. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_atlas_mapping.py +0 -0
  194. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_audit_sinks.py +0 -0
  195. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_audit_stats.py +0 -0
  196. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_audit_tail.py +0 -0
  197. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_auth.py +0 -0
  198. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_auth_default.py +0 -0
  199. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_autostart.py +0 -0
  200. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_boot_sequence.py +0 -0
  201. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_burst_windows.py +0 -0
  202. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_chinese_agent_hooks.py +0 -0
  203. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_chinese_agent_sprint_b.py +0 -0
  204. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_chinese_agent_sprint_c.py +0 -0
  205. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_chronicle_adapter.py +0 -0
  206. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_cli_backup.py +0 -0
  207. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_cli_commands.py +0 -0
  208. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_cli_env_var_names.py +0 -0
  209. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_cloudevents_adapter.py +0 -0
  210. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_compliance_digest.py +0 -0
  211. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_container_and_legacy_runtime.py +0 -0
  212. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_crewai_adapter.py +0 -0
  213. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_demo.py +0 -0
  214. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_detection_adi.py +0 -0
  215. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_detection_benchmark.py +0 -0
  216. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_detection_default_config.py +0 -0
  217. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_detection_disposition.py +0 -0
  218. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_detection_disposition_api.py +0 -0
  219. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_detection_duplicate_emission.py +0 -0
  220. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_detection_emit_durability.py +0 -0
  221. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_detection_engine.py +0 -0
  222. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_detection_evasion.py +0 -0
  223. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_detection_hf_incident.py +0 -0
  224. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_detection_quoted_command_words.py +0 -0
  225. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_detection_rule_identity.py +0 -0
  226. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_detection_rules_v2.py +0 -0
  227. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_dlp_agent_env_override.py +0 -0
  228. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_dlp_invisible_unicode.py +0 -0
  229. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_dlp_markdown_exfil.py +0 -0
  230. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_dlp_scanner.py +0 -0
  231. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_doctor.py +0 -0
  232. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_dogfood.py +0 -0
  233. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_dogfood_freeze.py +0 -0
  234. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_driver_paths.py +0 -0
  235. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_ecs_threat.py +0 -0
  236. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_event_bus.py +0 -0
  237. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_event_signing.py +0 -0
  238. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_evidence_pack.py +0 -0
  239. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_extensions.py +0 -0
  240. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_forwarder.py +0 -0
  241. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_forwarding_shape.py +0 -0
  242. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_funding_manifest.py +0 -0
  243. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_hash_key.py +0 -0
  244. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_heartbeat.py +0 -0
  245. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_hook_bootstrap.py +0 -0
  246. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_hook_enforcement_timing.py +0 -0
  247. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_hook_import_cost.py +0 -0
  248. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_hook_real_process.py +0 -0
  249. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_hook_spool.py +0 -0
  250. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_identity_enrich.py +0 -0
  251. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_identity_provider.py +0 -0
  252. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_ingest_roundtrip.py +0 -0
  253. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_isolation.py +0 -0
  254. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_launch_targets.py +0 -0
  255. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_live_detection.py +0 -0
  256. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_live_detection_store.py +0 -0
  257. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_mcp_audit_proxy.py +0 -0
  258. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_mcp_concealed_text.py +0 -0
  259. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_mcp_inventory.py +0 -0
  260. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_mcp_inventory_event.py +0 -0
  261. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_mcp_meta_fingerprint.py +0 -0
  262. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_mcp_proxy_packaged.py +0 -0
  263. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_mcp_schema.py +0 -0
  264. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_mitre_dlp.py +0 -0
  265. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_opensre_adapter.py +0 -0
  266. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_operator_attribution.py +0 -0
  267. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_otel_ingest.py +0 -0
  268. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_paths.py +0 -0
  269. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_powershell_encoding.py +0 -0
  270. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_redaction.py +0 -0
  271. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_replay.py +0 -0
  272. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_sentinel.py +0 -0
  273. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_serve_script_binds_loopback.py +0 -0
  274. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_shared_install.py +0 -0
  275. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_sigma_pack.py +0 -0
  276. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_static_serving.py +0 -0
  277. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_subagent_lifecycle.py +0 -0
  278. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_suite_leaves_real_home_alone.py +0 -0
  279. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_tool_policy.py +0 -0
  280. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_tool_policy_agent_cli.py +0 -0
  281. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_tool_policy_git_hooks.py +0 -0
  282. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_trail_anchor.py +0 -0
  283. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_trail_chain.py +0 -0
  284. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_trail_concurrency.py +0 -0
  285. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_trail_merkle.py +0 -0
  286. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_trusted_host.py +0 -0
  287. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_unattended_agent_policy.py +0 -0
  288. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_version.py +0 -0
  289. {agentmetry-0.9.4 → agentmetry-0.9.6}/tests/test_yaml_detection_rules.py +0 -0
  290. {agentmetry-0.9.4 → agentmetry-0.9.6}/tools/mcp_audit_proxy.py +0 -0
  291. {agentmetry-0.9.4 → agentmetry-0.9.6}/tools/vault_fs_server.py +0 -0
@@ -100,3 +100,11 @@ docs/*-audit-*.md
100
100
  # pytest-cov data file
101
101
  .coverage
102
102
  .coverage.*
103
+
104
+ # Demo video renders and their working files (scripts/make_demo_video.py).
105
+ # The site serves its own copy from ai-audit-watch/public/video/.
106
+ docs/assets/*.mp4
107
+ docs/assets/agentmetry-demo-poster.png
108
+ docs/assets/agentmetry-linkedin-*-poster.png
109
+ docs/assets/voice/
110
+ docs/assets/demo-stills/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentmetry
3
- Version: 0.9.4
3
+ Version: 0.9.6
4
4
  Summary: Local-first flight recorder for AI coding agents: hash-chained audit trail, MITRE-mapped sequence detection, DLP
5
5
  Project-URL: Homepage, https://agentmetry.ai
6
6
  Project-URL: Source, https://github.com/blitzcrieg1/agentmetry
@@ -37,7 +37,7 @@ Provides-Extra: dev
37
37
  Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
38
38
  Requires-Dist: pytest-cov>=6.0; extra == 'dev'
39
39
  Requires-Dist: pytest>=8.0; extra == 'dev'
40
- Requires-Dist: ruff==0.16.9; extra == 'dev'
40
+ Requires-Dist: ruff==0.16.10; extra == 'dev'
41
41
  Description-Content-Type: text/markdown
42
42
 
43
43
  # Agentmetry
@@ -526,7 +526,7 @@ def cmd_install(args: argparse.Namespace) -> int:
526
526
  #: live in ~/.gemini and are written by scripts/install_antigravity_hooks.ps1,
527
527
  #: which has no Python equivalent. Listing it here and quietly doing nothing
528
528
  #: would be the reporting failure this codebase keeps finding in itself.
529
- _HOOK_INSTALLERS = ("cursor", "claude", "codex", "qwen", "kimi", "qoder", "codebuddy")
529
+ _HOOK_INSTALLERS = ("cursor", "claude", "codex", "qwen", "kimi", "qoder", "codebuddy", "copilot")
530
530
 
531
531
 
532
532
  def _hooks_status() -> int:
@@ -972,6 +972,35 @@ def cmd_mcp(args: argparse.Namespace) -> int:
972
972
  return 0
973
973
 
974
974
 
975
+ def cmd_posture(args: argparse.Namespace) -> int:
976
+ """Report agent settings that take a human or a hook out of the loop.
977
+
978
+ Exits 0 with findings by default, like `mcp`: a posture is a risk report, and
979
+ a health check somebody wired into MDM should not fail on one. `--strict`
980
+ turns any high finding into exit 1 for a pipeline that wants exactly that.
981
+ """
982
+ import json as _json
983
+ from pathlib import Path as _Path
984
+
985
+ from agentmetry.core.diagnostics.posture import HIGH, NOT_CHECKED, collect, summary_lines
986
+
987
+ posture = collect(_Path(args.project).resolve() if args.project else _Path.cwd())
988
+ if args.json:
989
+ print(_json.dumps({
990
+ "findings": [{**f.wire(), "why": f.why} for f in posture.sorted()],
991
+ "counts": posture.counts(),
992
+ "files_read": len(posture.files_read),
993
+ "files_unreadable": len(posture.unreadable),
994
+ "not_checked": sorted(NOT_CHECKED),
995
+ }, indent=2))
996
+ else:
997
+ for line in summary_lines(posture):
998
+ print(line)
999
+ if args.strict and posture.counts()[HIGH]:
1000
+ return 1
1001
+ return 0
1002
+
1003
+
975
1004
  def cmd_otel(args: argparse.Namespace) -> int:
976
1005
  """Receive Claude Code's native OpenTelemetry stream and record it.
977
1006
 
@@ -1360,7 +1389,7 @@ def main(argv: list[str] | None = None) -> int:
1360
1389
  hooks.add_argument(
1361
1390
  "--managed",
1362
1391
  action="store_true",
1363
- help="use each vendor's admin-managed, machine-wide location (claude, cursor, codex); needs admin",
1392
+ help="use each vendor's admin-managed, machine-wide location (claude, cursor, codex, copilot); needs admin",
1364
1393
  )
1365
1394
  hooks.add_argument(
1366
1395
  "--lock",
@@ -1485,6 +1514,14 @@ def main(argv: list[str] | None = None) -> int:
1485
1514
  "--digest", action="store_true",
1486
1515
  help="also print the config digest, which names no server and is safe to publish",
1487
1516
  )
1517
+ posture = sub.add_parser(
1518
+ "posture", help="report agent settings that skip approval prompts or turn hooks off"
1519
+ )
1520
+ posture.add_argument("--project", default=None, help="project root to include (default: cwd)")
1521
+ posture.add_argument("--json", action="store_true", help="print findings as JSON")
1522
+ posture.add_argument(
1523
+ "--strict", action="store_true", help="exit 1 when any high-severity finding is present",
1524
+ )
1488
1525
  otel = sub.add_parser(
1489
1526
  "otel", help="receive Claude Code's native OpenTelemetry stream (no hooks needed)"
1490
1527
  )
@@ -1586,6 +1623,7 @@ def main(argv: list[str] | None = None) -> int:
1586
1623
  "prove": cmd_prove,
1587
1624
  "anchor": cmd_anchor,
1588
1625
  "mcp": cmd_mcp,
1626
+ "posture": cmd_posture,
1589
1627
  "dashboard": cmd_dashboard,
1590
1628
  "otel": cmd_otel,
1591
1629
  "import-agt": cmd_import_agt,
@@ -170,7 +170,7 @@ def attach_atlas(tool: Any) -> None:
170
170
  # adversary technique a whole sequence is evidence of, which is a claim only a
171
171
  # rule can make and never a single call.
172
172
  #
173
- # Deliberately small. Of the fifteen built-in rules, one describes an ATLAS
173
+ # Deliberately small. Of the fourteen published rules, one describes an ATLAS
174
174
  # technique; the rest are host and operations behaviour that ATT&CK already
175
175
  # covers, and tagging them would put an AI-threat label on a shell command.
176
176
  # The empty entries are the design, not a backlog.
@@ -1,4 +1,4 @@
1
- """Map durable outbox rows to Agentmetry canonical events (schema v1.1.0)."""
1
+ """Map durable outbox rows to Agentmetry canonical events (schema v1.2.0)."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import re
5
6
  import uuid
6
7
  from datetime import datetime, timezone
7
8
  from typing import Any, Literal
@@ -19,12 +20,12 @@ from agentmetry.core.config import settings
19
20
 
20
21
  ExternalApp = Literal[
21
22
  "cursor", "claude", "antigravity", "codex", "mcp_proxy",
22
- "qwen", "kimi", "qoder", "codebuddy", "trae", "crewai", "opensre", "agentmetry",
23
+ "qwen", "kimi", "qoder", "codebuddy", "trae", "crewai", "opensre", "copilot", "agentmetry",
23
24
  ]
24
25
 
25
26
  KNOWN_EXTERNAL_APPS = frozenset({
26
27
  "cursor", "claude", "antigravity", "codex", "mcp_proxy",
27
- "qwen", "kimi", "qoder", "codebuddy", "trae", "crewai", "opensre",
28
+ "qwen", "kimi", "qoder", "codebuddy", "trae", "crewai", "opensre", "copilot",
28
29
  })
29
30
 
30
31
  _ACTION_MAP = {
@@ -49,11 +50,34 @@ def _split_tool(qualified: str) -> tuple[str, str]:
49
50
  return "", qualified
50
51
 
51
52
 
53
+ #: What an agent that is not on the allowlist is recorded as.
54
+ UNKNOWN_APP = "unknown"
55
+
56
+ _CLAIMED_APP_UNSAFE = re.compile(r"[^a-z0-9_.-]")
57
+
58
+
59
+ def _resolve_source_app(raw: Any) -> tuple[str, str]:
60
+ """The app to record, and the name the client claimed when that differs.
61
+
62
+ An unlisted name used to be recorded as `cursor`. So an OpenClaw plugin, a
63
+ LangGraph handler or anything else posting to ingest landed in the trail,
64
+ the SIEM and the detections attributed to Cursor: a confident wrong answer
65
+ in a record whose value is that it is right. It is now `unknown`, with the
66
+ claimed name kept beside it, reduced to a short identifier so a client
67
+ cannot write arbitrary text into a field SIEM rules group on.
68
+
69
+ A missing name still means Cursor. That default predates `source_app`, and
70
+ hooks installed before it may still post without one.
71
+ """
72
+ source_app = str(raw or "cursor").strip().lower()
73
+ if source_app in KNOWN_EXTERNAL_APPS or source_app == "agentmetry":
74
+ return source_app, ""
75
+ return UNKNOWN_APP, _CLAIMED_APP_UNSAFE.sub("", source_app)[:64]
76
+
77
+
52
78
  def build_external_canonical(payload: dict[str, Any]) -> dict[str, Any]:
53
79
  """Normalize an adapter payload into canonical v1.1 JSON."""
54
- source_app = str(payload.get("source_app") or "cursor").lower()
55
- if source_app not in KNOWN_EXTERNAL_APPS and source_app != "agentmetry":
56
- source_app = "cursor"
80
+ source_app, claimed_app = _resolve_source_app(payload.get("source_app"))
57
81
 
58
82
  event_type = str(payload.get("event_type") or "tool_called")
59
83
  action_type, default_outcome = _ACTION_MAP.get(event_type, ("tool_called", "success"))
@@ -130,6 +154,8 @@ def build_external_canonical(payload: dict[str, Any]) -> dict[str, Any]:
130
154
  },
131
155
  "model": {"id": model_id, "provider": model_provider},
132
156
  }
157
+ if claimed_app:
158
+ event["source"]["claimed_app"] = claimed_app
133
159
 
134
160
  if tool_qualified:
135
161
  server = str(tool_block.get("server") or driver_name or source_app)
@@ -300,6 +300,7 @@ async def heartbeat_forever(interval: float | None = None) -> None:
300
300
  import asyncio
301
301
 
302
302
  from agentmetry.core.audit.mcp_inventory_event import InventoryState, maybe_emit_inventory
303
+ from agentmetry.core.audit.posture_event import PostureState, maybe_emit_posture
303
304
 
304
305
  period = interval if interval is not None else interval_seconds()
305
306
  if not period:
@@ -307,6 +308,7 @@ async def heartbeat_forever(interval: float | None = None) -> None:
307
308
  return
308
309
 
309
310
  inventory_state = InventoryState()
311
+ posture_state = PostureState()
310
312
  while True:
311
313
  try:
312
314
  event = await emit_heartbeat()
@@ -321,4 +323,9 @@ async def heartbeat_forever(interval: float | None = None) -> None:
321
323
  await maybe_emit_inventory(inventory_state)
322
324
  except Exception:
323
325
  logger.exception("MCP inventory event failed")
326
+ try:
327
+ # Opt-in, like the inventory, and in its own try for the same reason.
328
+ await maybe_emit_posture(posture_state)
329
+ except Exception:
330
+ logger.exception("Posture event failed")
324
331
  await asyncio.sleep(period)
@@ -555,6 +555,83 @@ def install_codex_global_hooks(
555
555
  return hooks_path
556
556
 
557
557
 
558
+ # --- GitHub Copilot (CLI and VS Code agent mode, ~/.copilot/hooks/) -----------
559
+ # One file serves both. Copilot CLI reads every *.json in ~/.copilot/hooks (or
560
+ # $COPILOT_HOME/hooks), and VS Code's Local harness reads the same directory and
561
+ # accepts the CLI's format, detected by its `version` field: lowerCamel event
562
+ # names, mapped to its own, and `bash` / `powershell` command fields, mapped to
563
+ # its per-OS commands (microsoft/vscode `hookSchema.ts`, `hookTypes.ts`).
564
+ #
565
+ # The file is ours alone, so it is written whole rather than merged: a user's own
566
+ # hooks live in their own files beside it, and Copilot loads them all.
567
+ #
568
+ # The PowerShell form needs `&`. VS Code on Windows runs a hook through
569
+ # `powershell -Command`, where a command that starts with a quoted path is a
570
+ # string expression, not a call. Without the call operator the hook would exit
571
+ # non-zero, which VS Code treats as a non-blocking error: nothing recorded and
572
+ # every deny an allow.
573
+ COPILOT_HOOK_FILE = "agentmetry.json"
574
+
575
+ #: (Copilot's event key, the PascalCase name our mapper and enforcement key on).
576
+ #: `permissionRequest` is CLI-only and VS Code does not map it; `preToolUse` is
577
+ #: the gate on both.
578
+ COPILOT_HOOK_EVENTS: tuple[tuple[str, str], ...] = (
579
+ ("sessionStart", "SessionStart"),
580
+ ("userPromptSubmitted", "UserPromptSubmit"),
581
+ ("preToolUse", "PreToolUse"),
582
+ ("postToolUse", "PostToolUse"),
583
+ ("sessionEnd", "SessionEnd"),
584
+ ("agentStop", "Stop"),
585
+ )
586
+
587
+
588
+ def copilot_hooks_dir() -> Path:
589
+ home = os.environ.get("COPILOT_HOME", "").strip()
590
+ return (Path(home).expanduser() if home else Path.home() / ".copilot") / "hooks"
591
+
592
+
593
+ def copilot_hooks_document(
594
+ *, python: str | None = None, ingest: Path | None = None, repo_root: Path | None = None
595
+ ) -> dict[str, Any]:
596
+ """The whole hooks file, in Copilot CLI's version-1 format."""
597
+ hooks: dict[str, Any] = {}
598
+ for key, event in COPILOT_HOOK_EVENTS:
599
+ command = hook_command("copilot", event, python=python, ingest=ingest, repo_root=repo_root)
600
+ hooks[key] = [
601
+ {
602
+ "type": "command",
603
+ "bash": command,
604
+ "powershell": f"& {command}",
605
+ "timeoutSec": 10,
606
+ }
607
+ ]
608
+ return {"version": 1, "hooks": hooks}
609
+
610
+
611
+ def install_copilot_global_hooks(
612
+ *, repo_root: Path | None = None, python: str | None = None
613
+ ) -> Path | None:
614
+ """Write ~/.copilot/hooks/agentmetry.json for Copilot CLI and VS Code agent mode.
615
+
616
+ Not called from `bootstrap_tier_b_hooks`. VS Code's hooks are a preview
617
+ feature an organisation can switch off, so a silent install could look like
618
+ coverage that is not there; installing is a decision `hooks install` makes.
619
+ """
620
+ root = repo_root or _repo_root()
621
+ if hook_target(root) == "none":
622
+ logger.warning("Copilot hook bootstrap skipped: no way to reach ingest from %s", root)
623
+ return None
624
+ doc = copilot_hooks_document(
625
+ python=python or sys.executable, ingest=_ingest_script(root), repo_root=root
626
+ )
627
+ hooks_dir = copilot_hooks_dir()
628
+ hooks_dir.mkdir(parents=True, exist_ok=True)
629
+ path = hooks_dir / COPILOT_HOOK_FILE
630
+ path.write_text(json.dumps(doc, indent=2) + "\n", encoding="utf-8")
631
+ logger.info("Installed global Copilot audit hooks -> %s", path)
632
+ return path
633
+
634
+
558
635
  def _kimi_config_dir() -> Path:
559
636
  home = os.environ.get("KIMI_CODE_HOME", "").strip()
560
637
  if home:
@@ -648,9 +725,10 @@ if __name__ == "__main__":
648
725
  "qoder": install_qoder_global_hooks,
649
726
  "codebuddy": install_codebuddy_global_hooks,
650
727
  "codex": install_codex_global_hooks,
728
+ "copilot": install_copilot_global_hooks,
651
729
  }
652
730
  fn = installers.get(target, bootstrap_tier_b_hooks)
653
- if target in ("qwen", "kimi", "qoder", "codebuddy", "codex"):
731
+ if target in ("qwen", "kimi", "qoder", "codebuddy", "codex", "copilot"):
654
732
  path = fn()
655
733
  if path:
656
734
  print(f"Installed global {target} hooks -> {path}")
@@ -18,6 +18,14 @@ survive anything the user does to their own config:
18
18
  - Codex: `requirements.toml` (Windows `%ProgramData%\\OpenAI\\Codex`, else
19
19
  `/etc/codex`), `[[hooks.<Event>]]` entries; `allow_managed_hooks_only`
20
20
  (opt-in) skips user, project and plugin hooks.
21
+ - GitHub Copilot CLI: a policy hook file in `policy.d` (Windows
22
+ `C:\\ProgramData\\GitHub\\Copilot\\policy.d`, else
23
+ `/etc/github-copilot/policy.d`). Policy hooks load before every other hook,
24
+ `disableAllHooks` cannot turn them off, and on POSIX Copilot ignores one that
25
+ is not root-owned or is group- or world-writable, so it is written 0644. There
26
+ is no lock to opt into: a policy hook already cannot be disabled. GitHub's
27
+ docs describe policy hooks for the CLI; whether VS Code agent mode reads them
28
+ is not documented, so the per-user install still matters there.
21
29
 
22
30
  Paths and keys are from the vendors' documentation as of 2026-10. Two of these
23
31
  files are shared with whatever else an organisation manages, so every write
@@ -38,7 +46,7 @@ from typing import Any
38
46
 
39
47
  from agentmetry.core.audit import hook_bootstrap as hb
40
48
 
41
- MANAGED_AGENTS = ("claude", "cursor", "codex")
49
+ MANAGED_AGENTS = ("claude", "cursor", "codex", "copilot")
42
50
  CLAUDE_DROPIN = "50-agentmetry.json"
43
51
  CODEX_BEGIN = "# agentmetry managed hooks begin (written by `agentmetry hooks install --managed`)"
44
52
  CODEX_END = "# agentmetry managed hooks end"
@@ -70,11 +78,18 @@ def codex_requirements() -> Path:
70
78
  return Path("/etc/codex/requirements.toml")
71
79
 
72
80
 
81
+ def copilot_policy_dir() -> Path:
82
+ if sys.platform == "win32":
83
+ return _program_data() / "GitHub" / "Copilot" / "policy.d"
84
+ return Path("/etc/github-copilot/policy.d")
85
+
86
+
73
87
  def managed_paths() -> dict[str, Path]:
74
88
  return {
75
89
  "claude": claude_managed_dir() / "managed-settings.d" / CLAUDE_DROPIN,
76
90
  "cursor": cursor_enterprise_hooks(),
77
91
  "codex": codex_requirements(),
92
+ "copilot": copilot_policy_dir() / hb.COPILOT_HOOK_FILE,
78
93
  }
79
94
 
80
95
 
@@ -190,9 +205,15 @@ def install(agent: str, *, lock: bool = False) -> Path:
190
205
  elif agent == "codex":
191
206
  current = path.read_text(encoding="utf-8") if path.is_file() else ""
192
207
  text = merge_codex_requirements(current, lock=lock)
208
+ elif agent == "copilot":
209
+ # Ours alone, like the Claude drop-in: written, not merged.
210
+ text = json.dumps(hb.copilot_hooks_document(), indent=2) + "\n"
193
211
  else:
194
212
  raise ValueError(f"no managed location for {agent}")
195
213
  _write(path, text)
214
+ if agent == "copilot" and sys.platform != "win32":
215
+ # Copilot skips a policy file that is group- or world-writable.
216
+ os.chmod(path, 0o644)
196
217
  return path
197
218
 
198
219
 
@@ -0,0 +1,104 @@
1
+ """Agent posture as an event a SIEM can see, when an operator asks.
2
+
3
+ `agentmetry posture` answers "which agents here skip approval or have hooks
4
+ off", for whoever runs it on that machine. A fleet wants the same answer in the
5
+ SIEM, by host, and wants to know the day it changed.
6
+
7
+ Same contract as the MCP inventory event (`mcp_inventory_event`): off unless
8
+ `AGENTMETRY_POSTURE=1`, emitted when the recorder starts, when the findings
9
+ change and once a day regardless, written to the trail first and then to every
10
+ sink. Each finding is agent, scope label, setting, value and severity: no file
11
+ path, so no home directory or username leaves the machine.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import time
17
+ import uuid
18
+ from dataclasses import dataclass
19
+ from typing import Any
20
+
21
+ from agentmetry.core.audit.canonical import SCHEMA_VERSION
22
+ from agentmetry.core.audit.identity import identity_fields
23
+
24
+ #: Re-emit an unchanged posture this often, so a fleet view over a day is whole.
25
+ SNAPSHOT_SECONDS = 24 * 3600
26
+
27
+
28
+ def enabled() -> bool:
29
+ from agentmetry.core.config import settings
30
+
31
+ return bool(getattr(settings, "posture_enabled", False))
32
+
33
+
34
+ @dataclass
35
+ class PostureState:
36
+ digest: str | None = None
37
+ emitted_at: float = 0.0
38
+
39
+
40
+ def build_posture_event(now_utc: str, posture: Any, *, outcome: str) -> dict[str, Any]:
41
+ from agentmetry.core.diagnostics.posture import NOT_CHECKED
42
+
43
+ findings = [f.wire() for f in posture.sorted()]
44
+ counts = posture.counts()
45
+ return {
46
+ "schema_version": SCHEMA_VERSION,
47
+ "event_id": str(uuid.uuid4()),
48
+ "session_id": "",
49
+ "correlation_id": "",
50
+ "timestamp_utc": now_utc,
51
+ **identity_fields(),
52
+ "source_topic": "agentmetry/posture",
53
+ "source": {"tier": "agentmetry", "app": "agentmetry", "adapter": "posture"},
54
+ "initiator": {"actor_type": "system", "trigger": "scheduled", "operator_id": ""},
55
+ "actor": {"type": "system", "id": "agentmetry", "role": "recorder"},
56
+ "action": {
57
+ "type": "posture",
58
+ "outcome": outcome,
59
+ "reason": f"{counts['high']} high, {counts['medium']} medium, {counts['low']} low",
60
+ },
61
+ "agent": {"name": "agentmetry", "skill_id": ""},
62
+ "posture": {
63
+ "findings": findings,
64
+ "counts": counts,
65
+ "digest": posture.digest()[:16],
66
+ "files_read": len(posture.files_read),
67
+ "files_unreadable": len(posture.unreadable),
68
+ "not_checked": sorted(NOT_CHECKED),
69
+ },
70
+ }
71
+
72
+
73
+ def due(state: PostureState, digest: str, now_monotonic: float) -> str | None:
74
+ """`snapshot`, `changed`, or None when there is nothing to say."""
75
+ if state.digest is None:
76
+ return "snapshot"
77
+ if digest != state.digest:
78
+ return "changed"
79
+ if now_monotonic - state.emitted_at >= SNAPSHOT_SECONDS:
80
+ return "snapshot"
81
+ return None
82
+
83
+
84
+ async def maybe_emit_posture(state: PostureState) -> dict[str, Any] | None:
85
+ """Emit the posture if the operator turned it on and there is news."""
86
+ if not enabled():
87
+ return None
88
+ from datetime import datetime, timezone
89
+
90
+ from agentmetry.core.audit.ingest import _get_sink
91
+ from agentmetry.core.audit.trail_db import get_trail_db
92
+ from agentmetry.core.diagnostics.posture import collect
93
+
94
+ posture = collect()
95
+ digest = posture.digest()
96
+ now = time.monotonic()
97
+ outcome = due(state, digest, now)
98
+ if outcome is None:
99
+ return None
100
+ event = build_posture_event(datetime.now(timezone.utc).isoformat(), posture, outcome=outcome)
101
+ get_trail_db().insert(event)
102
+ await _get_sink().emit(event)
103
+ state.digest, state.emitted_at = digest, now
104
+ return event
@@ -52,11 +52,12 @@ def _nested_arg_containers(hook_data: dict[str, Any]) -> tuple[list[dict[str, An
52
52
  Cursor shell hooks put `command` at the top level, but Claude and Codex nest
53
53
  it under `tool_input` and Antigravity under `toolCall.args`. Without those,
54
54
  a `command_pattern` rule matched nothing on three of the four supported IDEs
55
- — the shipped block_shell_rm rule was Cursor-only in practice.
55
+ — the shipped block_shell_rm rule was Cursor-only in practice. Copilot CLI
56
+ sends `toolArgs`, and the same gap would have opened again for it.
56
57
  """
57
58
  containers: list[dict[str, Any]] = []
58
59
  raw = ""
59
- for key in ("arguments", "args", "input", "tool_input", "toolInput"):
60
+ for key in ("arguments", "args", "input", "tool_input", "toolInput", "toolArgs"):
60
61
  val = hook_data.get(key)
61
62
  if isinstance(val, str):
62
63
  try:
@@ -220,6 +220,14 @@ class Settings(BaseSettings):
220
220
  validation_alias=AliasChoices("AGENTMETRY_MCP_INVENTORY"),
221
221
  )
222
222
 
223
+ # Agent settings that skip approval or turn hooks off, as a `posture` event
224
+ # (`agentmetry posture`). Off by default, for the same reason as the
225
+ # inventory: what reaches a SOC from a laptop is the operator's decision.
226
+ posture_enabled: bool = Field(
227
+ default=False,
228
+ validation_alias=AliasChoices("AGENTMETRY_POSTURE"),
229
+ )
230
+
223
231
  # Rewrite ~/.claude/settings.json and ~/.cursor/hooks.json on every boot to
224
232
  # point at this checkout. Off by default: it used to always run, so booting
225
233
  # any second checkout (a test clone, a feature branch, a demo instance)
@@ -99,6 +99,11 @@ class Surface:
99
99
  configs: tuple[str, ...]
100
100
  #: Overridden where our command is not the ingest script itself.
101
101
  markers: tuple[str, ...] = _DEFAULT_MARKERS
102
+ #: Paths under that directory that only this agent creates. Empty means the
103
+ #: directory is the agent's own and its existence is the answer. Set where
104
+ #: the directory is shared with another program, so that program's presence
105
+ #: does not read as this agent's.
106
+ presence: tuple[str, ...] = ()
102
107
 
103
108
  def directory(self) -> Path:
104
109
  override = os.environ.get(self.home_env, "").strip() if self.home_env else ""
@@ -106,6 +111,20 @@ class Surface:
106
111
  return Path(override).expanduser()
107
112
  return Path.home() / self.home_rel
108
113
 
114
+ def installed(self) -> bool:
115
+ """Whether this agent is on the machine, as far as the disk can say.
116
+
117
+ A config we write counts as presence too: if our hook is in one of its
118
+ files, somebody installed it for this agent, and reporting the agent
119
+ absent would hide a hook that is there.
120
+ """
121
+ root = self.directory()
122
+ if not root.is_dir():
123
+ return False
124
+ if not self.presence:
125
+ return True
126
+ return any((root / rel).exists() for rel in (*self.presence, *self.configs))
127
+
109
128
 
110
129
  #: Mirrors hook_bootstrap's installers, plus the two surfaces that have none.
111
130
  #: Adding an installer without adding a row here is the drift this module exists
@@ -124,16 +143,30 @@ SURFACES: tuple[Surface, ...] = (
124
143
  # Codex's hash-based trust prompt, which skips untrusted hooks silently, so
125
144
  # a covered Codex is configured rather than proven to be firing.
126
145
  Surface("codex", "", ".codex", ("hooks.json",)),
146
+ # Copilot CLI and VS Code agent mode both read ~/.copilot/hooks/*.json, and
147
+ # the installer writes its own file there. The directory is Copilot CLI's,
148
+ # so a machine with only VS Code Copilot may report absent: VS Code does not
149
+ # necessarily create it. What a file check cannot see is VS Code's
150
+ # `chat.useHooks` switch or an organisation policy that turns hooks off,
151
+ # which `agentmetry posture` reports.
152
+ Surface("copilot", "COPILOT_HOME", ".copilot", ("hooks/agentmetry.json",)),
127
153
  # Antigravity 2.0 usually runs from ~/.gemini/antigravity/scratch rather
128
154
  # than the repo, so scripts/install_antigravity_hooks.ps1 writes both. Either
129
155
  # one means the agent is recorded. Its command is a .cmd wrapper, not the
130
156
  # ingest script, hence the marker override.
157
+ #
158
+ # ~/.gemini is not Antigravity's alone: Gemini CLI keeps its settings.json
159
+ # there too. Treating the directory as the answer reported every machine
160
+ # with only Gemini CLI as an uncovered Antigravity, which degraded its beat
161
+ # for an agent that was never installed. Antigravity's own data lives in
162
+ # ~/.gemini/antigravity, so that, or one of our hook files, is presence.
131
163
  Surface(
132
164
  "antigravity",
133
165
  "",
134
166
  ".gemini",
135
167
  ("config/hooks.json", "antigravity/scratch/.agents/hooks.json"),
136
168
  markers=("agentmetry_antigravity_hook", *_DEFAULT_MARKERS),
169
+ presence=("antigravity",),
137
170
  ),
138
171
  )
139
172
 
@@ -175,7 +208,7 @@ def coverage() -> dict[str, str]:
175
208
  result: dict[str, str] = {}
176
209
  for surface in SURFACES:
177
210
  try:
178
- present = surface.directory().is_dir()
211
+ present = surface.installed()
179
212
  except OSError:
180
213
  present = False
181
214
  if not present: