Skip to content

Commit bdbcae1

Browse files
Update STABILITY.md to reflect the per-scan registry + dispatcher-managed runtime wrapper (PR #111 review)
Two prose claims in the v0.20 "Third-party adapter discovery" subsection were stale after the round-2 and round-3 review fixes: 1. "register on the live REGISTRY" — wrong since commit 14b943a. Adapters now register on a per-scan ``AdapterRegistry.clone()`` built at the start of each ``run_scan`` / ``inspect_sources``. The global ``REGISTRY`` stays builtin-only across the process. 2. "captures runtime exceptions … for callers that wrap the invocation themselves" — wrong since commit 943afda. The dispatcher in ``_load_sources`` now routes EVERY third-party adapter ``load()`` call through ``run_validated_adapter`` (both pass 1 per-source and pass 2 per-scan loops). Callers do not need to opt in. Rewrote the section to: - Document the per-scan registry contract explicitly and the two invariants it guarantees (``--no-plugins`` honest across scans; collision detection honest across scans). - Document the dispatcher's two trust mechanisms separately: artifact-class smuggling prevention (already covered for built-ins via ``_absorb`` ``TypeError``) and runtime-error capture (new third-party-only path through ``run_validated_adapter``). - Note that ``doctor`` (``inspect_sources``) uses the same per-scan registry + discovery + dispatcher path as ``scan`` and surfaces ``loaded_adapters[]`` in its payload. - Tighten the bad_protocol gate description to match the v0.20 PR #111 P2 #4 fix: "at least 3 positional slots (or *args)" and "no required keyword-only parameters" are now both gated, in addition to the "no more than 3 required positional" rule. - Add a new "Manifest ``tool_sources[].type``" paragraph documenting the Literal→str relaxation, the ``BUILTIN_TOOL_SOURCE_TYPES`` constant for documentation, the ``BUILTIN_PER_SCAN_ONLY_TOOL_SOURCE_TYPES`` rejection at manifest-load, and the ConfigError(2) dispatch-time fallback for unknown types. Pure docs change. ruff clean; public-surface contract tests pass (the existing drift guards already covered cross-doc consistency for the wedge positioning, gating signal, and schema version — none of which this edit touches). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 943afda commit bdbcae1

1 file changed

Lines changed: 15 additions & 3 deletions

File tree

STABILITY.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -377,17 +377,29 @@ Plugins that pass every gate run with the same trust as built-ins. Runtime valid
377377

378378
Third-party adapters register through the `agents_shipgate.adapters` Python entry-point group and provide a class (or instance) satisfying the `ToolSourceAdapter` Protocol — a `source_type: str` ClassVar, a `scope: Literal["per_source", "per_scan"]` ClassVar, an `artifact_class: type | None` ClassVar, and a `load(source, base_dir, manifest)` method returning `LoadedAdapterResult`. Discovery is gated by the same `AGENTS_SHIPGATE_ENABLE_PLUGINS=1` env var as plugin checks; `--no-plugins` forces it off.
379379

380-
Every discovered entry point is checked against four load-time gates before it can register on the scan's `REGISTRY`:
380+
Every discovered entry point is checked against four load-time gates before it can register on the scan's adapter registry:
381381

382382
1. **load**`entry_point.load()` must not raise. Captured as `validation_status="load_failed"`.
383-
2. **bad_protocol** — the loaded value (a class is instantiated with no args; an instance is used directly) must have all three ClassVars (`source_type` non-empty string, `scope`, `artifact_class`) and a callable `load` method accepting ≤ 3 required positional parameters. Captured as `validation_status="bad_protocol"`.
383+
2. **bad_protocol** — the loaded value (a class is instantiated with no args; an instance is used directly) must have all three ClassVars (`source_type` non-empty string, `scope`, `artifact_class`) and a callable `load` method that accepts the three positional arguments `(source, base_dir, manifest)`: at least three positional slots (or `*args`), no more than three required positional parameters, and no required keyword-only parameters. Captured as `validation_status="bad_protocol"`.
384384
3. **bad_scope**`scope` must be exactly `"per_source"` or `"per_scan"`. Out-of-range values would be silently skipped by the dispatcher. Captured as `validation_status="bad_scope"`.
385385
4. **source_type_collision** — the adapter's `source_type` must not shadow a built-in (`mcp`, `openapi`, `langchain`, etc.) or another third-party adapter discovered earlier in the same scan. **This is the load-bearing trust rule** — without it, a malicious plugin could displace a built-in adapter and intercept every scan targeting that source type. Captured as `validation_status="source_type_collision"`.
386386

387-
Adapters that pass every gate are registered on the live `REGISTRY` and dispatched by the same pass-1/pass-2 loop as built-ins. The dispatcher's existing `_absorb` artifact-class check fires `TypeError` if a third-party adapter declares one `artifact_class` but returns an artifact of another (the same artifact-smuggling-prevention rule built-ins are subject to). `run_validated_adapter` (in `inputs/adapter_validation.py`) additionally captures runtime exceptions and wrong-return-type failures into `loaded_adapters[].runtime_errors` for callers that wrap the invocation themselves.
387+
**Per-scan registry contract.** Adapters that pass every gate register on a **per-scan clone** of the global `REGISTRY` (built at the start of each `run_scan` / `inspect_sources` via `AdapterRegistry.clone()`), NOT on the global itself. The global stays builtin-only across the lifetime of the process. This guarantees two trust invariants:
388+
389+
- **`--no-plugins` is per-scan honest.** A later in-process scan with `plugins_enabled=False` sees a fresh builtin-only clone — no third-party adapters carried over from a prior enabled scan.
390+
- **Collision detection is per-scan honest.** The collision set is the clone's builtins-only state, so two consecutive scans of the same valid third-party adapter both classify as `validation_status="valid"`, never as `source_type_collision` against the adapter's own previous registration.
391+
392+
The dispatcher walks the per-scan registry in the same pass-1 (per-source, in `tool_sources[]` declared order) / pass-2 (per-scan, in canonical registry order) loops it uses for built-ins. Two trust mechanisms protect the dispatch path:
393+
394+
- **Artifact-class smuggling prevention.** The dispatcher's `_absorb` step fires `TypeError` if any adapter (built-in or third-party) declares one `artifact_class` but returns an artifact of another type. This is the structural counterpart to the `Finding.check_id` smuggling rule for plugin checks.
395+
- **Runtime-error capture for third-party adapters.** Third-party adapters that raise at runtime do NOT abort the scan. The dispatcher routes their `load()` call through `run_validated_adapter` (from `inputs/adapter_validation.py`), which catches every exception, captures it into `loaded_adapters[].runtime_errors` on the matching row, and signals the dispatcher to skip absorbing the (None) result. Built-in adapters keep the direct call shape — a built-in raising means the scanner itself is broken and must abort loudly.
396+
397+
`doctor` (`inspect_sources`) uses the same per-scan registry clone + discovery + dispatcher path as `scan`, so manifests referencing third-party `tool_sources[].type` values are introspectable. The doctor payload surfaces `loaded_adapters[]` alongside the existing `policy_packs` field.
388398

389399
`--strict-plugins` (v0.17+) covers BOTH plugin and adapter failures from v0.20+ — any non-`valid` `loaded_plugins[]` row, any non-empty `loaded_plugins[].runtime_errors`, any non-`valid` `loaded_adapters[]` row, OR any non-empty `loaded_adapters[].runtime_errors` elevates the scan to exit code 4. Default behavior remains lenient — failures are recorded in the respective provenance arrays and the scan proceeds.
390400

401+
**Manifest `tool_sources[].type`.** The field is `str` (relaxed from a closed `Literal` in v0.20) so manifests can reference third-party per-source adapters by name. Built-in source types are enumerated in `BUILTIN_TOOL_SOURCE_TYPES` for documentation and tooling; per-scan-only built-ins (`n8n`, `openai_api`, `anthropic_api`, `validation`) are still rejected at manifest-load time with a routable error pointing the user to the dedicated top-level manifest section. Unknown source types — both genuine third-party names with no registered adapter and typos of built-in names — fail with `ConfigError` (exit 2) when the dispatcher's `AdapterRegistry.require` cannot resolve them. The exit-2 contract is unchanged from prior releases; the failure layer (manifest-load vs dispatch) may differ.
402+
391403
### Manifest Schema
392404

393405
The manifest schema version (`version: "0.1"`) is independent of the CLI

0 commit comments

Comments
 (0)