agentmetry 0.9.8__tar.gz → 0.9.9__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 (309) hide show
  1. {agentmetry-0.9.8 → agentmetry-0.9.9}/PKG-INFO +4 -4
  2. {agentmetry-0.9.8 → agentmetry-0.9.9}/README.md +1 -1
  3. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/api/routes/audit.py +1 -1
  4. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/cli/__init__.py +82 -4
  5. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/external.py +18 -0
  6. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/heartbeat.py +30 -2
  7. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/hook_bootstrap.py +10 -8
  8. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/config.py +11 -0
  9. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/diagnostics/doctor.py +112 -2
  10. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/diagnostics/hook_coverage.py +21 -3
  11. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/diagnostics/machine_view.py +2 -2
  12. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/diagnostics/mcp_inventory.py +3 -2
  13. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/diagnostics/posture.py +17 -6
  14. agentmetry-0.9.9/agentmetry/core/managed_policy.py +818 -0
  15. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/paths.py +18 -0
  16. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/version.py +1 -1
  17. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/hooks/ingest.py +10 -0
  18. {agentmetry-0.9.8 → agentmetry-0.9.9}/pyproject.toml +2 -2
  19. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/conftest.py +11 -0
  20. agentmetry-0.9.9/tests/test_doctor_json_and_remediation.py +154 -0
  21. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_heartbeat.py +2 -0
  22. agentmetry-0.9.9/tests/test_managed_policy.py +518 -0
  23. agentmetry-0.9.9/tests/test_user_profile_dir.py +138 -0
  24. {agentmetry-0.9.8 → agentmetry-0.9.9}/.dockerignore +0 -0
  25. {agentmetry-0.9.8 → agentmetry-0.9.9}/.env.agentmetry-demo +0 -0
  26. {agentmetry-0.9.8 → agentmetry-0.9.9}/.env.example +0 -0
  27. {agentmetry-0.9.8 → agentmetry-0.9.9}/.gitignore +0 -0
  28. {agentmetry-0.9.8 → agentmetry-0.9.9}/Dockerfile +0 -0
  29. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/__init__.py +0 -0
  30. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/api/__init__.py +0 -0
  31. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/api/main.py +0 -0
  32. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/api/routes/__init__.py +0 -0
  33. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/api/routes/auth.py +0 -0
  34. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/api/trusted_host.py +0 -0
  35. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/api/websocket.py +0 -0
  36. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/api/ws_bridge.py +0 -0
  37. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/cli/__main__.py +0 -0
  38. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/__init__.py +0 -0
  39. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/api_token.py +0 -0
  40. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/__init__.py +0 -0
  41. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/adapters/__init__.py +0 -0
  42. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/adapters/agt.py +0 -0
  43. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/adapters/chronicle.py +0 -0
  44. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/adapters/cloudevents.py +0 -0
  45. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/adapters/ecs.py +0 -0
  46. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/adapters/sentinel.py +0 -0
  47. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/adapters/splunk.py +0 -0
  48. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/alerts.py +0 -0
  49. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/antigravity_trajectory.py +0 -0
  50. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/atlas.py +0 -0
  51. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/canonical.py +0 -0
  52. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/cloud_bridge.py +0 -0
  53. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/compliance_digest.py +0 -0
  54. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/__init__.py +0 -0
  55. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/benchmark.py +0 -0
  56. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_approval_denied_then_executed.jsonl +0 -0
  57. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_arbitrary_host_stage_execute.jsonl +0 -0
  58. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_autonomous_unapproved_write.jsonl +0 -0
  59. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_credential_exfil.jsonl +0 -0
  60. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_credential_then_cloud_api.jsonl +0 -0
  61. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_destructive_delete_burst.jsonl +0 -0
  62. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_discovery_then_collect.jsonl +0 -0
  63. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_dotfile_then_git_push.jsonl +0 -0
  64. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_encoded_command_download.jsonl +0 -0
  65. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_env_credential_exfil.jsonl +0 -0
  66. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_hashed_only_no_command.jsonl +0 -0
  67. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_interpreter_egress.jsonl +0 -0
  68. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_pr_merged_without_review.jsonl +0 -0
  69. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_proc_substitution_cradle.jsonl +0 -0
  70. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_remote_pipe_to_shell.jsonl +0 -0
  71. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_remote_staging_then_execute.jsonl +0 -0
  72. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_secret_manager_then_egress.jsonl +0 -0
  73. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_session_tool_burst.jsonl +0 -0
  74. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_single_command_exfil.jsonl +0 -0
  75. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_single_quoted_interpreter_egress.jsonl +0 -0
  76. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_ssh_directory_exfil.jsonl +0 -0
  77. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_subagent_swarm.jsonl +0 -0
  78. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_timestamp_collision.jsonl +0 -0
  79. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_untrusted_input_then_action.jsonl +0 -0
  80. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_authoring_merge_fixtures.jsonl +0 -0
  81. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_autonomous_after_approval.jsonl +0 -0
  82. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_build_artifact_cleanup.jsonl +0 -0
  83. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_ci_artifact_download.jsonl +0 -0
  84. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_commit_message_names_cloud_clis.jsonl +0 -0
  85. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_container_registry_publish.jsonl +0 -0
  86. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_database_migration.jsonl +0 -0
  87. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_dependency_audit_network_burst.jsonl +0 -0
  88. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_dependency_install_and_build.jsonl +0 -0
  89. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_download_release_archive.jsonl +0 -0
  90. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_env_example_read.jsonl +0 -0
  91. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_fetch_data_then_run_repo_script.jsonl +0 -0
  92. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_fetch_dataset_then_analyse.jsonl +0 -0
  93. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_fetch_lockfile_then_install.jsonl +0 -0
  94. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_fetch_piped_into_own_script.jsonl +0 -0
  95. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_git_review_and_push.jsonl +0 -0
  96. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_github_raw_docs_then_script.jsonl +0 -0
  97. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_human_driven_deletes.jsonl +0 -0
  98. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_local_api_probing.jsonl +0 -0
  99. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_long_but_calm_session.jsonl +0 -0
  100. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_loopback_is_not_egress.jsonl +0 -0
  101. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_loopback_pipe_to_interpreter.jsonl +0 -0
  102. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_mcp_text_mentions_credential_path.jsonl +0 -0
  103. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_ordinary_development.jsonl +0 -0
  104. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_package_manager_after_fetch.jsonl +0 -0
  105. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_reading_config_that_is_not_secret.jsonl +0 -0
  106. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_remote_api_call_no_credentials.jsonl +0 -0
  107. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_research_then_docs.jsonl +0 -0
  108. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_reversed_order_is_not_exfil.jsonl +0 -0
  109. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_secret_names_and_docs.jsonl +0 -0
  110. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_terraform_apply_with_profile.jsonl +0 -0
  111. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_test_and_fix_loop.jsonl +0 -0
  112. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_writing_about_credentials.jsonl +0 -0
  113. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/corpus.yaml +0 -0
  114. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/disposition.py +0 -0
  115. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/engine.py +0 -0
  116. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/live.py +0 -0
  117. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/live_store.py +0 -0
  118. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/models.py +0 -0
  119. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/rules.py +0 -0
  120. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/traits.py +0 -0
  121. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/yaml_config.py +0 -0
  122. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/yaml_rules.py +0 -0
  123. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/dlp/__init__.py +0 -0
  124. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/dlp/loader.py +0 -0
  125. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/dlp/models.py +0 -0
  126. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/dlp/scanner.py +0 -0
  127. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/dogfood.py +0 -0
  128. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/evidence_pack.py +0 -0
  129. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/forwarder.py +0 -0
  130. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/grok_bot_transcript.py +0 -0
  131. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/hashing.py +0 -0
  132. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/identity.py +0 -0
  133. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/ingest.py +0 -0
  134. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/managed_hooks.py +0 -0
  135. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/mcp_inventory_event.py +0 -0
  136. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/migrate.py +0 -0
  137. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/mitre.py +0 -0
  138. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/otel_ingest.py +0 -0
  139. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/policy.py +0 -0
  140. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/policy_settings.py +0 -0
  141. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/posture_event.py +0 -0
  142. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/redaction.py +0 -0
  143. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/replay.py +0 -0
  144. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/run_context.py +0 -0
  145. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/session_tree.py +0 -0
  146. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/signing.py +0 -0
  147. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/sinks.py +0 -0
  148. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/spool.py +0 -0
  149. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/tool_policy/__init__.py +0 -0
  150. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/tool_policy/capability.py +0 -0
  151. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/tool_policy/evaluator.py +0 -0
  152. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/tool_policy/loader.py +0 -0
  153. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/tool_policy/models.py +0 -0
  154. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/trail_anchor.py +0 -0
  155. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/trail_chain.py +0 -0
  156. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/trail_db.py +0 -0
  157. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/trail_merkle.py +0 -0
  158. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/trail_rotation.py +0 -0
  159. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/auth.py +0 -0
  160. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/bus/__init__.py +0 -0
  161. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/bus/audit_exporter.py +0 -0
  162. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/bus/bridges.py +0 -0
  163. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/bus/bus.py +0 -0
  164. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/bus/events.py +0 -0
  165. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/bus/outbox.py +0 -0
  166. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/dashboard_session.py +0 -0
  167. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/diagnostics/__init__.py +0 -0
  168. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/diagnostics/autostart.py +0 -0
  169. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/diagnostics/driver_paths.py +0 -0
  170. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/diagnostics/env_file.py +0 -0
  171. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/diagnostics/mcp_schema.py +0 -0
  172. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/drivers/__init__.py +0 -0
  173. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/drivers/host.py +0 -0
  174. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/drivers/permissions.py +0 -0
  175. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/drivers/spec.py +0 -0
  176. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/extensions.py +0 -0
  177. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/health.py +0 -0
  178. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/operator_identity.py +0 -0
  179. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/demo.py +0 -0
  180. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/hooks/__init__.py +0 -0
  181. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/hooks/mcp_proxy.py +0 -0
  182. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/policies/detection/manifest.yaml +0 -0
  183. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/policies/dlp/manifest.yaml +0 -0
  184. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/policies/opa/agent_rules.rego +0 -0
  185. {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/policies/tool/manifest.yaml +0 -0
  186. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/fixtures/agt_filesink_exfil.jsonl +0 -0
  187. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/fixtures/agt_filesink_mixed.jsonl +0 -0
  188. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/fixtures/codex_hooks/README.md +0 -0
  189. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/fixtures/codex_hooks/permission-request.command.output.schema.json +0 -0
  190. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/fixtures/codex_hooks/pre-tool-use.command.output.schema.json +0 -0
  191. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/fixtures/fake_mcp_server.py +0 -0
  192. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/fixtures/otel_claude_code_session.json +0 -0
  193. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_agentmetry_audit.py +0 -0
  194. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_agentmetry_ingest_client.py +0 -0
  195. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_agt_adapter.py +0 -0
  196. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_antigravity_trajectory.py +0 -0
  197. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_ask_gate_phase_a.py +0 -0
  198. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_atlas_detection_enrichment.py +0 -0
  199. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_atlas_mapping.py +0 -0
  200. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_audit_sinks.py +0 -0
  201. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_audit_stats.py +0 -0
  202. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_audit_tail.py +0 -0
  203. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_auth.py +0 -0
  204. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_auth_default.py +0 -0
  205. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_autostart.py +0 -0
  206. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_boot_sequence.py +0 -0
  207. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_burst_windows.py +0 -0
  208. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_chinese_agent_hooks.py +0 -0
  209. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_chinese_agent_sprint_b.py +0 -0
  210. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_chinese_agent_sprint_c.py +0 -0
  211. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_chronicle_adapter.py +0 -0
  212. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_cli_backup.py +0 -0
  213. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_cli_commands.py +0 -0
  214. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_cli_env_var_names.py +0 -0
  215. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_cloud_bridge.py +0 -0
  216. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_cloudevents_adapter.py +0 -0
  217. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_compliance_digest.py +0 -0
  218. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_container_and_legacy_runtime.py +0 -0
  219. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_copilot_hooks.py +0 -0
  220. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_crewai_adapter.py +0 -0
  221. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_demo.py +0 -0
  222. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_detection_adi.py +0 -0
  223. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_detection_benchmark.py +0 -0
  224. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_detection_default_config.py +0 -0
  225. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_detection_disposition.py +0 -0
  226. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_detection_disposition_api.py +0 -0
  227. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_detection_duplicate_emission.py +0 -0
  228. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_detection_emit_durability.py +0 -0
  229. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_detection_engine.py +0 -0
  230. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_detection_evasion.py +0 -0
  231. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_detection_hf_incident.py +0 -0
  232. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_detection_quoted_command_words.py +0 -0
  233. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_detection_rule_identity.py +0 -0
  234. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_detection_rules_v2.py +0 -0
  235. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_dlp_agent_env_override.py +0 -0
  236. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_dlp_invisible_unicode.py +0 -0
  237. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_dlp_markdown_exfil.py +0 -0
  238. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_dlp_scanner.py +0 -0
  239. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_doctor.py +0 -0
  240. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_dogfood.py +0 -0
  241. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_dogfood_freeze.py +0 -0
  242. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_driver_paths.py +0 -0
  243. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_ecs_threat.py +0 -0
  244. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_event_bus.py +0 -0
  245. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_event_signing.py +0 -0
  246. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_evidence_pack.py +0 -0
  247. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_extensions.py +0 -0
  248. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_external_ingest.py +0 -0
  249. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_forwarder.py +0 -0
  250. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_forwarding_shape.py +0 -0
  251. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_funding_manifest.py +0 -0
  252. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_grok_bot_transcript.py +0 -0
  253. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_hash_key.py +0 -0
  254. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_hook_bootstrap.py +0 -0
  255. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_hook_coverage.py +0 -0
  256. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_hook_decision_format.py +0 -0
  257. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_hook_enforcement_timing.py +0 -0
  258. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_hook_import_cost.py +0 -0
  259. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_hook_real_process.py +0 -0
  260. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_hook_spool.py +0 -0
  261. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_identity_enrich.py +0 -0
  262. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_identity_provider.py +0 -0
  263. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_ingest_roundtrip.py +0 -0
  264. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_isolation.py +0 -0
  265. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_launch_targets.py +0 -0
  266. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_live_detection.py +0 -0
  267. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_live_detection_store.py +0 -0
  268. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_machine_view.py +0 -0
  269. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_managed_hooks.py +0 -0
  270. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_mcp_audit_proxy.py +0 -0
  271. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_mcp_concealed_text.py +0 -0
  272. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_mcp_inventory.py +0 -0
  273. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_mcp_inventory_event.py +0 -0
  274. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_mcp_meta_fingerprint.py +0 -0
  275. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_mcp_proxy_packaged.py +0 -0
  276. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_mcp_schema.py +0 -0
  277. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_mitre_dlp.py +0 -0
  278. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_opensre_adapter.py +0 -0
  279. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_operator_attribution.py +0 -0
  280. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_otel_ingest.py +0 -0
  281. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_paths.py +0 -0
  282. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_posture.py +0 -0
  283. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_powershell_encoding.py +0 -0
  284. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_readme_claims.py +0 -0
  285. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_redaction.py +0 -0
  286. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_replay.py +0 -0
  287. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_sentinel.py +0 -0
  288. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_serve_script_binds_loopback.py +0 -0
  289. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_session_tree.py +0 -0
  290. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_shared_install.py +0 -0
  291. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_sigma_pack.py +0 -0
  292. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_static_serving.py +0 -0
  293. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_subagent_lifecycle.py +0 -0
  294. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_suite_leaves_real_home_alone.py +0 -0
  295. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_tool_policy.py +0 -0
  296. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_tool_policy_agent_cli.py +0 -0
  297. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_tool_policy_git_hooks.py +0 -0
  298. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_trail_anchor.py +0 -0
  299. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_trail_chain.py +0 -0
  300. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_trail_concurrency.py +0 -0
  301. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_trail_merkle.py +0 -0
  302. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_transcript_watcher.py +0 -0
  303. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_trusted_host.py +0 -0
  304. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_unattended_agent_policy.py +0 -0
  305. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_version.py +0 -0
  306. {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_yaml_detection_rules.py +0 -0
  307. {agentmetry-0.9.8 → agentmetry-0.9.9}/tools/generate_sigma_pack.py +0 -0
  308. {agentmetry-0.9.8 → agentmetry-0.9.9}/tools/mcp_audit_proxy.py +0 -0
  309. {agentmetry-0.9.8 → agentmetry-0.9.9}/tools/vault_fs_server.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentmetry
3
- Version: 0.9.8
4
- Summary: Local-first flight recorder for AI coding agents: hash-chained audit trail, MITRE-mapped sequence detection, DLP
3
+ Version: 0.9.9
4
+ Summary: Local-first endpoint sensor for AI coding agents: hash-chained audit trail, MITRE-mapped sequence detection, DLP
5
5
  Project-URL: Homepage, https://agentmetry.ai
6
6
  Project-URL: Source, https://github.com/blitzcrieg1/agentmetry
7
7
  Project-URL: Issues, https://github.com/blitzcrieg1/agentmetry/issues
@@ -37,12 +37,12 @@ Provides-Extra: dev
37
37
  Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
38
38
  Requires-Dist: pytest-cov>=6.0; extra == 'dev'
39
39
  Requires-Dist: pytest>=8.0; extra == 'dev'
40
- Requires-Dist: ruff==0.16.10; extra == 'dev'
40
+ Requires-Dist: ruff==0.17.1; extra == 'dev'
41
41
  Description-Content-Type: text/markdown
42
42
 
43
43
  # Agentmetry
44
44
 
45
- A local-first flight recorder for AI coding agents.
45
+ A local-first endpoint sensor for AI coding agents.
46
46
 
47
47
  Agentmetry hooks the tool lifecycle of Claude Code, Cursor, Codex and Antigravity,
48
48
  writes every tool call, approval and denial to a hash-chained JSONL trail on your
@@ -1,6 +1,6 @@
1
1
  # Agentmetry
2
2
 
3
- A local-first flight recorder for AI coding agents.
3
+ A local-first endpoint sensor for AI coding agents.
4
4
 
5
5
  Agentmetry hooks the tool lifecycle of Claude Code, Cursor, Codex and Antigravity,
6
6
  writes every tool call, approval and denial to a hash-chained JSONL trail on your
@@ -105,7 +105,7 @@ class ExternalIngestBody(BaseModel):
105
105
  )
106
106
  event_type: str = Field(
107
107
  ...,
108
- description="session_start | session_end | tool_called | tool_denied | tool_failed | approval_request | approval_response | mcp_schema",
108
+ description="session_start | session_end | tool_called | tool_denied | tool_failed | approval_request | approval_response | mcp_schema | hook_remediation",
109
109
  )
110
110
  correlation_id: str = ""
111
111
  session_id: str = ""
@@ -48,6 +48,70 @@ def _api_base_url(port: int) -> str:
48
48
  return os.environ.get("AGENTMETRY_URL", "").strip().rstrip("/") or _base_url(port)
49
49
 
50
50
 
51
+ def _add_dir_flags(parser: argparse.ArgumentParser) -> None:
52
+ """The path split. Omit the flags and nothing moves.
53
+
54
+ `SUPPRESS`, so the same flag on `agentmetry` and on the subcommand do not
55
+ overwrite each other with a default of None.
56
+ """
57
+ parser.add_argument(
58
+ "--data-dir", default=argparse.SUPPRESS,
59
+ help="trail, spool, token and logs (env AGENTMETRY_DATA_DIR)",
60
+ )
61
+ parser.add_argument(
62
+ "--user-profile-dir", default=argparse.SUPPRESS,
63
+ help="the developer home whose IDE configs to install into and check "
64
+ "(env AGENTMETRY_USER_PROFILE_DIR); for a recorder run as root or LocalSystem",
65
+ )
66
+ parser.add_argument(
67
+ "--policy-dir", default=argparse.SUPPRESS,
68
+ help="an extra managed policy location, checked like the platform one "
69
+ "(env AGENTMETRY_POLICY_DIR)",
70
+ )
71
+
72
+
73
+ def _apply_dir_flags(args: argparse.Namespace) -> None:
74
+ """Export the path flags before a command reads data_dir() or the profile.
75
+
76
+ Exported, not passed: `start` spawns `serve`, and the hook commands it
77
+ writes run in other processes, all of which read the environment.
78
+ """
79
+ global _DATA_DIR, _PID_FILE
80
+ if getattr(args, "user_profile_dir", None):
81
+ os.environ["AGENTMETRY_USER_PROFILE_DIR"] = args.user_profile_dir
82
+ if getattr(args, "policy_dir", None):
83
+ os.environ["AGENTMETRY_POLICY_DIR"] = args.policy_dir
84
+ if getattr(args, "data_dir", None):
85
+ os.environ["AGENTMETRY_DATA_DIR"] = args.data_dir
86
+ # Only when the flag was given: a caller that already pointed these at
87
+ # a temp tree must not have them recomputed under it.
88
+ _DATA_DIR = data_dir()
89
+ _PID_FILE = _DATA_DIR / "agentmetry.pid"
90
+
91
+
92
+ def _apply_managed_policy(args: argparse.Namespace) -> None:
93
+ """Locks over flags and environment, for this process and everything it starts.
94
+
95
+ After `_apply_dir_flags`, so `--data-dir` cannot move a trail a fleet
96
+ locked. Says so when it overrides a flag: a silently ignored flag reads as
97
+ a bug.
98
+ """
99
+ global _DATA_DIR, _PID_FILE
100
+ from agentmetry.core import managed_policy
101
+
102
+ try:
103
+ policy = managed_policy.apply_locks()
104
+ except Exception as exc:
105
+ print(f"Managed policy was not applied: {exc}", file=sys.stderr)
106
+ return
107
+ if "data_dir" not in policy.locked:
108
+ return
109
+ if getattr(args, "data_dir", None):
110
+ print("--data-dir ignored: managed policy locks data_dir.", file=sys.stderr)
111
+ _DATA_DIR = data_dir()
112
+ _PID_FILE = _DATA_DIR / "agentmetry.pid"
113
+
114
+
51
115
  def _api_headers() -> dict[str, str]:
52
116
  from agentmetry.core.api_token import client_token
53
117
 
@@ -636,11 +700,12 @@ def _hooks_status() -> int:
636
700
  """
637
701
  from agentmetry.core.diagnostics import hook_coverage
638
702
 
639
- if hook_coverage.is_service_profile():
703
+ if hook_coverage.profile_blind():
640
704
  print(
641
705
  "UNDETERMINABLE: running under a service profile. Hook configuration "
642
706
  "lives in each developer's profile and none is visible from here. "
643
- "Run this in the user's context.",
707
+ "Run this in the user's context, or name the developer's home with "
708
+ "--user-profile-dir.",
644
709
  file=sys.stderr,
645
710
  )
646
711
  return 2
@@ -1414,10 +1479,10 @@ def cmd_demo(args: argparse.Namespace) -> int:
1414
1479
  def cmd_doctor(args: argparse.Namespace) -> int:
1415
1480
  """SIEM preflight: manifests, trail chain, orchestrator health, hooks."""
1416
1481
  sys.path.insert(0, str(_ORCH_ROOT))
1417
- from agentmetry.core.diagnostics.doctor import format_report, run_doctor
1482
+ from agentmetry.core.diagnostics.doctor import print_report, run_doctor
1418
1483
 
1419
1484
  report = run_doctor(fix_drivers=getattr(args, "fix", False))
1420
- print("Agentmetry doctor\n" + format_report(report))
1485
+ print_report(report, as_json=getattr(args, "json", False))
1421
1486
  return report.exit_code
1422
1487
 
1423
1488
 
@@ -1451,6 +1516,7 @@ def main(argv: list[str] | None = None) -> int:
1451
1516
  prog="agentmetry",
1452
1517
  description="Agentmetry local ops. New here? Start with `agentmetry demo`.",
1453
1518
  )
1519
+ _add_dir_flags(parser)
1454
1520
  parser.add_argument("--port", type=int, default=8000)
1455
1521
  sub = parser.add_subparsers(dest="command", required=True)
1456
1522
  # First, so it heads the help: it is the one command that needs nothing set up.
@@ -1465,6 +1531,7 @@ def main(argv: list[str] | None = None) -> int:
1465
1531
 
1466
1532
  sub.add_parser("stop", help="stop the orchestrator")
1467
1533
  start = sub.add_parser("start", help="start the orchestrator (detached)")
1534
+ _add_dir_flags(start)
1468
1535
  start.add_argument(
1469
1536
  "--host",
1470
1537
  default="127.0.0.1",
@@ -1474,12 +1541,14 @@ def main(argv: list[str] | None = None) -> int:
1474
1541
  "serve",
1475
1542
  help="run the orchestrator in the foreground (what autostart registers)",
1476
1543
  )
1544
+ _add_dir_flags(serve)
1477
1545
  serve.add_argument("--host", default="127.0.0.1")
1478
1546
  serve.add_argument("--port", type=int, default=8000)
1479
1547
  hooks = sub.add_parser(
1480
1548
  "hooks",
1481
1549
  help="install IDE hook configs for the agents on this machine",
1482
1550
  )
1551
+ _add_dir_flags(hooks)
1483
1552
  hooks.add_argument(
1484
1553
  "action",
1485
1554
  choices=("install", "status"),
@@ -1681,6 +1750,13 @@ def main(argv: list[str] | None = None) -> int:
1681
1750
  doctor = sub.add_parser(
1682
1751
  "doctor", help="SIEM preflight (manifests, trail chain, health, hooks)"
1683
1752
  )
1753
+ _add_dir_flags(doctor)
1754
+ doctor.add_argument(
1755
+ "--json",
1756
+ action="store_true",
1757
+ help="the report as JSON, for a deployment script: exit_code, "
1758
+ "orchestrator_reachable, hooks, policy and findings",
1759
+ )
1684
1760
  doctor.add_argument(
1685
1761
  "--fix",
1686
1762
  action="store_true",
@@ -1708,6 +1784,8 @@ def main(argv: list[str] | None = None) -> int:
1708
1784
  replay.add_argument("--trail", default="", help="trail to read (default: the configured trail)")
1709
1785
 
1710
1786
  args = parser.parse_args(argv)
1787
+ _apply_dir_flags(args)
1788
+ _apply_managed_policy(args)
1711
1789
  handlers = {
1712
1790
  "demo": cmd_demo,
1713
1791
  "start": cmd_start,
@@ -38,8 +38,20 @@ _ACTION_MAP = {
38
38
  "tool_failed": ("tool_called", "error"),
39
39
  "approval_request": ("approval_request", "pending"),
40
40
  "approval_response": ("approval_response", "success"),
41
+ # A deployment tool (an Intune remediation, a LaunchAgent) reinstalled
42
+ # hooks. That changes this machine's configuration; it is not a tool call.
43
+ # Unmapped it was stored as `tool_called`, which tool-sequence rules count.
44
+ "hook_remediation": ("config_change", "success"),
41
45
  }
42
46
 
47
+ #: The only payload fields a remediation notice keeps. Anything session- or
48
+ #: tool-shaped a client attached is dropped: it would join the notice to an
49
+ #: agent's session, or give a detection a tool to read.
50
+ _REMEDIATION_FIELDS = frozenset({
51
+ "event_type", "source_app", "outcome", "reason", "timestamp_utc", "adapter",
52
+ "triggered_by", "initiator", "operator",
53
+ })
54
+
43
55
 
44
56
  def _utc_now() -> str:
45
57
  return datetime.now(timezone.utc).isoformat()
@@ -82,8 +94,14 @@ def build_external_canonical(payload: dict[str, Any]) -> dict[str, Any]:
82
94
  source_app, claimed_app = _resolve_source_app(payload.get("source_app"))
83
95
 
84
96
  event_type = str(payload.get("event_type") or "tool_called")
97
+ if event_type == "hook_remediation":
98
+ payload = {k: v for k, v in payload.items() if k in _REMEDIATION_FIELDS}
85
99
  action_type, default_outcome = _ACTION_MAP.get(event_type, ("tool_called", "success"))
86
100
  outcome = str(payload.get("outcome") or default_outcome)
101
+ if event_type == "hook_remediation" and outcome not in ("success", "error"):
102
+ # Anything else is not a confirmed success, and a fleet alerting on
103
+ # failed remediations must not have one hidden by a spelling.
104
+ outcome = "error"
87
105
  reason = str(payload.get("reason") or "")
88
106
 
89
107
  correlation_id = str(payload.get("correlation_id") or payload.get("thread_id") or "")
@@ -98,7 +98,31 @@ def _hook_facts() -> dict[str, Any]:
98
98
  "hook_coverage": states,
99
99
  "hooks_uncovered": hook_coverage.uncovered(states),
100
100
  "hooks_unverified": hook_coverage.unverified(states),
101
- "hook_profile": "service" if hook_coverage.is_service_profile() else "user",
101
+ "hook_profile": hook_coverage.attestation_profile(),
102
+ }
103
+
104
+
105
+ def _policy_facts() -> dict[str, Any]:
106
+ """What a managed policy locks here, and which managed sources were refused.
107
+
108
+ Read from disk every beat, so a policy an admin broke after the recorder
109
+ started shows up before the next restart would have applied it. A refused
110
+ source means the fleet's locks may not be in force on this machine, which
111
+ is a fleet-level fact to alert on, not a log line on one laptop.
112
+
113
+ Locations only, never the reason: a reason can name the SID of the user
114
+ who could edit the file, and the beat attests configuration, not people.
115
+ `doctor` gives the reason on the machine itself.
116
+ """
117
+ from agentmetry.core import managed_policy
118
+
119
+ try:
120
+ policy = managed_policy.load_policy()
121
+ except Exception as exc:
122
+ return {"policy_locked": [], "policy_rejected": [f"unreadable ({type(exc).__name__})"]}
123
+ return {
124
+ "policy_locked": sorted(policy.locked),
125
+ "policy_rejected": [s.location for s in policy.rejected],
102
126
  }
103
127
 
104
128
 
@@ -198,6 +222,7 @@ def attestation(root: tuple[str, int] | None = None) -> dict[str, Any]:
198
222
  schema_digest, schema_servers = _mcp_schema_facts()
199
223
  return {
200
224
  **_hook_facts(),
225
+ **_policy_facts(),
201
226
  "spool_depth": _spool_depth(),
202
227
  "trail_head_seq": _trail_head(),
203
228
  "trail_merkle_root": merkle,
@@ -226,7 +251,8 @@ def build_heartbeat_event(now_utc: str, root: tuple[str, int] | None = None) ->
226
251
  # Code degrade forever, which is how a fleet learns to ignore the signal.
227
252
  # A service profile does set it: from there no developer's configuration is
228
253
  # visible at all, so a confident green would be the worst of the answers.
229
- degraded = bool(uncovered) or facts["spool_depth"] > 0 or service_profile
254
+ rejected = facts["policy_rejected"]
255
+ degraded = bool(uncovered) or facts["spool_depth"] > 0 or service_profile or bool(rejected)
230
256
 
231
257
  reason = "recorder attesting"
232
258
  if service_profile:
@@ -234,6 +260,8 @@ def build_heartbeat_event(now_utc: str, root: tuple[str, int] | None = None) ->
234
260
  "recorder attesting from a service profile; no developer hook "
235
261
  "configuration is visible from here, so coverage is unknown"
236
262
  )
263
+ elif rejected:
264
+ reason = f"recorder attesting; managed policy REFUSED, its locks are not in force: {', '.join(rejected)}"
237
265
  elif uncovered:
238
266
  reason = f"recorder attesting; agents present but NOT recorded: {', '.join(uncovered)}"
239
267
  elif facts["spool_depth"] > 0:
@@ -10,6 +10,8 @@ import sys
10
10
  from pathlib import Path
11
11
  from typing import Any
12
12
 
13
+ from agentmetry.core.paths import user_profile_dir
14
+
13
15
  logger = logging.getLogger(__name__)
14
16
 
15
17
  # Hook subprocesses read these from orchestrator .env (Claude also gets them in settings env).
@@ -78,7 +80,7 @@ def install_cursor_global_hooks(
78
80
  return None
79
81
 
80
82
  py = python or sys.executable
81
- hooks_dir = Path.home() / ".cursor"
83
+ hooks_dir = user_profile_dir() / ".cursor"
82
84
  hooks_path = hooks_dir / "hooks.json"
83
85
  payload = cursor_hooks_payload(python=py, ingest=ingest, repo_root=root)
84
86
 
@@ -371,7 +373,7 @@ def install_claude_global_hooks(
371
373
  return None
372
374
 
373
375
  py = python or sys.executable
374
- settings_dir = Path.home() / ".claude"
376
+ settings_dir = user_profile_dir() / ".claude"
375
377
  settings_path = settings_dir / "settings.json"
376
378
 
377
379
  settings: dict[str, Any] = {}
@@ -400,7 +402,7 @@ def _qwen_settings_dir() -> Path:
400
402
  home = os.environ.get("QWEN_HOME", "").strip()
401
403
  if home:
402
404
  return Path(home).expanduser()
403
- return Path.home() / ".qwen"
405
+ return user_profile_dir() / ".qwen"
404
406
 
405
407
 
406
408
  def install_qwen_global_hooks(
@@ -413,7 +415,7 @@ def install_qwen_global_hooks(
413
415
 
414
416
 
415
417
  def _qoder_settings_dir() -> Path:
416
- return Path.home() / ".qoder"
418
+ return user_profile_dir() / ".qoder"
417
419
 
418
420
 
419
421
  def install_qoder_global_hooks(
@@ -426,7 +428,7 @@ def install_qoder_global_hooks(
426
428
 
427
429
 
428
430
  def _codebuddy_settings_dir() -> Path:
429
- return Path.home() / ".codebuddy"
431
+ return user_profile_dir() / ".codebuddy"
430
432
 
431
433
 
432
434
  def install_codebuddy_global_hooks(
@@ -561,7 +563,7 @@ def install_codex_global_hooks(
561
563
  return None
562
564
 
563
565
  py = python or sys.executable
564
- hooks_dir = Path.home() / ".codex"
566
+ hooks_dir = user_profile_dir() / ".codex"
565
567
  hooks_path = hooks_dir / "hooks.json"
566
568
 
567
569
  doc: dict[str, Any] = {}
@@ -617,7 +619,7 @@ COPILOT_HOOK_EVENTS: tuple[tuple[str, str], ...] = (
617
619
 
618
620
  def copilot_hooks_dir() -> Path:
619
621
  home = os.environ.get("COPILOT_HOME", "").strip()
620
- return (Path(home).expanduser() if home else Path.home() / ".copilot") / "hooks"
622
+ return (Path(home).expanduser() if home else user_profile_dir() / ".copilot") / "hooks"
621
623
 
622
624
 
623
625
  def copilot_hooks_document(
@@ -666,7 +668,7 @@ def _kimi_config_dir() -> Path:
666
668
  home = os.environ.get("KIMI_CODE_HOME", "").strip()
667
669
  if home:
668
670
  return Path(home).expanduser()
669
- return Path.home() / ".kimi-code"
671
+ return user_profile_dir() / ".kimi-code"
670
672
 
671
673
 
672
674
  def kimi_hooks_toml_block(*, python: str, ingest: Path, repo_root: Path | None = None) -> str:
@@ -1,10 +1,21 @@
1
+ import logging
1
2
  from pathlib import Path
2
3
 
3
4
  from pydantic import AliasChoices, Field
4
5
  from pydantic_settings import BaseSettings, SettingsConfigDict
5
6
 
7
+ from agentmetry.core import managed_policy
6
8
  from agentmetry.core.paths import data_dir, env_file
7
9
 
10
+ # Before anything below reads the environment: a locked data_dir decides where
11
+ # `.env` is looked for, and every locked key must beat the `.env` Settings reads.
12
+ # A broken policy degrades to the environment rather than stopping the
13
+ # recorder; doctor and the heartbeat report the source it refused.
14
+ try:
15
+ managed_policy.apply_locks()
16
+ except Exception:
17
+ logging.getLogger(__name__).exception("Managed policy was not applied")
18
+
8
19
  _ORCHESTRATOR_ROOT = Path(__file__).resolve().parents[2]
9
20
  # Data and .env no longer live beside the package in an installed wheel
10
21
  # (they were in site-packages). See core/paths.py.
@@ -36,6 +36,16 @@ class Finding:
36
36
  @dataclass
37
37
  class DoctorReport:
38
38
  findings: list[Finding] = field(default_factory=list)
39
+ #: Facts for `doctor --json`, set by the checks that establish them. A
40
+ #: deployment script reads these rather than the sentences, which get
41
+ #: reworded, and a compliance check that breaks on a wording change fails
42
+ #: silently.
43
+ hooks: dict[str, str] = field(default_factory=dict)
44
+ hook_profile: str = ""
45
+ orchestrator_reachable: bool = False
46
+ policy_locked: list[str] = field(default_factory=list)
47
+ policy_pinned: list[str] = field(default_factory=list)
48
+ policy_rejected: list[str] = field(default_factory=list)
39
49
 
40
50
  def ok(self, code: str, message: str) -> None:
41
51
  self.findings.append(Finding("ok", code, message))
@@ -61,6 +71,7 @@ def _check_health_endpoint(report: DoctorReport) -> None:
61
71
  try:
62
72
  with urllib.request.urlopen(url, timeout=2) as resp: # noqa: S310
63
73
  if resp.status == 200:
74
+ report.orchestrator_reachable = True
64
75
  report.ok("orchestrator_up", f"Orchestrator responding at {url}")
65
76
  return
66
77
  report.warn("orchestrator_up", f"Orchestrator returned HTTP {resp.status} at {url}")
@@ -210,7 +221,9 @@ def _check_hooks_installed(report: DoctorReport) -> None:
210
221
  """
211
222
  from agentmetry.core.diagnostics import hook_coverage
212
223
 
213
- if hook_coverage.is_service_profile():
224
+ report.hook_profile = hook_coverage.attestation_profile()
225
+ if hook_coverage.profile_blind():
226
+ report.hooks = {s.name: hook_coverage.UNKNOWN for s in hook_coverage.SURFACES}
214
227
  report.fail(
215
228
  "hooks",
216
229
  f"Running under a service profile ({Path.home()}). Hook configuration "
@@ -221,6 +234,7 @@ def _check_hooks_installed(report: DoctorReport) -> None:
221
234
  return
222
235
 
223
236
  states = hook_coverage.coverage()
237
+ report.hooks = dict(states)
224
238
  covered = sorted(n for n, st in states.items() if st == hook_coverage.COVERED)
225
239
  uncovered = hook_coverage.uncovered(states)
226
240
  unverified = hook_coverage.unverified(states)
@@ -707,6 +721,60 @@ def _check_ask_gate(report: DoctorReport) -> None:
707
721
  )
708
722
 
709
723
 
724
+ def _check_managed_policy(report: DoctorReport) -> None:
725
+ """Which keys a fleet locked, and every managed source that was refused.
726
+
727
+ A refused file fails rather than warns: an admin put it there to lock
728
+ something, and on this machine nothing it says is in force. The fix for a
729
+ file the developer could have edited is to take that ability away, never
730
+ to honour it, so the message says how.
731
+ """
732
+ from agentmetry.core import managed_policy
733
+
734
+ try:
735
+ policy = managed_policy.load_policy()
736
+ except Exception as exc:
737
+ report.fail("managed_policy", f"Managed policy could not be read: {exc}")
738
+ return
739
+ report.policy_locked = sorted(policy.locked)
740
+ report.policy_pinned = list(policy.pinned)
741
+ report.policy_rejected = [s.location for s in policy.rejected]
742
+ for source in policy.rejected:
743
+ hint = "" if source.location == managed_policy.REGISTRY_LOCATION else (
744
+ " " + managed_policy.remediation(source.location)
745
+ )
746
+ report.fail("managed_policy", f"Managed policy {source.location} {source.reason}.{hint}")
747
+ for source in policy.sources:
748
+ if source.accepted and source.unknown:
749
+ report.warn(
750
+ "managed_policy",
751
+ f"{source.location} sets keys this release does not know, ignored: "
752
+ f"{', '.join(source.unknown)}",
753
+ )
754
+ if not policy.locked:
755
+ if not policy.rejected:
756
+ report.ok("managed_policy", "No managed policy; the environment and .env apply")
757
+ return
758
+ where = ", ".join(s.location for s in policy.sources if s.accepted)
759
+ report.ok("managed_policy", f"Managed policy locks {', '.join(sorted(policy.locked))} ({where})")
760
+ for key in policy.pinned:
761
+ report.ok(
762
+ "managed_policy",
763
+ f"{key} pinned to the built-in manifest because {managed_policy.PINNED_BY[key]} "
764
+ f"is locked; lock {key} too to use another",
765
+ )
766
+ for key in managed_policy.PINNED_BY:
767
+ if key not in policy.locked:
768
+ continue
769
+ try:
770
+ managed_policy.read_trusted(Path(str(policy.locked[key])))
771
+ except (managed_policy.Untrusted, OSError) as exc:
772
+ report.warn(
773
+ "managed_policy",
774
+ f"Locked {key} is a file the developer may be able to edit: {exc}",
775
+ )
776
+
777
+
710
778
  def run_doctor(
711
779
  *,
712
780
  vault_path: Path | None = None,
@@ -779,6 +847,7 @@ def run_doctor(
779
847
  except OSError as exc:
780
848
  report.fail("data", f"Data directory not writable ({data_dir}): {exc}")
781
849
 
850
+ _check_managed_policy(report)
782
851
  _check_manifests(report)
783
852
  _check_ask_gate(report)
784
853
  _check_exposure(report)
@@ -808,14 +877,55 @@ def format_report(report: DoctorReport) -> str:
808
877
  return "\n".join(lines)
809
878
 
810
879
 
880
+ #: Bumped when a field changes meaning or goes away. Adding one does not bump it.
881
+ JSON_SCHEMA = 1
882
+
883
+
884
+ def report_payload(report: DoctorReport) -> dict:
885
+ """`doctor --json`: the report as fields a deployment script can test.
886
+
887
+ `hooks` uses the coverage vocabulary (`covered`, `uncovered`, `absent`,
888
+ `unknown`), and `absent` is an agent that is not installed, not drift.
889
+ `orchestrator_reachable` is true only when the health route answered 200.
890
+ `exit_code` is the one the command returns.
891
+ """
892
+ from agentmetry.core.version import __version__
893
+
894
+ return {
895
+ "schema": JSON_SCHEMA,
896
+ "version": __version__,
897
+ "exit_code": report.exit_code,
898
+ "orchestrator_reachable": report.orchestrator_reachable,
899
+ "hook_profile": report.hook_profile,
900
+ "hooks": dict(report.hooks),
901
+ "policy": {
902
+ "locked": list(report.policy_locked),
903
+ "pinned": list(report.policy_pinned),
904
+ "rejected": list(report.policy_rejected),
905
+ },
906
+ "findings": [
907
+ {"severity": f.severity, "code": f.code, "message": f.message}
908
+ for f in report.findings
909
+ ],
910
+ }
911
+
912
+
913
+ def print_report(report: DoctorReport, *, as_json: bool = False) -> None:
914
+ if as_json:
915
+ print(json.dumps(report_payload(report), indent=2))
916
+ else:
917
+ print("Agentmetry doctor\n" + format_report(report))
918
+
919
+
811
920
  def main(argv: list[str] | None = None) -> int:
812
921
  import argparse
813
922
 
814
923
  parser = argparse.ArgumentParser(prog="agentmetry-doctor")
815
924
  parser.add_argument("--fix", action="store_true", help="rewrite drivers.json to portable tokens")
925
+ parser.add_argument("--json", action="store_true", help="print the report as JSON")
816
926
  args = parser.parse_args(argv)
817
927
  report = run_doctor(fix_drivers=args.fix)
818
- print("Agentmetry doctor\n" + format_report(report))
928
+ print_report(report, as_json=args.json)
819
929
  return report.exit_code
820
930
 
821
931
 
@@ -45,6 +45,10 @@ found there, and reported as installed, while no developer's IDE ever reads that
45
45
  directory. A per-machine install would then attest full coverage for a fleet
46
46
  recording nothing. So a service profile is a definite failure rather than an
47
47
  unknown, and it degrades the beat.
48
+
49
+ `AGENTMETRY_USER_PROFILE_DIR` (or `--user-profile-dir`) is the way out: a
50
+ service told whose home to read reads that developer's configs, and is not
51
+ blind. `profile_blind()` is the one test for "nothing here can be seen".
48
52
  """
49
53
 
50
54
  from __future__ import annotations
@@ -53,6 +57,8 @@ import os
53
57
  from dataclasses import dataclass
54
58
  from pathlib import Path
55
59
 
60
+ from agentmetry.core.paths import named_user_profile, user_profile_dir
61
+
56
62
  #: What our hook command looks like in a config we merged into. Checking for the
57
63
  #: marker rather than for the file means an IDE config the user owns is judged on
58
64
  #: our half of it. `agentaudit_ingest` is the pre-rename name, still on disk for
@@ -109,7 +115,7 @@ class Surface:
109
115
  override = os.environ.get(self.home_env, "").strip() if self.home_env else ""
110
116
  if override:
111
117
  return Path(override).expanduser()
112
- return Path.home() / self.home_rel
118
+ return user_profile_dir() / self.home_rel
113
119
 
114
120
  def installed(self) -> bool:
115
121
  """Whether this agent is on the machine, as far as the disk can say.
@@ -183,6 +189,18 @@ def is_service_profile(home: Path | None = None) -> bool:
183
189
  return "/config/systemprofile" in text or "/windows/serviceprofiles/" in text
184
190
 
185
191
 
192
+ def profile_blind() -> bool:
193
+ """True when no developer's configs are in view: a service profile with no
194
+ `AGENTMETRY_USER_PROFILE_DIR` naming whose home to read."""
195
+ return is_service_profile() and not named_user_profile()
196
+
197
+
198
+ def attestation_profile() -> str:
199
+ """What a heartbeat says about whose configs it attests: `service` when
200
+ blind, else `user`, including a service that was named a developer."""
201
+ return "service" if profile_blind() else "user"
202
+
203
+
186
204
  def _has_marker(path: Path, markers: tuple[str, ...]) -> bool:
187
205
  try:
188
206
  if not path.is_file():
@@ -200,7 +218,7 @@ def coverage() -> dict[str, str]:
200
218
  a value captured at boot keeps asserting the configuration the machine had
201
219
  when it last restarted, which is precisely the lie being hunted.
202
220
  """
203
- if is_service_profile():
221
+ if profile_blind():
204
222
  # Every path below would describe the service's own profile. Reporting
205
223
  # confident per-agent answers from it is worse than reporting none.
206
224
  return {s.name: UNKNOWN for s in SURFACES}
@@ -256,7 +274,7 @@ def unverified(states: dict[str, str] | None = None) -> list[str]:
256
274
 
257
275
  def summary_lines(states: dict[str, str] | None = None) -> list[str]:
258
276
  states = states if states is not None else coverage()
259
- if is_service_profile():
277
+ if profile_blind():
260
278
  return [
261
279
  " hooks: UNKNOWN, running under a service profile "
262
280
  f"({Path.home()}); no developer's configuration is visible from here"
@@ -44,7 +44,7 @@ def snapshot() -> dict[str, Any]:
44
44
  from agentmetry.core.audit.identity import host_id
45
45
  from agentmetry.core.audit.policy_settings import policy_settings
46
46
  from agentmetry.core.audit.tool_policy.loader import load_tool_policy
47
- from agentmetry.core.diagnostics.hook_coverage import SURFACES, coverage, is_service_profile
47
+ from agentmetry.core.diagnostics.hook_coverage import SURFACES, attestation_profile, coverage
48
48
  from agentmetry.core.diagnostics.mcp_inventory import collect as collect_mcp
49
49
  from agentmetry.core.diagnostics.mcp_schema import load_store
50
50
  from agentmetry.core.diagnostics.posture import NOT_CHECKED, collect as collect_posture
@@ -67,7 +67,7 @@ def snapshot() -> dict[str, Any]:
67
67
 
68
68
  return {
69
69
  "host_id": host_id(),
70
- "profile": "service" if is_service_profile() else "user",
70
+ "profile": attestation_profile(),
71
71
  "agents": agents,
72
72
  "posture": {
73
73
  "findings": [{**item.wire(), "why": item.why} for item in posture.sorted()],
@@ -32,13 +32,14 @@ from __future__ import annotations
32
32
 
33
33
  import hashlib
34
34
  import json
35
- import os
36
35
  import re
37
36
  from dataclasses import dataclass, field
38
37
  from pathlib import Path
39
38
  from typing import Any, Iterable
40
39
  from urllib.parse import urlparse
41
40
 
41
+ from agentmetry.core.paths import user_profile_dir
42
+
42
43
  #: Fetch-and-run launchers. These resolve a package over the network on every
43
44
  #: invocation, so the bytes executed are decided later than the config was read.
44
45
  _FETCHING_LAUNCHER = re.compile(r"^(npx|uvx|pnpx|bunx|pipx)$", re.IGNORECASE)
@@ -192,7 +193,7 @@ def config_locations(project: Path | None = None) -> list[tuple[str, str, Path]]
192
193
  A missing file is normal rather than an error: a machine running Cursor and
193
194
  not Claude Code should report on Cursor and stay silent about the rest.
194
195
  """
195
- home = Path(os.path.expanduser("~"))
196
+ home = user_profile_dir()
196
197
  found: list[tuple[str, str, Path]] = [
197
198
  ("claude", "user", home / ".claude.json"),
198
199
  ("claude", "user", home / ".claude" / "settings.json"),