Add plugin validation pipeline + --strict-plugins (M5)#83
Conversation
Plugins were previously loaded with a single `callable(loaded)` check;
malformed entry points, signature mismatches, missing metadata,
id-collisions, and findings emitted under foreign check IDs all slipped
through silently. This change wraps every entry point in a five-gate
validator and a runtime result wrapper.
Load-time gates (`checks/plugin_validation.py::validate_entry_point`):
1. load — `entry_point.load()` exceptions are captured.
2. signature — must accept exactly one required positional param.
3. metadata — `AGENTS_SHIPGATE_METADATA` must parse as `CheckMetadata`;
both `id` and `check_id` are accepted as the identifier key (M5
alias for symmetry with `Finding.check_id`).
4. id_collision — plugin check IDs cannot shadow built-ins (including
legacy aliases) or earlier plugins.
5. bad_floor — `floor_severity` cannot exceed `default_severity`.
Runtime validation (`run_validated_plugin`):
- Exceptions during the plugin call are captured into
`loaded_plugins[].runtime_errors`; the scan continues.
- Returned values must be `list[Finding]`; otherwise dropped.
- Findings whose `check_id` differs from the plugin's declared id are
dropped — a plugin cannot smuggle findings under another check ID.
This is the load-bearing trust rule.
`loaded_plugins[]` gains three additive fields on every entry:
`validation_status`, `validation_errors`, `runtime_errors`. Fields stay
optional in the v0.16 JSON Schema (no schema-version bump) so the M5
PR remains purely additive; a future bump may promote them to required.
New CLI flag `--strict-plugins`: exit non-zero (code 4) if any plugin
failed validation or produced runtime errors. Default lenient mode
preserves v0.x behavior.
`CheckMetadata` gains `floor_severity: Severity | None` with a model
validator rejecting floor > default. The field is the foundation for
M1's manifest-side severity-override floor and is unused at the
catalog level today (all built-ins default to None).
Tests: 18 new cases in `tests/test_plugin_validation.py` (one per gate
plus runtime + strict-mode behavior); existing
`tests/test_plugins.py::test_report_includes_loaded_plugin_provenance`
updated for the new fields. Existing plugins using
`AGENTS_SHIPGATE_METADATA = {"id": ...}` continue to work unchanged.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
e6176ff to
5157336
Compare
|
Thanks for the thorough review. All three findings addressed in 5157336 (force-pushed). P1.1 — schema downgrade from v0.17 to v0.16. Confirmed. My branch forked at P1.2 — P1 follow-on — plugin validation fields are now P2 — signature gate misses required keyword-only params. Confirmed and fixed in
Full pytest suite green (Python 3.13 in a fresh Diff stat vs No file changes outside the M5 surface and the additive |
Summary
Hardens the third-party plugin loader on
agents_shipgate.checksentry points. Adds five load-time validation gates, a runtime result wrapper that blocks check-ID smuggling, and an opt-in--strict-pluginsflag. Promotes three newloaded_plugins[]fields to required at v0.17. Existing plugins continue to work unchanged.Part of the v0.17 Trust Hardening Pass (M5 in the trust-hardening plan).
What changes
Load-time gates (
src/agents_shipgate/checks/plugin_validation.py):load—entry_point.load()exceptions captured (no longer aborts the scan).signature— must accept exactly one required positional parameter; rejects required keyword-only parameters since the scanner calls plugins asplugin(context)with no kwargs.metadata—AGENTS_SHIPGATE_METADATAmust parse asCheckMetadata; bothidandcheck_idare accepted as the identifier key (v0.17 alias for symmetry withFinding.check_id).id_collision— plugin check IDs cannot shadow built-ins (including legacy aliases) or earlier plugins in the same scan.bad_floor—floor_severitycannot exceeddefault_severity.Runtime validation (
run_validated_plugin):loaded_plugins[].runtime_errors; scan continues.list[Finding]; otherwise dropped.check_iddiffers from the plugin's declared id are dropped — a plugin cannot smuggle findings under another check ID. This is the load-bearing trust rule.Report surface (
loaded_plugins[]— required at v0.17):validation_status:valid | load_failed | bad_signature | bad_metadata | id_collision | bad_floorvalidation_errors: list[str]— empty for clean plugins.runtime_errors: list[str]— empty for clean plugins.The v0.7 frozen schema continues to pin the original 5-field required list (per the immutability contract). v0.17's 8-field required list is locked by a new paired test
test_v17_loaded_plugins_required_includes_validation_fields.CLI (
--strict-plugins):--strict-pluginsexits 4 if any plugin failed validation or produced runtime errors.CheckMetadataextension:idorcheck_idviaAliasChoices.floor_severity: Severity | Nonewith a model validator that rejects floor > default. Foundation for M1 (manifest-side severity-override floor); unused at the catalog level today (all built-ins default to None).Backward compatibility
AGENTS_SHIPGATE_METADATA = {"id": ...}work unchanged.loaded_plugins[]shape is additive — only new fields appear.requiredis the right move at v0.17 since the scanner always emits them and v0.17 is the current contract onmain. The v0.7 frozen schema preserves the pre-M5 shape per the schema immutability rule.Test plan
tests/test_plugin_validation.pycovering each gate (including the keyword-only signature regression), runtime validation, and--strict-pluginsexit behaviortests/test_plugins.py::test_report_includes_loaded_plugin_provenanceupdated for the new fieldstests/test_reports.py::test_v17_loaded_plugins_required_includes_validation_fieldslocks the v0.17 8-field required shapepytest --no-header -qgreen on Python 3.13)python scripts/generate_schemas.py --checkclean (no schema drift); CI runs this step before testsdocs/checks.json,docs/report-schema.v0.17.json, andllms-full.txtregeneratedFiles
src/agents_shipgate/checks/plugin_validation.pystrict_failure_messagessrc/agents_shipgate/checks/registry.py_plugin_check_recordsto use validator; back-compat_plugin_checksfilter;run_checksdelegates plugins torun_validated_pluginsrc/agents_shipgate/core/models.pyCheckMetadataacceptsid/check_idalias; addsfloor_severitywith model validatorsrc/agents_shipgate/cli/_helpers.py_apply_strict_pluginspost-scan helper; threadsstrict_pluginsthrough_run_multi_scansrc/agents_shipgate/cli/_register_scan.py--strict-pluginsflag wired through single + multi-scan pathsscripts/generate_schemas.pyloaded_plugins[]fields to v0.17 required listtests/test_plugin_validation.pytests/test_plugins.pyloaded_plugins[]fieldstests/test_reports.pytest_v17_loaded_plugins_required_includes_validation_fieldsSTABILITY.mddocs/checks.mddocs/checks.jsonfloor_severity: nullto every catalog entry)docs/report-schema.v0.17.jsonloaded_plugins[].itemsllms-full.txt🤖 Generated with Claude Code