Skip to content

Commit 14b943a

Browse files
Address PR #111 review: per-scan registry + open type field + tighter signature gate
Four findings from PR #111 review — three P1s and one P2. All four addressed; 7 regression tests added (now 28 total in test_adapter_entry_point_discovery.py). P1 #1 — --no-plugins did not disable adapters after a prior enabled scan. Pre-fix, ``discover_third_party_adapters`` mutated the module-global ``REGISTRY``. A later in-process scan with ``plugins_enabled=False`` skipped discovery but ``_load_sources`` still resolved the already-registered adapter through that same global. Reproduced: ``report.loaded_adapters == []`` on scan two but the adapter still ran. Trust boundary violated. P1 #2 — repeated scans misclassified stable third-party adapters as ``source_type_collision``. Same root cause: collision detection used ``registry._adapters.keys()`` AFTER scan-one's third-party registration had mutated the global. Scan two thus saw the same adapter as a "built-in" collision while still executing it. Fix for both: per-scan registry clone. - New ``AdapterRegistry.clone()`` returns a shallow-copy registry with a fresh ``_adapters`` dict. Subsequent ``register()`` calls on the clone do NOT propagate to the global. - ``_load_inputs`` now builds a per-scan registry via ``scan_registry = REGISTRY.clone()``, runs discovery against ``scan_registry``, and threads ``scan_registry`` through to ``_load_sources`` via a new ``registry=`` kwarg. - ``_load_sources`` accepts the per-scan registry, falling back to the global only for legacy callers that bypass ``_load_inputs`` (existing ``tests/test_adapter_registry.py`` patterns). - Global ``REGISTRY`` stays builtin-only across the process. Tests that monkeypatch ``REGISTRY._adapters`` keep working because the clone captures the global's state at scan-start (after monkeypatch). Two new regression tests pin the fix: - ``test_no_plugins_disables_third_party_after_prior_enabled_scan``: scan-one runs with plugins enabled (adapter runs once); scan-two in the same process with ``plugins_enabled=False`` must see ``loaded_adapters == []`` AND the adapter must NOT run a second time. Pre-fix the invocation counter incremented; post-fix it stays at 1. - ``test_second_scan_does_not_misclassify_third_party_as_collision``: two consecutive scans with plugins enabled must both report ``validation_status == "valid"`` on the same adapter. Pre-fix scan two reported ``source_type_collision``. The autouse ``_reset_registry_after_test`` fixture is no longer needed — discovery doesn't mutate the global — and is replaced with a minimal ``_populate_registry`` fixture that just triggers lazy population. P1 #3 — per_source third-party adapters could not be referenced in shipgate.yaml. ``ToolSourceConfig.type`` was a closed ``Literal`` of built-in source types, so a manifest declaring ``type: my_third_party_source`` failed Pydantic validation BEFORE discovery had a chance to register the loader. The v0.20 third-party adapter surface was unusable for its main advertised use case. Fix: - ``ToolSourceConfig.type`` relaxed from ``Literal[...]`` to ``str``. - New module-level constant ``BUILTIN_TOOL_SOURCE_TYPES`` enumerates the seven built-in source types for documentation and for tests that need to iterate the curated set. - New constant ``BUILTIN_PER_SCAN_ONLY_TOOL_SOURCE_TYPES`` ({n8n, openai_api, anthropic_api, validation}) preserves the existing rejection: those four built-ins are per_scan-only and ingest config from a dedicated top-level manifest section, so they MUST NOT appear in ``tool_sources``. A new ``reject_per_scan_only_builtins`` model validator on ``ToolSourceConfig`` raises ``ValueError`` at manifest-load with a routable error pointing the user to the top-level section. - Unknown source types (typos like ``n8N``, genuinely-third-party types without a registered adapter) still fail at scan time with ``ConfigError`` from ``AdapterRegistry.require``. Exit-2 contract unchanged. - ``tests/test_n8n.py::test_n8n_top_level_config_is_accepted_and_tool_source_type_is_rejected`` updated: the case-variation (``type: n8N``) assertion now uses ``run_scan`` (dispatcher-time error) instead of ``load_manifest`` (parse-time error). Same exit code, different layer. - ``tests/test_adapter_registry.py`` + ``tests/harness/test_overlay_renderer.py`` updated to iterate ``BUILTIN_TOOL_SOURCE_TYPES`` instead of ``get_args(ToolSourceConfig.type)``. - New regression test ``test_per_source_third_party_adapter_referenced_from_manifest``: builds a manifest with ``type: demo_source``, registers a matching third-party per_source adapter via entry-points, asserts ``run_scan`` succeeds and the adapter actually runs. The JSON manifest schema (``docs/manifest-v0.1.json``) loses the ``enum`` constraint on ``tool_sources[].type`` — schema is now ``{"type": "string"}``. IDE autocomplete for built-in names is a follow-up; the trust gate is the dispatcher, not the schema. P2 #4 — signature validator accepted methods that crash the dispatcher. ``_protocol_error`` rejected only >3 required positional and zero positional. Two failure modes slipped through: - ``def load(self, source)`` — only 1 positional slot; dispatcher's ``adapter.load(source, base_dir, manifest)`` call fails with ``TypeError: too many positional arguments``. - ``def load(self, source, base_dir, manifest, *, must_set)`` — required keyword-only param; dispatcher passes no kwargs. Both now caught at the ``bad_protocol`` gate before registration. ``_protocol_error`` now: - Computes ``accepting_slots`` = count of (required + optional) positional, and ``has_var_positional`` = whether ``*args`` is present. - Rejects when ``accepting_slots < 3 AND not has_var_positional``. - Rejects any required keyword-only parameter (mirrors plugin validation). Four new regression tests: - ``test_gate_bad_protocol_load_too_few_positional``: ``load(self, source)`` lands in ``bad_protocol`` with "at least 3 positional" in the error. - ``test_gate_bad_protocol_load_required_keyword_only``: required kw-only lands in ``bad_protocol`` with "keyword-only" + the param name in the error. - ``test_gate_bad_protocol_accepts_var_positional``: ``load(self, *args)`` is valid (can bind 3 args). - ``test_gate_bad_protocol_accepts_optional_keyword_only``: optional kw-only with default is valid. Verification: 1264 tests pass; ruff clean; schema roundtrip --check exits 0; coverage 88.02%. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5400fb2 commit 14b943a

