agentmetry 0.7.0__tar.gz → 0.9.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 (252) hide show
  1. {agentmetry-0.7.0 → agentmetry-0.9.0}/PKG-INFO +3 -3
  2. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/api/routes/audit.py +4 -0
  3. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/cli/__init__.py +141 -1
  4. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/ingest.py +40 -7
  5. agentmetry-0.9.0/agentmetry/core/audit/otel_ingest.py +526 -0
  6. agentmetry-0.9.0/agentmetry/core/audit/tool_policy/capability.py +42 -0
  7. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/tool_policy/evaluator.py +20 -12
  8. agentmetry-0.9.0/agentmetry/core/audit/tool_policy/loader.py +83 -0
  9. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/tool_policy/models.py +4 -1
  10. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/diagnostics/doctor.py +37 -0
  11. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/diagnostics/mcp_schema.py +294 -3
  12. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/version.py +1 -1
  13. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/hooks/ingest.py +97 -9
  14. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/policies/dlp/manifest.yaml +26 -0
  15. {agentmetry-0.7.0 → agentmetry-0.9.0}/pyproject.toml +7 -2
  16. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/conftest.py +29 -0
  17. agentmetry-0.9.0/tests/fixtures/otel_claude_code_session.json +902 -0
  18. agentmetry-0.9.0/tests/test_ask_gate_phase_a.py +289 -0
  19. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_cli_commands.py +242 -0
  20. agentmetry-0.9.0/tests/test_cli_env_var_names.py +90 -0
  21. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_dlp_scanner.py +49 -0
  22. agentmetry-0.9.0/tests/test_funding_manifest.py +96 -0
  23. agentmetry-0.9.0/tests/test_hook_decision_format.py +198 -0
  24. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_hook_enforcement_timing.py +7 -2
  25. agentmetry-0.9.0/tests/test_identity_enrich.py +348 -0
  26. agentmetry-0.9.0/tests/test_mcp_concealed_text.py +302 -0
  27. agentmetry-0.9.0/tests/test_mcp_meta_fingerprint.py +203 -0
  28. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_mcp_schema.py +26 -5
  29. agentmetry-0.9.0/tests/test_otel_ingest.py +354 -0
  30. agentmetry-0.9.0/tests/test_powershell_encoding.py +102 -0
  31. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_readme_claims.py +43 -0
  32. agentmetry-0.9.0/tests/test_suite_leaves_real_home_alone.py +40 -0
  33. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_tool_policy_agent_cli.py +7 -1
  34. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_tool_policy_git_hooks.py +7 -1
  35. {agentmetry-0.7.0 → agentmetry-0.9.0}/tools/mcp_audit_proxy.py +6 -0
  36. agentmetry-0.7.0/agentmetry/core/audit/tool_policy/loader.py +0 -44
  37. {agentmetry-0.7.0 → agentmetry-0.9.0}/.dockerignore +0 -0
  38. {agentmetry-0.7.0 → agentmetry-0.9.0}/.env.agentmetry-demo +0 -0
  39. {agentmetry-0.7.0 → agentmetry-0.9.0}/.env.example +0 -0
  40. {agentmetry-0.7.0 → agentmetry-0.9.0}/.gitignore +0 -0
  41. {agentmetry-0.7.0 → agentmetry-0.9.0}/Dockerfile +0 -0
  42. {agentmetry-0.7.0 → agentmetry-0.9.0}/README.md +0 -0
  43. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/__init__.py +0 -0
  44. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/api/__init__.py +0 -0
  45. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/api/main.py +0 -0
  46. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/api/routes/__init__.py +0 -0
  47. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/api/websocket.py +0 -0
  48. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/api/ws_bridge.py +0 -0
  49. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/cli/__main__.py +0 -0
  50. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/__init__.py +0 -0
  51. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/__init__.py +0 -0
  52. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/adapters/__init__.py +0 -0
  53. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/adapters/agt.py +0 -0
  54. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/adapters/chronicle.py +0 -0
  55. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/adapters/cloudevents.py +0 -0
  56. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/adapters/ecs.py +0 -0
  57. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/adapters/splunk.py +0 -0
  58. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/alerts.py +0 -0
  59. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/atlas.py +0 -0
  60. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/canonical.py +0 -0
  61. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/compliance_digest.py +0 -0
  62. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/__init__.py +0 -0
  63. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/benchmark.py +0 -0
  64. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/attack_approval_denied_then_executed.jsonl +0 -0
  65. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/attack_arbitrary_host_stage_execute.jsonl +0 -0
  66. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/attack_autonomous_unapproved_write.jsonl +0 -0
  67. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/attack_credential_exfil.jsonl +0 -0
  68. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/attack_credential_then_cloud_api.jsonl +0 -0
  69. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/attack_destructive_delete_burst.jsonl +0 -0
  70. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/attack_discovery_then_collect.jsonl +0 -0
  71. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/attack_dotfile_then_git_push.jsonl +0 -0
  72. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/attack_encoded_command_download.jsonl +0 -0
  73. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/attack_env_credential_exfil.jsonl +0 -0
  74. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/attack_hashed_only_no_command.jsonl +0 -0
  75. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/attack_interpreter_egress.jsonl +0 -0
  76. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/attack_pr_merged_without_review.jsonl +0 -0
  77. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/attack_proc_substitution_cradle.jsonl +0 -0
  78. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/attack_remote_pipe_to_shell.jsonl +0 -0
  79. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/attack_remote_staging_then_execute.jsonl +0 -0
  80. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/attack_secret_manager_then_egress.jsonl +0 -0
  81. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/attack_session_tool_burst.jsonl +0 -0
  82. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/attack_single_command_exfil.jsonl +0 -0
  83. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/attack_single_quoted_interpreter_egress.jsonl +0 -0
  84. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/attack_ssh_directory_exfil.jsonl +0 -0
  85. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/attack_subagent_swarm.jsonl +0 -0
  86. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/attack_timestamp_collision.jsonl +0 -0
  87. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/attack_untrusted_input_then_action.jsonl +0 -0
  88. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/benign_authoring_merge_fixtures.jsonl +0 -0
  89. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/benign_autonomous_after_approval.jsonl +0 -0
  90. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/benign_build_artifact_cleanup.jsonl +0 -0
  91. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/benign_ci_artifact_download.jsonl +0 -0
  92. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/benign_commit_message_names_cloud_clis.jsonl +0 -0
  93. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/benign_database_migration.jsonl +0 -0
  94. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/benign_dependency_install_and_build.jsonl +0 -0
  95. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/benign_download_release_archive.jsonl +0 -0
  96. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/benign_env_example_read.jsonl +0 -0
  97. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/benign_fetch_data_then_run_repo_script.jsonl +0 -0
  98. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/benign_fetch_dataset_then_analyse.jsonl +0 -0
  99. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/benign_fetch_lockfile_then_install.jsonl +0 -0
  100. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/benign_fetch_piped_into_own_script.jsonl +0 -0
  101. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/benign_git_review_and_push.jsonl +0 -0
  102. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/benign_github_raw_docs_then_script.jsonl +0 -0
  103. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/benign_human_driven_deletes.jsonl +0 -0
  104. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/benign_local_api_probing.jsonl +0 -0
  105. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/benign_long_but_calm_session.jsonl +0 -0
  106. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/benign_loopback_is_not_egress.jsonl +0 -0
  107. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/benign_loopback_pipe_to_interpreter.jsonl +0 -0
  108. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/benign_mcp_text_mentions_credential_path.jsonl +0 -0
  109. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/benign_ordinary_development.jsonl +0 -0
  110. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/benign_package_manager_after_fetch.jsonl +0 -0
  111. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/benign_reading_config_that_is_not_secret.jsonl +0 -0
  112. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/benign_remote_api_call_no_credentials.jsonl +0 -0
  113. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/benign_research_then_docs.jsonl +0 -0
  114. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/benign_reversed_order_is_not_exfil.jsonl +0 -0
  115. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/benign_secret_names_and_docs.jsonl +0 -0
  116. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/benign_test_and_fix_loop.jsonl +0 -0
  117. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/benign_writing_about_credentials.jsonl +0 -0
  118. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/corpus/corpus.yaml +0 -0
  119. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/disposition.py +0 -0
  120. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/engine.py +0 -0
  121. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/live.py +0 -0
  122. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/live_store.py +0 -0
  123. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/models.py +0 -0
  124. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/rules.py +0 -0
  125. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/traits.py +0 -0
  126. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/yaml_config.py +0 -0
  127. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/detection/yaml_rules.py +0 -0
  128. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/dlp/__init__.py +0 -0
  129. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/dlp/loader.py +0 -0
  130. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/dlp/models.py +0 -0
  131. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/dlp/scanner.py +0 -0
  132. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/dogfood.py +0 -0
  133. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/evidence_pack.py +0 -0
  134. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/external.py +0 -0
  135. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/hashing.py +0 -0
  136. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/heartbeat.py +0 -0
  137. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/hook_bootstrap.py +0 -0
  138. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/identity.py +0 -0
  139. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/migrate.py +0 -0
  140. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/mitre.py +0 -0
  141. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/policy.py +0 -0
  142. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/redaction.py +0 -0
  143. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/replay.py +0 -0
  144. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/run_context.py +0 -0
  145. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/sinks.py +0 -0
  146. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/spool.py +0 -0
  147. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/tool_policy/__init__.py +0 -0
  148. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/trail_anchor.py +0 -0
  149. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/trail_chain.py +0 -0
  150. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/trail_db.py +0 -0
  151. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/audit/trail_merkle.py +0 -0
  152. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/auth.py +0 -0
  153. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/bus/__init__.py +0 -0
  154. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/bus/audit_exporter.py +0 -0
  155. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/bus/bridges.py +0 -0
  156. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/bus/bus.py +0 -0
  157. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/bus/events.py +0 -0
  158. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/bus/outbox.py +0 -0
  159. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/config.py +0 -0
  160. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/diagnostics/__init__.py +0 -0
  161. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/diagnostics/autostart.py +0 -0
  162. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/diagnostics/driver_paths.py +0 -0
  163. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/diagnostics/env_file.py +0 -0
  164. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/diagnostics/hook_coverage.py +0 -0
  165. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/diagnostics/mcp_inventory.py +0 -0
  166. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/drivers/__init__.py +0 -0
  167. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/drivers/host.py +0 -0
  168. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/drivers/permissions.py +0 -0
  169. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/drivers/spec.py +0 -0
  170. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/extensions.py +0 -0
  171. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/core/health.py +0 -0
  172. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/hooks/__init__.py +0 -0
  173. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/policies/detection/manifest.yaml +0 -0
  174. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/policies/opa/agent_rules.rego +0 -0
  175. {agentmetry-0.7.0 → agentmetry-0.9.0}/agentmetry/policies/tool/manifest.yaml +0 -0
  176. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/fixtures/agt_filesink_exfil.jsonl +0 -0
  177. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/fixtures/agt_filesink_mixed.jsonl +0 -0
  178. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/fixtures/fake_mcp_server.py +0 -0
  179. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_agentmetry_audit.py +0 -0
  180. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_agentmetry_ingest_client.py +0 -0
  181. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_agt_adapter.py +0 -0
  182. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_atlas_detection_enrichment.py +0 -0
  183. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_atlas_mapping.py +0 -0
  184. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_audit_sinks.py +0 -0
  185. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_audit_stats.py +0 -0
  186. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_audit_tail.py +0 -0
  187. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_auth.py +0 -0
  188. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_autostart.py +0 -0
  189. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_boot_sequence.py +0 -0
  190. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_burst_windows.py +0 -0
  191. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_chinese_agent_hooks.py +0 -0
  192. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_chinese_agent_sprint_b.py +0 -0
  193. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_chinese_agent_sprint_c.py +0 -0
  194. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_chronicle_adapter.py +0 -0
  195. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_cli_backup.py +0 -0
  196. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_cloudevents_adapter.py +0 -0
  197. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_compliance_digest.py +0 -0
  198. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_crewai_adapter.py +0 -0
  199. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_detection_adi.py +0 -0
  200. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_detection_benchmark.py +0 -0
  201. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_detection_default_config.py +0 -0
  202. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_detection_disposition.py +0 -0
  203. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_detection_disposition_api.py +0 -0
  204. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_detection_duplicate_emission.py +0 -0
  205. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_detection_emit_durability.py +0 -0
  206. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_detection_engine.py +0 -0
  207. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_detection_evasion.py +0 -0
  208. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_detection_hf_incident.py +0 -0
  209. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_detection_quoted_command_words.py +0 -0
  210. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_detection_rule_identity.py +0 -0
  211. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_detection_rules_v2.py +0 -0
  212. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_dlp_agent_env_override.py +0 -0
  213. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_dlp_invisible_unicode.py +0 -0
  214. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_dlp_markdown_exfil.py +0 -0
  215. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_doctor.py +0 -0
  216. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_dogfood.py +0 -0
  217. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_dogfood_freeze.py +0 -0
  218. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_driver_paths.py +0 -0
  219. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_ecs_threat.py +0 -0
  220. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_event_bus.py +0 -0
  221. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_evidence_pack.py +0 -0
  222. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_extensions.py +0 -0
  223. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_external_ingest.py +0 -0
  224. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_forwarding_shape.py +0 -0
  225. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_heartbeat.py +0 -0
  226. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_hook_bootstrap.py +0 -0
  227. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_hook_coverage.py +0 -0
  228. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_hook_spool.py +0 -0
  229. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_ingest_roundtrip.py +0 -0
  230. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_isolation.py +0 -0
  231. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_launch_targets.py +0 -0
  232. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_live_detection.py +0 -0
  233. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_live_detection_store.py +0 -0
  234. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_mcp_audit_proxy.py +0 -0
  235. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_mcp_inventory.py +0 -0
  236. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_mitre_dlp.py +0 -0
  237. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_opensre_adapter.py +0 -0
  238. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_redaction.py +0 -0
  239. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_replay.py +0 -0
  240. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_sigma_pack.py +0 -0
  241. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_static_serving.py +0 -0
  242. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_subagent_lifecycle.py +0 -0
  243. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_tool_policy.py +0 -0
  244. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_trail_anchor.py +0 -0
  245. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_trail_chain.py +0 -0
  246. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_trail_concurrency.py +0 -0
  247. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_trail_merkle.py +0 -0
  248. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_unattended_agent_policy.py +0 -0
  249. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_version.py +0 -0
  250. {agentmetry-0.7.0 → agentmetry-0.9.0}/tests/test_yaml_detection_rules.py +0 -0
  251. {agentmetry-0.7.0 → agentmetry-0.9.0}/tools/generate_sigma_pack.py +0 -0
  252. {agentmetry-0.7.0 → agentmetry-0.9.0}/tools/vault_fs_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentmetry
