agentmetry 0.9.0__tar.gz → 0.9.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.0 → agentmetry-0.9.1}/PKG-INFO +1 -1
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/api/routes/audit.py +15 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/cli/__init__.py +11 -1
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/disposition.py +10 -4
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/dlp/loader.py +8 -1
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/dlp/scanner.py +7 -4
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/evidence_pack.py +2 -1
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/external.py +9 -7
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/otel_ingest.py +1 -1
- agentmetry-0.9.1/agentmetry/core/audit/policy_settings.py +162 -0
- agentmetry-0.9.1/agentmetry/core/audit/run_context.py +197 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/tool_policy/evaluator.py +4 -3
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/tool_policy/loader.py +7 -1
- agentmetry-0.9.1/agentmetry/core/operator_identity.py +80 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/version.py +1 -1
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/hooks/ingest.py +62 -24
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/conftest.py +8 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_cli_commands.py +6 -1
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_dlp_scanner.py +10 -7
- agentmetry-0.9.1/tests/test_hook_import_cost.py +179 -0
- agentmetry-0.9.1/tests/test_hook_real_process.py +100 -0
- agentmetry-0.9.1/tests/test_operator_attribution.py +231 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_readme_claims.py +22 -3
- agentmetry-0.9.0/agentmetry/core/audit/run_context.py +0 -129
- {agentmetry-0.9.0 → agentmetry-0.9.1}/.dockerignore +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/.env.agentmetry-demo +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/.env.example +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/.gitignore +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/Dockerfile +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/README.md +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/__init__.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/api/__init__.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/api/main.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/api/routes/__init__.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/api/websocket.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/api/ws_bridge.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/cli/__main__.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/__init__.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/__init__.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/adapters/__init__.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/adapters/agt.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/adapters/chronicle.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/adapters/cloudevents.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/adapters/ecs.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/adapters/splunk.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/alerts.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/atlas.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/canonical.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/compliance_digest.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/__init__.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/benchmark.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/attack_approval_denied_then_executed.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/attack_arbitrary_host_stage_execute.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/attack_autonomous_unapproved_write.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/attack_credential_exfil.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/attack_credential_then_cloud_api.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/attack_destructive_delete_burst.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/attack_discovery_then_collect.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/attack_dotfile_then_git_push.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/attack_encoded_command_download.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/attack_env_credential_exfil.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/attack_hashed_only_no_command.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/attack_interpreter_egress.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/attack_pr_merged_without_review.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/attack_proc_substitution_cradle.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/attack_remote_pipe_to_shell.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/attack_remote_staging_then_execute.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/attack_secret_manager_then_egress.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/attack_session_tool_burst.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/attack_single_command_exfil.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/attack_single_quoted_interpreter_egress.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/attack_ssh_directory_exfil.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/attack_subagent_swarm.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/attack_timestamp_collision.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/attack_untrusted_input_then_action.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/benign_authoring_merge_fixtures.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/benign_autonomous_after_approval.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/benign_build_artifact_cleanup.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/benign_ci_artifact_download.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/benign_commit_message_names_cloud_clis.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/benign_database_migration.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/benign_dependency_install_and_build.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/benign_download_release_archive.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/benign_env_example_read.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/benign_fetch_data_then_run_repo_script.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/benign_fetch_dataset_then_analyse.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/benign_fetch_lockfile_then_install.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/benign_fetch_piped_into_own_script.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/benign_git_review_and_push.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/benign_github_raw_docs_then_script.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/benign_human_driven_deletes.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/benign_local_api_probing.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/benign_long_but_calm_session.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/benign_loopback_is_not_egress.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/benign_loopback_pipe_to_interpreter.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/benign_mcp_text_mentions_credential_path.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/benign_ordinary_development.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/benign_package_manager_after_fetch.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/benign_reading_config_that_is_not_secret.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/benign_remote_api_call_no_credentials.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/benign_research_then_docs.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/benign_reversed_order_is_not_exfil.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/benign_secret_names_and_docs.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/benign_test_and_fix_loop.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/benign_writing_about_credentials.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/corpus/corpus.yaml +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/engine.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/live.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/live_store.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/models.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/rules.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/traits.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/yaml_config.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/detection/yaml_rules.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/dlp/__init__.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/dlp/models.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/dogfood.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/hashing.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/heartbeat.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/hook_bootstrap.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/identity.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/ingest.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/migrate.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/mitre.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/policy.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/redaction.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/replay.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/sinks.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/spool.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/tool_policy/__init__.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/tool_policy/capability.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/tool_policy/models.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/trail_anchor.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/trail_chain.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/trail_db.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/audit/trail_merkle.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/auth.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/bus/__init__.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/bus/audit_exporter.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/bus/bridges.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/bus/bus.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/bus/events.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/bus/outbox.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/config.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/diagnostics/__init__.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/diagnostics/autostart.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/diagnostics/doctor.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/diagnostics/driver_paths.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/diagnostics/env_file.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/diagnostics/hook_coverage.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/diagnostics/mcp_inventory.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/diagnostics/mcp_schema.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/drivers/__init__.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/drivers/host.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/drivers/permissions.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/drivers/spec.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/extensions.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/core/health.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/hooks/__init__.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/policies/detection/manifest.yaml +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/policies/dlp/manifest.yaml +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/policies/opa/agent_rules.rego +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/agentmetry/policies/tool/manifest.yaml +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/pyproject.toml +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/fixtures/agt_filesink_exfil.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/fixtures/agt_filesink_mixed.jsonl +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/fixtures/fake_mcp_server.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/fixtures/otel_claude_code_session.json +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_agentmetry_audit.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_agentmetry_ingest_client.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_agt_adapter.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_ask_gate_phase_a.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_atlas_detection_enrichment.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_atlas_mapping.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_audit_sinks.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_audit_stats.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_audit_tail.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_auth.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_autostart.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_boot_sequence.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_burst_windows.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_chinese_agent_hooks.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_chinese_agent_sprint_b.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_chinese_agent_sprint_c.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_chronicle_adapter.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_cli_backup.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_cli_env_var_names.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_cloudevents_adapter.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_compliance_digest.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_crewai_adapter.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_detection_adi.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_detection_benchmark.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_detection_default_config.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_detection_disposition.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_detection_disposition_api.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_detection_duplicate_emission.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_detection_emit_durability.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_detection_engine.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_detection_evasion.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_detection_hf_incident.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_detection_quoted_command_words.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_detection_rule_identity.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_detection_rules_v2.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_dlp_agent_env_override.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_dlp_invisible_unicode.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_dlp_markdown_exfil.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_doctor.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_dogfood.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_dogfood_freeze.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_driver_paths.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_ecs_threat.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_event_bus.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_evidence_pack.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_extensions.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_external_ingest.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_forwarding_shape.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_funding_manifest.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_heartbeat.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_hook_bootstrap.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_hook_coverage.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_hook_decision_format.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_hook_enforcement_timing.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_hook_spool.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_identity_enrich.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_ingest_roundtrip.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_isolation.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_launch_targets.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_live_detection.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_live_detection_store.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_mcp_audit_proxy.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_mcp_concealed_text.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_mcp_inventory.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_mcp_meta_fingerprint.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_mcp_schema.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_mitre_dlp.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_opensre_adapter.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_otel_ingest.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_powershell_encoding.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_redaction.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_replay.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_sigma_pack.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_static_serving.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_subagent_lifecycle.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_suite_leaves_real_home_alone.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_tool_policy.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_tool_policy_agent_cli.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_tool_policy_git_hooks.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_trail_anchor.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_trail_chain.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_trail_concurrency.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_trail_merkle.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_unattended_agent_policy.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_version.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tests/test_yaml_detection_rules.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tools/generate_sigma_pack.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.1}/tools/mcp_audit_proxy.py +0 -0
- {agentmetry-0.9.0 → agentmetry-0.9.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.9.
|
|
3
|
+
Version: 0.9.1
|
|
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
|
|
@@ -70,6 +70,19 @@ class IngestInitiatorBody(BaseModel):
|
|
|
70
70
|
return v if v in _ACTOR_TYPES else "agent"
|
|
71
71
|
|
|
72
72
|
|
|
73
|
+
class IngestOperatorBody(BaseModel):
|
|
74
|
+
"""Which account the capture process ran as, and how it knew.
|
|
75
|
+
|
|
76
|
+
Separate from `initiator`, which says what kind of actor acted. A claim,
|
|
77
|
+
like everything a client sends: `run_context.resolve_operator` decides what
|
|
78
|
+
is recorded, and an orchestrator-side AGENTMETRY_OPERATOR_ID overrides an
|
|
79
|
+
id the hook only read from the operating system.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
id: str = ""
|
|
83
|
+
source: str = ""
|
|
84
|
+
|
|
85
|
+
|
|
73
86
|
class ExternalIngestBody(BaseModel):
|
|
74
87
|
"""Adapter payload — normalized to canonical v1.2 on ingest.
|
|
75
88
|
|
|
@@ -113,6 +126,8 @@ class ExternalIngestBody(BaseModel):
|
|
|
113
126
|
tool_policy: dict[str, Any] | None = None
|
|
114
127
|
# Who the capture surface says triggered this. Honoured by external.py.
|
|
115
128
|
initiator: IngestInitiatorBody | None = None
|
|
129
|
+
# Which account ran the agent, resolved by the hook as the developer.
|
|
130
|
+
operator: IngestOperatorBody | None = None
|
|
116
131
|
# Compact `tools/list` fingerprint from mcp_audit_proxy. Hash only: the
|
|
117
132
|
# description never leaves the proxy process.
|
|
118
133
|
schema_fingerprint: str = ""
|
|
@@ -284,6 +284,15 @@ def cmd_detections(args: argparse.Namespace) -> int:
|
|
|
284
284
|
return 0
|
|
285
285
|
|
|
286
286
|
|
|
287
|
+
def _triage_operator() -> str:
|
|
288
|
+
configured = os.environ.get("AGENTMETRY_OPERATOR_ID", "").strip()
|
|
289
|
+
if configured:
|
|
290
|
+
return configured
|
|
291
|
+
from agentmetry.core.operator_identity import os_operator
|
|
292
|
+
|
|
293
|
+
return os_operator()
|
|
294
|
+
|
|
295
|
+
|
|
287
296
|
def cmd_disposition(args: argparse.Namespace) -> int:
|
|
288
297
|
"""Close a detection from the shell, using the same API as the dashboard."""
|
|
289
298
|
note = args.note.strip()
|
|
@@ -299,7 +308,8 @@ def cmd_disposition(args: argparse.Namespace) -> int:
|
|
|
299
308
|
"rule_id": args.rule_id,
|
|
300
309
|
"status": args.status,
|
|
301
310
|
"note": note,
|
|
302
|
-
|
|
311
|
+
# The CLI runs as the person triaging; the orchestrator may not.
|
|
312
|
+
"decided_by": args.decided_by or _triage_operator(),
|
|
303
313
|
},
|
|
304
314
|
headers=_api_headers(),
|
|
305
315
|
timeout=10.0,
|
|
@@ -431,8 +431,14 @@ def build_disposition_event(
|
|
|
431
431
|
`action.type:detection_disposition AND action.outcome:risk_accepted`
|
|
432
432
|
without understanding Agentmetry's vocabulary. The note is operator-written
|
|
433
433
|
text, never captured command content, so it is safe to forward.
|
|
434
|
+
|
|
435
|
+
Who decided falls back to the resolved operator, not to the raw setting:
|
|
436
|
+
with AGENTMETRY_OPERATOR_ID unset that was an empty string, so a triage
|
|
437
|
+
record could not say who triaged.
|
|
434
438
|
"""
|
|
435
|
-
from agentmetry.core.
|
|
439
|
+
from agentmetry.core.audit.run_context import resolve_operator
|
|
440
|
+
|
|
441
|
+
decided_by = decided_by or resolve_operator()[0]
|
|
436
442
|
|
|
437
443
|
return {
|
|
438
444
|
"schema_version": SCHEMA_VERSION,
|
|
@@ -443,8 +449,8 @@ def build_disposition_event(
|
|
|
443
449
|
**identity_fields(),
|
|
444
450
|
"source_topic": f"disposition/{rule_id}",
|
|
445
451
|
"source": {"tier": "detection", "app": "agentmetry"},
|
|
446
|
-
"actor": {"id": decided_by
|
|
447
|
-
"initiator": {"type": "human", "id": decided_by
|
|
452
|
+
"actor": {"id": decided_by, "type": "human"},
|
|
453
|
+
"initiator": {"type": "human", "id": decided_by},
|
|
448
454
|
"action": {
|
|
449
455
|
"type": DISPOSITION_EVENT_TYPE,
|
|
450
456
|
"outcome": status,
|
|
@@ -459,7 +465,7 @@ def build_disposition_event(
|
|
|
459
465
|
"previous_status": previous_status,
|
|
460
466
|
"assignee": assignee,
|
|
461
467
|
"note": note,
|
|
462
|
-
"decided_by": decided_by
|
|
468
|
+
"decided_by": decided_by,
|
|
463
469
|
},
|
|
464
470
|
}
|
|
465
471
|
|
|
@@ -4,6 +4,13 @@ from typing import List
|
|
|
4
4
|
|
|
5
5
|
from .models import RuleMeta
|
|
6
6
|
|
|
7
|
+
#: libyaml where it is installed. The pure-Python parser was most of the hook's
|
|
8
|
+
#: runtime: the DLP and tool manifests are re-read by every hook process, and
|
|
9
|
+
#: parsing them took ~55 ms of an ~85 ms tool call (#171). Same SafeConstructor,
|
|
10
|
+
#: so the same Python objects, which `test_hook_import_cost.py` checks.
|
|
11
|
+
SAFE_LOADER = getattr(yaml, "CSafeLoader", yaml.SafeLoader)
|
|
12
|
+
|
|
13
|
+
|
|
7
14
|
def load_dlp_rules(manifest_path: Path | str) -> List[RuleMeta]:
|
|
8
15
|
"""Load DLP rules from a YAML manifest file."""
|
|
9
16
|
path = Path(manifest_path)
|
|
@@ -11,7 +18,7 @@ def load_dlp_rules(manifest_path: Path | str) -> List[RuleMeta]:
|
|
|
11
18
|
return []
|
|
12
19
|
|
|
13
20
|
with open(path, "r", encoding="utf-8") as f:
|
|
14
|
-
data = yaml.
|
|
21
|
+
data = yaml.load(f, Loader=SAFE_LOADER) # noqa: S506 - a safe loader
|
|
15
22
|
|
|
16
23
|
if not data or "rules" not in data:
|
|
17
24
|
return []
|
|
@@ -5,7 +5,9 @@ from typing import Any, Dict
|
|
|
5
5
|
|
|
6
6
|
from .models import DlpVerdict, DlpMatch
|
|
7
7
|
from .loader import load_dlp_rules
|
|
8
|
-
|
|
8
|
+
# Not core.config: that import is pydantic, and the hook paid ~220 ms for it
|
|
9
|
+
# on every tool call to read three values (#171).
|
|
10
|
+
from ..policy_settings import policy_settings
|
|
9
11
|
|
|
10
12
|
logger = logging.getLogger(__name__)
|
|
11
13
|
|
|
@@ -21,9 +23,10 @@ def reset_rules() -> None:
|
|
|
21
23
|
def _init_rules() -> None:
|
|
22
24
|
if _COMPILED_RULES:
|
|
23
25
|
return
|
|
24
|
-
|
|
26
|
+
cfg = policy_settings()
|
|
27
|
+
rules = load_dlp_rules(cfg.dlp_rules_path)
|
|
25
28
|
for r in rules:
|
|
26
|
-
if not
|
|
29
|
+
if not cfg.dlp_pii and r.category == "pii":
|
|
27
30
|
continue
|
|
28
31
|
try:
|
|
29
32
|
_COMPILED_RULES.append((re.compile(r.pattern), r))
|
|
@@ -59,7 +62,7 @@ def scan(tool_qualified: str, arguments: Dict[str, Any] | str, mode: str | None
|
|
|
59
62
|
metadata is returned — never the matched value.
|
|
60
63
|
"""
|
|
61
64
|
if mode is None:
|
|
62
|
-
mode =
|
|
65
|
+
mode = policy_settings().dlp_mode
|
|
63
66
|
if mode == "disable":
|
|
64
67
|
return DlpVerdict(matched=False, mode=mode)
|
|
65
68
|
|
|
@@ -23,6 +23,7 @@ from pathlib import Path
|
|
|
23
23
|
from typing import Any
|
|
24
24
|
|
|
25
25
|
from agentmetry.core.audit.detection.disposition import extract_dispositions
|
|
26
|
+
from agentmetry.core.audit.run_context import resolve_operator
|
|
26
27
|
from agentmetry.core.config import settings
|
|
27
28
|
from agentmetry.core.version import __version__
|
|
28
29
|
|
|
@@ -290,7 +291,7 @@ def _controls_snapshot() -> dict[str, Any]:
|
|
|
290
291
|
"detection": {
|
|
291
292
|
"off_hours_enabled": settings.detect_off_hours,
|
|
292
293
|
},
|
|
293
|
-
"operator_id":
|
|
294
|
+
"operator_id": resolve_operator()[0],
|
|
294
295
|
}
|
|
295
296
|
|
|
296
297
|
|
|
@@ -10,7 +10,11 @@ from agentmetry.core.audit.canonical import SCHEMA_VERSION
|
|
|
10
10
|
from agentmetry.core.audit.identity import identity_fields
|
|
11
11
|
from agentmetry.core.audit.hashing import arguments_sha256
|
|
12
12
|
from agentmetry.core.audit.redaction import scrub_arg_values, scrub_secrets
|
|
13
|
-
from agentmetry.core.audit.run_context import
|
|
13
|
+
from agentmetry.core.audit.run_context import (
|
|
14
|
+
actor_from_initiator,
|
|
15
|
+
build_initiator,
|
|
16
|
+
operator_from_payload,
|
|
17
|
+
)
|
|
14
18
|
from agentmetry.core.config import settings
|
|
15
19
|
|
|
16
20
|
ExternalApp = Literal[
|
|
@@ -34,10 +38,6 @@ _ACTION_MAP = {
|
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
|
|
37
|
-
def _operator_id() -> str:
|
|
38
|
-
return settings.operator_id.strip() or "local"
|
|
39
|
-
|
|
40
|
-
|
|
41
41
|
def _utc_now() -> str:
|
|
42
42
|
return datetime.now(timezone.utc).isoformat()
|
|
43
43
|
|
|
@@ -63,16 +63,18 @@ def build_external_canonical(payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
63
63
|
correlation_id = str(payload.get("correlation_id") or payload.get("thread_id") or "")
|
|
64
64
|
session_id = str(payload.get("session_id") or "")
|
|
65
65
|
|
|
66
|
+
operator = operator_from_payload(payload)
|
|
66
67
|
initiator_raw = payload.get("initiator")
|
|
67
68
|
if isinstance(initiator_raw, dict) and initiator_raw.get("actor_type"):
|
|
68
69
|
initiator = {
|
|
69
70
|
"actor_type": str(initiator_raw.get("actor_type") or "human"),
|
|
70
71
|
"trigger": str(initiator_raw.get("trigger") or "manual"),
|
|
71
|
-
"operator_id":
|
|
72
|
+
"operator_id": operator[0],
|
|
73
|
+
"operator_source": operator[1],
|
|
72
74
|
}
|
|
73
75
|
else:
|
|
74
76
|
trigger = str(payload.get("triggered_by") or "manual")
|
|
75
|
-
initiator = build_initiator(trigger)
|
|
77
|
+
initiator = build_initiator(trigger, operator)
|
|
76
78
|
|
|
77
79
|
tool_block = payload.get("tool") if isinstance(payload.get("tool"), dict) else {}
|
|
78
80
|
tool_qualified = str(
|
|
@@ -276,7 +276,7 @@ def _map_tool_decision(attrs: dict[str, Any], ts_iso: str) -> tuple[dict[str, An
|
|
|
276
276
|
"outcome": "success",
|
|
277
277
|
"reason": f"otel_decision:{source}",
|
|
278
278
|
"tool_qualified": str(attrs.get("tool_name") or ""),
|
|
279
|
-
"initiator": {"actor_type": "human", "trigger": "manual", "operator_id": "
|
|
279
|
+
"initiator": {"actor_type": "human", "trigger": "manual", "operator_id": ""},
|
|
280
280
|
})
|
|
281
281
|
return payload, ""
|
|
282
282
|
if decision == "reject" and source in _HUMAN_REJECT:
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""The five settings DLP and tool policy read, without importing pydantic (#171).
|
|
2
|
+
|
|
3
|
+
The hook runs in front of every tool call, and on a blocking hook the agent waits
|
|
4
|
+
for it. The DLP scanner and the tool-policy evaluator used to import
|
|
5
|
+
`core.config` to learn a mode and a manifest path each, and that one import
|
|
6
|
+
pulled in pydantic and pydantic-settings: about 220 ms of a 626 ms hook, paid on
|
|
7
|
+
every tool call to read five values.
|
|
8
|
+
|
|
9
|
+
So there are two ways to get them, and one answer:
|
|
10
|
+
|
|
11
|
+
* Where `core.config` is already loaded, the orchestrator and the test suite,
|
|
12
|
+
the real `settings` object is read. A test that monkeypatches
|
|
13
|
+
`settings.dlp_mode` still changes what the scanner sees.
|
|
14
|
+
* Where it is not, the hook, they are resolved here the way `Settings` resolves
|
|
15
|
+
them: environment first, matched case-insensitively, then the orchestrator's
|
|
16
|
+
`.env`, then the same defaults. `test_hook_import_cost.py` checks the two
|
|
17
|
+
agree.
|
|
18
|
+
|
|
19
|
+
Standard library only. That is the whole point of the module.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
import sys
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
_HERE = Path(__file__).resolve()
|
|
30
|
+
_PACKAGE_ROOT = _HERE.parents[2] # agentmetry/, as config._PACKAGE_ROOT
|
|
31
|
+
_ORCHESTRATOR_ROOT = _HERE.parents[3] # where Settings reads .env, as config._ORCHESTRATOR_ROOT
|
|
32
|
+
|
|
33
|
+
#: pydantic's own boolean vocabulary, so `AGENTMETRY_DLP_PII=off` means the same
|
|
34
|
+
#: thing to the hook as to the orchestrator.
|
|
35
|
+
_TRUE = frozenset({"1", "on", "t", "true", "y", "yes"})
|
|
36
|
+
_FALSE = frozenset({"0", "off", "f", "false", "n", "no"})
|
|
37
|
+
|
|
38
|
+
_DOTENV_CACHE: tuple[tuple[str, float, int] | None, dict[str, str]] = (None, {})
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class PolicySettings:
|
|
43
|
+
dlp_mode: str
|
|
44
|
+
dlp_rules_path: Path
|
|
45
|
+
dlp_pii: bool
|
|
46
|
+
tool_policy_mode: str
|
|
47
|
+
tool_policy_path: Path
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _defaults() -> PolicySettings:
|
|
51
|
+
policies = _PACKAGE_ROOT / "policies"
|
|
52
|
+
return PolicySettings(
|
|
53
|
+
dlp_mode="log",
|
|
54
|
+
dlp_rules_path=policies / "dlp" / "manifest.yaml",
|
|
55
|
+
dlp_pii=True,
|
|
56
|
+
tool_policy_mode="log",
|
|
57
|
+
tool_policy_path=policies / "tool" / "manifest.yaml",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _parse_dotenv(text: str) -> dict[str, str]:
|
|
62
|
+
"""The subset of python-dotenv that a `.env` for this project uses.
|
|
63
|
+
|
|
64
|
+
`export` prefixes, single or double quotes, and a ` #` comment after an
|
|
65
|
+
unquoted value. Keys are lowercased, because Settings matches them
|
|
66
|
+
case-insensitively.
|
|
67
|
+
"""
|
|
68
|
+
out: dict[str, str] = {}
|
|
69
|
+
for raw in text.splitlines():
|
|
70
|
+
line = raw.strip()
|
|
71
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
72
|
+
continue
|
|
73
|
+
if line.startswith("export "):
|
|
74
|
+
line = line[len("export "):].lstrip()
|
|
75
|
+
key, _, value = line.partition("=")
|
|
76
|
+
value = value.strip()
|
|
77
|
+
if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
|
|
78
|
+
value = value[1:-1]
|
|
79
|
+
elif " #" in value:
|
|
80
|
+
value = value.split(" #", 1)[0].rstrip()
|
|
81
|
+
out[key.strip().lower()] = value
|
|
82
|
+
return out
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _dotenv() -> dict[str, str]:
|
|
86
|
+
"""The orchestrator's `.env`, parsed once per change to the file."""
|
|
87
|
+
global _DOTENV_CACHE
|
|
88
|
+
path = _ORCHESTRATOR_ROOT / ".env"
|
|
89
|
+
try:
|
|
90
|
+
stat = path.stat()
|
|
91
|
+
except OSError:
|
|
92
|
+
return {}
|
|
93
|
+
stamp = (str(path), stat.st_mtime, stat.st_size)
|
|
94
|
+
if _DOTENV_CACHE[0] == stamp:
|
|
95
|
+
return _DOTENV_CACHE[1]
|
|
96
|
+
try:
|
|
97
|
+
parsed = _parse_dotenv(path.read_text(encoding="utf-8"))
|
|
98
|
+
except (OSError, UnicodeDecodeError):
|
|
99
|
+
return {}
|
|
100
|
+
_DOTENV_CACHE = (stamp, parsed)
|
|
101
|
+
return parsed
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _lookup(name: str, environ: dict[str, str], dotenv: dict[str, str]) -> str | None:
|
|
105
|
+
# Present-but-empty counts as set, as it does for Settings (env_ignore_empty
|
|
106
|
+
# is off): an empty mode is not "use the default".
|
|
107
|
+
key = name.lower()
|
|
108
|
+
if key in environ:
|
|
109
|
+
return environ[key]
|
|
110
|
+
return dotenv.get(key)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _bool(raw: str | None, default: bool) -> bool:
|
|
114
|
+
if raw is None:
|
|
115
|
+
return default
|
|
116
|
+
text = raw.strip().lower()
|
|
117
|
+
if text in _TRUE:
|
|
118
|
+
return True
|
|
119
|
+
if text in _FALSE:
|
|
120
|
+
return False
|
|
121
|
+
# Settings refuses to start on this. The hook must not fail a tool call over
|
|
122
|
+
# a typo, so it keeps the default and the orchestrator's error stays the
|
|
123
|
+
# place the operator finds out.
|
|
124
|
+
return default
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def resolve() -> PolicySettings:
|
|
128
|
+
"""Resolve without pydantic, as the hook does."""
|
|
129
|
+
environ = {k.lower(): v for k, v in os.environ.items()}
|
|
130
|
+
dotenv = _dotenv()
|
|
131
|
+
base = _defaults()
|
|
132
|
+
|
|
133
|
+
def text(name: str, default: str) -> str:
|
|
134
|
+
value = _lookup(name, environ, dotenv)
|
|
135
|
+
return default if value is None else value
|
|
136
|
+
|
|
137
|
+
def path(name: str, default: Path) -> Path:
|
|
138
|
+
value = _lookup(name, environ, dotenv)
|
|
139
|
+
return default if value is None else Path(value)
|
|
140
|
+
|
|
141
|
+
return PolicySettings(
|
|
142
|
+
dlp_mode=text("AGENTMETRY_DLP_MODE", base.dlp_mode),
|
|
143
|
+
dlp_rules_path=path("AGENTMETRY_DLP_RULES_PATH", base.dlp_rules_path),
|
|
144
|
+
dlp_pii=_bool(_lookup("AGENTMETRY_DLP_PII", environ, dotenv), base.dlp_pii),
|
|
145
|
+
tool_policy_mode=text("AGENTMETRY_TOOL_POLICY_MODE", base.tool_policy_mode),
|
|
146
|
+
tool_policy_path=path("AGENTMETRY_TOOL_POLICY_PATH", base.tool_policy_path),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def policy_settings() -> PolicySettings:
|
|
151
|
+
"""The settings, from the loaded `Settings` object when there is one."""
|
|
152
|
+
config = sys.modules.get("agentmetry.core.config")
|
|
153
|
+
settings = getattr(config, "settings", None) if config is not None else None
|
|
154
|
+
if settings is None:
|
|
155
|
+
return resolve()
|
|
156
|
+
return PolicySettings(
|
|
157
|
+
dlp_mode=settings.dlp_mode,
|
|
158
|
+
dlp_rules_path=Path(settings.dlp_rules_path),
|
|
159
|
+
dlp_pii=bool(settings.dlp_pii),
|
|
160
|
+
tool_policy_mode=settings.tool_policy_mode,
|
|
161
|
+
tool_policy_path=Path(settings.tool_policy_path),
|
|
162
|
+
)
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Per-run audit context — initiator provenance and last gated tool (schema v1.1)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from agentmetry.core import operator_identity as oid
|
|
9
|
+
from agentmetry.core.config import settings
|
|
10
|
+
|
|
11
|
+
# thread_id → initiator block (set at run start, server-derived only)
|
|
12
|
+
_thread_initiators: dict[str, dict[str, str]] = {}
|
|
13
|
+
# thread_id → last successful tool call on this run (for approval binding)
|
|
14
|
+
_thread_last_tool: dict[str, dict[str, str]] = {}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@lru_cache(maxsize=1)
|
|
18
|
+
def _orchestrator_os() -> str:
|
|
19
|
+
# The account the recorder runs as does not change under a running process.
|
|
20
|
+
return oid.os_operator()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def resolve_operator(client_id: object = "", client_source: object = "") -> tuple[str, str]:
|
|
24
|
+
"""The operator id to record, and where it came from (`initiator.operator_source`).
|
|
25
|
+
|
|
26
|
+
Every event used to say `local`, so the trail could prove a record was not
|
|
27
|
+
altered and could not say who it was about (#168). In order:
|
|
28
|
+
|
|
29
|
+
1. An id the developer configured on their side, which the hook reports as
|
|
30
|
+
`configured` (`hook_configured`). The most specific statement anybody
|
|
31
|
+
made.
|
|
32
|
+
2. AGENTMETRY_OPERATOR_ID on this orchestrator (`orchestrator_configured`):
|
|
33
|
+
an admin override, for a service account or a shared machine. It used to
|
|
34
|
+
apply to no hook event at all, because every hook sent `local` and a
|
|
35
|
+
client value always won.
|
|
36
|
+
3. The account the hook ran as (`hook_os`). The hook runs inside the
|
|
37
|
+
developer's agent, so this is the developer.
|
|
38
|
+
4. An id a client sent without saying how (`client`).
|
|
39
|
+
5. The account this orchestrator runs as (`orchestrator_os`). On one
|
|
40
|
+
developer's machine that is the developer; under a fleet service it is
|
|
41
|
+
the service account, and the label says so rather than letting it pass
|
|
42
|
+
for a person.
|
|
43
|
+
6. `local`, labelled `default`, when nothing resolves.
|
|
44
|
+
|
|
45
|
+
Everything a client sends is a claim, and the `hook_` and `client` labels
|
|
46
|
+
say so. The legacy `local` a hook up to 0.9.0 sends counts as not stated.
|
|
47
|
+
"""
|
|
48
|
+
claimed = oid.stated(client_id)
|
|
49
|
+
source = str(client_source or "")
|
|
50
|
+
if claimed and source == oid.CONFIGURED:
|
|
51
|
+
return claimed, oid.HOOK_CONFIGURED
|
|
52
|
+
configured = settings.operator_id.strip()
|
|
53
|
+
if configured:
|
|
54
|
+
return configured, oid.ORCHESTRATOR_CONFIGURED
|
|
55
|
+
if claimed:
|
|
56
|
+
return claimed, oid.HOOK_OS if source == oid.OS else oid.CLIENT
|
|
57
|
+
local = _orchestrator_os()
|
|
58
|
+
if local:
|
|
59
|
+
return local, oid.ORCHESTRATOR_OS
|
|
60
|
+
return oid.LEGACY_PLACEHOLDER, oid.DEFAULT
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def operator_from_payload(payload: dict[str, Any]) -> tuple[str, str]:
|
|
64
|
+
"""Resolve from an ingest body: its `operator` block, else `initiator.operator_id`.
|
|
65
|
+
|
|
66
|
+
An initiator this orchestrator built already carries a recorded source, and
|
|
67
|
+
re-resolving it would relabel the recorder's own answer as a client claim.
|
|
68
|
+
Only internal payloads can carry one: `IngestInitiatorBody` does not declare
|
|
69
|
+
`operator_source`, so pydantic drops it from anything sent over HTTP.
|
|
70
|
+
"""
|
|
71
|
+
op = payload.get("operator")
|
|
72
|
+
if isinstance(op, dict) and oid.stated(op.get("id")):
|
|
73
|
+
return resolve_operator(op.get("id"), op.get("source"))
|
|
74
|
+
init = payload.get("initiator")
|
|
75
|
+
if isinstance(init, dict):
|
|
76
|
+
recorded = str(init.get("operator_source") or "")
|
|
77
|
+
if recorded in oid.RECORDED_SOURCES and oid.stated(init.get("operator_id")):
|
|
78
|
+
return str(init["operator_id"]), recorded
|
|
79
|
+
return resolve_operator(init.get("operator_id"), init.get("operator_source"))
|
|
80
|
+
return resolve_operator()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _operator_id() -> str:
|
|
84
|
+
return resolve_operator()[0]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def build_initiator(
|
|
88
|
+
triggered_by: str, operator: tuple[str, str] | None = None
|
|
89
|
+
) -> dict[str, str]:
|
|
90
|
+
"""Derive initiator from run origin. Must never trust client-supplied headers."""
|
|
91
|
+
operator_id, operator_source = operator or resolve_operator()
|
|
92
|
+
if triggered_by == "manual":
|
|
93
|
+
actor_type, trigger = "human", "manual"
|
|
94
|
+
elif triggered_by.startswith("channel:"):
|
|
95
|
+
actor_type, trigger = "human", "channel"
|
|
96
|
+
else:
|
|
97
|
+
# cron, vault_watch, ingress, recovery, and anything unrecognised.
|
|
98
|
+
actor_type, trigger = "autonomous", triggered_by
|
|
99
|
+
return {
|
|
100
|
+
"actor_type": actor_type,
|
|
101
|
+
"trigger": trigger,
|
|
102
|
+
"operator_id": operator_id,
|
|
103
|
+
"operator_source": operator_source,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def actor_from_initiator(initiator: dict[str, str]) -> dict[str, str]:
|
|
108
|
+
human = initiator.get("actor_type") == "human"
|
|
109
|
+
return {
|
|
110
|
+
"type": "user" if human else "agent",
|
|
111
|
+
"id": initiator.get("operator_id") or _operator_id(),
|
|
112
|
+
"role": "operator",
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def default_initiator() -> dict[str, str]:
|
|
117
|
+
return build_initiator("manual")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def set_thread_initiator(thread_id: str, triggered_by: str) -> dict[str, str]:
|
|
121
|
+
initiator = build_initiator(triggered_by)
|
|
122
|
+
_thread_initiators[thread_id] = initiator
|
|
123
|
+
return initiator
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def get_thread_initiator(thread_id: str) -> dict[str, str] | None:
|
|
127
|
+
return _thread_initiators.get(thread_id)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def resolve_initiator(
|
|
131
|
+
payload: dict[str, Any], thread_id: str = ""
|
|
132
|
+
) -> dict[str, str]:
|
|
133
|
+
"""Read initiator from payload or thread cache; fallback manual human."""
|
|
134
|
+
raw = payload.get("initiator")
|
|
135
|
+
if isinstance(raw, dict) and raw.get("actor_type"):
|
|
136
|
+
operator_id, operator_source = operator_from_payload(payload)
|
|
137
|
+
return {
|
|
138
|
+
"actor_type": str(raw.get("actor_type") or "human"),
|
|
139
|
+
"trigger": str(raw.get("trigger") or "manual"),
|
|
140
|
+
"operator_id": operator_id,
|
|
141
|
+
"operator_source": operator_source,
|
|
142
|
+
}
|
|
143
|
+
triggered_by = str(payload.get("triggered_by") or "")
|
|
144
|
+
if triggered_by:
|
|
145
|
+
return build_initiator(triggered_by, operator_from_payload(payload))
|
|
146
|
+
if thread_id:
|
|
147
|
+
cached = _thread_initiators.get(thread_id)
|
|
148
|
+
if cached:
|
|
149
|
+
return cached
|
|
150
|
+
return default_initiator()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def record_tool_call(thread_id: str, qualified: str, arguments_sha256: str) -> None:
|
|
154
|
+
if not thread_id:
|
|
155
|
+
return
|
|
156
|
+
server = qualified.split(".", 1)[0] if "." in qualified else ""
|
|
157
|
+
_thread_last_tool[thread_id] = {
|
|
158
|
+
"tool": qualified,
|
|
159
|
+
"server": server,
|
|
160
|
+
"input_hash": arguments_sha256,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def last_gated_action(thread_id: str) -> dict[str, str] | None:
|
|
165
|
+
if not thread_id:
|
|
166
|
+
return None
|
|
167
|
+
action = _thread_last_tool.get(thread_id)
|
|
168
|
+
if not action or not action.get("tool"):
|
|
169
|
+
return None
|
|
170
|
+
return dict(action)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def clear_run_context(thread_id: str) -> None:
|
|
174
|
+
_thread_initiators.pop(thread_id, None)
|
|
175
|
+
_thread_last_tool.pop(thread_id, None)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def audit_payload(
|
|
179
|
+
thread_id: str,
|
|
180
|
+
triggered_by: str | None = None,
|
|
181
|
+
*,
|
|
182
|
+
initiator: dict[str, str] | None = None,
|
|
183
|
+
) -> dict[str, Any]:
|
|
184
|
+
"""Extra bus payload fields for canonical v1.1."""
|
|
185
|
+
init = initiator or (
|
|
186
|
+
get_thread_initiator(thread_id)
|
|
187
|
+
if thread_id
|
|
188
|
+
else None
|
|
189
|
+
)
|
|
190
|
+
if init is None and triggered_by:
|
|
191
|
+
init = build_initiator(triggered_by)
|
|
192
|
+
if init is None:
|
|
193
|
+
init = default_initiator()
|
|
194
|
+
out: dict[str, Any] = {"initiator": init}
|
|
195
|
+
if triggered_by:
|
|
196
|
+
out["triggered_by"] = triggered_by
|
|
197
|
+
return out
|
|
@@ -6,7 +6,8 @@ from typing import Any
|
|
|
6
6
|
|
|
7
7
|
from .loader import load_tool_policy
|
|
8
8
|
from .models import ToolPolicyMatch, ToolPolicyRule, ToolPolicyVerdict
|
|
9
|
-
|
|
9
|
+
# Not core.config, for the same reason as the DLP scanner (#171).
|
|
10
|
+
from ..policy_settings import policy_settings
|
|
10
11
|
|
|
11
12
|
logger = logging.getLogger(__name__)
|
|
12
13
|
|
|
@@ -22,7 +23,7 @@ def reset_policy() -> None:
|
|
|
22
23
|
def _init_policy() -> tuple[list[ToolPolicyRule], str]:
|
|
23
24
|
global _POLICY
|
|
24
25
|
if _POLICY is None:
|
|
25
|
-
_POLICY = load_tool_policy(
|
|
26
|
+
_POLICY = load_tool_policy(policy_settings().tool_policy_path)
|
|
26
27
|
return _POLICY
|
|
27
28
|
|
|
28
29
|
|
|
@@ -143,7 +144,7 @@ def evaluate(
|
|
|
143
144
|
) -> ToolPolicyVerdict:
|
|
144
145
|
"""Evaluate tool allow/deny policy. Runs on plaintext hook data before hashing."""
|
|
145
146
|
if mode is None:
|
|
146
|
-
mode =
|
|
147
|
+
mode = policy_settings().tool_policy_mode
|
|
147
148
|
if mode == "disable":
|
|
148
149
|
return ToolPolicyVerdict(matched=False, blocked=False, mode=mode)
|
|
149
150
|
|
|
@@ -7,6 +7,12 @@ from .models import ToolPolicyRule
|
|
|
7
7
|
|
|
8
8
|
logger = logging.getLogger(__name__)
|
|
9
9
|
|
|
10
|
+
#: libyaml where it is installed. The pure-Python parser was most of the hook's
|
|
11
|
+
#: runtime: the DLP and tool manifests are re-read by every hook process, and
|
|
12
|
+
#: parsing them took ~55 ms of an ~85 ms tool call (#171). Same SafeConstructor,
|
|
13
|
+
#: so the same Python objects, which `test_hook_import_cost.py` checks.
|
|
14
|
+
SAFE_LOADER = getattr(yaml, "CSafeLoader", yaml.SafeLoader)
|
|
15
|
+
|
|
10
16
|
ACTIONS = ("allow", "deny", "ask")
|
|
11
17
|
|
|
12
18
|
#: `ask` is not a valid default yet. "Ask about everything not allowlisted" is
|
|
@@ -47,7 +53,7 @@ def load_tool_policy(manifest_path: Path | str) -> tuple[list[ToolPolicyRule], s
|
|
|
47
53
|
return [], "allow"
|
|
48
54
|
|
|
49
55
|
with open(path, encoding="utf-8") as fh:
|
|
50
|
-
data = yaml.
|
|
56
|
+
data = yaml.load(fh, Loader=SAFE_LOADER) # noqa: S506 - a safe loader
|
|
51
57
|
|
|
52
58
|
if not data or "rules" not in data:
|
|
53
59
|
return [], _normalise_default(data.get("default") if data else None)
|