9 files changed

Lines changed: 576 additions & 68 deletions

File tree

docs/manifest-v0.1.json

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1502,15 +1502,6 @@
15021502
"title": "Trust"
15031503
},
15041504
"type": {
1505-
"enum": [
1506-
"mcp",
1507-
"openapi",
1508-
"openai_agents_sdk",
1509-
"google_adk",
1510-
"langchain",
1511-
"crewai",
1512-
"codex_plugin"
1513-
],
15141505
"title": "Type",
15151506
"type": "string"
15161507
}

src/agents_shipgate/cli/scan.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -366,13 +366,25 @@ def _load_inputs(
366366
"""
367367
from agents_shipgate.inputs.protocol import discover_third_party_adapters
368368

369+
# v0.20 (PR #111 review fix P1 #1+#2): build a per-scan registry
370+
# clone so third-party discovery NEVER mutates the global
371+
# ``REGISTRY``. Without this, a later ``--no-plugins`` scan would
372+
# still see adapters registered by an earlier scan, and the
373+
# collision check on scan-two would misclassify stable third-
374+
# party adapters as ``source_type_collision`` (the global already
375+
# has them from scan-one). The clone captures any monkeypatch
376+
# state at this exact moment, so existing tests that
377+
# ``monkeypatch.setitem(REGISTRY._adapters, …)`` still work.
378+
scan_registry = REGISTRY.clone()
369379
loaded_adapters: list[dict[str, Any]] = []
370380
discover_third_party_adapters(
371-
REGISTRY,
381+
scan_registry,
372382
plugins_enabled=plugins_enabled,
373383
loaded_adapters=loaded_adapters,
374384
)
375-
loaded_sources, artifact_bag = _load_sources(manifest, base_dir, verbose=verbose)
385+
loaded_sources, artifact_bag = _load_sources(
386+
manifest, base_dir, verbose=verbose, registry=scan_registry
387+
)
376388
logger.debug(
377389
"loaded sources",
378390
extra={
@@ -1386,8 +1398,9 @@ def _load_sources(
13861398
base_dir: Path,
13871399
*,
13881400
verbose: bool,
1401+
registry: Any = None,
13891402
) -> tuple[list[LoadedToolSource], ArtifactBag]:
1390-
"""Dispatch every adapter through ``REGISTRY``.
1403+
"""Dispatch every adapter through the supplied ``registry``.
13911404
13921405
Returns ``(loaded_sources, artifact_bag)``. ``artifact_bag`` is a
13931406
typed ``ArtifactBag`` with per-scan adapter artifacts keyed by
@@ -1397,7 +1410,7 @@ def _load_sources(
13971410
Ordering is deterministic and matches the legacy run_scan order:
13981411
13991412
1. per-source loaders in tool_sources declared order
1400-
2. per-scan adapters in REGISTRY iteration order:
1413+
2. per-scan adapters in registry iteration order:
14011414
google_adk → langchain → crewai → n8n → openai_api
14021415
→ anthropic_api → codex_plugin → validation
14031416
@@ -1409,32 +1422,42 @@ def _load_sources(
14091422
Per-scan source types appearing in tool_sources are ignored by
14101423
pass 1 — they would be redundant; framework loaders already iterate
14111424
every matching entry internally via the manifest.
1425+
1426+
v0.20 (PR #111 review fix): ``registry`` is the per-scan registry
1427+
built by ``_load_inputs`` (``REGISTRY.clone()`` plus any
1428+
third-party adapters validated in this scan). Defaults to the
1429+
module-global ``REGISTRY`` only for callers that bypass
1430+
``_load_inputs`` (notably the legacy tests in
1431+
``tests/test_adapter_registry.py``). New code should always pass
1432+
a per-scan registry.
14121433
"""
1434+
if registry is None:
1435+
registry = REGISTRY
14131436
per_source_loaded: list[LoadedToolSource] = []
14141437
per_scan_loaded: list[LoadedToolSource] = []
14151438
bag = ArtifactBag()
14161439

14171440
# Pass 1 — per-source adapters only, in tool_sources declared
14181441
# order. Per-scan source types (langchain, crewai, etc.) are
1419-
# skipped here; pass 2 invokes them in canonical REGISTRY order
1442+
# skipped here; pass 2 invokes them in canonical registry order
14201443
# regardless of where they appear in tool_sources. This protects
14211444
# the dedup tie-break in _flatten_and_deduplicate_tools from
14221445
# changing based on user-facing tool_sources ordering.
14231446
for source in manifest.tool_sources:
1424-
adapter = REGISTRY.require(source.type)
1447+
adapter = registry.require(source.type)
14251448
if adapter.scope != "per_source":
14261449
continue
14271450
result = _invoke_per_source_adapter(
14281451
adapter, source, base_dir, manifest, verbose=verbose
14291452
)
14301453
_absorb(result, source.type, per_source_loaded, bag, adapter)
14311454

1432-
# Pass 2 — every per-scan adapter fires once, in REGISTRY order.
1455+
# Pass 2 — every per-scan adapter fires once, in registry order.
14331456
# Covers framework adapters (always check their manifest section
14341457
# internally and may emit zero LoadedToolSource entries when not
14351458
# configured) and manifest-only adapters (openai_api,
14361459
# anthropic_api, n8n).
1437-
for adapter in REGISTRY.per_scan_adapters():
1460+
for adapter in registry.per_scan_adapters():
14381461
result = adapter.load(None, base_dir, manifest)
14391462
_absorb(result, adapter.source_type, per_scan_loaded, bag, adapter)
14401463

src/agents_shipgate/inputs/adapter_validation.py

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -312,8 +312,22 @@ def _protocol_error(adapter: Any) -> str | None:
312312
except (TypeError, ValueError):
313313
# Builtins / C extensions; accept — runtime wrapper will catch.
314314
return None
315-
# ToolSourceAdapter.load takes (source, base_dir, manifest). The
316-
# bound method drops ``self``, leaving 3 positional parameters.
315+
# ToolSourceAdapter.load is invoked as
316+
# ``adapter.load(source, base_dir, manifest)`` — three positional
317+
# arguments, never any keyword arguments. The validator must accept
318+
# any signature compatible with that exact call shape and reject
319+
# everything else. Three failure modes the original v0.20 (PR #111
320+
# review fix P2 #4) implementation missed:
321+
#
322+
# - Too FEW positional slots, e.g. ``load(self, source)`` — the
323+
# bound method has only 1 positional slot; the dispatcher's
324+
# 3-arg call would crash with ``TypeError: too many positional
325+
# arguments``.
326+
# - Required keyword-only parameters, e.g.
327+
# ``load(self, source, base_dir, manifest, *, must_set)`` — the
328+
# dispatcher never passes kwargs; the call would crash with
329+
# ``TypeError: missing 1 required keyword-only argument``.
330+
# - Too MANY required positional params (preserved from v0.20).
317331
positional = [
318332
p
319333
for p in sig.parameters.values()
@@ -324,24 +338,54 @@ def _protocol_error(adapter: Any) -> str | None:
324338
inspect.Parameter.VAR_POSITIONAL,
325339
)
326340
]
341+
if not positional:
342+
return (
343+
"adapter.load must accept positional arguments "
344+
"(source, base_dir, manifest)"
345+
)
327346
required = [
328347
p
329348
for p in positional
330349
if p.kind != inspect.Parameter.VAR_POSITIONAL
331350
and p.default is inspect.Parameter.empty
332351
]
333-
# The wrapper calls adapter.load(source, base_dir, manifest) — three
334-
# positional arguments. Accept up to 3 required positional slots;
335-
# extra optional slots are fine.
336352
if len(required) > 3:
337353
return (
338354
"adapter.load must accept at most 3 required positional "
339355
f"parameters (source, base_dir, manifest); got {len(required)}"
340356
)
341-
if not positional:
357+
# Count how many positional slots can absorb arguments. Required +
358+
# optional contribute one each; ``*args`` absorbs any remaining.
359+
accepting_slots = sum(
360+
1
361+
for p in positional
362+
if p.kind != inspect.Parameter.VAR_POSITIONAL
363+
)
364+
has_var_positional = any(
365+
p.kind == inspect.Parameter.VAR_POSITIONAL for p in positional
366+
)
367+
if accepting_slots < 3 and not has_var_positional:
342368
return (
343-
"adapter.load must accept positional arguments "
344-
"(source, base_dir, manifest)"
369+
"adapter.load must accept at least 3 positional arguments "
370+
f"(source, base_dir, manifest); got {accepting_slots} "
371+
"positional slot(s) and no *args"
372+
)
373+
# Required keyword-only parameters are not satisfiable by the
374+
# ``plugin(source, base_dir, manifest)`` call shape. Optional
375+
# kw-only (with defaults) and ``**kwargs`` are fine. Mirrors the
376+
# plugin-validation signature gate.
377+
required_kw_only = [
378+
p
379+
for p in sig.parameters.values()
380+
if p.kind == inspect.Parameter.KEYWORD_ONLY
381+
and p.default is inspect.Parameter.empty
382+
]
383+
if required_kw_only:
384+
names = ", ".join(p.name for p in required_kw_only)
385+
return (
386+
"adapter.load has required keyword-only parameter(s) "
387+
f"({names}); the dispatcher calls adapter.load(source, "
388+
"base_dir, manifest) with no keyword arguments"
345389
)
346390
return None
347391