3
- Version: 0.7.0
3
+ Version: 0.9.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
@@ -25,7 +25,7 @@ Requires-Python: >=3.11
25
25
  Requires-Dist: aiosqlite>=0.20.0
26
26
  Requires-Dist: fastapi>=0.115.0
27
27
  Requires-Dist: httpx>=0.28.0
28
- Requires-Dist: mcp>=1.2
28
+ Requires-Dist: mcp<2,>=1.2
29
29
  Requires-Dist: pydantic-settings>=2.6.0
30
30
  Requires-Dist: pydantic>=2.9.0
31
31
  Requires-Dist: pyyaml>=6.0
@@ -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.4; extra == 'dev'
40
+ Requires-Dist: ruff==0.16.6; extra == 'dev'
41
41
  Description-Content-Type: text/markdown
42
42
 
43
43
  # Agentmetry
@@ -122,6 +122,10 @@ class ExternalIngestBody(BaseModel):
122
122
  # fields that separate a shipped release from a rug pull. All hashes and
123
123
  # flags: no tool name, no description, nothing model-visible.
124
124
  schema_tool_digests: dict[str, str] = Field(default_factory=dict)
125
+ # Concealed-character counts per category. Declared here because pydantic
126
+ # drops what it does not know, silently, which has already cost this file
127
+ # two shipped bugs.
128
+ schema_concealed: dict[str, int] = Field(default_factory=dict)
125
129
  server_version: str = ""
