agentmetry 0.9.9__tar.gz → 0.10.1__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.9 → agentmetry-0.10.1}/.gitignore +2 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/PKG-INFO +36 -10
- agentmetry-0.10.1/README.md +71 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/api/main.py +97 -16
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/api/routes/audit.py +58 -3
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/cli/__init__.py +123 -16
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/api_token.py +76 -0
- agentmetry-0.10.1/agentmetry/core/audit/cursor_cloud_cache.py +305 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/heartbeat.py +30 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/hook_bootstrap.py +91 -12
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/ingest.py +13 -1
- agentmetry-0.10.1/agentmetry/core/audit/ingest_quota.py +138 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/trail_db.py +13 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/auth.py +20 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/config.py +17 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/diagnostics/doctor.py +119 -2
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/diagnostics/hook_coverage.py +41 -0
- agentmetry-0.10.1/agentmetry/core/scanner/__init__.py +12 -0
- agentmetry-0.10.1/agentmetry/core/scanner/injection.py +209 -0
- agentmetry-0.10.1/agentmetry/core/scanner/known_packages.py +120 -0
- agentmetry-0.10.1/agentmetry/core/scanner/scan.py +266 -0
- agentmetry-0.10.1/agentmetry/core/scanner/typosquat.py +115 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/version.py +1 -1
- agentmetry-0.10.1/agentmetry/dashboard_static/.gitkeep +0 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/404.html +1 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/__next.__PAGE__.txt +27 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/__next._full.txt +23 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/__next._tree.txt +5 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/_next/static/NZBzSYttw3Vkl_rHtxNJp/_buildManifest.js +11 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/_next/static/NZBzSYttw3Vkl_rHtxNJp/_clientMiddlewareManifest.js +1 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/_next/static/NZBzSYttw3Vkl_rHtxNJp/_ssgManifest.js +1 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/_next/static/chunks/0cz1d0mv5g_q7.js +1 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/_next/static/chunks/0umf92h1sb77c.js +1 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/_next/static/chunks/1ix59xuhq-q14.css +1 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/_next/static/chunks/1q292q_rc4_p6.js +22 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/_next/static/chunks/2-sq6__g8tt5-.js +4 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/_next/static/chunks/2gvu8dyrhibpa.js +1 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/_next/static/chunks/31qp8jfkw0x2h.js +31 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/_next/static/chunks/3ia-1961js2lo.js +1 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/_next/static/chunks/3in77zrqn0wml.css +8 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/_next/static/chunks/turbopack-3-ry682tkyiqk.js +1 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/_next/static/media/ibm-plex-mono-latin-400-normal.2lhng2ntocry9.woff +0 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/_next/static/media/ibm-plex-mono-latin-400-normal.3xdfs0-p_zi4c.woff2 +0 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/_next/static/media/ibm-plex-mono-latin-500-normal.11q3guvgd1r20.woff2 +0 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/_next/static/media/ibm-plex-mono-latin-500-normal.1fgk-kmy-32_a.woff +0 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/_next/static/media/ibm-plex-sans-latin-400-normal.19i24--p24z3r.woff +0 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/_next/static/media/ibm-plex-sans-latin-400-normal.1xf5nwxyfigak.woff2 +0 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/_next/static/media/ibm-plex-sans-latin-500-normal.14sc3d7sk_zzh.woff +0 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/_next/static/media/ibm-plex-sans-latin-500-normal.2i4a1iuj6lbvw.woff2 +0 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/_next/static/media/ibm-plex-sans-latin-600-normal.2ovda_azncp9b.woff2 +0 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/_next/static/media/ibm-plex-sans-latin-600-normal.2wn5p1s7u3utm.woff +0 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/_not-found/__next._full.txt +22 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/_not-found/__next._not-found.__PAGE__.txt +28 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/_not-found/__next._tree.txt +4 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/_not-found.html +1 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/_not-found.txt +22 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/agentmetry-icon-white.svg +4 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/index.html +1 -0
- agentmetry-0.10.1/agentmetry/dashboard_static/index.txt +23 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/hooks/ingest.py +9 -1
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/hooks/mcp_proxy.py +6 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/pyproject.toml +9 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/conftest.py +14 -0
- agentmetry-0.10.1/tests/fixtures/scanner/benign_skill/SKILL.md +13 -0
- agentmetry-0.10.1/tests/fixtures/scanner/benign_tools.json +29 -0
- agentmetry-0.10.1/tests/fixtures/scanner/poisoned_skill/SKILL.md +11 -0
- agentmetry-0.10.1/tests/fixtures/scanner/poisoned_tools.json +26 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_cli_commands.py +54 -1
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_cloud_bridge.py +57 -0
- agentmetry-0.10.1/tests/test_codex_windows_command.py +160 -0
- agentmetry-0.10.1/tests/test_coverage_observed.py +178 -0
- agentmetry-0.10.1/tests/test_cursor_cloud_cache.py +191 -0
- agentmetry-0.10.1/tests/test_demo_isolation.py +74 -0
- agentmetry-0.10.1/tests/test_demo_mode_flag.py +60 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_doctor.py +20 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_heartbeat.py +4 -0
- agentmetry-0.10.1/tests/test_ingest_rate_limit.py +165 -0
- agentmetry-0.10.1/tests/test_ingest_token.py +284 -0
- agentmetry-0.10.1/tests/test_scanner.py +223 -0
- agentmetry-0.10.1/tests/test_static_serving.py +98 -0
- agentmetry-0.9.9/README.md +0 -45
- agentmetry-0.9.9/tests/test_static_serving.py +0 -46
- {agentmetry-0.9.9 → agentmetry-0.10.1}/.dockerignore +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/.env.agentmetry-demo +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/.env.example +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/Dockerfile +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/__init__.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/api/__init__.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/api/routes/__init__.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/api/routes/auth.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/api/trusted_host.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/api/websocket.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/api/ws_bridge.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/cli/__main__.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/__init__.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/__init__.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/adapters/__init__.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/adapters/agt.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/adapters/chronicle.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/adapters/cloudevents.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/adapters/ecs.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/adapters/sentinel.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/adapters/splunk.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/alerts.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/antigravity_trajectory.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/atlas.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/canonical.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/cloud_bridge.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/compliance_digest.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/__init__.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/benchmark.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/attack_approval_denied_then_executed.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/attack_arbitrary_host_stage_execute.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/attack_autonomous_unapproved_write.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/attack_credential_exfil.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/attack_credential_then_cloud_api.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/attack_destructive_delete_burst.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/attack_discovery_then_collect.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/attack_dotfile_then_git_push.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/attack_encoded_command_download.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/attack_env_credential_exfil.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/attack_hashed_only_no_command.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/attack_interpreter_egress.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/attack_pr_merged_without_review.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/attack_proc_substitution_cradle.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/attack_remote_pipe_to_shell.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/attack_remote_staging_then_execute.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/attack_secret_manager_then_egress.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/attack_session_tool_burst.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/attack_single_command_exfil.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/attack_single_quoted_interpreter_egress.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/attack_ssh_directory_exfil.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/attack_subagent_swarm.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/attack_timestamp_collision.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/attack_untrusted_input_then_action.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/benign_authoring_merge_fixtures.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/benign_autonomous_after_approval.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/benign_build_artifact_cleanup.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/benign_ci_artifact_download.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/benign_commit_message_names_cloud_clis.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/benign_container_registry_publish.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/benign_database_migration.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/benign_dependency_audit_network_burst.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/benign_dependency_install_and_build.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/benign_download_release_archive.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/benign_env_example_read.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/benign_fetch_data_then_run_repo_script.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/benign_fetch_dataset_then_analyse.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/benign_fetch_lockfile_then_install.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/benign_fetch_piped_into_own_script.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/benign_git_review_and_push.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/benign_github_raw_docs_then_script.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/benign_human_driven_deletes.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/benign_local_api_probing.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/benign_long_but_calm_session.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/benign_loopback_is_not_egress.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/benign_loopback_pipe_to_interpreter.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/benign_mcp_text_mentions_credential_path.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/benign_ordinary_development.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/benign_package_manager_after_fetch.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/benign_reading_config_that_is_not_secret.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/benign_remote_api_call_no_credentials.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/benign_research_then_docs.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/benign_reversed_order_is_not_exfil.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/benign_secret_names_and_docs.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/benign_terraform_apply_with_profile.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/benign_test_and_fix_loop.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/benign_writing_about_credentials.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/corpus/corpus.yaml +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/disposition.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/engine.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/live.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/live_store.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/models.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/rules.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/traits.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/yaml_config.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/detection/yaml_rules.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/dlp/__init__.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/dlp/loader.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/dlp/models.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/dlp/scanner.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/dogfood.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/evidence_pack.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/external.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/forwarder.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/grok_bot_transcript.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/hashing.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/identity.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/managed_hooks.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/mcp_inventory_event.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/migrate.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/mitre.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/otel_ingest.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/policy.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/policy_settings.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/posture_event.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/redaction.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/replay.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/run_context.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/session_tree.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/signing.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/sinks.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/spool.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/tool_policy/__init__.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/tool_policy/capability.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/tool_policy/evaluator.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/tool_policy/loader.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/tool_policy/models.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/trail_anchor.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/trail_chain.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/trail_merkle.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/audit/trail_rotation.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/bus/__init__.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/bus/audit_exporter.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/bus/bridges.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/bus/bus.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/bus/events.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/bus/outbox.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/dashboard_session.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/diagnostics/__init__.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/diagnostics/autostart.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/diagnostics/driver_paths.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/diagnostics/env_file.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/diagnostics/machine_view.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/diagnostics/mcp_inventory.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/diagnostics/mcp_schema.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/diagnostics/posture.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/drivers/__init__.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/drivers/host.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/drivers/permissions.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/drivers/spec.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/extensions.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/health.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/managed_policy.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/operator_identity.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/core/paths.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/demo.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/hooks/__init__.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/policies/detection/manifest.yaml +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/policies/dlp/manifest.yaml +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/policies/opa/agent_rules.rego +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/agentmetry/policies/tool/manifest.yaml +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/fixtures/agt_filesink_exfil.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/fixtures/agt_filesink_mixed.jsonl +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/fixtures/codex_hooks/README.md +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/fixtures/codex_hooks/permission-request.command.output.schema.json +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/fixtures/codex_hooks/pre-tool-use.command.output.schema.json +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/fixtures/fake_mcp_server.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/fixtures/otel_claude_code_session.json +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_agentmetry_audit.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_agentmetry_ingest_client.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_agt_adapter.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_antigravity_trajectory.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_ask_gate_phase_a.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_atlas_detection_enrichment.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_atlas_mapping.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_audit_sinks.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_audit_stats.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_audit_tail.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_auth.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_auth_default.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_autostart.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_boot_sequence.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_burst_windows.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_chinese_agent_hooks.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_chinese_agent_sprint_b.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_chinese_agent_sprint_c.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_chronicle_adapter.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_cli_backup.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_cli_env_var_names.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_cloudevents_adapter.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_compliance_digest.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_container_and_legacy_runtime.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_copilot_hooks.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_crewai_adapter.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_demo.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_detection_adi.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_detection_benchmark.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_detection_default_config.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_detection_disposition.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_detection_disposition_api.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_detection_duplicate_emission.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_detection_emit_durability.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_detection_engine.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_detection_evasion.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_detection_hf_incident.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_detection_quoted_command_words.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_detection_rule_identity.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_detection_rules_v2.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_dlp_agent_env_override.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_dlp_invisible_unicode.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_dlp_markdown_exfil.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_dlp_scanner.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_doctor_json_and_remediation.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_dogfood.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_dogfood_freeze.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_driver_paths.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_ecs_threat.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_event_bus.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_event_signing.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_evidence_pack.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_extensions.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_external_ingest.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_forwarder.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_forwarding_shape.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_funding_manifest.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_grok_bot_transcript.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_hash_key.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_hook_bootstrap.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_hook_coverage.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_hook_decision_format.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_hook_enforcement_timing.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_hook_import_cost.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_hook_real_process.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_hook_spool.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_identity_enrich.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_identity_provider.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_ingest_roundtrip.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_isolation.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_launch_targets.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_live_detection.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_live_detection_store.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_machine_view.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_managed_hooks.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_managed_policy.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_mcp_audit_proxy.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_mcp_concealed_text.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_mcp_inventory.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_mcp_inventory_event.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_mcp_meta_fingerprint.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_mcp_proxy_packaged.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_mcp_schema.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_mitre_dlp.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_opensre_adapter.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_operator_attribution.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_otel_ingest.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_paths.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_posture.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_powershell_encoding.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_readme_claims.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_redaction.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_replay.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_sentinel.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_serve_script_binds_loopback.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_session_tree.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_shared_install.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_sigma_pack.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_subagent_lifecycle.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_suite_leaves_real_home_alone.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_tool_policy.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_tool_policy_agent_cli.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_tool_policy_git_hooks.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_trail_anchor.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_trail_chain.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_trail_concurrency.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_trail_merkle.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_transcript_watcher.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_trusted_host.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_unattended_agent_policy.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_user_profile_dir.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_version.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tests/test_yaml_detection_rules.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tools/generate_sigma_pack.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tools/mcp_audit_proxy.py +0 -0
- {agentmetry-0.9.9 → agentmetry-0.10.1}/tools/vault_fs_server.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agentmetry
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.1
|
|
4
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
|
|
@@ -44,19 +44,38 @@ Description-Content-Type: text/markdown
|
|
|
44
44
|
|
|
45
45
|
A local-first endpoint sensor for AI coding agents.
|
|
46
46
|
|
|
47
|
-
Agentmetry hooks the tool lifecycle of Claude Code, Cursor, Codex
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
by network egress becomes one finding
|
|
47
|
+
Agentmetry hooks the tool lifecycle of Claude Code, Cursor, Codex, GitHub Copilot,
|
|
48
|
+
Antigravity, Qwen, Kimi, Qoder and CodeBuddy, writes every tool call, approval and
|
|
49
|
+
denial to a hash-chained JSONL trail on your machine, and runs sequence detection
|
|
50
|
+
over the session. A credential read followed by network egress becomes one finding
|
|
51
|
+
rather than two unremarkable log lines. Findings carry MITRE ATT&CK labels, and
|
|
52
|
+
MITRE ATLAS labels where ATLAS says something ATT&CK cannot.
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
Nothing leaves the machine unless you configure it, and there is no telemetry.
|
|
55
|
+
Forwarding to Splunk HEC, Elastic ECS, Google SecOps, Microsoft Sentinel or a
|
|
56
|
+
webhook (canonical JSON or CloudEvents) is off by default.
|
|
57
|
+
|
|
58
|
+
## Try it with nothing installed
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
uvx agentmetry demo
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
It replays a session into a temporary directory it deletes: a private-key read,
|
|
65
|
+
then an upload. That raises one critical finding. It then edits the trail and
|
|
66
|
+
shows `agentmetry verify` fail.
|
|
67
|
+
|
|
68
|
+
## Record a real agent
|
|
54
69
|
|
|
55
70
|
```bash
|
|
56
71
|
pip install agentmetry
|
|
72
|
+
agentmetry hooks install
|
|
73
|
+
agentmetry start
|
|
57
74
|
agentmetry doctor
|
|
58
75
|
```
|
|
59
76
|
|
|
77
|
+
`agentmetry dashboard` opens the local console with a one-time sign-in link.
|
|
78
|
+
|
|
60
79
|
## Check the detection claims yourself
|
|
61
80
|
|
|
62
81
|
The corpus ships inside the package, so this works from a clean install:
|
|
@@ -72,12 +91,19 @@ matters: any tool can fire on an attack, and a feed that cries wolf gets muted.
|
|
|
72
91
|
## What it does not do
|
|
73
92
|
|
|
74
93
|
- **It is not a CASB.** It records the agents you wire in. An unmanaged browser
|
|
75
|
-
assistant is invisible to it.
|
|
76
|
-
- **It is a recorder, not a sandbox.**
|
|
77
|
-
|
|
94
|
+
assistant, or an IDE with its hooks off, is invisible to it.
|
|
95
|
+
- **It is a recorder, not a sandbox.** By default it records. DLP and tool policy
|
|
96
|
+
can deny a call in the hook, before it runs, when you set them to block.
|
|
78
97
|
- **The DLP is regex, not ML.** A starting pack you extend in YAML.
|
|
98
|
+
- **macOS is in preview.** CI runs the full suite on Windows, Linux and macOS, but
|
|
99
|
+
only Windows and Linux are required checks, and no hook has been exercised
|
|
100
|
+
against a live agent on a Mac.
|
|
79
101
|
- **It is a public alpha.** Integration surfaces may still change.
|
|
80
102
|
|
|
103
|
+
For a fleet, a managed policy file can lock settings such as the DLP and tool
|
|
104
|
+
policy modes so a developer cannot change them. See
|
|
105
|
+
[docs/managed-policy.md](https://github.com/blitzcrieg1/agentmetry/blob/master/docs/managed-policy.md).
|
|
106
|
+
|
|
81
107
|
Full documentation, the event schema, and the SIEM integration guides are in the
|
|
82
108
|
repository.
|
|
83
109
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Agentmetry
|
|
2
|
+
|
|
3
|
+
A local-first endpoint sensor for AI coding agents.
|
|
4
|
+
|
|
5
|
+
Agentmetry hooks the tool lifecycle of Claude Code, Cursor, Codex, GitHub Copilot,
|
|
6
|
+
Antigravity, Qwen, Kimi, Qoder and CodeBuddy, writes every tool call, approval and
|
|
7
|
+
denial to a hash-chained JSONL trail on your machine, and runs sequence detection
|
|
8
|
+
over the session. A credential read followed by network egress becomes one finding
|
|
9
|
+
rather than two unremarkable log lines. Findings carry MITRE ATT&CK labels, and
|
|
10
|
+
MITRE ATLAS labels where ATLAS says something ATT&CK cannot.
|
|
11
|
+
|
|
12
|
+
Nothing leaves the machine unless you configure it, and there is no telemetry.
|
|
13
|
+
Forwarding to Splunk HEC, Elastic ECS, Google SecOps, Microsoft Sentinel or a
|
|
14
|
+
webhook (canonical JSON or CloudEvents) is off by default.
|
|
15
|
+
|
|
16
|
+
## Try it with nothing installed
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
uvx agentmetry demo
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
It replays a session into a temporary directory it deletes: a private-key read,
|
|
23
|
+
then an upload. That raises one critical finding. It then edits the trail and
|
|
24
|
+
shows `agentmetry verify` fail.
|
|
25
|
+
|
|
26
|
+
## Record a real agent
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install agentmetry
|
|
30
|
+
agentmetry hooks install
|
|
31
|
+
agentmetry start
|
|
32
|
+
agentmetry doctor
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`agentmetry dashboard` opens the local console with a one-time sign-in link.
|
|
36
|
+
|
|
37
|
+
## Check the detection claims yourself
|
|
38
|
+
|
|
39
|
+
The corpus ships inside the package, so this works from a clean install:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
agentmetry benchmark
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
It replays recorded sessions through the real rule engine and exits non-zero on
|
|
46
|
+
any missed rule or any false positive. The benign half is the number that
|
|
47
|
+
matters: any tool can fire on an attack, and a feed that cries wolf gets muted.
|
|
48
|
+
|
|
49
|
+
## What it does not do
|
|
50
|
+
|
|
51
|
+
- **It is not a CASB.** It records the agents you wire in. An unmanaged browser
|
|
52
|
+
assistant, or an IDE with its hooks off, is invisible to it.
|
|
53
|
+
- **It is a recorder, not a sandbox.** By default it records. DLP and tool policy
|
|
54
|
+
can deny a call in the hook, before it runs, when you set them to block.
|
|
55
|
+
- **The DLP is regex, not ML.** A starting pack you extend in YAML.
|
|
56
|
+
- **macOS is in preview.** CI runs the full suite on Windows, Linux and macOS, but
|
|
57
|
+
only Windows and Linux are required checks, and no hook has been exercised
|
|
58
|
+
against a live agent on a Mac.
|
|
59
|
+
- **It is a public alpha.** Integration surfaces may still change.
|
|
60
|
+
|
|
61
|
+
For a fleet, a managed policy file can lock settings such as the DLP and tool
|
|
62
|
+
policy modes so a developer cannot change them. See
|
|
63
|
+
[docs/managed-policy.md](https://github.com/blitzcrieg1/agentmetry/blob/master/docs/managed-policy.md).
|
|
64
|
+
|
|
65
|
+
Full documentation, the event schema, and the SIEM integration guides are in the
|
|
66
|
+
repository.
|
|
67
|
+
|
|
68
|
+
- Source and issues: https://github.com/blitzcrieg1/agentmetry
|
|
69
|
+
- Website: https://agentmetry.ai
|
|
70
|
+
|
|
71
|
+
Apache-2.0.
|
|
@@ -79,6 +79,22 @@ def _start_forwarders() -> list[asyncio.Task]:
|
|
|
79
79
|
return tasks
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
def antigravity_watcher_wanted() -> bool:
|
|
83
|
+
"""Whether this recorder should tail the developer's Antigravity transcripts.
|
|
84
|
+
|
|
85
|
+
It used to start whenever the script existed, so every recorder did it,
|
|
86
|
+
including the dashboard demo, whose trail is supposed to hold seeded events
|
|
87
|
+
only. The watcher reads `~/.gemini/antigravity/brain`, the real one, so a
|
|
88
|
+
demo shown to someone could carry the presenter's actual agent activity, and
|
|
89
|
+
a second recorder doubled every Antigravity call. It now runs only where the
|
|
90
|
+
recorder accepts ingest, and `AGENTMETRY_ANTIGRAVITY_WATCHER=0` turns it off.
|
|
91
|
+
"""
|
|
92
|
+
if not settings.audit_ingest_enabled:
|
|
93
|
+
return False
|
|
94
|
+
flag = os.environ.get("AGENTMETRY_ANTIGRAVITY_WATCHER", "").strip().lower()
|
|
95
|
+
return flag not in ("0", "false", "no", "off")
|
|
96
|
+
|
|
97
|
+
|
|
82
98
|
def start_transcript_watcher(watcher_path: Path) -> subprocess.Popen:
|
|
83
99
|
"""Start the Antigravity transcript watcher, tied to this process's life.
|
|
84
100
|
|
|
@@ -111,6 +127,20 @@ def stop_transcript_watcher(process: subprocess.Popen) -> None:
|
|
|
111
127
|
async def lifespan(app: FastAPI):
|
|
112
128
|
from agentmetry.core.audit.migrate import backfill_db_from_jsonl
|
|
113
129
|
|
|
130
|
+
# The ingest-only token, which hooks prefer and the ingest route accepts.
|
|
131
|
+
# Without it a hook running as another account (Codex's Windows sandbox)
|
|
132
|
+
# could not authenticate and every event it sent was lost (#302). Owner-
|
|
133
|
+
# only like the full token; `hooks install` widens it to Codex's sandbox
|
|
134
|
+
# accounts where they exist. A failure here costs that, never the boot.
|
|
135
|
+
from agentmetry.core import api_token
|
|
136
|
+
from agentmetry.core.auth import auth_is_disabled
|
|
137
|
+
|
|
138
|
+
if not auth_is_disabled():
|
|
139
|
+
try:
|
|
140
|
+
api_token.ensure_ingest_token()
|
|
141
|
+
except OSError:
|
|
142
|
+
logger.exception("Could not write the ingest-only token; hooks fall back to the full token")
|
|
143
|
+
|
|
114
144
|
# Backfill SQLite from the existing JSONL trail. A broken or locked audit DB
|
|
115
145
|
# must degrade the dashboard, never stop the daemon from booting: the
|
|
116
146
|
# recorder's job is to keep recording.
|
|
@@ -211,7 +241,7 @@ async def lifespan(app: FastAPI):
|
|
|
211
241
|
watcher_process = None
|
|
212
242
|
try:
|
|
213
243
|
watcher_path = Path(__file__).resolve().parents[4] / "scripts" / "antigravity_transcript_watcher.py"
|
|
214
|
-
if watcher_path.exists():
|
|
244
|
+
if watcher_path.exists() and antigravity_watcher_wanted():
|
|
215
245
|
watcher_process = start_transcript_watcher(watcher_path)
|
|
216
246
|
logger.info("Started Antigravity transcript watcher (PID: %s)", watcher_process.pid)
|
|
217
247
|
except Exception as exc:
|
|
@@ -285,28 +315,79 @@ async def websocket_endpoint(
|
|
|
285
315
|
ws_manager.disconnect(websocket, session_id)
|
|
286
316
|
|
|
287
317
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
318
|
+
#: A source checkout's build, which a developer rebuilds and expects to see.
|
|
319
|
+
_CHECKOUT_DASHBOARD_DIR = Path(__file__).resolve().parents[3] / "dashboard" / "out"
|
|
320
|
+
#: The same export, bundled inside the wheel by the release build. Until 0.10.1
|
|
321
|
+
#: nothing put it there, so `pip install agentmetry`, `agentmetry dashboard`
|
|
322
|
+
#: signed the browser in and landed it on a JSON 404.
|
|
323
|
+
_PACKAGED_DASHBOARD_DIR = Path(__file__).resolve().parents[1] / "dashboard_static"
|
|
324
|
+
|
|
325
|
+
_DASHBOARD_MISSING_PAGE = """<!DOCTYPE html>
|
|
326
|
+
<html lang="en"><head><meta charset="utf-8"><title>Agentmetry</title>
|
|
327
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
328
|
+
<style>body{font:16px/1.6 system-ui,sans-serif;max-width:40rem;margin:4rem auto;padding:0 1rem}
|
|
329
|
+
code{background:#f2f2f2;padding:.1em .3em;border-radius:3px}</style></head>
|
|
330
|
+
<body><h1>Agentmetry is recording.</h1>
|
|
331
|
+
<p>The dashboard is not part of this install. Packages from 0.10.1 include it:
|
|
332
|
+
<code>pip install --upgrade agentmetry</code>, then restart the recorder.</p>
|
|
333
|
+
<p>Recording, detection and the trail do not depend on it. From a shell:
|
|
334
|
+
<code>agentmetry stats</code>, <code>agentmetry replay <session></code>,
|
|
335
|
+
<code>agentmetry verify --trail</code>.</p></body></html>"""
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def dashboard_dir() -> Path | None:
|
|
339
|
+
"""Where the dashboard export is: a checkout's build first, then the package's."""
|
|
340
|
+
for candidate in (_CHECKOUT_DASHBOARD_DIR, _PACKAGED_DASHBOARD_DIR):
|
|
341
|
+
if (candidate / "index.html").is_file():
|
|
342
|
+
return candidate
|
|
343
|
+
return None
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def mount_dashboard(target: FastAPI, directory: Path | None = None) -> bool:
|
|
347
|
+
"""Serve the dashboard's static export when there is one.
|
|
348
|
+
|
|
349
|
+
Single-process mode: the orchestrator serves the export at the root,
|
|
350
|
+
mounted last so it only catches paths not already claimed by the API or
|
|
351
|
+
WebSocket. Dev uses the :3000 dev server instead. With no export, the root
|
|
352
|
+
serves a page saying so, because the sign-in link redirects there and a
|
|
353
|
+
JSON 404 reads as a broken product.
|
|
298
354
|
"""
|
|
299
|
-
if
|
|
355
|
+
if directory is None:
|
|
356
|
+
directory = dashboard_dir()
|
|
357
|
+
if directory is None or not directory.is_dir():
|
|
300
358
|
logger.info(
|
|
301
|
-
"Dashboard export not found
|
|
302
|
-
|
|
303
|
-
|
|
359
|
+
"Dashboard export not found (looked in %s and %s); serving a notice at /",
|
|
360
|
+
_CHECKOUT_DASHBOARD_DIR,
|
|
361
|
+
_PACKAGED_DASHBOARD_DIR,
|
|
304
362
|
)
|
|
363
|
+
from fastapi.responses import HTMLResponse
|
|
364
|
+
|
|
365
|
+
@target.get("/", include_in_schema=False, name="dashboard-missing")
|
|
366
|
+
async def dashboard_missing() -> HTMLResponse:
|
|
367
|
+
return HTMLResponse(_DASHBOARD_MISSING_PAGE, headers={"Cache-Control": "no-cache"})
|
|
368
|
+
|
|
305
369
|
return False
|
|
306
370
|
|
|
307
371
|
from fastapi.staticfiles import StaticFiles
|
|
308
372
|
|
|
309
|
-
|
|
373
|
+
class DashboardFiles(StaticFiles):
|
|
374
|
+
"""The export, with its HTML pages marked `no-cache`.
|
|
375
|
+
|
|
376
|
+
Served with no cache header at all, a browser may reuse a stale page on
|
|
377
|
+
an ordinary reload, and the page names the old scripts, so an upgraded
|
|
378
|
+
recorder kept showing the previous dashboard until a hard reload.
|
|
379
|
+
`no-cache` still lets the browser keep a copy; it only has to ask
|
|
380
|
+
whether the page changed. The scripts are not touched: their names
|
|
381
|
+
change with every build.
|
|
382
|
+
"""
|
|
383
|
+
|
|
384
|
+
async def get_response(self, path: str, scope): # type: ignore[override]
|
|
385
|
+
response = await super().get_response(path, scope)
|
|
386
|
+
if response.headers.get("content-type", "").startswith("text/html"):
|
|
387
|
+
response.headers["Cache-Control"] = "no-cache"
|
|
388
|
+
return response
|
|
389
|
+
|
|
390
|
+
target.mount("/", DashboardFiles(directory=str(directory), html=True), name="dashboard")
|
|
310
391
|
logger.info("Serving dashboard from %s", directory)
|
|
311
392
|
return True
|
|
312
393
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import logging
|
|
6
7
|
from datetime import datetime, timedelta, timezone
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
from typing import Any, Literal
|
|
@@ -11,12 +12,13 @@ from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
|
|
11
12
|
from fastapi.responses import FileResponse
|
|
12
13
|
from pydantic import BaseModel, Field, field_validator
|
|
13
14
|
|
|
14
|
-
from agentmetry.core.auth import require_api_key
|
|
15
|
+
from agentmetry.core.auth import require_api_key, require_ingest_key
|
|
15
16
|
from agentmetry.core.audit.detection.disposition import STATUSES, get_disposition_store
|
|
16
17
|
from agentmetry.core.audit.ingest import ingest_external_event
|
|
17
18
|
from agentmetry.core.config import settings
|
|
18
19
|
|
|
19
20
|
router = APIRouter(prefix="/audit", tags=["audit"])
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
20
22
|
|
|
21
23
|
_RUN_ACTION_TYPES = frozenset({
|
|
22
24
|
"session_start",
|
|
@@ -142,6 +144,8 @@ class ExternalIngestBody(BaseModel):
|
|
|
142
144
|
# drops what it does not know, silently, which has already cost this file
|
|
143
145
|
# two shipped bugs.
|
|
144
146
|
schema_concealed: dict[str, int] = Field(default_factory=dict)
|
|
147
|
+
# Model-steering text in a tool description (#188), counts per category.
|
|
148
|
+
schema_injection: dict[str, int] = Field(default_factory=dict)
|
|
145
149
|
server_version: str = ""
|
|
146
150
|
list_changed: bool | None = None
|
|
147
151
|
|
|
@@ -191,7 +195,24 @@ def _event_source_app(event: dict[str, Any]) -> str:
|
|
|
191
195
|
return "agentmetry"
|
|
192
196
|
|
|
193
197
|
|
|
194
|
-
|
|
198
|
+
_THROTTLE_WARNED: dict[str, float] = {}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _warn_throttled(source_app: str) -> None:
|
|
202
|
+
"""Log throttling once a minute per source, so a flood does not flood the log too."""
|
|
203
|
+
import time
|
|
204
|
+
|
|
205
|
+
now = time.monotonic()
|
|
206
|
+
if now - _THROTTLE_WARNED.get(source_app, -60.0) >= 60.0:
|
|
207
|
+
_THROTTLE_WARNED[source_app] = now
|
|
208
|
+
# The caller passes a resolved, allowlisted name, so this changes
|
|
209
|
+
# nothing today. It keeps a future caller that passes client text from
|
|
210
|
+
# forging log lines, and it is the guard code scanning can see.
|
|
211
|
+
safe = (source_app or "unknown").replace("\r", "").replace("\n", "")
|
|
212
|
+
logger.warning("Ingest rate limit reached for source %s; events are spooled for replay", safe)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@router.post("/ingest", dependencies=[Depends(require_ingest_key)])
|
|
195
216
|
async def audit_ingest(body: ExternalIngestBody):
|
|
196
217
|
"""Accept canonical adapter events from Cursor, Claude, MCP proxy, etc."""
|
|
197
218
|
if not settings.audit_ingest_enabled:
|
|
@@ -199,6 +220,26 @@ async def audit_ingest(body: ExternalIngestBody):
|
|
|
199
220
|
if not settings.audit_export_enabled:
|
|
200
221
|
raise HTTPException(status_code=503, detail="Audit export is disabled")
|
|
201
222
|
|
|
223
|
+
# Rate limit per source app and per session (#312). The hook spools a 429
|
|
224
|
+
# and the recorder's drain replays it with the original timestamps, so a
|
|
225
|
+
# burst past the cap is late, not lost.
|
|
226
|
+
from agentmetry.core.audit.external import _resolve_source_app
|
|
227
|
+
from agentmetry.core.audit.ingest_quota import get_limiter
|
|
228
|
+
|
|
229
|
+
# Keyed and logged by the name the trail will record, not the one the
|
|
230
|
+
# client sent: an unlisted name becomes `unknown`. Keyed on raw text, a
|
|
231
|
+
# flood inventing a name per event got a fresh bucket each time, and the
|
|
232
|
+
# warning wrote client text into the log.
|
|
233
|
+
source = _resolve_source_app(body.source_app)[0]
|
|
234
|
+
limiter = get_limiter()
|
|
235
|
+
if not limiter.allow(source, body.session_id or body.correlation_id):
|
|
236
|
+
_warn_throttled(source)
|
|
237
|
+
raise HTTPException(
|
|
238
|
+
status_code=429,
|
|
239
|
+
detail="Ingest rate limit reached; the hook spools this event and the recorder replays it",
|
|
240
|
+
headers={"Retry-After": str(limiter.retry_after())},
|
|
241
|
+
)
|
|
242
|
+
|
|
202
243
|
payload = body.model_dump(exclude_none=True)
|
|
203
244
|
try:
|
|
204
245
|
canonical = await ingest_external_event(payload)
|
|
@@ -495,11 +536,19 @@ async def audit_status():
|
|
|
495
536
|
Powers the freshness badge and `selftest` — makes silent hook failure visible
|
|
496
537
|
instead of the operator falsely believing they are being audited.
|
|
497
538
|
"""
|
|
539
|
+
from agentmetry.core.audit.ingest_quota import get_limiter
|
|
498
540
|
from agentmetry.core.audit.spool import spool_depth, spool_oldest_age_seconds
|
|
499
541
|
from agentmetry.core.audit.trail_db import get_trail_db
|
|
500
542
|
path = Path(settings.audit_export_path)
|
|
501
543
|
if not settings.audit_export_enabled:
|
|
502
|
-
return {
|
|
544
|
+
return {
|
|
545
|
+
"enabled": False,
|
|
546
|
+
"last_event_utc": None,
|
|
547
|
+
"recent": 0,
|
|
548
|
+
"by_source": {},
|
|
549
|
+
"path": str(path),
|
|
550
|
+
"demo": settings.demo_mode,
|
|
551
|
+
}
|
|
503
552
|
|
|
504
553
|
status_data = get_trail_db().status()
|
|
505
554
|
# Pending spool depth belongs in the same payload as freshness. A feed that
|
|
@@ -516,6 +565,12 @@ async def audit_status():
|
|
|
516
565
|
# Per SIEM: the last seq it accepted, and how long it has been failing.
|
|
517
566
|
"forwarding": _forward_status(),
|
|
518
567
|
"spool_oldest_age_seconds": spool_oldest_age_seconds(),
|
|
568
|
+
# Events refused by the ingest rate limit since the recorder started.
|
|
569
|
+
# They went to the hook spool, so a non-zero count is late work, and a
|
|
570
|
+
# growing one is an agent sending far faster than agents do (#312).
|
|
571
|
+
"ingest_throttled": get_limiter().snapshot(),
|
|
572
|
+
# A demo server's events are synthetic. The dashboard says so from here.
|
|
573
|
+
"demo": settings.demo_mode,
|
|
519
574
|
}
|
|
520
575
|
|
|
521
576
|
|
|
@@ -312,16 +312,41 @@ def cmd_status(args: argparse.Namespace) -> int:
|
|
|
312
312
|
return 0
|
|
313
313
|
|
|
314
314
|
|
|
315
|
-
def
|
|
316
|
-
"""
|
|
315
|
+
def _api_get_json(port: int, path: str, what: str, params: dict | None = None) -> dict | None:
|
|
316
|
+
"""GET an API route with this install's token, or print why not and return None.
|
|
317
|
+
|
|
318
|
+
Every route but health has needed the per-install token since 0.9.3, and
|
|
319
|
+
`stats` and `detections` were still calling without it. The 401 body was
|
|
320
|
+
read as if it were the answer, so both printed an empty result over a trail
|
|
321
|
+
with events and a critical detection in it. A refusal is now an error.
|
|
322
|
+
"""
|
|
317
323
|
try:
|
|
318
|
-
|
|
319
|
-
f"{
|
|
320
|
-
params=
|
|
324
|
+
resp = httpx.get(
|
|
325
|
+
f"{_api_base_url(port)}{path}",
|
|
326
|
+
params=params,
|
|
327
|
+
headers=_api_headers(),
|
|
321
328
|
timeout=10.0,
|
|
322
|
-
)
|
|
329
|
+
)
|
|
323
330
|
except Exception:
|
|
324
|
-
print("Not running - start Agentmetry first (
|
|
331
|
+
print(f"Not running - start Agentmetry first ({what} reads via the API).")
|
|
332
|
+
return None
|
|
333
|
+
if resp.status_code in (401, 403):
|
|
334
|
+
print(
|
|
335
|
+
f"The recorder refused the request (HTTP {resp.status_code}): this shell has no "
|
|
336
|
+
"valid API token for it. Run it as the account that started the recorder, or "
|
|
337
|
+
"set AGENTMETRY_API_KEY."
|
|
338
|
+
)
|
|
339
|
+
return None
|
|
340
|
+
if resp.status_code >= 400:
|
|
341
|
+
print(f"FAILED: HTTP {resp.status_code} from {path}")
|
|
342
|
+
return None
|
|
343
|
+
return resp.json()
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def cmd_stats(args: argparse.Namespace) -> int:
|
|
347
|
+
"""Audit trail metrics over a window — dogfood weekly counts."""
|
|
348
|
+
data = _api_get_json(args.port, "/api/v1/audit/stats", "stats", {"days": args.days})
|
|
349
|
+
if data is None:
|
|
325
350
|
return 1
|
|
326
351
|
|
|
327
352
|
if not data.get("enabled", True):
|
|
@@ -349,13 +374,10 @@ def cmd_stats(args: argparse.Namespace) -> int:
|
|
|
349
374
|
|
|
350
375
|
|
|
351
376
|
def cmd_detections(args: argparse.Namespace) -> int:
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
).json()
|
|
357
|
-
except Exception:
|
|
358
|
-
print("Not running - start Agentmetry first (detections reads via the API).")
|
|
377
|
+
data = _api_get_json(
|
|
378
|
+
args.port, f"/api/v1/audit/detections/{args.correlation_id}", "detections"
|
|
379
|
+
)
|
|
380
|
+
if data is None:
|
|
359
381
|
return 1
|
|
360
382
|
if not data.get("enabled", True):
|
|
361
383
|
print("Audit export is disabled, so no sessions are recorded.")
|
|
@@ -595,6 +617,39 @@ _HOOK_INSTALLERS = ("cursor", "claude", "codex", "qwen", "kimi", "qoder", "codeb
|
|
|
595
617
|
|
|
596
618
|
|
|
597
619
|
|
|
620
|
+
def _cloud_cursor_cache(args: argparse.Namespace) -> int:
|
|
621
|
+
"""Record Cursor cloud-agent tool calls from the desktop app's local cache.
|
|
622
|
+
|
|
623
|
+
No vendor key and no network call to Cursor: the calls are read from the
|
|
624
|
+
state database the Cursor app keeps on this machine, read-only. See
|
|
625
|
+
`core/audit/cursor_cloud_cache.py` for what that cache does and does not hold.
|
|
626
|
+
"""
|
|
627
|
+
from agentmetry.core.audit.cursor_cloud_cache import STATE_FILE, default_db_path, import_cache
|
|
628
|
+
from agentmetry.core.paths import data_dir
|
|
629
|
+
from agentmetry.hooks.ingest import post_ingest
|
|
630
|
+
|
|
631
|
+
db = Path(args.db).expanduser() if args.db else default_db_path()
|
|
632
|
+
if db is None or not db.is_file():
|
|
633
|
+
print(f"No Cursor state database at {db}. Is the Cursor desktop app installed here? (--db to point at one)")
|
|
634
|
+
return 2
|
|
635
|
+
|
|
636
|
+
def post(payload: dict) -> bool:
|
|
637
|
+
# No spool: a call that did not land is simply not marked as recorded,
|
|
638
|
+
# and the next run picks it up from the cache again.
|
|
639
|
+
return post_ingest(payload, quiet=True, spool=False)
|
|
640
|
+
|
|
641
|
+
result = import_cache(db, post=post, state_path=data_dir() / STATE_FILE, dry_run=args.dry_run)
|
|
642
|
+
verb = "would record" if args.dry_run else "recorded"
|
|
643
|
+
count = result.new if args.dry_run else result.posted
|
|
644
|
+
print(f"cursor-cache: {result.agents} cloud agent conversation(s), {result.calls} finished tool call(s).")
|
|
645
|
+
print(f" {verb} {count}, already recorded {result.already_seen}, failed {result.failed}.")
|
|
646
|
+
if result.skipped:
|
|
647
|
+
print(f" skipped: {dict(result.skipped)}")
|
|
648
|
+
if result.tools:
|
|
649
|
+
print(" by tool: " + ", ".join(f"{name} {n}" for name, n in result.tools.most_common()))
|
|
650
|
+
return 1 if result.failed else 0
|
|
651
|
+
|
|
652
|
+
|
|
598
653
|
def cmd_cloud(args: argparse.Namespace) -> int:
|
|
599
654
|
"""Poll a vendor cloud-agent API once and record what it exposes.
|
|
600
655
|
|
|
@@ -605,6 +660,9 @@ def cmd_cloud(args: argparse.Namespace) -> int:
|
|
|
605
660
|
"""
|
|
606
661
|
import os
|
|
607
662
|
|
|
663
|
+
if args.provider == "cursor-cache":
|
|
664
|
+
return _cloud_cursor_cache(args)
|
|
665
|
+
|
|
608
666
|
import httpx
|
|
609
667
|
|
|
610
668
|
from agentmetry.core.audit.cloud_bridge import (
|
|
@@ -628,7 +686,14 @@ def cmd_cloud(args: argparse.Namespace) -> int:
|
|
|
628
686
|
headers = {"Content-Type": "application/json"}
|
|
629
687
|
if key:
|
|
630
688
|
headers["Authorization"] = f"Bearer {key}"
|
|
631
|
-
|
|
689
|
+
# A 429 is the ingest rate limit (#312). Unlike a hook, this importer
|
|
690
|
+
# has no spool, so it waits as told and tries again rather than drop
|
|
691
|
+
# the event.
|
|
692
|
+
for _ in range(6):
|
|
693
|
+
resp = httpx.post(f"{url}/api/v1/audit/ingest", json=payload, headers=headers, timeout=15)
|
|
694
|
+
if resp.status_code != 429:
|
|
695
|
+
break
|
|
696
|
+
time.sleep(min(30.0, float(resp.headers.get("Retry-After", "1") or 1)))
|
|
632
697
|
ok = resp.status_code < 300
|
|
633
698
|
if ok:
|
|
634
699
|
posted += 1
|
|
@@ -1139,6 +1204,32 @@ def cmd_mcp(args: argparse.Namespace) -> int:
|
|
|
1139
1204
|
return 0
|
|
1140
1205
|
|
|
1141
1206
|
|
|
1207
|
+
def cmd_scan(args: argparse.Namespace) -> int:
|
|
1208
|
+
"""Check what an agent is about to depend on: MCP servers, skills, packages.
|
|
1209
|
+
|
|
1210
|
+
Exits 1 when anything is found and 2 when nothing could be read. Unlike
|
|
1211
|
+
`mcp` and `posture`, which a health check runs in the background, this is
|
|
1212
|
+
run on purpose before something is installed, so a finding should stop the
|
|
1213
|
+
script that ran it. See core/scanner/scan.py.
|
|
1214
|
+
"""
|
|
1215
|
+
import json as _json
|
|
1216
|
+
from pathlib import Path as _Path
|
|
1217
|
+
|
|
1218
|
+
from agentmetry.core.scanner.scan import run
|
|
1219
|
+
|
|
1220
|
+
report = run(
|
|
1221
|
+
args.target,
|
|
1222
|
+
project=_Path(args.project).resolve() if args.project else _Path.cwd(),
|
|
1223
|
+
known_file=args.known,
|
|
1224
|
+
)
|
|
1225
|
+
if args.json:
|
|
1226
|
+
print(_json.dumps(report.to_json(), indent=2))
|
|
1227
|
+
else:
|
|
1228
|
+
for line in report.lines():
|
|
1229
|
+
print(line)
|
|
1230
|
+
return report.exit_code()
|
|
1231
|
+
|
|
1232
|
+
|
|
1142
1233
|
def cmd_posture(args: argparse.Namespace) -> int:
|
|
1143
1234
|
"""Report agent settings that take a human or a hook out of the loop.
|
|
1144
1235
|
|
|
@@ -1591,10 +1682,13 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1591
1682
|
stats = sub.add_parser("stats", help="audit trail metrics for dogfood (events, detections)")
|
|
1592
1683
|
stats.add_argument("--days", type=int, default=7)
|
|
1593
1684
|
cloud = sub.add_parser("cloud", help="record what a vendor cloud-agent API exposes (Cursor, Claude)")
|
|
1594
|
-
cloud.add_argument("provider", choices=("cursor", "claude"))
|
|
1685
|
+
cloud.add_argument("provider", choices=("cursor", "claude", "cursor-cache"))
|
|
1686
|
+
cloud.add_argument("--db", default="", help="cursor-cache: Cursor state database (default: the desktop app's)")
|
|
1687
|
+
cloud.add_argument("--dry-run", action="store_true", help="cursor-cache: count what would be recorded, post nothing")
|
|
1595
1688
|
cloud.add_argument("--url", default="", help="vendor API base override")
|
|
1596
1689
|
cloud.add_argument("--limit", type=int, default=10, help="max sessions/agents per poll")
|
|
1597
1690
|
cloud.add_argument("--skip-runs", action="store_true", help="cursor: agents only, skip per-run fetch")
|
|
1691
|
+
cloud.add_argument("--remote", action="store_true", help="claude: the Compliance API's remote sessions instead of local ones (not Claude Code on the web, which that API excludes)")
|
|
1598
1692
|
detections = sub.add_parser("detections", help="list detections for one session")
|
|
1599
1693
|
detections.add_argument("correlation_id", help="correlation_id / session id")
|
|
1600
1694
|
disposition = sub.add_parser("disposition", help="close an audit detection from the shell")
|
|
@@ -1698,6 +1792,18 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1698
1792
|
posture.add_argument(
|
|
1699
1793
|
"--strict", action="store_true", help="exit 1 when any high-severity finding is present",
|
|
1700
1794
|
)
|
|
1795
|
+
scan = sub.add_parser(
|
|
1796
|
+
"scan",
|
|
1797
|
+
help="check MCP servers, skills or a package name before an agent depends on them",
|
|
1798
|
+
)
|
|
1799
|
+
scan.add_argument(
|
|
1800
|
+
"target", nargs="?", default=None,
|
|
1801
|
+
help="a file or folder (skills, rules, a saved tools/list), npm:NAME or pypi:NAME; "
|
|
1802
|
+
"omit to scan the MCP servers configured on this machine",
|
|
1803
|
+
)
|
|
1804
|
+
scan.add_argument("--project", default=None, help="project root whose MCP config to include (default: cwd)")
|
|
1805
|
+
scan.add_argument("--known", default=None, help="file of extra known package names, one per line (npm:/pypi: or bare)")
|
|
1806
|
+
scan.add_argument("--json", action="store_true", help="print the report as JSON")
|
|
1701
1807
|
otel = sub.add_parser(
|
|
1702
1808
|
"otel", help="receive Claude Code's native OpenTelemetry stream (no hooks needed)"
|
|
1703
1809
|
)
|
|
@@ -1810,6 +1916,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1810
1916
|
"anchor": cmd_anchor,
|
|
1811
1917
|
"mcp": cmd_mcp,
|
|
1812
1918
|
"posture": cmd_posture,
|
|
1919
|
+
"scan": cmd_scan,
|
|
1813
1920
|
"dashboard": cmd_dashboard,
|
|
1814
1921
|
"otel": cmd_otel,
|
|
1815
1922
|
"import-agt": cmd_import_agt,
|