You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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>
Copy file name to clipboardExpand all lines: STABILITY.md
+15-3Lines changed: 15 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -377,17 +377,29 @@ Plugins that pass every gate run with the same trust as built-ins. Runtime valid
377
377
378
378
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.
379
379
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:
381
381
382
382
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"`.
384
384
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"`.
385
385
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"`.
386
386
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.
388
398
389
399
`--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.
390
400
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
+
391
403
### Manifest Schema
392
404
393
405
The manifest schema version (`version: "0.1"`) is independent of the CLI
0 commit comments