126
130
  list_changed: bool | None = None
127
131
 
@@ -34,6 +34,7 @@ _BACKUP_EXCLUDE_DIRS = {"logs"}
34
34
  logger = logging.getLogger(__name__)
35
35
 
36
36
  _BACKUP_EXCLUDE_SUFFIXES = {".pid"}
37
+ _CLOSING_DISPOSITIONS = ("resolved", "false_positive", "risk_accepted")
37
38
 
38
39
 
39
40
  def _base_url(port: int, host: str = "127.0.0.1") -> str:
@@ -41,6 +42,15 @@ def _base_url(port: int, host: str = "127.0.0.1") -> str:
41
42
  return f"http://{display}:{port}"
42
43
 
43
44
 
45
+ def _api_base_url(port: int) -> str:
46
+ return os.environ.get("AGENTMETRY_URL", "").strip().rstrip("/") or _base_url(port)
47
+
48
+
49
+ def _api_headers() -> dict[str, str]:
50
+ key = os.environ.get("AGENTMETRY_API_KEY", "").strip()
51
+ return {"X-API-Key": key} if key else {}
52
+
53
+
44
54
  def _lan_ip() -> str | None:
45
55
  """Best-effort local IPv4 for phone/LAN access hints."""
46
56
  try:
@@ -219,7 +229,8 @@ def cmd_stats(args: argparse.Namespace) -> int:
219
229
  return 1