src/agents_shipgate/inputs/protocol.py

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,38 @@ def register(self, adapter: ToolSourceAdapter) -> None:
152152
)
153153
self._adapters[adapter.source_type] = adapter
154154

155+
def clone(self) -> AdapterRegistry:
156+
"""v0.20 (PR #111 review follow-up): return a shallow-copy
157+
registry snapshot for use as a *per-scan* adapter registry.
158+
159+
The clone shares no mutable state with the original — its
160+
``_adapters`` dict is a fresh copy. Subsequent ``register()`` /
161+
third-party additions on the clone do NOT propagate to the
162+
global ``REGISTRY``. Closes two P1 review findings:
163+
164+
- ``--no-plugins`` on a later in-process scan now actually
165+
disables third-party adapters discovered by an earlier scan
166+
(which previously persisted in the global registry).
167+
- Collision detection on the second scan sees only the
168+
builtins (clone-time state), not adapters carried over from
169+
scan one, so a stable third-party adapter no longer
170+
mis-reports as ``source_type_collision``.
171+
172+
Tests that monkeypatch ``REGISTRY._adapters`` before
173+
``_load_inputs`` runs are preserved: the clone captures the
174+
global's state at that moment, including monkeypatch
175+
modifications.
176+
"""
177+
178+
clone = AdapterRegistry(autopopulate=False)
179+
# Lazy-populate ourselves first so the clone inherits the
180+
# canonical builtin set even when this is the global REGISTRY's
181+
# first read.
182+
self._ensure_populated()
183+
clone._adapters = dict(self._adapters)
184+
clone._populated = True
185+
return clone
186+
155187
def get(self, source_type: str) -> ToolSourceAdapter | None:
156188
self._ensure_populated()
157189
return self._adapters.get(source_type)
@@ -258,6 +290,21 @@ def discover_third_party_adapters(
258290
"""Walk ``entry_points('agents_shipgate.adapters')``, validate each
259291
third-party adapter, and register the valid ones onto ``registry``.
260292
293+
**Per-scan registry contract (v0.20 review fix).** ``registry``
294+
MUST be a per-scan instance (typically ``REGISTRY.clone()`` from
295+
``_load_inputs``), NOT the global ``REGISTRY`` itself. Mutating
296+
the global broke two trust invariants:
297+
298+
1. ``--no-plugins`` on a later scan didn't disable adapters
299+
registered by an earlier scan, because the dispatcher still
300+
resolved them through the global registry.
301+
2. Collision detection on the second scan misclassified
302+
previously-registered third-party adapters as
303+
``source_type_collision`` against themselves.
304+
305+
Callers pass a clone so each scan starts from a known-good
306+
builtins-only snapshot.
307+
261308
Returns the list of ``LoadedAdapter`` records (both valid and
262309
invalid). When ``loaded_adapters`` is provided, the per-row
263310
``info`` dicts are appended to it for downstream
@@ -268,14 +315,6 @@ def discover_third_party_adapters(
268315
``plugins_enabled`` is not explicitly ``True``. ``--no-plugins``
269316
on the CLI translates to ``plugins_enabled=False`` and forces this
270317
off even when the env var is set.
271-
272-
Calling this twice with the same ``registry`` is safe: collision
273-
detection runs against the registry's current state, and
274-
already-registered third-party adapters are rejected with
275-
``source_type_collision``. A test that monkeypatches
276-
``entry_points`` should also reset the registry between runs (use
277-
a fresh ``AdapterRegistry(autopopulate=False)`` or
278-
``monkeypatch.setattr`` for full isolation).
279318
"""
280319

281320
from agents_shipgate.inputs.adapter_validation import (

src/agents_shipgate/schemas/manifest.py

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,24 +47,80 @@ class EnvironmentConfig(BaseModel):
4747
promotion_to: str | None = None
4848

4949

50+
#: v0.20 (PR #111 review fix P1 #3): the curated set of built-in
51+
#: source types that may legitimately appear in ``tool_sources[].type``.
52+
#: Used for documentation only — ``ToolSourceConfig.type`` is open
53+
#: (``str``) so third-party adapters from the ``agents_shipgate.adapters``
54+
#: entry-point group can declare custom source types. Unknown source
55+
#: types fail at dispatcher time via ``AdapterRegistry.require``.
56+
BUILTIN_TOOL_SOURCE_TYPES: tuple[str, ...] = (
57+
"mcp",
58+
"openapi",
59+
"openai_agents_sdk",
60+
"google_adk",
61+
"langchain",
62+
"crewai",
63+
"codex_plugin",
64+
)
65+
66+
#: Built-in adapters that are intentionally NOT permitted in
67+
#: ``tool_sources[]`` because they are per_scan-only and ingest
68+
#: configuration from their dedicated top-level manifest section.
69+
#: Placing one of these in ``tool_sources`` is a user mistake; we
70+
#: reject it at manifest-load time with a clear error rather than
71+
#: silently no-op'ing it in pass 1 of the dispatcher.
72+
BUILTIN_PER_SCAN_ONLY_TOOL_SOURCE_TYPES: frozenset[str] = frozenset(
73+
{"n8n", "openai_api", "anthropic_api", "validation"}
74+
)
75+
76+
5077
class ToolSourceConfig(BaseModel):
5178
model_config = STRICT_MODEL_CONFIG
5279

5380
id: str
54-
type: Literal[
55-
"mcp",
56-
"openapi",
57-
"openai_agents_sdk",
58-
"google_adk",
59-
"langchain",
60-
"crewai",
61-
"codex_plugin",
62-
]
81+
# v0.20 (PR #111 review fix P1 #3): opened from a closed Literal to
82+
# ``str`` so manifests can reference third-party per_source adapters
83+
# registered via the ``agents_shipgate.adapters`` entry-point group.
84+
# Without this relaxation, ``ToolSourceConfig.model_validate``
85+
# rejected ``type: my_custom_source`` at manifest-load time —
86+
# before adapter discovery had a chance to register the loader —
87+
# making the v0.20 third-party adapter surface unusable for its
88+
# main advertised use case.
89+
#
90+
# Built-in source types are enumerated in
91+
# ``BUILTIN_TOOL_SOURCE_TYPES`` above (documentation only). Unknown
92+
# source types are still rejected with a routable ``ConfigError``
93+
# (exit 2) at dispatch time via ``AdapterRegistry.require``. Typos
94+
# in built-in names therefore fail loudly with the same exit code
95+
# as before — just from a different code path.
96+
#
97+
# Per_scan-only built-ins (``n8n``, ``openai_api``, ``anthropic_api``,
98+
# ``validation``) remain explicitly REJECTED here (model validator
99+
# below) because they ingest config from their dedicated top-level
100+
# manifest section, not from ``tool_sources[]``.
101+
type: str
63102
path: str | None = None
64103
trust: str | None = None
65104
mode: str | None = None
66105
optional: bool = False
67106

107+
@model_validator(mode="after")
108+
def reject_per_scan_only_builtins(self) -> ToolSourceConfig:
109+
# Each per_scan-only built-in has its own dedicated manifest
110+
# section (``n8n.workflows``, ``openai_api.tools``, …); putting
111+
# it in ``tool_sources`` is always a user mistake. Third-party
112+
# per_scan adapters (also discovered via entry points) do NOT
113+
# require ``tool_sources`` entries either; the dispatcher
114+
# iterates them in pass-2 regardless.
115+
if self.type in BUILTIN_PER_SCAN_ONLY_TOOL_SOURCE_TYPES:
116+
raise ValueError(
117+
f"tool_sources entry {self.id!r} declares type "
118+
f"{self.type!r}, which is a per_scan-only built-in. "
119+
f"Move this configuration to the top-level "
120+
f"``{self.type}:`` manifest section."
121+
)
122+
return self
123+
68124
@model_validator(mode="after")
69125
def require_path_when_needed(self) -> ToolSourceConfig:
70126
if (

tests/harness/test_overlay_renderer.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,21 @@ def test_40_shipgate_yaml_renders_clean_for_every_archetype(tmp_path: Path) -> N
4545

4646

4747
def test_every_archetype_uses_a_valid_tool_source_type() -> None:
48-
"""Each archetype's ``ToolSource.type`` must be in the v0.1 manifest enum.
49-
50-
Catches regressions like the original ``openai_api`` typo for
51-
clean-read-only that would otherwise only surface when an operator runs
48+
"""Each archetype's ``ToolSource.type`` must be a known built-in
49+
source type (third-party extensions are intentionally NOT used in
50+
canonical archetypes — the harness ships a fixed set).
51+
52+
v0.20 PR #111 review fix: ``ToolSourceConfig.type`` relaxed from
53+
Literal to ``str`` to support third-party adapters; this test now
54+
pins the archetype set against the explicit
55+
``BUILTIN_TOOL_SOURCE_TYPES`` constant. Catches regressions like
56+
the original ``openai_api`` typo for clean-read-only that would
57+
otherwise only surface when an operator runs
5258
``agents-shipgate doctor`` on a rendered 40-shipgate-yaml manifest.
5359
"""
54-
from agents_shipgate.schemas.manifest import ToolSourceConfig
60+
from agents_shipgate.schemas.manifest import BUILTIN_TOOL_SOURCE_TYPES
5561

56-
allowed = set(ToolSourceConfig.model_fields["type"].annotation.__args__)
62+
allowed = set(BUILTIN_TOOL_SOURCE_TYPES)
5763
for archetype, ctx in ctx_mod.ARCHETYPE_CONTEXTS.items():
5864
for ts in ctx.tool_sources:
5965
assert ts.type in allowed, (

0 commit comments

Comments
 (0)