agentmetry 0.5.0__tar.gz → 0.7.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 (239) hide show
  1. {agentmetry-0.5.0 → agentmetry-0.7.0}/.gitignore +4 -0
  2. {agentmetry-0.5.0 → agentmetry-0.7.0}/PKG-INFO +3 -2
  3. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/api/routes/audit.py +51 -2
  4. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/cli/__init__.py +30 -7
  5. agentmetry-0.7.0/agentmetry/core/audit/detection/corpus/benign_env_example_read.jsonl +4 -0
  6. agentmetry-0.7.0/agentmetry/core/audit/detection/corpus/benign_fetch_piped_into_own_script.jsonl +3 -0
  7. agentmetry-0.7.0/agentmetry/core/audit/detection/corpus/benign_github_raw_docs_then_script.jsonl +3 -0
  8. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_loopback_pipe_to_interpreter.jsonl +3 -3
  9. agentmetry-0.7.0/agentmetry/core/audit/detection/corpus/benign_mcp_text_mentions_credential_path.jsonl +3 -0
  10. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/corpus.yaml +42 -0
  11. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/disposition.py +52 -6
  12. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/rules.py +31 -2
  13. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/traits.py +42 -5
  14. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/yaml_config.py +8 -2
  15. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/ingest.py +155 -21
  16. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/mitre.py +42 -2
  17. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/trail_anchor.py +55 -6
  18. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/trail_db.py +10 -3
  19. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/bus/events.py +1 -1
  20. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/diagnostics/doctor.py +1 -1
  21. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/diagnostics/driver_paths.py +6 -4
  22. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/diagnostics/mcp_schema.py +175 -10
  23. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/version.py +1 -1
  24. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/hooks/ingest.py +38 -6
  25. {agentmetry-0.5.0 → agentmetry-0.7.0}/pyproject.toml +34 -2
  26. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_agentmetry_ingest_client.py +47 -0
  27. agentmetry-0.7.0/tests/test_cli_commands.py +467 -0
  28. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_detection_adi.py +36 -1
  29. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_detection_disposition.py +47 -0
  30. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_detection_rule_identity.py +16 -4
  31. agentmetry-0.7.0/tests/test_ingest_roundtrip.py +163 -0
  32. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_mcp_audit_proxy.py +61 -7
  33. agentmetry-0.7.0/tests/test_mcp_schema.py +490 -0
  34. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_readme_claims.py +53 -0
  35. agentmetry-0.7.0/tests/test_sigma_pack.py +131 -0
  36. agentmetry-0.7.0/tools/generate_sigma_pack.py +203 -0
  37. {agentmetry-0.5.0 → agentmetry-0.7.0}/tools/mcp_audit_proxy.py +81 -7
  38. agentmetry-0.5.0/tests/test_mcp_schema.py +0 -233
  39. {agentmetry-0.5.0 → agentmetry-0.7.0}/.dockerignore +0 -0
  40. {agentmetry-0.5.0 → agentmetry-0.7.0}/.env.agentmetry-demo +0 -0
  41. {agentmetry-0.5.0 → agentmetry-0.7.0}/.env.example +0 -0
  42. {agentmetry-0.5.0 → agentmetry-0.7.0}/Dockerfile +0 -0
  43. {agentmetry-0.5.0 → agentmetry-0.7.0}/README.md +0 -0
  44. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/__init__.py +0 -0
  45. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/api/__init__.py +0 -0
  46. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/api/main.py +0 -0
  47. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/api/routes/__init__.py +0 -0
  48. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/api/websocket.py +0 -0
  49. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/api/ws_bridge.py +0 -0
  50. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/cli/__main__.py +0 -0
  51. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/__init__.py +0 -0
  52. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/__init__.py +0 -0
  53. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/adapters/__init__.py +0 -0
  54. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/adapters/agt.py +0 -0
  55. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/adapters/chronicle.py +0 -0
  56. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/adapters/cloudevents.py +0 -0
  57. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/adapters/ecs.py +0 -0
  58. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/adapters/splunk.py +0 -0
  59. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/alerts.py +0 -0
  60. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/atlas.py +0 -0
  61. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/canonical.py +0 -0
  62. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/compliance_digest.py +0 -0
  63. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/__init__.py +0 -0
  64. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/benchmark.py +0 -0
  65. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_approval_denied_then_executed.jsonl +0 -0
  66. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_arbitrary_host_stage_execute.jsonl +0 -0
  67. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_autonomous_unapproved_write.jsonl +0 -0
  68. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_credential_exfil.jsonl +0 -0
  69. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_credential_then_cloud_api.jsonl +0 -0
  70. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_destructive_delete_burst.jsonl +0 -0
  71. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_discovery_then_collect.jsonl +0 -0
  72. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_dotfile_then_git_push.jsonl +0 -0
  73. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_encoded_command_download.jsonl +0 -0
  74. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_env_credential_exfil.jsonl +0 -0
  75. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_hashed_only_no_command.jsonl +0 -0
  76. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_interpreter_egress.jsonl +0 -0
  77. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_pr_merged_without_review.jsonl +0 -0
  78. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_proc_substitution_cradle.jsonl +0 -0
  79. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_remote_pipe_to_shell.jsonl +0 -0
  80. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_remote_staging_then_execute.jsonl +0 -0
  81. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_secret_manager_then_egress.jsonl +0 -0
  82. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_session_tool_burst.jsonl +0 -0
  83. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_single_command_exfil.jsonl +0 -0
  84. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_single_quoted_interpreter_egress.jsonl +0 -0
  85. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_ssh_directory_exfil.jsonl +0 -0
  86. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_subagent_swarm.jsonl +0 -0
  87. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_timestamp_collision.jsonl +0 -0
  88. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_untrusted_input_then_action.jsonl +0 -0
  89. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_authoring_merge_fixtures.jsonl +0 -0
  90. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_autonomous_after_approval.jsonl +0 -0
  91. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_build_artifact_cleanup.jsonl +0 -0
  92. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_ci_artifact_download.jsonl +0 -0
  93. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_commit_message_names_cloud_clis.jsonl +0 -0
  94. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_database_migration.jsonl +0 -0
  95. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_dependency_install_and_build.jsonl +0 -0
  96. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_download_release_archive.jsonl +0 -0
  97. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_fetch_data_then_run_repo_script.jsonl +0 -0
  98. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_fetch_dataset_then_analyse.jsonl +0 -0
  99. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_fetch_lockfile_then_install.jsonl +0 -0
  100. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_git_review_and_push.jsonl +0 -0
  101. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_human_driven_deletes.jsonl +0 -0
  102. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_local_api_probing.jsonl +0 -0
  103. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_long_but_calm_session.jsonl +0 -0
  104. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_loopback_is_not_egress.jsonl +0 -0
  105. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_ordinary_development.jsonl +0 -0
  106. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_package_manager_after_fetch.jsonl +0 -0
  107. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_reading_config_that_is_not_secret.jsonl +0 -0
  108. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_remote_api_call_no_credentials.jsonl +0 -0
  109. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_research_then_docs.jsonl +0 -0
  110. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_reversed_order_is_not_exfil.jsonl +0 -0
  111. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_secret_names_and_docs.jsonl +0 -0
  112. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_test_and_fix_loop.jsonl +0 -0
  113. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_writing_about_credentials.jsonl +0 -0
  114. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/engine.py +0 -0
  115. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/live.py +0 -0
  116. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/live_store.py +0 -0
  117. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/models.py +0 -0
  118. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/yaml_rules.py +0 -0
  119. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/dlp/__init__.py +0 -0
  120. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/dlp/loader.py +0 -0
  121. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/dlp/models.py +0 -0
  122. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/dlp/scanner.py +0 -0
  123. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/dogfood.py +0 -0
  124. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/evidence_pack.py +0 -0
  125. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/external.py +0 -0
  126. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/hashing.py +0 -0
  127. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/heartbeat.py +0 -0
  128. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/hook_bootstrap.py +0 -0
  129. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/identity.py +0 -0
  130. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/migrate.py +0 -0
  131. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/policy.py +0 -0
  132. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/redaction.py +0 -0
  133. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/replay.py +0 -0
  134. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/run_context.py +0 -0
  135. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/sinks.py +0 -0
  136. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/spool.py +0 -0
  137. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/tool_policy/__init__.py +0 -0
  138. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/tool_policy/evaluator.py +0 -0
  139. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/tool_policy/loader.py +0 -0
  140. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/tool_policy/models.py +0 -0
  141. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/trail_chain.py +0 -0
  142. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/trail_merkle.py +0 -0
  143. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/auth.py +0 -0
  144. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/bus/__init__.py +0 -0
  145. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/bus/audit_exporter.py +0 -0
  146. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/bus/bridges.py +0 -0
  147. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/bus/bus.py +0 -0
  148. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/bus/outbox.py +0 -0
  149. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/config.py +0 -0
  150. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/diagnostics/__init__.py +0 -0
  151. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/diagnostics/autostart.py +0 -0
  152. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/diagnostics/env_file.py +0 -0
  153. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/diagnostics/hook_coverage.py +0 -0
  154. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/diagnostics/mcp_inventory.py +0 -0
  155. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/drivers/__init__.py +0 -0
  156. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/drivers/host.py +0 -0
  157. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/drivers/permissions.py +0 -0
  158. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/drivers/spec.py +0 -0
  159. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/extensions.py +0 -0
  160. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/health.py +0 -0
  161. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/hooks/__init__.py +0 -0
  162. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/policies/detection/manifest.yaml +0 -0
  163. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/policies/dlp/manifest.yaml +0 -0
  164. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/policies/opa/agent_rules.rego +0 -0
  165. {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/policies/tool/manifest.yaml +0 -0
  166. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/conftest.py +0 -0
  167. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/fixtures/agt_filesink_exfil.jsonl +0 -0
  168. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/fixtures/agt_filesink_mixed.jsonl +0 -0
  169. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/fixtures/fake_mcp_server.py +0 -0
  170. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_agentmetry_audit.py +0 -0
  171. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_agt_adapter.py +0 -0
  172. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_atlas_detection_enrichment.py +0 -0
  173. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_atlas_mapping.py +0 -0
  174. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_audit_sinks.py +0 -0
  175. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_audit_stats.py +0 -0
  176. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_audit_tail.py +0 -0
  177. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_auth.py +0 -0
  178. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_autostart.py +0 -0
  179. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_boot_sequence.py +0 -0
  180. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_burst_windows.py +0 -0
  181. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_chinese_agent_hooks.py +0 -0
  182. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_chinese_agent_sprint_b.py +0 -0
  183. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_chinese_agent_sprint_c.py +0 -0
  184. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_chronicle_adapter.py +0 -0
  185. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_cli_backup.py +0 -0
  186. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_cloudevents_adapter.py +0 -0
  187. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_compliance_digest.py +0 -0
  188. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_crewai_adapter.py +0 -0
  189. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_detection_benchmark.py +0 -0
  190. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_detection_default_config.py +0 -0
  191. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_detection_disposition_api.py +0 -0
  192. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_detection_duplicate_emission.py +0 -0
  193. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_detection_emit_durability.py +0 -0
  194. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_detection_engine.py +0 -0
  195. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_detection_evasion.py +0 -0
  196. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_detection_hf_incident.py +0 -0
  197. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_detection_quoted_command_words.py +0 -0
  198. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_detection_rules_v2.py +0 -0
  199. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_dlp_agent_env_override.py +0 -0
  200. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_dlp_invisible_unicode.py +0 -0
  201. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_dlp_markdown_exfil.py +0 -0
  202. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_dlp_scanner.py +0 -0
  203. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_doctor.py +0 -0
  204. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_dogfood.py +0 -0
  205. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_dogfood_freeze.py +0 -0
  206. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_driver_paths.py +0 -0
  207. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_ecs_threat.py +0 -0
  208. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_event_bus.py +0 -0
  209. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_evidence_pack.py +0 -0
  210. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_extensions.py +0 -0
  211. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_external_ingest.py +0 -0
  212. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_forwarding_shape.py +0 -0
  213. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_heartbeat.py +0 -0
  214. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_hook_bootstrap.py +0 -0
  215. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_hook_coverage.py +0 -0
  216. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_hook_enforcement_timing.py +0 -0
  217. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_hook_spool.py +0 -0
  218. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_isolation.py +0 -0
  219. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_launch_targets.py +0 -0
  220. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_live_detection.py +0 -0
  221. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_live_detection_store.py +0 -0
  222. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_mcp_inventory.py +0 -0
  223. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_mitre_dlp.py +0 -0
  224. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_opensre_adapter.py +0 -0
  225. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_redaction.py +0 -0
  226. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_replay.py +0 -0
  227. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_static_serving.py +0 -0
  228. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_subagent_lifecycle.py +0 -0
  229. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_tool_policy.py +0 -0
  230. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_tool_policy_agent_cli.py +0 -0
  231. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_tool_policy_git_hooks.py +0 -0
  232. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_trail_anchor.py +0 -0
  233. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_trail_chain.py +0 -0
  234. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_trail_concurrency.py +0 -0
  235. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_trail_merkle.py +0 -0
  236. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_unattended_agent_policy.py +0 -0
  237. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_version.py +0 -0
  238. {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_yaml_detection_rules.py +0 -0
  239. {agentmetry-0.5.0 → agentmetry-0.7.0}/tools/vault_fs_server.py +0 -0
@@ -96,3 +96,7 @@ docs/*-review-*.md
96
96
  docs/*audit*prompt*.md
97
97
  docs/audit-*.md
98
98
  docs/*-audit-*.md
99
+
100
+ # pytest-cov data file
101
+ .coverage
102
+ .coverage.*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentmetry
3
- Version: 0.5.0
3
+ Version: 0.7.0
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
@@ -35,8 +35,9 @@ Requires-Dist: uvicorn[standard]>=0.32.0
35
35
  Requires-Dist: websockets>=14.0
36
36
  Provides-Extra: dev
37
37
  Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
38
+ Requires-Dist: pytest-cov>=6.0; extra == 'dev'
38
39
  Requires-Dist: pytest>=8.0; extra == 'dev'
39
- Requires-Dist: ruff==0.16.0; extra == 'dev'
40
+ Requires-Dist: ruff==0.16.4; extra == 'dev'
40
41
  Description-Content-Type: text/markdown
41
42
 
42
43
  # Agentmetry
@@ -8,7 +8,7 @@ from typing import Any, Literal
8
8
 
9
9
  from fastapi import APIRouter, Depends, HTTPException, Query
10
10
  from fastapi.responses import FileResponse
11
- from pydantic import BaseModel, Field
11
+ from pydantic import BaseModel, Field, field_validator
12
12
 
13
13
  from agentmetry.core.auth import require_api_key
14
14
  from agentmetry.core.audit.detection.disposition import STATUSES, get_disposition_store
@@ -42,8 +42,48 @@ class IngestToolBody(BaseModel):
42
42
  mitre: dict[str, str] | None = None
43
43
 
44
44
 
45
+ #: Actor kinds a capture surface may assert. Anything else is coerced to
46
+ #: `agent`, which is the safe direction: never silently promoted to `human`
47
+ #: (whose approvals reset detection gates) and never to `autonomous` (which
48
+ #: `autonomous-unapproved-write` keys on). A surface that needs a new kind adds
49
+ #: it here deliberately rather than by sending a novel string.
50
+ _ACTOR_TYPES = frozenset({"human", "agent", "autonomous", "system"})
51
+
52
+
53
+ class IngestInitiatorBody(BaseModel):
54
+ """Who triggered this call, as the capture surface saw it.
55
+
56
+ This is not identity. `identity_fields()` stamps `host_id` and `fleet_id` on
57
+ the receiving orchestrator whatever a client sends, and that lockdown is
58
+ unchanged here: this field says what *kind* of actor acted, never which
59
+ machine it was. `external.py` already reads and honours it; without the
60
+ field declared, pydantic dropped it before that code ever saw it.
61
+ """
62
+
63
+ actor_type: str = "human"
64
+ trigger: str = "manual"
65
+ operator_id: str = ""
66
+
67
+ @field_validator("actor_type")
68
+ @classmethod
69
+ def _known_actor(cls, v: str) -> str:
70
+ return v if v in _ACTOR_TYPES else "agent"
71
+
72
+
45
73
  class ExternalIngestBody(BaseModel):
46
- """Adapter payload — normalized to canonical v1.1 on ingest."""
74
+ """Adapter payload — normalized to canonical v1.2 on ingest.
75
+
76
+ Every field a capture surface sends must be declared here. Pydantic ignores
77
+ what it does not know, silently, so an undeclared field is not an error at
78
+ ingest: it is a feature that works in unit tests and disappears over HTTP.
79
+
80
+ That has now happened twice. `traits` and `mitre` were dropped, which made
81
+ hashed-only events invisible to every command rule. Then 0.6.0 shipped MCP
82
+ per-tool digests and the initialize handshake, and those were dropped too,
83
+ so the release headline did not survive the wire. `test_ingest_roundtrip.py`
84
+ exists so there is not a third time: it builds real proxy payloads and fails
85
+ if any key does not survive this model.
86
+ """
47
87
 
48
88
  source_app: str = Field(
49
89
  ...,
@@ -71,10 +111,19 @@ class ExternalIngestBody(BaseModel):
71
111
  # pydantic drops it and a `log`-mode match is silently lost.
72
112
  dlp: dict[str, Any] | None = None
73
113
  tool_policy: dict[str, Any] | None = None
114
+ # Who the capture surface says triggered this. Honoured by external.py.
115
+ initiator: IngestInitiatorBody | None = None
74
116
  # Compact `tools/list` fingerprint from mcp_audit_proxy. Hash only: the
75
117
  # description never leaves the proxy process.
76
118
  schema_fingerprint: str = ""
77
119
  schema_tool_count: int = 0
120
+ # 0.6.0 MCP fields. Per-tool digests so a schema move names the tool that
121
+ # moved rather than only the server, and the two initialize handshake
122
+ # fields that separate a shipped release from a rug pull. All hashes and
123
+ # flags: no tool name, no description, nothing model-visible.
124
+ schema_tool_digests: dict[str, str] = Field(default_factory=dict)
125
+ server_version: str = ""
126
+ list_changed: bool | None = None
78
127
 
79
128
 
80
129
  def _parse_event_ts(event: dict[str, Any]) -> datetime | None:
@@ -7,6 +7,7 @@ Pure stdlib + httpx; never imports the FastAPI app (fast startup, no side effect
7
7
  from __future__ import annotations
8
8
 
9
9
  import argparse
10
+ import logging
10
11
  import os
11
12
  import shutil
12
13
  import socket
@@ -30,11 +31,13 @@ _TASK_NAME = "Agentmetry Orchestrator"
30
31
  # Paths bundled by backup, relative to the repo root.
31
32
  _BACKUP_PREFIXES = ("vault/", "apps/orchestrator/data/")
32
33
  _BACKUP_EXCLUDE_DIRS = {"logs"}
34
+ logger = logging.getLogger(__name__)
35
+
33
36
  _BACKUP_EXCLUDE_SUFFIXES = {".pid"}
34
37
 
35
38
 
36
39
  def _base_url(port: int, host: str = "127.0.0.1") -> str:
37
- display = host if host != "0.0.0.0" else "127.0.0.1"
40
+ display = host if host != "0.0.0.0" else "127.0.0.1" # noqa: S104
38
41
  return f"http://{display}:{port}"
39
42
 
40
43
 
@@ -65,8 +68,12 @@ def _fetch_health(port: int) -> dict | None:
65
68
  resp = httpx.get(f"{_base_url(port)}/api/v1/health", timeout=10.0)
66
69
  if resp.status_code == 200:
67
70
  return resp.json()
68
- except Exception:
69
- pass
71
+ except Exception as exc:
72
+ # Not running is the expected answer here and `None` says so. Logged at
73
+ # debug anyway: a probe that fails for a reason other than "nothing is
74
+ # listening" (TLS, a proxy, a permission error) otherwise looks identical
75
+ # to a stopped orchestrator, and that is an hour of somebody's afternoon.
76
+ logger.debug("health probe on port %s failed: %s", port, exc)
70
77
  return None
71
78
 
72
79
 
@@ -77,7 +84,7 @@ def cmd_start(args: argparse.Namespace) -> int:
77
84
  host = getattr(args, "host", "127.0.0.1")
78
85
  if _fetch_health(args.port):
79
86
  print(f"Already running on {_base_url(args.port, host)}")
80
- if host == "0.0.0.0":
87
+ if host == "0.0.0.0": # noqa: S104
81
88
  _print_lan_hint(args.port)
82
89
  return 0
83
90
 
@@ -112,7 +119,7 @@ def cmd_start(args: argparse.Namespace) -> int:
112
119
  while time.monotonic() < deadline:
113
120
  if _fetch_health(args.port):
114
121
  print(f"Agentmetry running on {_base_url(args.port, host)} (pid {proc.pid})")
115
- if host == "0.0.0.0":
122
+ if host == "0.0.0.0": # noqa: S104
116
123
  _print_lan_hint(args.port)
117
124
  return 0
118
125
  if proc.poll() is not None:
@@ -937,7 +944,7 @@ def cmd_verify(args: argparse.Namespace) -> int:
937
944
  print(line)
938
945
  if source == "config" and not anchor_log.is_file():
939
946
  print(f" AGENTMETRY_ANCHOR_LOG points at {anchor_log}, which does not exist")
940
- if not coverage.ok:
947
+ if coverage.tampering:
941
948
  # The chain printed OK several lines ago and it was telling the
942
949
  # truth: the file is internally consistent, because whoever
943
950
  # rewrote it made it so. Leaving that as the last word would let
@@ -947,6 +954,17 @@ def cmd_verify(args: argparse.Namespace) -> int:
947
954
  "published anchor. The file was rewritten."
948
955
  )
949
956
  return 1
957
+ if not coverage.ok:
958
+ # Checkpoints exist and none of them cover this file. Worth
959
+ # failing, because an operator who configured an anchor log
960
+ # believes they are anchored. Not worth calling tampering:
961
+ # nothing here contradicts the trail, and a trust command that
962
+ # says "rewritten" about a clean file gets muted.
963
+ print(
964
+ "FAILED — no checkpoint in this anchor log covers this trail. "
965
+ "Check AGENTMETRY_ANCHOR_LOG, or anchor this file."
966
+ )
967
+ return 1
950
968
  return 0
951
969
  print(f"FAILED — {result.message}")
952
970
  if result.first_bad_line:
@@ -1055,7 +1073,12 @@ def main(argv: list[str] | None = None) -> int:
1055
1073
  # platform. Same guard as scripts/demo.py.
1056
1074
  try:
1057
1075
  sys.stdout.reconfigure(encoding="utf-8") # type: ignore[union-attr]
1058
- except Exception: # pragma: no cover - depends on the host console
1076
+ except Exception: # noqa: S110 # pragma: no cover - host console dependent
1077
+ # The one swallow with nowhere to swallow into: this runs before logging
1078
+ # is configured, and the failure means the console cannot render UTF-8,
1079
+ # which is also why a log line about it would not render. The fallback is
1080
+ # the console's own encoding, which is what would have happened without
1081
+ # the call.
1059
1082
  pass
1060
1083
 
1061
1084
  parser = argparse.ArgumentParser(prog="agentmetry", description="Agentmetry local ops")
@@ -0,0 +1,4 @@
1
+ {"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "fp44-1", "event_id": "fp44-1-0", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.2.0", "session_id": "fp44-1", "source": {"app": "claude"}, "timestamp_utc": "2026-08-26T10:00:00+00:00", "tool": {"command": "cat .env.example", "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "claude.Bash", "server": "claude"}}
2
+ {"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "fp44-1", "event_id": "fp44-1-1", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.2.0", "session_id": "fp44-1", "source": {"app": "claude"}, "timestamp_utc": "2026-08-26T10:01:00+00:00", "tool": {"command": "cp .env.example .env.local", "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "claude.Bash", "server": "claude"}}
3
+ {"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "fp44-1", "event_id": "fp44-1-2", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.2.0", "session_id": "fp44-1", "source": {"app": "claude"}, "timestamp_utc": "2026-08-26T10:02:00+00:00", "tool": {"command": "cat config/.env.template", "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "claude.Read", "server": "claude"}}
4
+ {"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "fp44-1", "event_id": "fp44-1-3", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.2.0", "session_id": "fp44-1", "source": {"app": "claude"}, "timestamp_utc": "2026-08-26T10:03:00+00:00", "tool": {"command": "npm run dev", "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "claude.Bash", "server": "claude"}}
@@ -0,0 +1,3 @@
1
+ {"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "fp50-1", "event_id": "fp50-1-0", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.2.0", "session_id": "fp50-1", "source": {"app": "claude"}, "timestamp_utc": "2026-08-26T10:00:00+00:00", "tool": {"command": "curl -s https://api.example.com/schema.json | python -c \"import sys,json;print(len(json.load(sys.stdin)))\"", "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "claude.Bash", "server": "claude"}}
2
+ {"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "fp50-1", "event_id": "fp50-1-1", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.2.0", "session_id": "fp50-1", "source": {"app": "claude"}, "timestamp_utc": "2026-08-26T10:02:00+00:00", "tool": {"command": "curl -s https://registry.example.com/v1/index | node -e \"let d='';process.stdin.on('data',c=>d+=c)\"", "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "claude.Bash", "server": "claude"}}
3
+ {"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "fp50-1", "event_id": "fp50-1-2", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.2.0", "session_id": "fp50-1", "source": {"app": "claude"}, "timestamp_utc": "2026-08-26T10:04:00+00:00", "tool": {"command": "pytest -q", "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "claude.Bash", "server": "claude"}}
@@ -0,0 +1,3 @@
1
+ {"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "fp51-1", "event_id": "fp51-1-0", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.2.0", "session_id": "fp51-1", "source": {"app": "claude"}, "timestamp_utc": "2026-08-26T10:00:00+00:00", "tool": {"command": "curl -s https://raw.githubusercontent.com/psf/requests/main/README.md", "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "claude.Bash", "server": "claude"}}
2
+ {"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "fp51-1", "event_id": "fp51-1-1", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.2.0", "session_id": "fp51-1", "source": {"app": "claude"}, "timestamp_utc": "2026-08-26T10:06:00+00:00", "tool": {"command": "python -c \"import json;print(json.dumps({'ok':True}))\"", "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "claude.Bash", "server": "claude"}}
3
+ {"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "fp51-1", "event_id": "fp51-1-2", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.2.0", "session_id": "fp51-1", "source": {"app": "claude"}, "timestamp_utc": "2026-08-26T10:09:00+00:00", "tool": {"command": "pytest -q", "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "claude.Bash", "server": "claude"}}
@@ -1,3 +1,3 @@
1
- {"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "bl-1", "event_id": "bl-1-0", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.1.0", "session_id": "bl-1", "source": {"app": "claude"}, "timestamp_utc": "2026-07-30T10:00:00+00:00", "tool": {"command": "curl -s http://127.0.0.1:8000/api/v1/audit/status | python -c \"import sys,json;print(json.load(sys.stdin))\"", "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "claude.Bash", "server": "claude", "traits": ["pipe_to_shell_local", "risky_exec"]}}
2
- {"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "bl-1", "event_id": "bl-1-1", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.1.0", "session_id": "bl-1", "source": {"app": "claude"}, "timestamp_utc": "2026-07-30T10:02:00+00:00", "tool": {"command": "curl -s http://localhost:3000/health | node -e \"process.stdin.pipe(process.stdout)\"", "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "claude.Bash", "server": "claude", "traits": ["pipe_to_shell_local", "risky_exec"]}}
3
- {"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "bl-1", "event_id": "bl-1-2", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.1.0", "session_id": "bl-1", "source": {"app": "claude"}, "timestamp_utc": "2026-07-30T10:05:00+00:00", "tool": {"command": "npm run test", "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "claude.Bash", "server": "claude", "traits": []}}
1
+ {"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "bl-1", "event_id": "bl-1-0", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.2.0", "session_id": "bl-1", "source": {"app": "claude"}, "timestamp_utc": "2026-08-26T10:00:00+00:00", "tool": {"command": "curl -s http://127.0.0.1:8000/tmp/bootstrap.sh | bash", "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "claude.Bash", "server": "claude", "traits": ["pipe_to_shell_local", "risky_exec"]}}
2
+ {"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "bl-1", "event_id": "bl-1-1", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.2.0", "session_id": "bl-1", "source": {"app": "claude"}, "timestamp_utc": "2026-08-26T10:02:00+00:00", "tool": {"command": "curl -s http://localhost:3000/setup.sh | sh", "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "claude.Bash", "server": "claude", "traits": ["pipe_to_shell_local", "risky_exec"]}}
3
+ {"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "bl-1", "event_id": "bl-1-2", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.2.0", "session_id": "bl-1", "source": {"app": "claude"}, "timestamp_utc": "2026-08-26T10:04:00+00:00", "tool": {"command": "npm run test", "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "claude.Bash", "server": "claude"}}
@@ -0,0 +1,3 @@
1
+ {"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "fp49-1", "event_id": "fp49-1-0", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.2.0", "session_id": "fp49-1", "source": {"app": "claude"}, "timestamp_utc": "2026-08-26T10:00:00+00:00", "tool": {"arguments": {"channel": "#eng", "text": "reminder: never commit ~/.aws/credentials or ~/.ssh/id_rsa"}, "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "mcp.slack.post_message", "server": "claude"}}
2
+ {"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "fp49-1", "event_id": "fp49-1-1", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.2.0", "session_id": "fp49-1", "source": {"app": "claude"}, "timestamp_utc": "2026-08-26T10:02:00+00:00", "tool": {"arguments": {"description": "the old key is still in ~/.aws/credentials on the build box", "title": "Rotate keys"}, "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "mcp.linear.create_issue", "server": "claude"}}
3
+ {"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "fp49-1", "event_id": "fp49-1-2", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.2.0", "session_id": "fp49-1", "source": {"app": "claude"}, "timestamp_utc": "2026-08-26T10:04:00+00:00", "tool": {"command": "git status", "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "claude.Bash", "server": "claude"}}
@@ -454,6 +454,48 @@ cases:
454
454
  at low with no T1105 and no TA0011, neither of which a loopback fetch
455
455
  earns.
456
456
 
457
+ - name: fetch piped into the interpreter's own script
458
+ session: benign_fetch_piped_into_own_script.jsonl
459
+ expect: []
460
+ benign: true
461
+ note: >
462
+ Issue #50. `curl api/x.json | python -c 'json.load(sys.stdin)'` was
463
+ critical. An interpreter handed its own program does not execute the pipe,
464
+ it reads it as data, so this is a data pipeline and not a cradle. The
465
+ cradle shapes have no inline script and still fire: see the two cases
466
+ above.
467
+
468
+ - name: read .env.example while setting a project up
469
+ session: benign_env_example_read.jsonl
470
+ expect: []
471
+ benign: true
472
+ note: >
473
+ Issue #44, six of seventeen week-one dogfood findings and every one a
474
+ critical. `.env.example` is the file you commit *instead* of a credential
475
+ file, and reading it is day one in any repository. A bare `.env` and
476
+ `.env.local` still map to credential access.
477
+
478
+ - name: fetch docs from GitHub raw, then run an unrelated script
479
+ session: benign_github_raw_docs_then_script.jsonl
480
+ expect: []
481
+ benign: true
482
+ note: >
483
+ Issue #51. Any staging-host fetch followed by any `python -c` was critical
484
+ with nothing linking the two. GitHub raw serves documentation as well as
485
+ payloads, and a fetch that only printed staged no artifact for a later
486
+ command to run. The bound form, where the file downloaded is the file
487
+ executed, still fires.
488
+
489
+ - name: MCP message that names a credential path
490
+ session: benign_mcp_text_mentions_credential_path.jsonl
491
+ expect: []
492
+ benign: true
493
+ note: >
494
+ Issue #49, the sibling of #44. Structured tool arguments have no shell
495
+ quoting to mask, so a path inside a free-text field read as a credential
496
+ access. Naming `~/.aws/credentials` in a Slack message is a mention. The
497
+ same path arriving in a `path` argument is still a read.
498
+
457
499
  - name: remote pipe to shell is still a cradle
458
500
  session: attack_remote_pipe_to_shell.jsonl
459
501
  expect: [encoded-command-download]
@@ -62,6 +62,44 @@ _NOTE_REQUIRED = frozenset({"false_positive", "risk_accepted"})
62
62
  #: States that mean no further action is expected.
63
63
  CLOSED_STATUSES = frozenset({"resolved", "false_positive", "risk_accepted"})
64
64
 
65
+
66
+ def _loggable(value: object, *, limit: int = 120) -> str:
67
+ """Flatten a value so it cannot forge log lines.
68
+
69
+ `rule_id`, `decided_by` and the disposition keys derived from
70
+ `correlation_id` all originate outside this process: a rule id can come from
71
+ an operator's YAML, a correlation id comes from the agent. A newline in any
72
+ of them lets the writer append whatever they like to the orchestrator log,
73
+ including lines that look like ours.
74
+
75
+ The bound is worth stating precisely. The trail is unaffected: canonical
76
+ events are JSON and the encoder escapes a newline inside a string, so
77
+ evidence was never forgeable this way. This protects the operator log, which
78
+ is what a human reads while deciding whether the evidence is worth opening.
79
+
80
+ The replacements are written out one call at a time on purpose. An earlier
81
+ version did the same job with `str.encode("unicode_escape")`, which is
82
+ shorter and which CodeQL cannot follow, so `py/log-injection` stayed open on
83
+ code that was already fixed. A sanitizer the scanner does not recognise
84
+ leaves you arguing with a dashboard instead of reading it.
85
+
86
+ Line breaks become visible escapes rather than disappearing, so a value that
87
+ contained one still looks different from one that did not.
88
+ """
89
+ text = str(value)
90
+ if len(text) > limit:
91
+ text = text[: limit - 3] + "..."
92
+ # Backslash first, or the escapes introduced below get double-escaped.
93
+ text = text.replace("\\", "\\\\")
94
+ text = text.replace("\r", "\\r")
95
+ text = text.replace("\n", "\\n")
96
+ text = text.replace("\t", "\\t")
97
+ # Anything else non-printable (NUL, the ESC that starts an ANSI sequence)
98
+ # is dropped rather than escaped: it carries no meaning a reader needs and
99
+ # a terminal will act on some of it.
100
+ return "".join(ch for ch in text if ch.isprintable())
101
+
102
+
65
103
  _MAX_NOTE_CHARS = 4000
66
104
  _MAX_ASSIGNEE_CHARS = 128
67
105
 
@@ -234,7 +272,11 @@ class DispositionStore:
234
272
  "DELETE FROM detection_dispositions WHERE detection_key = ?",
235
273
  (stale_key,),
236
274
  )
237
- logger.info("Migrated disposition %s -> %s after rule rename", stale_key, key)
275
+ logger.info(
276
+ "Migrated disposition %s -> %s after rule rename",
277
+ _loggable(stale_key),
278
+ _loggable(key),
279
+ )
238
280
 
239
281
  history.append({
240
282
  "status": normalized,
@@ -535,14 +577,18 @@ async def apply_disposition(
535
577
  try:
536
578
  await sink.emit(event)
537
579
  except Exception:
538
- logger.exception("Failed to forward disposition for %s", rule_id)
580
+ logger.exception("Failed to forward disposition for %s", _loggable(rule_id))
539
581
 
582
+ # `previous` and `normalized` are validated against STATUSES before they get
583
+ # here, so passing them raw was safe and CodeQL kept flagging them anyway.
584
+ # Sanitizing a closed-set value is a no-op, and a no-op costs less than a
585
+ # dismissal somebody has to re-justify every time the tab is read.
540
586
  logger.info(
541
587
  "DISPOSITION %s %s -> %s by %s",
542
- rule_id,
543
- previous,
544
- normalized,
545
- decided_by or "operator",
588
+ _loggable(rule_id),
589
+ _loggable(previous),
590
+ _loggable(normalized),
591
+ _loggable(decided_by or "operator"),
546
592
  )
547
593
  return current
548
594
 
@@ -43,6 +43,7 @@ from .traits import (
43
43
  PR_MERGE_COMMAND as _PR_MERGE_COMMAND,
44
44
  RAW_IP_URL as _RAW_IP_URL,
45
45
  RISKY_EXEC_AFTER_STAGING as _RISKY_EXEC_AFTER_STAGING,
46
+ STAGES_ARTIFACT as _STAGES_ARTIFACT,
46
47
  STAGING_FETCH as _STAGING_FETCH,
47
48
  STAGING_HOST as _STAGING_HOST,
48
49
  UNTRUSTED_INPUT_COMMAND as _UNTRUSTED_INPUT_COMMAND,
@@ -307,7 +308,14 @@ def _is_staging_fetch(event: dict[str, Any]) -> bool:
307
308
  if not _STAGING_HOST.search(cmd):
308
309
  return False
309
310
  words = _command_words(event)
310
- return bool(_STAGING_FETCH.search(words) or _DOWNLOAD_EXEC.search(words))
311
+ if not (_STAGING_FETCH.search(words) or _DOWNLOAD_EXEC.search(words)):
312
+ return False
313
+ # A fetch that only printed staged nothing, so there is no artifact for
314
+ # a later command to run. `curl raw.githubusercontent.com/.../README.md`
315
+ # followed an hour later by any `python -c` was reported as critical
316
+ # staging with no link between the two (issue #51). GitHub raw is where
317
+ # documentation lives as well as where payloads do.
318
+ return bool(_STAGES_ARTIFACT.search(cmd))
311
319
  return _has_trait(event, "staging_fetch")
312
320
 
313
321
 
@@ -1265,9 +1273,30 @@ HOST_REGISTRY = [
1265
1273
  #: and the alternative is calling every rule to find out what it might say.
1266
1274
  #: `test_detection_rule_identity.py` greps this module and fails if the two
1267
1275
  #: disagree, so it cannot drift silently.
1276
+ #: Rules that are in the tree and run, but are NOT part of the published set.
1277
+ #:
1278
+ #: `autonomous-unapproved-write` keys on `initiator.actor_type == "autonomous"`.
1279
+ #: The bus and SDK paths do produce that (cron, vault_watch, ingress, recovery),
1280
+ #: so the rule is correct and still registered. The IDE hook path never does:
1281
+ #: across roughly 32,000 events of real dogfood traffic from five agent
1282
+ #: surfaces, the actor is `human`, `agent` or `system` and never once
1283
+ #: `autonomous`. Ingest coercion deliberately cannot promote a client into it
1284
+ #: either, because that would let anyone fake this rule.
1285
+ #:
1286
+ #: So on every capture surface a user actually installs, it cannot fire. It was
1287
+ #: being counted in "fifteen detection rules", shipped in the Sigma pack at
1288
+ #: severity high, and named in the pitch as the flagship no-default-self-approve
1289
+ #: story. A published rule that cannot fire is a claim, not a detection.
1290
+ #:
1291
+ #: It stays registered so that a session which really is autonomous is still
1292
+ #: caught. It leaves the published set until a capture surface produces the
1293
+ #: signal it reads.
1294
+ EXPERIMENTAL_RULE_IDS: frozenset[str] = frozenset({
1295
+ "autonomous-unapproved-write",
1296
+ })
1297
+
1268
1298
  BUILTIN_RULE_IDS: frozenset[str] = frozenset({
1269
1299
  "credential-exfil",
1270
- "autonomous-unapproved-write",
1271
1300
  "discovery-then-collect",
1272
1301
  "approval-denied-then-executed",
1273
1302
  "encoded-command-download",
@@ -44,11 +44,22 @@ DOWNLOAD_EXEC = re.compile(
44
44
  ENCODED_CMD = re.compile(r"-enc(odedcommand)?\b|frombase64string", re.IGNORECASE)
45
45
 
46
46
  # Fetch remote content and feed it straight to an interpreter (ADI §4.2).
47
+ # An interpreter given its own program does not execute the pipe; it reads it as
48
+ # data. `curl api/x.json | python -c 'json.load(sys.stdin)'` is a data pipeline,
49
+ # and calling it a download cradle put a critical on ordinary scripting (issue
50
+ # #50). `curl x.sh | python` has no script of its own, so the download *is* the
51
+ # program, and that stays a cradle.
52
+ #
53
+ # Residual, stated rather than hidden: `| python -c "exec(sys.stdin.read())"`
54
+ # reaches for the download deliberately and slips this. INLINE_EVAL sees that
55
+ # shape, and a cradle written that way is no longer hiding.
56
+ _INLINE_SCRIPT = r"(?!\s*-\w*[ce]\b)"
57
+
47
58
  PIPE_TO_SHELL = re.compile(
48
59
  r"\b(curl|wget|iwr|invoke-webrequest|invoke-restmethod)\b[^|;&]*[|]\s*"
49
- r"(sudo\s+)?\b(ba|z|k|da)?sh\b|"
60
+ r"(sudo\s+)?\b(ba|z|k|da)?sh\b" + _INLINE_SCRIPT + r"|"
50
61
  r"\b(curl|wget|iwr|invoke-webrequest|invoke-restmethod)\b[^|;&]*[|]\s*"
51
- r"(iex|invoke-expression|python\d?|perl|ruby|node)\b",
62
+ r"(iex|invoke-expression|python\d?|perl|ruby|node)\b" + _INLINE_SCRIPT,
52
63
  re.IGNORECASE,
53
64
  )
54
65
 
@@ -314,6 +325,18 @@ RISKY_EXEC_AFTER_STAGING = re.compile(
314
325
  r"\bpowershell(?:\.exe)?\s+-(?:enc|f|file)\b",
315
326
  re.IGNORECASE,
316
327
  )
328
+ #: A fetch that leaves something behind: written to disk, or handed onward. A
329
+ #: fetch that only printed staged nothing, so there is no artifact for a later
330
+ #: command to run (issue #51).
331
+ STAGES_ARTIFACT = re.compile(
332
+ r"\s-[a-zA-Z]*[oO]|" # curl -o / -O, wget -O
333
+ r"\s--output(?:-document)?|"
334
+ r"\s>\s*\S|" # shell redirect to a file
335
+ r"\s-[a-zA-Z]*P|" # wget -P <dir>
336
+ r"[|]", # piped onward, including into an interpreter
337
+ re.IGNORECASE,
338
+ )
339
+
317
340
  BENIGN_AFTER_STAGING = re.compile(
318
341
  r"\b(npm|yarn|pnpm|pip|pip3|cargo|go)\s+(?:install|run|build)\b",
319
342
  re.IGNORECASE,
@@ -359,13 +382,27 @@ CREDENTIAL_PATH = re.compile(
359
382
  # directly after something that reads a file. A bare mention in prose does not
360
383
  # qualify, which costs nothing: nobody reads a credential file without naming a
361
384
  # path or a verb.
385
+ # `.env.example` is not a credential file. It is the file you commit *instead*
386
+ # of one, and reading it is what every developer does on their first day in a
387
+ # repository. It was six of seventeen week-one dogfood findings, every one a
388
+ # critical, which is the exact rate at which people stop reading criticals
389
+ # (issue #44).
390
+ #
391
+ # The lookahead sits immediately after `\.env` rather than at the end, because
392
+ # at the end it fails and the engine simply matches the shorter `.env` prefix,
393
+ # leaving the placeholder tagged anyway.
394
+ #
395
+ # Placeholders only. `.env.local`, `.env.production` and a bare `.env` still
396
+ # match, because those hold real values.
397
+ _ENV_PLACEHOLDER = r"(?!\.(?:example|sample|template|dist|defaults|placeholder)\b)"
398
+
362
399
  ENV_FILE = re.compile(
363
- r"[\w.~$-]*[/\\]\.env(?:\.[A-Za-z0-9_-]+)?\b|"
400
+ r"[\w.~$-]*[/\\]\.env" + _ENV_PLACEHOLDER + r"(?:\.[A-Za-z0-9_-]+)?\b|"
364
401
  r"\b(?:cat|bat|less|more|head|tail|type|source|export|dotenv|load_dotenv|"
365
402
  r"get-content|gc|cp|mv|scp|rsync|base64|xxd|od|strings|"
366
403
  r"grep|rg|ag|awk|sed|nano|vim|vi|emacs|code|open|start)"
367
- r"\s+(?:-[-\w]+\s+)*\.env(?:\.[A-Za-z0-9_-]+)?\b|"
368
- r"^\s*\.env(?:\.[A-Za-z0-9_-]+)?\b",
404
+ r"\s+(?:-[-\w]+\s+)*\.env" + _ENV_PLACEHOLDER + r"(?:\.[A-Za-z0-9_-]+)?\b|"
405
+ r"^\s*\.env" + _ENV_PLACEHOLDER + r"(?:\.[A-Za-z0-9_-]+)?\b",
369
406
  re.IGNORECASE | re.MULTILINE,
370
407
  )
371
408
 
@@ -2,11 +2,14 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import logging
5
6
  from pathlib import Path
6
7
  from typing import Any
7
8
 
8
9
  import yaml
9
10
 
11
+ logger = logging.getLogger(__name__)
12
+
10
13
  _ORCH_ROOT = Path(__file__).resolve().parents[4]
11
14
  _DEFAULT_MANIFEST = _ORCH_ROOT.parent.parent / "policies" / "detection" / "manifest.yaml"
12
15
 
@@ -35,8 +38,11 @@ def _manifest_path() -> Path:
35
38
  custom = settings.detection_rules_path
36
39
  if custom:
37
40
  return Path(custom)
38
- except Exception:
39
- pass
41
+ except Exception as exc:
42
+ # Falling back to the packaged manifest is correct, but doing it in
43
+ # silence means an operator who configured their own rules runs the
44
+ # default ones and is never told.
45
+ logger.debug("could not read detection_rules_path from settings: %s", exc)
40
46
  return _DEFAULT_MANIFEST
41
47
 
42
48