220
230
 
221
231
  if not data.get("enabled", True):
222
- print("Audit export disabled — enable AGENTMETRY_AUDIT_EXPORT to collect stats.")
232
+ print("Audit export is disabled, so no events are recorded and stats are empty.")
233
+ print("Set AGENTMETRY_AUDIT_EXPORT_ENABLED=1 and restart.")
223
234
  return 1
224
235
 
225
236
  days = data.get("window_days", args.days)
@@ -241,6 +252,76 @@ def cmd_stats(args: argparse.Namespace) -> int:
241
252
  return 0
242
253
 
243
254
 
255
+ def cmd_detections(args: argparse.Namespace) -> int:
256
+ try:
257
+ data = httpx.get(
258
+ f"{_base_url(args.port)}/api/v1/audit/detections/{args.correlation_id}",
259
+ timeout=10.0,
260
+ ).json()
261
+ except Exception:
262
+ print("Not running - start Agentmetry first (detections reads via the API).")
263
+ return 1
264
+ if not data.get("enabled", True):
265
+ print("Audit export is disabled, so no sessions are recorded.")
266
+ print("Set AGENTMETRY_AUDIT_EXPORT_ENABLED=1 and restart.")
267
+ return 1
268
+ detections = data.get("detections") or []
269
+ if not detections:
270
+ print(f"No detections for {args.correlation_id}.")
271
+ return 0
272
+ rule_width = max(len("Rule"), *(len(str(d.get("rule_id", ""))) for d in detections))
273
+ severity_width = max(
274
+ len("Severity"), *(len(str(d.get("severity", ""))) for d in detections)
275
+ )
276
+ print(f"{'Rule':<{rule_width}} {'Severity':<{severity_width}} Summary")
277
+ print(f"{'-' * rule_width} {'-' * severity_width} {'-' * len('Summary')}")
278
+ for detection in detections:
279
+ print(
280
+ f"{str(detection.get('rule_id', '')):<{rule_width}} "
281
+ f"{str(detection.get('severity', '')):<{severity_width}} "
282
+ f"{detection.get('summary', '')}"
283
+ )
284
+ return 0
285
+
286
+
287
+ def cmd_disposition(args: argparse.Namespace) -> int:
288
+ """Close a detection from the shell, using the same API as the dashboard."""
289
+ note = args.note.strip()
290
+ if args.status in {"false_positive", "risk_accepted"} and not note:
291
+ print(f"--note is required for {args.status}")
292
+ return 1
293
+
294
+ try:
295
+ resp = httpx.post(
296
+ f"{_api_base_url(args.port)}/api/v1/audit/detections/disposition",
297
+ json={
298
+ "correlation_id": args.correlation_id,
299
+ "rule_id": args.rule_id,
300
+ "status": args.status,
301
+ "note": note,
302
+ "decided_by": args.decided_by,
303
+ },
304
+ headers=_api_headers(),
305
+ timeout=10.0,
306
+ )
307
+ except Exception:
308
+ print("Not running - start Agentmetry first (disposition writes via the API).")
309
+ return 1
310
+
311
+ if resp.status_code >= 400:
312
+ try:
313
+ detail = resp.json().get("detail")
314
+ except Exception:
315
+ detail = None
316
+ print(f"FAILED — {detail or resp.text or f'HTTP {resp.status_code}'}")
317
+ return 1
318
+
319
+ current = resp.json().get("disposition", {})
320
+ status = current.get("status", args.status)
321
+ print(f"Disposition set: {args.correlation_id} {args.rule_id} -> {status}")
322
+ return 0
323
+
324
+
244
325
  def cmd_logs(args: argparse.Namespace) -> int:
245
326
  log = _DATA_DIR / "logs" / "orchestrator.log"
246
327
  if not log.exists():
@@ -815,6 +896,35 @@ def cmd_mcp(args: argparse.Namespace) -> int:
815
896
  return 0
816
897
 
817
898
 
899
+ def cmd_otel(args: argparse.Namespace) -> int:
900
+ """Receive Claude Code's native OpenTelemetry stream and record it.
901
+
902
+ The other way in for Claude Code, with no hooks installed. It sees what ran,
903
+ after it ran, so it records and detects but enforces nothing.
904
+ """
905
+ from agentmetry.core.audit import otel_ingest
906
+
907
+ port = args.listen_port or otel_ingest.default_port()
908
+ if args.print_mapping:
909
+ for line in otel_ingest.mapping_lines():
910
+ print(line)
911
+ return 0
912
+ if args.print_env:
913
+ for line in otel_ingest.claude_env_lines(port):
914
+ print(line)
915
+ return 0
916
+
917
+ # The receiver forwards through the hook client, which reads its target and
918
+ # its command-text setting from the environment. Setting them here, in this
919
+ # process only, is what makes --port and --keep-command mean the same thing
920
+ # they mean everywhere else.
921
+ orchestrator = _api_base_url(args.port)
922
+ os.environ["AGENTMETRY_URL"] = orchestrator
923
+ if args.keep_command:
924
+ os.environ["AGENTMETRY_LOG_COMMANDS"] = "1"
925
+ return otel_ingest.serve(port, orchestrator)
926
+
927
+
818
928
  def cmd_anchor(args: argparse.Namespace) -> int:
819
929
  """Publish, list, or check the checkpoints that commit the trail externally.
820
930
 
@@ -1132,6 +1242,14 @@ def main(argv: list[str] | None = None) -> int:
1132
1242
  sub.add_parser("status", help="orchestrator health and audit export status")
1133
1243
  stats = sub.add_parser("stats", help="audit trail metrics for dogfood (events, detections)")
1134
1244
  stats.add_argument("--days", type=int, default=7)
1245
+ detections = sub.add_parser("detections", help="list detections for one session")
1246
+ detections.add_argument("correlation_id", help="correlation_id / session id")
1247
+ disposition = sub.add_parser("disposition", help="close an audit detection from the shell")
1248
+ disposition.add_argument("correlation_id", help="session/correlation id that owns the detection")
1249
+ disposition.add_argument("rule_id", help="detection rule id to close")
1250
+ disposition.add_argument("--status", required=True, choices=_CLOSING_DISPOSITIONS)
1251
+ disposition.add_argument("--note", default="")
1252
+ disposition.add_argument("--decided-by", default="")
1135
1253
  logs = sub.add_parser("logs", help="tail the orchestrator log")
1136
1254
  logs.add_argument("-n", "--lines", type=int, default=50)
1137
1255
  logs.add_argument("-f", "--follow", action="store_true")
@@ -1219,6 +1337,25 @@ def main(argv: list[str] | None = None) -> int:
1219
1337
  "--digest", action="store_true",
1220
1338
  help="also print the config digest, which names no server and is safe to publish",
1221
1339
  )
1340
+ otel = sub.add_parser(
1341
+ "otel", help="receive Claude Code's native OpenTelemetry stream (no hooks needed)"
1342
+ )
1343
+ otel.add_argument(
1344
+ "--listen-port", type=int, default=None,
1345
+ help="port on 127.0.0.1 to receive OTLP on (default AGENTMETRY_OTEL_PORT or 4318)",
1346
+ )
1347
+ otel.add_argument(
1348
+ "--keep-command", action="store_true",
1349
+ help="keep scrubbed command text, as AGENTMETRY_LOG_COMMANDS=1 does for hooks",
1350
+ )
1351
+ otel.add_argument(
1352
+ "--print-mapping", action="store_true",
1353
+ help="list which events are mapped, counted, or unmapped, and exit",
1354
+ )
1355
+ otel.add_argument(
1356
+ "--print-env", action="store_true",
1357
+ help="print the environment Claude Code needs to send here, and exit",
1358
+ )
1222
1359
  verify = sub.add_parser("verify", help="verify evidence pack or JSONL trail chain")
1223
1360
  verify.add_argument(
1224
1361
  "path",
@@ -1273,6 +1410,8 @@ def main(argv: list[str] | None = None) -> int:
1273
1410
  "hook": cmd_hook,
1274
1411
  "status": cmd_status,
1275
1412
  "stats": cmd_stats,
1413
+ "detections": cmd_detections,
1414
+ "disposition": cmd_disposition,
1276
1415
  "logs": cmd_logs,
1277
1416
  "backup": cmd_backup,
1278
1417
  "restore": cmd_restore,
@@ -1283,6 +1422,7 @@ def main(argv: list[str] | None = None) -> int:
1283
1422
  "prove": cmd_prove,
1284
1423
  "anchor": cmd_anchor,
1285
1424
  "mcp": cmd_mcp,
1425
+ "otel": cmd_otel,
1286
1426
  "import-agt": cmd_import_agt,
1287
1427
  "doctor": cmd_doctor,
1288
1428
  "benchmark": cmd_benchmark,
@@ -187,6 +187,7 @@ class _SchemaFields(NamedTuple):
187
187
  server_version: str
188
188
  list_changed: bool | None
189
189
  tool_digests: dict[str, str]
190
+ concealed: dict[str, int]
190
191
 
191
192
 
192
193
  def _schema_payload_fields(payload: dict[str, Any]) -> _SchemaFields:
@@ -202,6 +203,12 @@ def _schema_payload_fields(payload: dict[str, Any]) -> _SchemaFields:
202
203
  list_changed = payload.get("list_changed")
203
204
  if list_changed is not None and not isinstance(list_changed, bool):
204
205
  list_changed = None
206
+ raw_concealed = payload.get("schema_concealed")
207
+ concealed = (
208
+ {str(k): int(v) for k, v in raw_concealed.items() if isinstance(v, int)}
209
+ if isinstance(raw_concealed, dict)
210
+ else {}
211
+ )
205
212
  raw_digests = payload.get("schema_tool_digests")
206
213
  tool_digests = (
207
214
  {str(k): str(v) for k, v in raw_digests.items() if isinstance(v, str)}
@@ -209,7 +216,8 @@ def _schema_payload_fields(payload: dict[str, Any]) -> _SchemaFields:
209
216
  else {}
210
217
  )
211
218
  return _SchemaFields(
212
- server, fingerprint, tool_count, source, server_version, list_changed, tool_digests
219
+ server, fingerprint, tool_count, source, server_version, list_changed,
220
+ tool_digests, concealed,
213
221
  )
214
222
 
215
223
 
@@ -232,22 +240,47 @@ def build_schema_canonical(
232
240
  from agentmetry.core.audit.canonical import SCHEMA_VERSION
233
241
  from agentmetry.core.audit.identity import identity_fields
234
242
  from agentmetry.core.audit.atlas import RUG_PULL
235
- from agentmetry.core.diagnostics.mcp_schema import server_id
243
+ from agentmetry.core.diagnostics.mcp_schema import FORMATTING_CATEGORY, server_id
236
244
 
237
245
  fields = _schema_payload_fields(payload)
238
246
  server, fingerprint, tool_count = fields.server, fields.fingerprint, fields.tool_count
239
247
  server_version, list_changed = fields.server_version, fields.list_changed
240
248
  outcome = "changed" if status == "changed" else "success"
241
- reason = (
242
- "MCP tool schema changed; config may be unchanged (rug-pull candidate)"
243
- if status == "changed"
244
- else "MCP tool schema observed"
245
- )
249
+ _REASONS = {
250
+ "changed": "MCP tool schema changed; config may be unchanged (rug-pull candidate)",
251
+ # Says what it is and what it is not. A re-baseline adopts whatever the
252
+ # server serves today without comparing it to anything, so if the server
253
+ # was already poisoned before our hashing changed, this event is the
254
+ # moment that state became the trusted one. `new` would have hidden that
255
+ # behind a word that also means "nothing was ever wrong here" (#146).
256
+ "rebaselined": (
257
+ "MCP tool schema re-baselined after a fingerprint change; "
258
+ "trust-on-first-use, not compared against the previous baseline"
259
+ ),
260
+ }
261
+ reason = _REASONS.get(status, "MCP tool schema observed")
262
+ _formatting = {k: v for k, v in fields.concealed.items() if k == FORMATTING_CATEGORY}
263
+ _concealment = {k: v for k, v in fields.concealed.items() if k != FORMATTING_CATEGORY}
246
264
  mcp_schema: dict[str, Any] = {
247
265
  "server_id": server_id(server) if server else "",
248
266
  "fingerprint": fingerprint,
249
267
  "tool_count": tool_count,
250
268
  "status": status,
269
+ # A baseline nobody has verified against a predecessor. Structured
270
+ # rather than left in the reason string so a SIEM can count them
271
+ # instead of matching prose.
272
+ **({"unverified_baseline": True} if status == "rebaselined" else {}),
273
+ # Concealed characters in strings the model reads. Counts per category,
274
+ # never the text. Attached on every status including `new`, because
275
+ # unlike everything else here it does not need a baseline: this is the
276
+ # one poisoning visible on a first sighting.
277
+ **({"concealed": _concealment} if _concealment else {}),
278
+ # Typography, kept apart from the evidence. A soft hyphen or a
279
+ # zero-width space arrives in descriptions imported from formatted
280
+ # documentation, and no neighbouring character can clear it the way one
281
+ # clears a ZWJ. Reporting it beside real concealment would make an
282
+ # operator who reads one finding distrust the next.
283
+ **({"formatting": _formatting} if _formatting else {}),
251
284
  # Only a schema that MOVED is the technique. `new` is the first
252
285
  # sight of a server and `same` is a quiet reconnect; tagging either
253
286
  # as a rug pull would put a Defense Evasion label on installing a