agentmetry 0.9.4__tar.gz → 0.9.5__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.4 → agentmetry-0.9.5}/.gitignore +8 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/PKG-INFO +2 -2
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/atlas.py +1 -1
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/canonical.py +1 -1
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/version.py +1 -1
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/hooks/ingest.py +47 -1
- {agentmetry-0.9.4 → agentmetry-0.9.5}/pyproject.toml +1 -1
- agentmetry-0.9.5/tests/fixtures/codex_hooks/README.md +10 -0
- agentmetry-0.9.5/tests/fixtures/codex_hooks/permission-request.command.output.schema.json +91 -0
- agentmetry-0.9.5/tests/fixtures/codex_hooks/pre-tool-use.command.output.schema.json +93 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_atlas_detection_enrichment.py +1 -1
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_hook_decision_format.py +95 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_readme_claims.py +40 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tools/generate_sigma_pack.py +3 -3
- {agentmetry-0.9.4 → agentmetry-0.9.5}/.dockerignore +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/.env.agentmetry-demo +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/.env.example +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/Dockerfile +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/README.md +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/__init__.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/api/__init__.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/api/main.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/api/routes/__init__.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/api/routes/audit.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/api/routes/auth.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/api/trusted_host.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/api/websocket.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/api/ws_bridge.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/cli/__init__.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/cli/__main__.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/__init__.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/api_token.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/__init__.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/adapters/__init__.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/adapters/agt.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/adapters/chronicle.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/adapters/cloudevents.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/adapters/ecs.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/adapters/sentinel.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/adapters/splunk.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/alerts.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/compliance_digest.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/__init__.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/benchmark.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_approval_denied_then_executed.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_arbitrary_host_stage_execute.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_autonomous_unapproved_write.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_credential_exfil.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_credential_then_cloud_api.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_destructive_delete_burst.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_discovery_then_collect.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_dotfile_then_git_push.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_encoded_command_download.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_env_credential_exfil.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_hashed_only_no_command.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_interpreter_egress.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_pr_merged_without_review.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_proc_substitution_cradle.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_remote_pipe_to_shell.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_remote_staging_then_execute.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_secret_manager_then_egress.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_session_tool_burst.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_single_command_exfil.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_single_quoted_interpreter_egress.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_ssh_directory_exfil.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_subagent_swarm.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_timestamp_collision.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/attack_untrusted_input_then_action.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_authoring_merge_fixtures.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_autonomous_after_approval.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_build_artifact_cleanup.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_ci_artifact_download.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_commit_message_names_cloud_clis.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_database_migration.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_dependency_install_and_build.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_download_release_archive.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_env_example_read.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_fetch_data_then_run_repo_script.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_fetch_dataset_then_analyse.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_fetch_lockfile_then_install.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_fetch_piped_into_own_script.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_git_review_and_push.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_github_raw_docs_then_script.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_human_driven_deletes.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_local_api_probing.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_long_but_calm_session.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_loopback_is_not_egress.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_loopback_pipe_to_interpreter.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_mcp_text_mentions_credential_path.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_ordinary_development.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_package_manager_after_fetch.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_reading_config_that_is_not_secret.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_remote_api_call_no_credentials.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_research_then_docs.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_reversed_order_is_not_exfil.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_secret_names_and_docs.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_test_and_fix_loop.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/benign_writing_about_credentials.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/corpus/corpus.yaml +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/disposition.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/engine.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/live.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/live_store.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/models.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/rules.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/traits.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/yaml_config.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/detection/yaml_rules.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/dlp/__init__.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/dlp/loader.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/dlp/models.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/dlp/scanner.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/dogfood.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/evidence_pack.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/external.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/forwarder.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/hashing.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/heartbeat.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/hook_bootstrap.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/identity.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/ingest.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/managed_hooks.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/mcp_inventory_event.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/migrate.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/mitre.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/otel_ingest.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/policy.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/policy_settings.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/redaction.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/replay.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/run_context.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/signing.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/sinks.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/spool.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/tool_policy/__init__.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/tool_policy/capability.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/tool_policy/evaluator.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/tool_policy/loader.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/tool_policy/models.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/trail_anchor.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/trail_chain.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/trail_db.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/trail_merkle.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/audit/trail_rotation.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/auth.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/bus/__init__.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/bus/audit_exporter.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/bus/bridges.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/bus/bus.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/bus/events.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/bus/outbox.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/config.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/dashboard_session.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/diagnostics/__init__.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/diagnostics/autostart.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/diagnostics/doctor.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/diagnostics/driver_paths.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/diagnostics/env_file.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/diagnostics/hook_coverage.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/diagnostics/mcp_inventory.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/diagnostics/mcp_schema.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/drivers/__init__.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/drivers/host.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/drivers/permissions.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/drivers/spec.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/extensions.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/health.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/operator_identity.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/core/paths.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/demo.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/hooks/__init__.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/hooks/mcp_proxy.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/policies/detection/manifest.yaml +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/policies/dlp/manifest.yaml +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/policies/opa/agent_rules.rego +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/agentmetry/policies/tool/manifest.yaml +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/conftest.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/fixtures/agt_filesink_exfil.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/fixtures/agt_filesink_mixed.jsonl +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/fixtures/fake_mcp_server.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/fixtures/otel_claude_code_session.json +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_agentmetry_audit.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_agentmetry_ingest_client.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_agt_adapter.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_ask_gate_phase_a.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_atlas_mapping.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_audit_sinks.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_audit_stats.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_audit_tail.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_auth.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_auth_default.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_autostart.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_boot_sequence.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_burst_windows.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_chinese_agent_hooks.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_chinese_agent_sprint_b.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_chinese_agent_sprint_c.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_chronicle_adapter.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_cli_backup.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_cli_commands.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_cli_env_var_names.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_cloudevents_adapter.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_compliance_digest.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_container_and_legacy_runtime.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_crewai_adapter.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_demo.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_detection_adi.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_detection_benchmark.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_detection_default_config.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_detection_disposition.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_detection_disposition_api.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_detection_duplicate_emission.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_detection_emit_durability.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_detection_engine.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_detection_evasion.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_detection_hf_incident.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_detection_quoted_command_words.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_detection_rule_identity.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_detection_rules_v2.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_dlp_agent_env_override.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_dlp_invisible_unicode.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_dlp_markdown_exfil.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_dlp_scanner.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_doctor.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_dogfood.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_dogfood_freeze.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_driver_paths.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_ecs_threat.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_event_bus.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_event_signing.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_evidence_pack.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_extensions.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_external_ingest.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_forwarder.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_forwarding_shape.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_funding_manifest.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_hash_key.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_heartbeat.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_hook_bootstrap.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_hook_coverage.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_hook_enforcement_timing.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_hook_import_cost.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_hook_real_process.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_hook_spool.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_identity_enrich.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_identity_provider.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_ingest_roundtrip.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_isolation.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_launch_targets.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_live_detection.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_live_detection_store.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_managed_hooks.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_mcp_audit_proxy.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_mcp_concealed_text.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_mcp_inventory.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_mcp_inventory_event.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_mcp_meta_fingerprint.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_mcp_proxy_packaged.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_mcp_schema.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_mitre_dlp.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_opensre_adapter.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_operator_attribution.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_otel_ingest.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_paths.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_powershell_encoding.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_redaction.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_replay.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_sentinel.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_serve_script_binds_loopback.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_shared_install.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_sigma_pack.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_static_serving.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_subagent_lifecycle.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_suite_leaves_real_home_alone.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_tool_policy.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_tool_policy_agent_cli.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_tool_policy_git_hooks.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_trail_anchor.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_trail_chain.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_trail_concurrency.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_trail_merkle.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_trusted_host.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_unattended_agent_policy.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_version.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tests/test_yaml_detection_rules.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tools/mcp_audit_proxy.py +0 -0
- {agentmetry-0.9.4 → agentmetry-0.9.5}/tools/vault_fs_server.py +0 -0
|
@@ -100,3 +100,11 @@ docs/*-audit-*.md
|
|
|
100
100
|
# pytest-cov data file
|
|
101
101
|
.coverage
|
|
102
102
|
.coverage.*
|
|
103
|
+
|
|
104
|
+
# Demo video renders and their working files (scripts/make_demo_video.py).
|
|
105
|
+
# The site serves its own copy from ai-audit-watch/public/video/.
|
|
106
|
+
docs/assets/*.mp4
|
|
107
|
+
docs/assets/agentmetry-demo-poster.png
|
|
108
|
+
docs/assets/agentmetry-linkedin-*-poster.png
|
|
109
|
+
docs/assets/voice/
|
|
110
|
+
docs/assets/demo-stills/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agentmetry
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.5
|
|
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
|
|
@@ -37,7 +37,7 @@ Provides-Extra: dev
|
|
|
37
37
|
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
38
38
|
Requires-Dist: pytest-cov>=6.0; extra == 'dev'
|
|
39
39
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
40
|
-
Requires-Dist: ruff==0.16.
|
|
40
|
+
Requires-Dist: ruff==0.16.10; extra == 'dev'
|
|
41
41
|
Description-Content-Type: text/markdown
|
|
42
42
|
|
|
43
43
|
# Agentmetry
|
|
@@ -170,7 +170,7 @@ def attach_atlas(tool: Any) -> None:
|
|
|
170
170
|
# adversary technique a whole sequence is evidence of, which is a claim only a
|
|
171
171
|
# rule can make and never a single call.
|
|
172
172
|
#
|
|
173
|
-
# Deliberately small. Of the
|
|
173
|
+
# Deliberately small. Of the fourteen published rules, one describes an ATLAS
|
|
174
174
|
# technique; the rest are host and operations behaviour that ATT&CK already
|
|
175
175
|
# covers, and tagging them would put an AI-threat label on a shell command.
|
|
176
176
|
# The empty entries are the design, not a backlog.
|
|
@@ -1489,7 +1489,7 @@ def map_hook(hook_name: str, data: dict[str, Any]) -> dict[str, Any] | None:
|
|
|
1489
1489
|
|
|
1490
1490
|
|
|
1491
1491
|
def _emit_hook_stdout(hook_name: str) -> None:
|
|
1492
|
-
"""Antigravity requires JSON on stdout;
|
|
1492
|
+
"""Antigravity requires JSON on stdout; the rest print a decision only when enforcing."""
|
|
1493
1493
|
source = _source_app()
|
|
1494
1494
|
enforce = os.environ.get("AGENTMETRY_ENFORCE", "").strip().lower()
|
|
1495
1495
|
|
|
@@ -1546,6 +1546,49 @@ def _is_blocking_hook(hook_name: str, data: dict[str, Any]) -> bool:
|
|
|
1546
1546
|
#: the vendor's published schema; the rest inherit it.
|
|
1547
1547
|
_CLAUDE_PROTOCOL = frozenset({"claude", "qwen", "qoder", "codebuddy", "kimi"})
|
|
1548
1548
|
|
|
1549
|
+
#: Codex's deny reason must be non-empty, or Codex rejects the whole output.
|
|
1550
|
+
_CODEX_DEFAULT_REASON = "Blocked by Agentmetry policy"
|
|
1551
|
+
|
|
1552
|
+
|
|
1553
|
+
def _codex_decision_output(hook_name: str, decision: str, reason: str) -> dict[str, Any]:
|
|
1554
|
+
"""Codex's hook schema, which is Claude-shaped and strict where Claude is not.
|
|
1555
|
+
|
|
1556
|
+
Checked against Codex's parser (openai/codex `codex-rs/hooks`, 806d2d1,
|
|
1557
|
+
2026-10-08), not only its docs, because every way this goes wrong fails
|
|
1558
|
+
open. Codex parses hook stdout with `deny_unknown_fields`. Output it cannot
|
|
1559
|
+
parse, or parses and rejects, marks the hook run `Failed` and the tool runs.
|
|
1560
|
+
Until this fix Codex was sent Cursor's `{"permission": "deny"}`, which is an
|
|
1561
|
+
unknown field, so every Agentmetry deny on Codex ran the tool while the
|
|
1562
|
+
trail recorded `outcome: denied`.
|
|
1563
|
+
|
|
1564
|
+
The other rejections, each also a silent allow:
|
|
1565
|
+
|
|
1566
|
+
* `permissionDecision: "deny"` without a non-empty reason.
|
|
1567
|
+
* `permissionDecision: "ask"`, "parsed but not supported yet".
|
|
1568
|
+
* `permissionDecision: "allow"` without `updatedInput`.
|
|
1569
|
+
|
|
1570
|
+
So on PreToolUse a deny always carries a reason, an ask becomes a deny, and
|
|
1571
|
+
an allow is an empty object, which Codex reads as no objection. On
|
|
1572
|
+
PermissionRequest the behaviour is allow|deny, and an ask becomes a deny.
|
|
1573
|
+
"""
|
|
1574
|
+
reason = reason.strip() or _CODEX_DEFAULT_REASON
|
|
1575
|
+
if decision != "allow":
|
|
1576
|
+
decision = "deny"
|
|
1577
|
+
if hook_name == "PermissionRequest":
|
|
1578
|
+
verdict: dict[str, Any] = {"behavior": decision}
|
|
1579
|
+
if decision == "deny":
|
|
1580
|
+
verdict["message"] = reason
|
|
1581
|
+
return {"hookSpecificOutput": {"hookEventName": hook_name, "decision": verdict}}
|
|
1582
|
+
if decision == "allow":
|
|
1583
|
+
return {}
|
|
1584
|
+
return {
|
|
1585
|
+
"hookSpecificOutput": {
|
|
1586
|
+
"hookEventName": hook_name,
|
|
1587
|
+
"permissionDecision": "deny",
|
|
1588
|
+
"permissionDecisionReason": reason,
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1549
1592
|
|
|
1550
1593
|
def _decision_output(hook_name: str, decision: str, reason: str = "") -> dict[str, Any]:
|
|
1551
1594
|
"""A hook decision in the exact shape the calling agent honours.
|
|
@@ -1568,10 +1611,13 @@ def _decision_output(hook_name: str, decision: str, reason: str = "") -> dict[st
|
|
|
1568
1611
|
The safe direction, never the other way.
|
|
1569
1612
|
* Cursor: top-level `permission`. Unchanged, because it already worked.
|
|
1570
1613
|
* Antigravity: top-level `decision`.
|
|
1614
|
+
* Codex: Claude-shaped, but strict. See `_codex_decision_output`.
|
|
1571
1615
|
"""
|
|
1572
1616
|
source = _source_app()
|
|
1573
1617
|
if source == "antigravity":
|
|
1574
1618
|
return {"decision": decision}
|
|
1619
|
+
if source == "codex":
|
|
1620
|
+
return _codex_decision_output(hook_name, decision, reason)
|
|
1575
1621
|
if source in _CLAUDE_PROTOCOL:
|
|
1576
1622
|
if hook_name == "PermissionRequest":
|
|
1577
1623
|
behaviour = "allow" if decision == "allow" else "deny"
|
|
@@ -52,7 +52,7 @@ dependencies = [
|
|
|
52
52
|
# so a linter release could turn a green branch red with no change to this repo.
|
|
53
53
|
# It did: 0.16.0 widened the default rule set and CI reported 132 errors against
|
|
54
54
|
# code that passed locally on 0.15.x. Bump this deliberately, not by accident.
|
|
55
|
-
dev = ["pytest>=8.0", "pytest-asyncio>=0.24", "pytest-cov>=6.0", "ruff==0.16.
|
|
55
|
+
dev = ["pytest>=8.0", "pytest-asyncio>=0.24", "pytest-cov>=6.0", "ruff==0.16.10"]
|
|
56
56
|
|
|
57
57
|
[project.urls]
|
|
58
58
|
Homepage = "https://agentmetry.ai"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Codex hook output schemas
|
|
2
|
+
|
|
3
|
+
Copied unchanged from [openai/codex](https://github.com/openai/codex) (Apache-2.0),
|
|
4
|
+
`codex-rs/hooks/schema/generated/`, at commit
|
|
5
|
+
`806d2d1cc194548170a606946aa6c68e99927e58` (2026-10-08).
|
|
6
|
+
|
|
7
|
+
`tests/test_hook_decision_format.py` validates every decision the hook prints
|
|
8
|
+
for Codex against these, because Codex parses hook output with
|
|
9
|
+
`deny_unknown_fields` and runs the tool when it cannot. When Codex changes its
|
|
10
|
+
schema, refresh both files from the same path and rerun that test.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"additionalProperties": false,
|
|
4
|
+
"definitions": {
|
|
5
|
+
"PermissionRequestBehaviorWire": {
|
|
6
|
+
"enum": [
|
|
7
|
+
"allow",
|
|
8
|
+
"deny"
|
|
9
|
+
],
|
|
10
|
+
"type": "string"
|
|
11
|
+
},
|
|
12
|
+
"PermissionRequestDecisionWire": {
|
|
13
|
+
"additionalProperties": false,
|
|
14
|
+
"properties": {
|
|
15
|
+
"behavior": {
|
|
16
|
+
"$ref": "#/definitions/PermissionRequestBehaviorWire"
|
|
17
|
+
},
|
|
18
|
+
"interrupt": {
|
|
19
|
+
"default": false,
|
|
20
|
+
"description": "Reserved for future short-circuiting semantics.\n\nPermissionRequest hooks currently fail closed if this field is `true`.",
|
|
21
|
+
"type": "boolean"
|
|
22
|
+
},
|
|
23
|
+
"message": {
|
|
24
|
+
"default": null,
|
|
25
|
+
"type": "string"
|
|
26
|
+
},
|
|
27
|
+
"updatedInput": {
|
|
28
|
+
"default": null,
|
|
29
|
+
"description": "Reserved for a future input-rewrite capability.\n\nPermissionRequest hooks currently fail closed if this field is present."
|
|
30
|
+
},
|
|
31
|
+
"updatedPermissions": {
|
|
32
|
+
"default": null,
|
|
33
|
+
"description": "Reserved for a future permission-rewrite capability.\n\nPermissionRequest hooks currently fail closed if this field is present."
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"required": [
|
|
37
|
+
"behavior"
|
|
38
|
+
],
|
|
39
|
+
"type": "object"
|
|
40
|
+
},
|
|
41
|
+
"PermissionRequestHookSpecificOutputWire": {
|
|
42
|
+
"additionalProperties": false,
|
|
43
|
+
"properties": {
|
|
44
|
+
"decision": {
|
|
45
|
+
"allOf": [
|
|
46
|
+
{
|
|
47
|
+
"$ref": "#/definitions/PermissionRequestDecisionWire"
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
"default": null
|
|
51
|
+
},
|
|
52
|
+
"hookEventName": {
|
|
53
|
+
"const": "PermissionRequest",
|
|
54
|
+
"type": "string"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"required": [
|
|
58
|
+
"hookEventName"
|
|
59
|
+
],
|
|
60
|
+
"type": "object"
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"properties": {
|
|
64
|
+
"continue": {
|
|
65
|
+
"default": true,
|
|
66
|
+
"type": "boolean"
|
|
67
|
+
},
|
|
68
|
+
"hookSpecificOutput": {
|
|
69
|
+
"allOf": [
|
|
70
|
+
{
|
|
71
|
+
"$ref": "#/definitions/PermissionRequestHookSpecificOutputWire"
|
|
72
|
+
}
|
|
73
|
+
],
|
|
74
|
+
"default": null
|
|
75
|
+
},
|
|
76
|
+
"stopReason": {
|
|
77
|
+
"default": null,
|
|
78
|
+
"type": "string"
|
|
79
|
+
},
|
|
80
|
+
"suppressOutput": {
|
|
81
|
+
"default": false,
|
|
82
|
+
"type": "boolean"
|
|
83
|
+
},
|
|
84
|
+
"systemMessage": {
|
|
85
|
+
"default": null,
|
|
86
|
+
"type": "string"
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"title": "permission-request.command.output",
|
|
90
|
+
"type": "object"
|
|
91
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"additionalProperties": false,
|
|
4
|
+
"definitions": {
|
|
5
|
+
"PreToolUseDecisionWire": {
|
|
6
|
+
"enum": [
|
|
7
|
+
"approve",
|
|
8
|
+
"block"
|
|
9
|
+
],
|
|
10
|
+
"type": "string"
|
|
11
|
+
},
|
|
12
|
+
"PreToolUseHookSpecificOutputWire": {
|
|
13
|
+
"additionalProperties": false,
|
|
14
|
+
"properties": {
|
|
15
|
+
"additionalContext": {
|
|
16
|
+
"default": null,
|
|
17
|
+
"type": "string"
|
|
18
|
+
},
|
|
19
|
+
"hookEventName": {
|
|
20
|
+
"const": "PreToolUse",
|
|
21
|
+
"type": "string"
|
|
22
|
+
},
|
|
23
|
+
"permissionDecision": {
|
|
24
|
+
"allOf": [
|
|
25
|
+
{
|
|
26
|
+
"$ref": "#/definitions/PreToolUsePermissionDecisionWire"
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
"default": null
|
|
30
|
+
},
|
|
31
|
+
"permissionDecisionReason": {
|
|
32
|
+
"default": null,
|
|
33
|
+
"type": "string"
|
|
34
|
+
},
|
|
35
|
+
"updatedInput": {
|
|
36
|
+
"default": null
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"required": [
|
|
40
|
+
"hookEventName"
|
|
41
|
+
],
|
|
42
|
+
"type": "object"
|
|
43
|
+
},
|
|
44
|
+
"PreToolUsePermissionDecisionWire": {
|
|
45
|
+
"enum": [
|
|
46
|
+
"allow",
|
|
47
|
+
"deny",
|
|
48
|
+
"ask"
|
|
49
|
+
],
|
|
50
|
+
"type": "string"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"properties": {
|
|
54
|
+
"continue": {
|
|
55
|
+
"default": true,
|
|
56
|
+
"type": "boolean"
|
|
57
|
+
},
|
|
58
|
+
"decision": {
|
|
59
|
+
"allOf": [
|
|
60
|
+
{
|
|
61
|
+
"$ref": "#/definitions/PreToolUseDecisionWire"
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"default": null
|
|
65
|
+
},
|
|
66
|
+
"hookSpecificOutput": {
|
|
67
|
+
"allOf": [
|
|
68
|
+
{
|
|
69
|
+
"$ref": "#/definitions/PreToolUseHookSpecificOutputWire"
|
|
70
|
+
}
|
|
71
|
+
],
|
|
72
|
+
"default": null
|
|
73
|
+
},
|
|
74
|
+
"reason": {
|
|
75
|
+
"default": null,
|
|
76
|
+
"type": "string"
|
|
77
|
+
},
|
|
78
|
+
"stopReason": {
|
|
79
|
+
"default": null,
|
|
80
|
+
"type": "string"
|
|
81
|
+
},
|
|
82
|
+
"suppressOutput": {
|
|
83
|
+
"default": false,
|
|
84
|
+
"type": "boolean"
|
|
85
|
+
},
|
|
86
|
+
"systemMessage": {
|
|
87
|
+
"default": null,
|
|
88
|
+
"type": "string"
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"title": "pre-tool-use.command.output",
|
|
92
|
+
"type": "object"
|
|
93
|
+
}
|
|
@@ -4,7 +4,7 @@ ATT&CK describes what one tool call did. ATLAS at the detection level describes
|
|
|
4
4
|
the adversary technique the sequence is evidence of, which is a claim only a
|
|
5
5
|
rule is in a position to make.
|
|
6
6
|
|
|
7
|
-
The mapping is deliberately one rule wide. Of the
|
|
7
|
+
The mapping is deliberately one rule wide. Of the fourteen published rules, one
|
|
8
8
|
describes an ATLAS technique; the rest are host and operations behaviour that
|
|
9
9
|
ATT&CK already covers, and labelling those would put an AI-threat technique on
|
|
10
10
|
a shell command. Most of this file exists to hold that line, because the
|
|
@@ -19,6 +19,10 @@ Schemas, from each vendor's hook documentation:
|
|
|
19
19
|
Claude Code PermissionRequest hookSpecificOutput.decision.behavior allow|deny, no ask
|
|
20
20
|
Cursor top-level permission allow|deny|ask
|
|
21
21
|
Antigravity top-level decision
|
|
22
|
+
Codex Claude-shaped, strict; see the Codex section
|
|
23
|
+
|
|
24
|
+
Codex was the one agent the 0.9.0 fix missed. It kept receiving Cursor's key,
|
|
25
|
+
and its parser rejects unknown fields, so every deny ran the tool.
|
|
22
26
|
"""
|
|
23
27
|
|
|
24
28
|
from __future__ import annotations
|
|
@@ -131,6 +135,97 @@ def test_antigravity_output_is_unchanged(monkeypatch):
|
|
|
131
135
|
assert _decision(monkeypatch, "antigravity", "PreToolUse", "deny") == {"decision": "deny"}
|
|
132
136
|
|
|
133
137
|
|
|
138
|
+
# --- Codex: checked against its own schema and parser rules ------------------
|
|
139
|
+
#
|
|
140
|
+
# Codex parses hook stdout with `deny_unknown_fields` (openai/codex
|
|
141
|
+
# codex-rs/hooks, 806d2d1). Anything it cannot parse, or parses and rejects,
|
|
142
|
+
# marks the hook `Failed` and the tool runs. So a shape check is not enough:
|
|
143
|
+
# these validate against Codex's published schema, then apply the parser's
|
|
144
|
+
# rejections that the schema cannot express.
|
|
145
|
+
|
|
146
|
+
_CODEX_SCHEMAS = Path(__file__).parent / "fixtures" / "codex_hooks"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _codex_blocks(hook: str, out: dict) -> bool:
|
|
150
|
+
"""What Codex does with this output: True blocks, False runs the tool.
|
|
151
|
+
|
|
152
|
+
Raises if Codex would mark the hook `Failed`, which also runs the tool, but
|
|
153
|
+
is a malformed decision rather than a deliberate allow.
|
|
154
|
+
"""
|
|
155
|
+
import jsonschema
|
|
156
|
+
|
|
157
|
+
name = {"PreToolUse": "pre-tool-use", "PermissionRequest": "permission-request"}[hook]
|
|
158
|
+
schema = json.loads((_CODEX_SCHEMAS / f"{name}.command.output.schema.json").read_text())
|
|
159
|
+
jsonschema.validate(out, schema)
|
|
160
|
+
specific = out.get("hookSpecificOutput") or {}
|
|
161
|
+
if hook == "PermissionRequest":
|
|
162
|
+
return (specific.get("decision") or {}).get("behavior") == "deny"
|
|
163
|
+
decision = specific.get("permissionDecision")
|
|
164
|
+
if decision == "ask":
|
|
165
|
+
raise AssertionError("Codex rejects permissionDecision:ask")
|
|
166
|
+
if decision == "allow" and "updatedInput" not in specific:
|
|
167
|
+
raise AssertionError("Codex rejects permissionDecision:allow without updatedInput")
|
|
168
|
+
if decision == "deny":
|
|
169
|
+
if not str(specific.get("permissionDecisionReason") or "").strip():
|
|
170
|
+
raise AssertionError("Codex rejects a deny without a non-empty reason")
|
|
171
|
+
return True
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_codex_is_never_sent_the_cursor_key(monkeypatch):
|
|
176
|
+
"""The defect: Codex rejected `permission` as an unknown field and ran the tool."""
|
|
177
|
+
import jsonschema
|
|
178
|
+
|
|
179
|
+
old = {"permission": "deny"}
|
|
180
|
+
schema = json.loads((_CODEX_SCHEMAS / "pre-tool-use.command.output.schema.json").read_text())
|
|
181
|
+
with pytest.raises(jsonschema.ValidationError):
|
|
182
|
+
jsonschema.validate(old, schema)
|
|
183
|
+
for hook in ("PreToolUse", "PermissionRequest"):
|
|
184
|
+
for decision in ("allow", "deny", "ask"):
|
|
185
|
+
assert "permission" not in _decision(monkeypatch, "codex", hook, decision), (hook, decision)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@pytest.mark.parametrize("hook", ["PreToolUse", "PermissionRequest"])
|
|
189
|
+
@pytest.mark.parametrize("reason", ["dlp:aws_key", ""])
|
|
190
|
+
def test_codex_deny_blocks(monkeypatch, hook, reason):
|
|
191
|
+
"""Including an empty reason, which Codex would otherwise reject as a fail-open."""
|
|
192
|
+
assert _codex_blocks(hook, _decision(monkeypatch, "codex", hook, "deny", reason))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@pytest.mark.parametrize("hook", ["PreToolUse", "PermissionRequest"])
|
|
196
|
+
def test_codex_ask_becomes_deny_never_allow(monkeypatch, hook):
|
|
197
|
+
"""Codex marks an ask `Failed` and runs the tool, so an ask has to be a deny."""
|
|
198
|
+
assert _codex_blocks(hook, _decision(monkeypatch, "codex", hook, "ask", "tool_policy:x"))
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@pytest.mark.parametrize("hook", ["PreToolUse", "PermissionRequest"])
|
|
202
|
+
def test_codex_allow_is_a_clean_allow(monkeypatch, hook):
|
|
203
|
+
assert not _codex_blocks(hook, _decision(monkeypatch, "codex", hook, "allow"))
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_codex_deny_keeps_the_rule_in_the_reason(monkeypatch):
|
|
207
|
+
out = _decision(monkeypatch, "codex", "PreToolUse", "deny", "tool_policy:block_shell_rm")
|
|
208
|
+
assert out["hookSpecificOutput"]["permissionDecisionReason"] == "tool_policy:block_shell_rm"
|
|
209
|
+
out = _decision(monkeypatch, "codex", "PermissionRequest", "deny", "dlp:aws_key")
|
|
210
|
+
assert out["hookSpecificOutput"]["decision"]["message"] == "dlp:aws_key"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_a_real_policy_block_on_codex_blocks(monkeypatch, capsys):
|
|
214
|
+
"""End to end through hook_main, with Codex's own Bash tool input."""
|
|
215
|
+
monkeypatch.setenv("AGENTMETRY_SOURCE_APP", "codex")
|
|
216
|
+
_block(monkeypatch, "block_shell_rm")
|
|
217
|
+
monkeypatch.setattr(
|
|
218
|
+
ingest,
|
|
219
|
+
"read_hook_stdin",
|
|
220
|
+
lambda: ({"session_id": "s1", "hook_event_name": "PreToolUse", "tool_name": "Bash",
|
|
221
|
+
"tool_input": {"command": "rm -rf /"}}, False),
|
|
222
|
+
)
|
|
223
|
+
ingest.hook_main("PreToolUse")
|
|
224
|
+
out = _last_json(capsys)
|
|
225
|
+
assert _codex_blocks("PreToolUse", out)
|
|
226
|
+
assert out["hookSpecificOutput"]["permissionDecisionReason"] == "tool_policy:block_shell_rm"
|
|
227
|
+
|
|
228
|
+
|
|
134
229
|
# --- through the real hook path ---------------------------------------------
|
|
135
230
|
|
|
136
231
|
|
|
@@ -241,3 +241,43 @@ def test_readme_sigma_count_matches_the_pack():
|
|
|
241
241
|
assert int(match.group(1)) == shipped, (
|
|
242
242
|
f"README claims {match.group(1)} Sigma rules, pack ships {shipped}"
|
|
243
243
|
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def test_readme_collected_count_matches_pytest():
|
|
247
|
+
"""The README quotes the suite size. Collection is the check.
|
|
248
|
+
|
|
249
|
+
A full `pytest -q` is too slow to pin the pass count here. The collected
|
|
250
|
+
total moves on the same commits, and it is what a reader can reproduce
|
|
251
|
+
without waiting on the suite.
|
|
252
|
+
"""
|
|
253
|
+
import subprocess
|
|
254
|
+
import sys
|
|
255
|
+
|
|
256
|
+
match = re.search(r"(\d+) tests collected", _readme())
|
|
257
|
+
assert match, "README no longer states a collected test count"
|
|
258
|
+
|
|
259
|
+
root = Path(__file__).resolve().parents[1]
|
|
260
|
+
proc = subprocess.run(
|
|
261
|
+
[sys.executable, "-m", "pytest", "--collect-only", "-q", "-p", "no:cacheprovider"],
|
|
262
|
+
cwd=root,
|
|
263
|
+
capture_output=True,
|
|
264
|
+
text=True,
|
|
265
|
+
check=False,
|
|
266
|
+
)
|
|
267
|
+
combined = proc.stdout + "\n" + proc.stderr
|
|
268
|
+
assert proc.returncode == 0, combined
|
|
269
|
+
collected = re.search(r"(\d+) tests collected", combined)
|
|
270
|
+
assert collected, combined
|
|
271
|
+
assert int(match.group(1)) == int(collected.group(1)), (
|
|
272
|
+
f"README says {match.group(1)} tests collected; pytest collected {collected.group(1)}"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def test_readme_coverage_floor_matches_ci():
|
|
277
|
+
"""The durable coverage claim is the CI floor, not last Tuesday's percentage."""
|
|
278
|
+
match = re.search(r"CI fails the coverage job under (\d+)%", _readme())
|
|
279
|
+
assert match, "README no longer states the CI coverage floor"
|
|
280
|
+
ci = Path(__file__).resolve().parents[3] / ".github" / "workflows" / "ci.yml"
|
|
281
|
+
floor = re.search(r"cov-fail-under=(\d+)", ci.read_text(encoding="utf-8"))
|
|
282
|
+
assert floor, "ci.yml lost cov-fail-under"
|
|
283
|
+
assert match.group(1) == floor.group(1)
|
|
@@ -8,12 +8,12 @@ Run from the orchestrator:
|
|
|
8
8
|
|
|
9
9
|
The Sigma pack that already existed covers the recorder's own health: heartbeat
|
|
10
10
|
silence, degraded coverage, MCP schema drift, denial bursts. Useful, and none of
|
|
11
|
-
it is the product. The
|
|
11
|
+
it is the product. The published sequence detections, which are what a SOC would
|
|
12
12
|
actually route on, had no Sigma representation at all, so a Splunk or Sentinel
|
|
13
13
|
team wanting to alert on `credential-exfil` had to write the search themselves
|
|
14
14
|
from a schema document.
|
|
15
15
|
|
|
16
|
-
Hand-writing
|
|
16
|
+
Hand-writing them would put rule ids, severities and MITRE ids in a second
|
|
17
17
|
place that drifts from the first. So the metadata is harvested by replaying the
|
|
18
18
|
benchmark corpus through the real engine and reading the `Detection` objects it
|
|
19
19
|
emits. A severity that changes in `rules.py` changes here on the next run, and
|
|
@@ -107,7 +107,7 @@ def _stable_uuid(rule_id: str, severity: str = "") -> str:
|
|
|
107
107
|
|
|
108
108
|
Sigma requires a UUID and consumers key on it, so running this twice must
|
|
109
109
|
produce no diff. Severity joins the seed only for a split rule, which keeps
|
|
110
|
-
the
|
|
110
|
+
the unsplit ids exactly where they are.
|
|
111
111
|
"""
|
|
112
112
|
seed = f"agentmetry.sigma.{rule_id}" + (f".{severity}" if severity else "")
|
|
113
113
|
return str(uuid.UUID(bytes=hashlib.sha256(seed.encode()).digest()[:16], version=5))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|