agentmetry 0.9.2__tar.gz → 0.9.4__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.2 → agentmetry-0.9.4}/.env.example +22 -0
- agentmetry-0.9.4/Dockerfile +42 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/PKG-INFO +1 -1
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/api/main.py +66 -14
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/api/routes/audit.py +29 -3
- agentmetry-0.9.4/agentmetry/api/routes/auth.py +84 -0
- agentmetry-0.9.4/agentmetry/api/trusted_host.py +72 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/cli/__init__.py +166 -8
- agentmetry-0.9.4/agentmetry/core/api_token.py +164 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/adapters/agt.py +4 -2
- agentmetry-0.9.4/agentmetry/core/audit/adapters/sentinel.py +79 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/canonical.py +4 -3
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/dogfood.py +3 -1
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/external.py +10 -5
- agentmetry-0.9.4/agentmetry/core/audit/forwarder.py +379 -0
- agentmetry-0.9.4/agentmetry/core/audit/hashing.py +47 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/heartbeat.py +3 -1
- agentmetry-0.9.4/agentmetry/core/audit/identity.py +84 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/ingest.py +3 -24
- agentmetry-0.9.4/agentmetry/core/audit/managed_hooks.py +212 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/migrate.py +21 -19
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/policy_settings.py +8 -2
- agentmetry-0.9.4/agentmetry/core/audit/replay.py +95 -0
- agentmetry-0.9.4/agentmetry/core/audit/signing.py +63 -0
- agentmetry-0.9.4/agentmetry/core/audit/sinks.py +675 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/spool.py +3 -1
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/trail_chain.py +98 -76
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/trail_merkle.py +23 -21
- agentmetry-0.9.4/agentmetry/core/audit/trail_rotation.py +157 -0
- agentmetry-0.9.4/agentmetry/core/auth.py +117 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/bus/audit_exporter.py +4 -23
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/bus/outbox.py +2 -1
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/config.py +89 -5
- agentmetry-0.9.4/agentmetry/core/dashboard_session.py +84 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/diagnostics/doctor.py +100 -27
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/diagnostics/mcp_schema.py +2 -2
- agentmetry-0.9.4/agentmetry/core/paths.py +97 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/version.py +1 -1
- agentmetry-0.9.4/agentmetry/demo.py +472 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/hooks/ingest.py +77 -11
- agentmetry-0.9.2/tools/mcp_audit_proxy.py → agentmetry-0.9.4/agentmetry/hooks/mcp_proxy.py +42 -21
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/conftest.py +22 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_audit_sinks.py +1 -1
- agentmetry-0.9.4/tests/test_auth_default.py +368 -0
- agentmetry-0.9.4/tests/test_container_and_legacy_runtime.py +66 -0
- agentmetry-0.9.4/tests/test_demo.py +76 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_doctor.py +51 -8
- agentmetry-0.9.4/tests/test_event_signing.py +66 -0
- agentmetry-0.9.4/tests/test_forwarder.py +356 -0
- agentmetry-0.9.4/tests/test_hash_key.py +128 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_hook_import_cost.py +86 -1
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_hook_spool.py +25 -2
- agentmetry-0.9.4/tests/test_identity_provider.py +68 -0
- agentmetry-0.9.4/tests/test_managed_hooks.py +168 -0
- agentmetry-0.9.4/tests/test_mcp_proxy_packaged.py +133 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_operator_attribution.py +4 -4
- agentmetry-0.9.4/tests/test_paths.py +98 -0
- agentmetry-0.9.4/tests/test_sentinel.py +158 -0
- agentmetry-0.9.4/tests/test_serve_script_binds_loopback.py +23 -0
- agentmetry-0.9.4/tests/test_shared_install.py +115 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_suite_leaves_real_home_alone.py +22 -4
- agentmetry-0.9.4/tests/test_trail_chain.py +344 -0
- agentmetry-0.9.4/tests/test_trusted_host.py +62 -0
- agentmetry-0.9.4/tools/mcp_audit_proxy.py +28 -0
- agentmetry-0.9.2/Dockerfile +0 -18
- agentmetry-0.9.2/agentmetry/core/audit/hashing.py +0 -21
- agentmetry-0.9.2/agentmetry/core/audit/identity.py +0 -39
- agentmetry-0.9.2/agentmetry/core/audit/replay.py +0 -54
- agentmetry-0.9.2/agentmetry/core/audit/sinks.py +0 -369
- agentmetry-0.9.2/agentmetry/core/auth.py +0 -54
- agentmetry-0.9.2/tests/test_trail_chain.py +0 -145
- {agentmetry-0.9.2 → agentmetry-0.9.4}/.dockerignore +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/.env.agentmetry-demo +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/.gitignore +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/README.md +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/__init__.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/api/__init__.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/api/routes/__init__.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/api/websocket.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/api/ws_bridge.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/cli/__main__.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/__init__.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/__init__.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/adapters/__init__.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/adapters/chronicle.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/adapters/cloudevents.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/adapters/ecs.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/adapters/splunk.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/alerts.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/atlas.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/compliance_digest.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/__init__.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/benchmark.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_approval_denied_then_executed.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_arbitrary_host_stage_execute.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_autonomous_unapproved_write.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_credential_exfil.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_credential_then_cloud_api.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_destructive_delete_burst.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_discovery_then_collect.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_dotfile_then_git_push.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_encoded_command_download.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_env_credential_exfil.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_hashed_only_no_command.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_interpreter_egress.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_pr_merged_without_review.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_proc_substitution_cradle.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_remote_pipe_to_shell.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_remote_staging_then_execute.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_secret_manager_then_egress.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_session_tool_burst.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_single_command_exfil.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_single_quoted_interpreter_egress.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_ssh_directory_exfil.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_subagent_swarm.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_timestamp_collision.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/attack_untrusted_input_then_action.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_authoring_merge_fixtures.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_autonomous_after_approval.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_build_artifact_cleanup.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_ci_artifact_download.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_commit_message_names_cloud_clis.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_database_migration.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_dependency_install_and_build.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_download_release_archive.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_env_example_read.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_fetch_data_then_run_repo_script.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_fetch_dataset_then_analyse.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_fetch_lockfile_then_install.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_fetch_piped_into_own_script.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_git_review_and_push.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_github_raw_docs_then_script.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_human_driven_deletes.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_local_api_probing.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_long_but_calm_session.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_loopback_is_not_egress.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_loopback_pipe_to_interpreter.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_mcp_text_mentions_credential_path.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_ordinary_development.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_package_manager_after_fetch.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_reading_config_that_is_not_secret.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_remote_api_call_no_credentials.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_research_then_docs.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_reversed_order_is_not_exfil.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_secret_names_and_docs.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_test_and_fix_loop.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/benign_writing_about_credentials.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/corpus/corpus.yaml +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/disposition.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/engine.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/live.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/live_store.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/models.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/rules.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/traits.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/yaml_config.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/detection/yaml_rules.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/dlp/__init__.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/dlp/loader.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/dlp/models.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/dlp/scanner.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/evidence_pack.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/hook_bootstrap.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/mcp_inventory_event.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/mitre.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/otel_ingest.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/policy.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/redaction.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/run_context.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/tool_policy/__init__.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/tool_policy/capability.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/tool_policy/evaluator.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/tool_policy/loader.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/tool_policy/models.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/trail_anchor.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/audit/trail_db.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/bus/__init__.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/bus/bridges.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/bus/bus.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/bus/events.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/diagnostics/__init__.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/diagnostics/autostart.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/diagnostics/driver_paths.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/diagnostics/env_file.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/diagnostics/hook_coverage.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/diagnostics/mcp_inventory.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/drivers/__init__.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/drivers/host.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/drivers/permissions.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/drivers/spec.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/extensions.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/health.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/core/operator_identity.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/hooks/__init__.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/policies/detection/manifest.yaml +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/policies/dlp/manifest.yaml +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/policies/opa/agent_rules.rego +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/agentmetry/policies/tool/manifest.yaml +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/pyproject.toml +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/fixtures/agt_filesink_exfil.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/fixtures/agt_filesink_mixed.jsonl +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/fixtures/fake_mcp_server.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/fixtures/otel_claude_code_session.json +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_agentmetry_audit.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_agentmetry_ingest_client.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_agt_adapter.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_ask_gate_phase_a.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_atlas_detection_enrichment.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_atlas_mapping.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_audit_stats.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_audit_tail.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_auth.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_autostart.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_boot_sequence.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_burst_windows.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_chinese_agent_hooks.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_chinese_agent_sprint_b.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_chinese_agent_sprint_c.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_chronicle_adapter.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_cli_backup.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_cli_commands.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_cli_env_var_names.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_cloudevents_adapter.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_compliance_digest.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_crewai_adapter.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_detection_adi.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_detection_benchmark.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_detection_default_config.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_detection_disposition.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_detection_disposition_api.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_detection_duplicate_emission.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_detection_emit_durability.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_detection_engine.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_detection_evasion.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_detection_hf_incident.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_detection_quoted_command_words.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_detection_rule_identity.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_detection_rules_v2.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_dlp_agent_env_override.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_dlp_invisible_unicode.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_dlp_markdown_exfil.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_dlp_scanner.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_dogfood.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_dogfood_freeze.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_driver_paths.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_ecs_threat.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_event_bus.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_evidence_pack.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_extensions.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_external_ingest.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_forwarding_shape.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_funding_manifest.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_heartbeat.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_hook_bootstrap.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_hook_coverage.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_hook_decision_format.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_hook_enforcement_timing.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_hook_real_process.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_identity_enrich.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_ingest_roundtrip.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_isolation.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_launch_targets.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_live_detection.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_live_detection_store.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_mcp_audit_proxy.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_mcp_concealed_text.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_mcp_inventory.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_mcp_inventory_event.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_mcp_meta_fingerprint.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_mcp_schema.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_mitre_dlp.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_opensre_adapter.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_otel_ingest.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_powershell_encoding.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_readme_claims.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_redaction.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_replay.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_sigma_pack.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_static_serving.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_subagent_lifecycle.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_tool_policy.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_tool_policy_agent_cli.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_tool_policy_git_hooks.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_trail_anchor.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_trail_concurrency.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_trail_merkle.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_unattended_agent_policy.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_version.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tests/test_yaml_detection_rules.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tools/generate_sigma_pack.py +0 -0
- {agentmetry-0.9.2 → agentmetry-0.9.4}/tools/vault_fs_server.py +0 -0
|
@@ -10,6 +10,22 @@ AGENTMETRY_VAULT_PATH=../../vault
|
|
|
10
10
|
# override: an email, or a pseudonym to keep account names out of a SIEM.
|
|
11
11
|
AGENTMETRY_OPERATOR_ID=
|
|
12
12
|
AGENTMETRY_FLEET_ID=
|
|
13
|
+
|
|
14
|
+
# Per-fleet secret for tool-argument fingerprints. Unset: plain SHA-256, which
|
|
15
|
+
# anyone with the trail or SIEM can recompute for a guessable argument. Set:
|
|
16
|
+
# HMAC-SHA256 under this key, labelled input_redaction "hmac". Use the same
|
|
17
|
+
# value on every host in a fleet so fingerprints still correlate.
|
|
18
|
+
AGENTMETRY_HASH_KEY=
|
|
19
|
+
|
|
20
|
+
# Host names the API answers to besides localhost / 127.0.0.1 / ::1. Anything
|
|
21
|
+
# else gets 400 (DNS-rebinding guard). Set this when the API is reached by
|
|
22
|
+
# another name: a container service name, a LAN address, a reverse proxy.
|
|
23
|
+
# AGENTMETRY_TRUSTED_HOSTS=orchestrator,devbox.corp.example
|
|
24
|
+
|
|
25
|
+
# The API needs a token by default: the first start writes one, owner-only,
|
|
26
|
+
# to the data directory, and the hooks and CLI read it from there. Set this
|
|
27
|
+
# only to distribute your own key across a fleet; it then replaces the file.
|
|
28
|
+
# AGENTMETRY_AUTH_DISABLED=1 turns authentication off (development only).
|
|
13
29
|
AGENTMETRY_API_KEY=
|
|
14
30
|
|
|
15
31
|
# Hook boundary — log | block | disable
|
|
@@ -19,6 +35,12 @@ AGENTMETRY_DLP_MODE=log
|
|
|
19
35
|
# Audit export (JSONL + SQLite index)
|
|
20
36
|
AGENTMETRY_AUDIT_EXPORT_ENABLED=1
|
|
21
37
|
AGENTMETRY_AUDIT_SINK=file
|
|
38
|
+
# Network sinks forward from the trail with a cursor, batching and retrying
|
|
39
|
+
# while the SIEM is down. 0 = the old inline one-request-per-event sinks.
|
|
40
|
+
# AGENTMETRY_AUDIT_FORWARDER=1
|
|
41
|
+
# Rotate the trail into <trail>.archive/ at this size; the chain continues
|
|
42
|
+
# across segments and nothing is deleted (docs/trail-retention.md). 0 = never.
|
|
43
|
+
# AGENTMETRY_TRAIL_ROTATE_BYTES=268435456
|
|
22
44
|
|
|
23
45
|
# Anchor log, when it is not the file sitting beside the trail. Point this at a
|
|
24
46
|
# working copy of a remote this machine cannot rewrite and `verify --trail` and
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Agentmetry recorder in one image: the orchestrator API, serving the dashboard's
|
|
2
|
+
# static export. Build from the repository root:
|
|
3
|
+
#
|
|
4
|
+
# docker build -f apps/orchestrator/Dockerfile -t agentmetry .
|
|
5
|
+
#
|
|
6
|
+
# It used to start `uvicorn api.main:app`, a module path that has not existed
|
|
7
|
+
# since the package became `agentmetry`, so the image could not boot. It also
|
|
8
|
+
# installed gcc and libpq for a Postgres the recorder no longer uses.
|
|
9
|
+
|
|
10
|
+
FROM node:20-alpine AS dashboard
|
|
11
|
+
WORKDIR /dashboard
|
|
12
|
+
COPY apps/dashboard/package.json apps/dashboard/package-lock.json ./
|
|
13
|
+
RUN npm ci
|
|
14
|
+
COPY apps/dashboard/ ./
|
|
15
|
+
# Same-origin build: the dashboard talks to the API that serves it. No API key
|
|
16
|
+
# is baked into the bundle.
|
|
17
|
+
ENV NEXT_PUBLIC_SAME_ORIGIN=true
|
|
18
|
+
RUN npm run build
|
|
19
|
+
|
|
20
|
+
FROM python:3.12-slim
|
|
21
|
+
WORKDIR /app
|
|
22
|
+
|
|
23
|
+
COPY apps/orchestrator/ ./
|
|
24
|
+
RUN pip install --no-cache-dir . \
|
|
25
|
+
&& useradd --system --uid 10001 --home-dir /data agentmetry \
|
|
26
|
+
&& mkdir -p /data && chown agentmetry /data
|
|
27
|
+
|
|
28
|
+
# agentmetry/api/main.py serves <three levels up>/dashboard/out.
|
|
29
|
+
COPY --from=dashboard /dashboard/out /dashboard/out
|
|
30
|
+
|
|
31
|
+
ENV AGENTMETRY_AUDIT_EXPORT_PATH=/data/audit-forward.jsonl \
|
|
32
|
+
AGENTMETRY_AUDIT_DB_PATH=/data/audit.db \
|
|
33
|
+
AGENTMETRY_DETECTION_LIVE_DB_PATH=/data/detection_live.db \
|
|
34
|
+
AGENTMETRY_DETECTION_DISPOSITION_DB_PATH=/data/detection_dispositions.db
|
|
35
|
+
|
|
36
|
+
USER agentmetry
|
|
37
|
+
VOLUME ["/data"]
|
|
38
|
+
EXPOSE 8000
|
|
39
|
+
|
|
40
|
+
# 0.0.0.0 inside the container is required for port publishing to work; the
|
|
41
|
+
# compose file publishes it on the host's loopback only.
|
|
42
|
+
CMD ["uvicorn", "agentmetry.api.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agentmetry
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.4
|
|
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
|
|
@@ -9,6 +9,9 @@ from pathlib import Path
|
|
|
9
9
|
from fastapi import FastAPI, Query, WebSocket, WebSocketDisconnect
|
|
10
10
|
from fastapi.middleware.cors import CORSMiddleware
|
|
11
11
|
|
|
12
|
+
from agentmetry.api.routes.auth import router as auth_router
|
|
13
|
+
from agentmetry.api.trusted_host import TrustedHostMiddleware
|
|
14
|
+
|
|
12
15
|
from agentmetry.api.routes.audit import router as audit_router
|
|
13
16
|
from agentmetry.api.websocket import ws_manager
|
|
14
17
|
from agentmetry.api.ws_bridge import ws_event_bridge
|
|
@@ -18,13 +21,14 @@ from agentmetry.core.bus.bridges import outbox_persister
|
|
|
18
21
|
from agentmetry.core.bus.bus import bus
|
|
19
22
|
from agentmetry.core.bus.outbox import get_outbox
|
|
20
23
|
from agentmetry.core.config import settings
|
|
24
|
+
from agentmetry.core.paths import data_dir
|
|
21
25
|
from agentmetry.core.extensions import load_extensions
|
|
22
26
|
from agentmetry.core.health import get_system_health
|
|
23
27
|
from agentmetry.core.version import __version__
|
|
24
28
|
|
|
25
29
|
logger = logging.getLogger(__name__)
|
|
26
30
|
|
|
27
|
-
_LOG_DIR =
|
|
31
|
+
_LOG_DIR = data_dir() / "logs"
|
|
28
32
|
|
|
29
33
|
|
|
30
34
|
def _setup_logging() -> None:
|
|
@@ -57,6 +61,21 @@ _setup_logging()
|
|
|
57
61
|
|
|
58
62
|
|
|
59
63
|
|
|
64
|
+
def _start_forwarders() -> list[asyncio.Task]:
|
|
65
|
+
"""One task per network sink, tailing the trail (pilot hardening item 19)."""
|
|
66
|
+
if not (settings.audit_export_enabled and settings.audit_forwarder):
|
|
67
|
+
return []
|
|
68
|
+
from agentmetry.core.audit.forwarder import Forwarder
|
|
69
|
+
from agentmetry.core.audit.sinks import forward_destinations
|
|
70
|
+
|
|
71
|
+
tasks = []
|
|
72
|
+
for destination in forward_destinations(settings):
|
|
73
|
+
forwarder = Forwarder(destination, settings.audit_export_path)
|
|
74
|
+
tasks.append(asyncio.create_task(forwarder.run(), name=f"forward-{destination.name}"))
|
|
75
|
+
logger.info("Forwarding the trail to %s from its cursor", destination.name)
|
|
76
|
+
return tasks
|
|
77
|
+
|
|
78
|
+
|
|
60
79
|
@asynccontextmanager
|
|
61
80
|
async def lifespan(app: FastAPI):
|
|
62
81
|
from agentmetry.core.audit.migrate import backfill_db_from_jsonl
|
|
@@ -114,22 +133,48 @@ async def lifespan(app: FastAPI):
|
|
|
114
133
|
# if presence was being asserted.
|
|
115
134
|
asyncio.create_task(heartbeat_forever(), name="heartbeat"),
|
|
116
135
|
]
|
|
136
|
+
bridge_tasks += _start_forwarders()
|
|
117
137
|
|
|
118
|
-
#
|
|
119
|
-
|
|
138
|
+
# The removed agent runtime's MCP driver host. Off unless
|
|
139
|
+
# AGENTMETRY_LEGACY_DRIVERS=1 (#209): it spawned tools/vault_fs_server.py on
|
|
140
|
+
# every boot of a recorder that has no use for it.
|
|
141
|
+
mount_task = None
|
|
142
|
+
if settings.legacy_drivers:
|
|
143
|
+
from agentmetry.core.drivers.host import get_mcp_host
|
|
120
144
|
|
|
121
|
-
|
|
145
|
+
# Mount in the background: a slow npx download must not delay boot.
|
|
146
|
+
mount_task = asyncio.create_task(get_mcp_host().mount_all(), name="driver-mounts")
|
|
122
147
|
|
|
123
|
-
|
|
148
|
+
# Create the per-install API token now, before the first hook asks for it.
|
|
149
|
+
from agentmetry.core.auth import effective_key
|
|
124
150
|
|
|
125
151
|
try:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
152
|
+
if effective_key():
|
|
153
|
+
logger.info("API authentication on; local clients read the token from the data directory")
|
|
154
|
+
else:
|
|
155
|
+
logger.warning("API authentication is OFF (AGENTMETRY_AUTH_DISABLED); development only")
|
|
156
|
+
except OSError as exc:
|
|
157
|
+
logger.error("Could not create the API token (%s); every request will be refused", exc)
|
|
158
|
+
|
|
159
|
+
# Opt-in (AGENTMETRY_AUTO_INSTALL_HOOKS=1). Unconditional, this rewrote the
|
|
160
|
+
# developer's global IDE hook configs to point at whichever checkout booted,
|
|
161
|
+
# which is how a test clone or demo instance took over live hooks.
|
|
162
|
+
if settings.auto_install_hooks:
|
|
163
|
+
from agentmetry.core.audit.hook_bootstrap import bootstrap_tier_b_hooks
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
hook_paths = bootstrap_tier_b_hooks()
|
|
167
|
+
if hook_paths.get("cursor"):
|
|
168
|
+
logger.info("Global Cursor hooks ready: %s", hook_paths["cursor"])
|
|
169
|
+
if hook_paths.get("claude"):
|
|
170
|
+
logger.info("Global Claude hooks ready: %s", hook_paths["claude"])
|
|
171
|
+
except Exception as exc:
|
|
172
|
+
logger.warning("Tier B hook bootstrap failed: %s", exc)
|
|
173
|
+
else:
|
|
174
|
+
logger.info(
|
|
175
|
+
"IDE hooks are not rewritten at boot. Install or repair them with "
|
|
176
|
+
"`agentmetry hooks install`, or set AGENTMETRY_AUTO_INSTALL_HOOKS=1."
|
|
177
|
+
)
|
|
133
178
|
|
|
134
179
|
# Launch transcript watcher for Antigravity in the background
|
|
135
180
|
import subprocess
|
|
@@ -148,8 +193,11 @@ async def lifespan(app: FastAPI):
|
|
|
148
193
|
logger.warning("Failed to start Antigravity transcript watcher: %s", exc)
|
|
149
194
|
|
|
150
195
|
yield
|
|
151
|
-
mount_task
|
|
152
|
-
|
|
196
|
+
if mount_task is not None:
|
|
197
|
+
from agentmetry.core.drivers.host import get_mcp_host
|
|
198
|
+
|
|
199
|
+
mount_task.cancel()
|
|
200
|
+
await get_mcp_host().unmount_all()
|
|
153
201
|
for task in bridge_tasks:
|
|
154
202
|
task.cancel()
|
|
155
203
|
if watcher_process:
|
|
@@ -177,8 +225,12 @@ app.add_middleware(
|
|
|
177
225
|
allow_methods=["*"],
|
|
178
226
|
allow_headers=["*"],
|
|
179
227
|
)
|
|
228
|
+
# Outermost, so a request with a foreign Host header is refused before CORS or
|
|
229
|
+
# any route sees it (DNS rebinding; pilot hardening item 9).
|
|
230
|
+
app.add_middleware(TrustedHostMiddleware)
|
|
180
231
|
|
|
181
232
|
app.include_router(audit_router, prefix="/api/v1")
|
|
233
|
+
app.include_router(auth_router, prefix="/api/v1")
|
|
182
234
|
|
|
183
235
|
load_extensions(app, settings=settings)
|
|
184
236
|
|
|
@@ -6,7 +6,7 @@ from datetime import datetime, timedelta, timezone
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Any, Literal
|
|
8
8
|
|
|
9
|
-
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
9
|
+
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
|
10
10
|
from fastapi.responses import FileResponse
|
|
11
11
|
from pydantic import BaseModel, Field, field_validator
|
|
12
12
|
|
|
@@ -350,8 +350,32 @@ class DispositionBody(BaseModel):
|
|
|
350
350
|
severity: str = ""
|
|
351
351
|
|
|
352
352
|
|
|
353
|
+
def _forward_status() -> dict:
|
|
354
|
+
from agentmetry.core.audit.forwarder import forward_status
|
|
355
|
+
|
|
356
|
+
return forward_status()
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _decider(request: Request, claimed: str) -> str:
|
|
360
|
+
"""Who made this decision, as far as the server can tell.
|
|
361
|
+
|
|
362
|
+
A dashboard session and an extension principal are server-side facts, so
|
|
363
|
+
they win over anything the body claims. A token-authenticated caller (the
|
|
364
|
+
CLI, on this machine) states who it is; with nothing stated, the resolved
|
|
365
|
+
operator is recorded rather than an empty string.
|
|
366
|
+
"""
|
|
367
|
+
kind, who = getattr(request.state, "auth", ("", ""))
|
|
368
|
+
if kind in ("session", "extension") and who:
|
|
369
|
+
return who
|
|
370
|
+
if claimed.strip():
|
|
371
|
+
return claimed.strip()
|
|
372
|
+
from agentmetry.core.audit.run_context import resolve_operator
|
|
373
|
+
|
|
374
|
+
return resolve_operator()[0]
|
|
375
|
+
|
|
376
|
+
|
|
353
377
|
@router.post("/detections/disposition", dependencies=[Depends(require_api_key)])
|
|
354
|
-
async def audit_set_disposition(body: DispositionBody):
|
|
378
|
+
async def audit_set_disposition(body: DispositionBody, request: Request):
|
|
355
379
|
"""Record what a human decided about a detection.
|
|
356
380
|
|
|
357
381
|
The decision is appended to the trail as a `detection_disposition` event
|
|
@@ -369,7 +393,7 @@ async def audit_set_disposition(body: DispositionBody):
|
|
|
369
393
|
status=body.status,
|
|
370
394
|
assignee=body.assignee,
|
|
371
395
|
note=body.note,
|
|
372
|
-
decided_by=body.decided_by,
|
|
396
|
+
decided_by=_decider(request, body.decided_by),
|
|
373
397
|
severity=body.severity,
|
|
374
398
|
)
|
|
375
399
|
except DispositionError as exc:
|
|
@@ -453,6 +477,8 @@ async def audit_status():
|
|
|
453
477
|
"by_source": status_data["by_source"],
|
|
454
478
|
"path": str(path),
|
|
455
479
|
"spool_pending": spool_depth(),
|
|
480
|
+
# Per SIEM: the last seq it accepted, and how long it has been failing.
|
|
481
|
+
"forwarding": _forward_status(),
|
|
456
482
|
"spool_oldest_age_seconds": spool_oldest_age_seconds(),
|
|
457
483
|
}
|
|
458
484
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Dashboard sign-in (pilot hardening items 8 and 13).
|
|
2
|
+
|
|
3
|
+
`agentmetry dashboard` calls POST /auth/dashboard-link with the API token and
|
|
4
|
+
opens the URL it returns. That URL exchanges a one-time code for an HttpOnly,
|
|
5
|
+
SameSite=Strict session cookie, so the browser is signed in without ever
|
|
6
|
+
holding the key.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
13
|
+
from fastapi.responses import RedirectResponse
|
|
14
|
+
from pydantic import BaseModel
|
|
15
|
+
|
|
16
|
+
from agentmetry.core.auth import require_api_key
|
|
17
|
+
from agentmetry.core.dashboard_session import (
|
|
18
|
+
CODE_SECONDS,
|
|
19
|
+
SESSION_COOKIE,
|
|
20
|
+
SESSION_SECONDS,
|
|
21
|
+
sessions,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
router = APIRouter(prefix="/auth", tags=["auth"])
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DashboardLinkBody(BaseModel):
|
|
29
|
+
operator: str = ""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
#: Where a sign-in may land. A lookup, not validation: the Location header is
|
|
33
|
+
#: always one of these constants, never anything taken from the request. The
|
|
34
|
+
#: dashboard is one page, so `/` is the only real destination; `dev` is the
|
|
35
|
+
#: Next dev server on its default port, for working on the dashboard itself.
|
|
36
|
+
#: (The previous "starts with one slash" check let `/\evil.example` through,
|
|
37
|
+
#: which browsers read as `//evil.example`.)
|
|
38
|
+
_LANDINGS = {
|
|
39
|
+
"": "/",
|
|
40
|
+
"/": "/",
|
|
41
|
+
"dev": "http://localhost:3000/",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _safe_next(target: str) -> str:
|
|
46
|
+
"""Where to land after sign-in. Anything not in the table lands on `/`."""
|
|
47
|
+
return _LANDINGS.get(target or "", "/")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@router.post("/dashboard-link")
|
|
51
|
+
async def dashboard_link(body: DashboardLinkBody, request: Request, _: None = Depends(require_api_key)):
|
|
52
|
+
"""A one-time sign-in link. Needs the token itself, not a session."""
|
|
53
|
+
kind, _who = getattr(request.state, "auth", ("", ""))
|
|
54
|
+
if kind == "session":
|
|
55
|
+
raise HTTPException(status_code=403, detail="a dashboard session cannot mint sign-in links")
|
|
56
|
+
if not body.operator.strip():
|
|
57
|
+
from agentmetry.core.audit.run_context import resolve_operator
|
|
58
|
+
|
|
59
|
+
operator = resolve_operator()[0]
|
|
60
|
+
else:
|
|
61
|
+
operator = body.operator.strip()
|
|
62
|
+
code = sessions.issue_code(operator)
|
|
63
|
+
return {"url": f"/api/v1/auth/dashboard?code={code}", "expires_in": CODE_SECONDS}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@router.get("/dashboard")
|
|
67
|
+
async def dashboard_sign_in(request: Request, code: str = "", next: str = ""): # noqa: A002
|
|
68
|
+
session_id = sessions.redeem(code)
|
|
69
|
+
if session_id is None:
|
|
70
|
+
raise HTTPException(status_code=401, detail="sign-in link expired or already used; run `agentmetry dashboard`")
|
|
71
|
+
resp = RedirectResponse(_safe_next(next), status_code=303)
|
|
72
|
+
resp.set_cookie(
|
|
73
|
+
SESSION_COOKIE, session_id, max_age=SESSION_SECONDS, httponly=True,
|
|
74
|
+
samesite="strict", secure=request.url.scheme == "https", path="/",
|
|
75
|
+
)
|
|
76
|
+
return resp
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@router.post("/logout")
|
|
80
|
+
async def dashboard_logout(request: Request):
|
|
81
|
+
sessions.revoke(request.cookies.get(SESSION_COOKIE))
|
|
82
|
+
resp = RedirectResponse("/", status_code=303)
|
|
83
|
+
resp.delete_cookie(SESSION_COOKIE, path="/")
|
|
84
|
+
return resp
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Refuse requests whose Host header is not this machine (pilot hardening item 9).
|
|
2
|
+
|
|
3
|
+
The orchestrator listens on loopback, and that is not enough on its own. A web
|
|
4
|
+
page the developer visits can make its own hostname resolve to 127.0.0.1 (DNS
|
|
5
|
+
rebinding) and then read the local API as same-origin, trail included, because
|
|
6
|
+
the browser believes it is talking to the attacker's site. The one thing that
|
|
7
|
+
gives it away is the Host header, which still names the attacker's domain.
|
|
8
|
+
|
|
9
|
+
So only loopback names and the names an operator configures are accepted:
|
|
10
|
+
`localhost`, `127.0.0.1`, `::1`, plus `AGENTMETRY_TRUSTED_HOSTS`
|
|
11
|
+
(comma-separated, `*` to disable the check). `mobile.bat` adds the LAN address
|
|
12
|
+
it detects.
|
|
13
|
+
|
|
14
|
+
Written as plain ASGI rather than Starlette's TrustedHostMiddleware because
|
|
15
|
+
that one fixes its list at startup, and this reads settings on each request so
|
|
16
|
+
a test or an edited `.env` takes effect.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
LOOPBACK = frozenset({"localhost", "127.0.0.1", "::1"})
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def allowed_hosts() -> frozenset[str]:
|
|
27
|
+
from agentmetry.core.config import settings
|
|
28
|
+
|
|
29
|
+
configured = {h.strip().lower() for h in str(getattr(settings, "trusted_hosts", "")).split(",") if h.strip()}
|
|
30
|
+
return LOOPBACK | frozenset(configured)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def host_of(header: str) -> str:
|
|
34
|
+
"""The hostname in a Host header, without the port, lowercased."""
|
|
35
|
+
value = header.strip().lower()
|
|
36
|
+
if value.startswith("["): # [::1]:8000
|
|
37
|
+
end = value.find("]")
|
|
38
|
+
return value[1:end] if end > 0 else value
|
|
39
|
+
if value.count(":") == 1:
|
|
40
|
+
return value.split(":", 1)[0]
|
|
41
|
+
return value
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TrustedHostMiddleware:
|
|
45
|
+
def __init__(self, app: Any) -> None:
|
|
46
|
+
self.app = app
|
|
47
|
+
|
|
48
|
+
async def __call__(self, scope: dict, receive: Any, send: Any) -> None:
|
|
49
|
+
if scope.get("type") not in ("http", "websocket"):
|
|
50
|
+
return await self.app(scope, receive, send)
|
|
51
|
+
allowed = allowed_hosts()
|
|
52
|
+
if "*" in allowed:
|
|
53
|
+
return await self.app(scope, receive, send)
|
|
54
|
+
header = ""
|
|
55
|
+
for name, value in scope.get("headers") or []:
|
|
56
|
+
if name == b"host":
|
|
57
|
+
header = value.decode("latin-1")
|
|
58
|
+
break
|
|
59
|
+
if host_of(header) in allowed:
|
|
60
|
+
return await self.app(scope, receive, send)
|
|
61
|
+
if scope["type"] == "websocket":
|
|
62
|
+
message = await receive()
|
|
63
|
+
if message.get("type") == "websocket.connect":
|
|
64
|
+
await send({"type": "websocket.close", "code": 1008})
|
|
65
|
+
return None
|
|
66
|
+
body = b"Invalid host header"
|
|
67
|
+
await send({
|
|
68
|
+
"type": "http.response.start", "status": 400,
|
|
69
|
+
"headers": [(b"content-type", b"text/plain"), (b"content-length", str(len(body)).encode())],
|
|
70
|
+
})
|
|
71
|
+
await send({"type": "http.response.body", "body": body})
|
|
72
|
+
return None
|