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.
- {agentmetry-0.9.8 → agentmetry-0.9.9}/PKG-INFO +4 -4
- {agentmetry-0.9.8 → agentmetry-0.9.9}/README.md +1 -1
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/api/routes/audit.py +1 -1
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/cli/__init__.py +82 -4
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/external.py +18 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/heartbeat.py +30 -2
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/hook_bootstrap.py +10 -8
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/config.py +11 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/diagnostics/doctor.py +112 -2
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/diagnostics/hook_coverage.py +21 -3
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/diagnostics/machine_view.py +2 -2
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/diagnostics/mcp_inventory.py +3 -2
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/diagnostics/posture.py +17 -6
- agentmetry-0.9.9/agentmetry/core/managed_policy.py +818 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/paths.py +18 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/version.py +1 -1
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/hooks/ingest.py +10 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/pyproject.toml +2 -2
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/conftest.py +11 -0
- agentmetry-0.9.9/tests/test_doctor_json_and_remediation.py +154 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_heartbeat.py +2 -0
- agentmetry-0.9.9/tests/test_managed_policy.py +518 -0
- agentmetry-0.9.9/tests/test_user_profile_dir.py +138 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/.dockerignore +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/.env.agentmetry-demo +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/.env.example +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/.gitignore +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/Dockerfile +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/__init__.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/api/__init__.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/api/main.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/api/routes/__init__.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/api/routes/auth.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/api/trusted_host.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/api/websocket.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/api/ws_bridge.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/cli/__main__.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/__init__.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/api_token.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/__init__.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/adapters/__init__.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/adapters/agt.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/adapters/chronicle.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/adapters/cloudevents.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/adapters/ecs.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/adapters/sentinel.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/adapters/splunk.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/alerts.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/antigravity_trajectory.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/atlas.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/canonical.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/cloud_bridge.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/compliance_digest.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/__init__.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/benchmark.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_approval_denied_then_executed.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_arbitrary_host_stage_execute.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_autonomous_unapproved_write.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_credential_exfil.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_credential_then_cloud_api.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_destructive_delete_burst.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_discovery_then_collect.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_dotfile_then_git_push.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_encoded_command_download.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_env_credential_exfil.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_hashed_only_no_command.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_interpreter_egress.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_pr_merged_without_review.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_proc_substitution_cradle.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_remote_pipe_to_shell.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_remote_staging_then_execute.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_secret_manager_then_egress.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_session_tool_burst.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_single_command_exfil.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_single_quoted_interpreter_egress.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_ssh_directory_exfil.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_subagent_swarm.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_timestamp_collision.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/attack_untrusted_input_then_action.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_authoring_merge_fixtures.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_autonomous_after_approval.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_build_artifact_cleanup.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_ci_artifact_download.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_commit_message_names_cloud_clis.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_container_registry_publish.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_database_migration.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_dependency_audit_network_burst.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_dependency_install_and_build.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_download_release_archive.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_env_example_read.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_fetch_data_then_run_repo_script.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_fetch_dataset_then_analyse.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_fetch_lockfile_then_install.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_fetch_piped_into_own_script.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_git_review_and_push.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_github_raw_docs_then_script.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_human_driven_deletes.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_local_api_probing.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_long_but_calm_session.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_loopback_is_not_egress.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_loopback_pipe_to_interpreter.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_mcp_text_mentions_credential_path.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_ordinary_development.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_package_manager_after_fetch.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_reading_config_that_is_not_secret.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_remote_api_call_no_credentials.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_research_then_docs.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_reversed_order_is_not_exfil.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_secret_names_and_docs.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_terraform_apply_with_profile.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_test_and_fix_loop.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/benign_writing_about_credentials.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/corpus/corpus.yaml +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/disposition.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/engine.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/live.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/live_store.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/models.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/rules.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/traits.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/yaml_config.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/detection/yaml_rules.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/dlp/__init__.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/dlp/loader.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/dlp/models.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/dlp/scanner.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/dogfood.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/evidence_pack.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/forwarder.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/grok_bot_transcript.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/hashing.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/identity.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/ingest.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/managed_hooks.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/mcp_inventory_event.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/migrate.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/mitre.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/otel_ingest.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/policy.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/policy_settings.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/posture_event.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/redaction.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/replay.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/run_context.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/session_tree.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/signing.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/sinks.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/spool.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/tool_policy/__init__.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/tool_policy/capability.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/tool_policy/evaluator.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/tool_policy/loader.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/tool_policy/models.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/trail_anchor.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/trail_chain.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/trail_db.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/trail_merkle.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/audit/trail_rotation.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/auth.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/bus/__init__.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/bus/audit_exporter.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/bus/bridges.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/bus/bus.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/bus/events.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/bus/outbox.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/dashboard_session.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/diagnostics/__init__.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/diagnostics/autostart.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/diagnostics/driver_paths.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/diagnostics/env_file.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/diagnostics/mcp_schema.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/drivers/__init__.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/drivers/host.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/drivers/permissions.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/drivers/spec.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/extensions.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/health.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/core/operator_identity.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/demo.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/hooks/__init__.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/hooks/mcp_proxy.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/policies/detection/manifest.yaml +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/policies/dlp/manifest.yaml +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/policies/opa/agent_rules.rego +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/agentmetry/policies/tool/manifest.yaml +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/fixtures/agt_filesink_exfil.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/fixtures/agt_filesink_mixed.jsonl +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/fixtures/codex_hooks/README.md +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/fixtures/codex_hooks/permission-request.command.output.schema.json +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/fixtures/codex_hooks/pre-tool-use.command.output.schema.json +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/fixtures/fake_mcp_server.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/fixtures/otel_claude_code_session.json +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_agentmetry_audit.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_agentmetry_ingest_client.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_agt_adapter.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_antigravity_trajectory.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_ask_gate_phase_a.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_atlas_detection_enrichment.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_atlas_mapping.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_audit_sinks.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_audit_stats.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_audit_tail.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_auth.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_auth_default.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_autostart.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_boot_sequence.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_burst_windows.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_chinese_agent_hooks.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_chinese_agent_sprint_b.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_chinese_agent_sprint_c.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_chronicle_adapter.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_cli_backup.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_cli_commands.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_cli_env_var_names.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_cloud_bridge.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_cloudevents_adapter.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_compliance_digest.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_container_and_legacy_runtime.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_copilot_hooks.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_crewai_adapter.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_demo.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_detection_adi.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_detection_benchmark.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_detection_default_config.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_detection_disposition.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_detection_disposition_api.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_detection_duplicate_emission.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_detection_emit_durability.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_detection_engine.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_detection_evasion.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_detection_hf_incident.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_detection_quoted_command_words.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_detection_rule_identity.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_detection_rules_v2.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_dlp_agent_env_override.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_dlp_invisible_unicode.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_dlp_markdown_exfil.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_dlp_scanner.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_doctor.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_dogfood.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_dogfood_freeze.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_driver_paths.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_ecs_threat.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_event_bus.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_event_signing.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_evidence_pack.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_extensions.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_external_ingest.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_forwarder.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_forwarding_shape.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_funding_manifest.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_grok_bot_transcript.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_hash_key.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_hook_bootstrap.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_hook_coverage.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_hook_decision_format.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_hook_enforcement_timing.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_hook_import_cost.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_hook_real_process.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_hook_spool.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_identity_enrich.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_identity_provider.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_ingest_roundtrip.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_isolation.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_launch_targets.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_live_detection.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_live_detection_store.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_machine_view.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_managed_hooks.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_mcp_audit_proxy.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_mcp_concealed_text.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_mcp_inventory.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_mcp_inventory_event.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_mcp_meta_fingerprint.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_mcp_proxy_packaged.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_mcp_schema.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_mitre_dlp.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_opensre_adapter.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_operator_attribution.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_otel_ingest.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_paths.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_posture.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_powershell_encoding.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_readme_claims.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_redaction.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_replay.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_sentinel.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_serve_script_binds_loopback.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_session_tree.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_shared_install.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_sigma_pack.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_static_serving.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_subagent_lifecycle.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_suite_leaves_real_home_alone.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_tool_policy.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_tool_policy_agent_cli.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_tool_policy_git_hooks.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_trail_anchor.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_trail_chain.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_trail_concurrency.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_trail_merkle.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_transcript_watcher.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_trusted_host.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_unattended_agent_policy.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_version.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tests/test_yaml_detection_rules.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tools/generate_sigma_pack.py +0 -0
- {agentmetry-0.9.8 → agentmetry-0.9.9}/tools/mcp_audit_proxy.py +0 -0
- {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.
|
|
4
|
-
Summary: Local-first
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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":
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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":
|
|
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 =
|
|
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"),
|