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