Versioned provider plugin API with selected-only entry-point loading (#171) - #183
Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implement ProviderPluginRegistry that discovers third-party LLM provider plugins via importlib.metadata distributions but only calls .load() on the selected entry point. Unselected plugins are never imported. Key behaviors: - normalize_provider_name: lowercase, collapse [-_.] to hyphens - Deterministic collision errors naming both conflicting distributions - Metadata validation (api_version check) - Auth method validation against plugin's declared auth_methods - Bounded non-secret error messages for import/factory failures - Single-instance cache per normalized name - Factory wraps result in ValidatedPluginProvider Tests use real dist-info/entry_points.txt fixtures with an unselected module that raises on import to prove selected-only loading. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Address all 6 review findings with TDD:
1. Validate metadata.name matches entry-point normalized name; reject
mismatches with bounded error.
2. Enforce API-v1 auth_methods allowlist {none, api_key, entra};
reject disallowed values, duplicates, and non-string entries.
3. Add malformed metadata property-raises test with fake secret payload;
verify bounded actionable error excludes payload.
4. Remove ProviderPluginError passthrough — ALL plugin.create()
exceptions are translated to bounded type/stage errors.
5. Explicit LLMProvider isinstance check before ValidatedPluginProvider
wrapping; non-LLMProvider output is ProviderPluginError.
6. Make _MAX_ERROR_LENGTH=200 real: all externally-influenced labels
(entry-point/distribution names) are sanitized via _bounded() and
final messages capped via _bounded_error(). Add long-label tests.
Extract _resolve_entry_point/_validate_metadata to fix C901.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…eation - Add plugin_registry and options parameters to create_provider() without changing built-in behavior; unknown names route through the plugin registry - Build credentials first for plugins, validate metadata auth support, close credentials on plugin construction failure - Create one ProviderPluginRegistry per _build_agent_wiring; pass it to initial creation, ProviderConfigurator, and rebuild (shared cache lifetime) - Convert initial ProviderPluginError to config/startup warning while leaving provider None — base TUI remains operational - Rebuild raises ProviderPluginError for _apply_agent_settings to catch and surface via existing error notification path - Seed AgentSettings.options from config and preserve across model changes - Extract _create_initial_provider helper to keep _build_agent_wiring within C901 complexity limit Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…close - _create_via_plugin no longer swallows ProviderPluginError; it propagates to callers. _create_initial_provider catches it and appends an actionable warning to startup_warnings list. - Credential close uses a module-level strong-reference set with a done-callback (_reap) that discards from the set and logs exceptions at debug level, mirroring _close_provider_in_background. - test_agent_wiring_initial_plugin_error_becomes_warning now uses a production-real path: a fake ProviderPluginRegistry whose load_selected raises ProviderPluginError, injected through the real create_provider pipeline. - Added test_credential_close_consumes_exceptions_without_secret_leak proving aclose errors are consumed without leaking secrets. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Address final-review blockers:
1) Full metadata boundary:
- Runtime isinstance check for ProviderPluginMetadata (rejects dicts)
- Validate all field types: api_version (non-bool int == 1),
name/display_name (non-empty str, bounded), auth_methods (tuple),
supports_generic_setup (strict bool)
- Cache validated metadata alongside plugin in load_selected;
create() uses cached metadata, never re-reads plugin.metadata
- Extract _validate_metadata_fields helper (fixes C901)
2) Reserved names:
- Centralize RESERVED_PROVIDER_NAMES in plugin_registry.py
- Normalize provider input via normalize_provider_name() in
create_provider BEFORE built-in dispatch; openai_compat,
OpenAI_Compat, github_copilot, ' ollama' all route to built-ins
- Defense-in-depth: load_selected rejects reserved names BEFORE
discovery/load with bounded error
Tests: 140 passed (87 plugin_registry + 40 registry + 53 main_wiring)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Not ready to approve
Provider rebuild atomicity, error containment, option validation, and credential lifecycle have unresolved correctness and security issues.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Pull request overview
Introduces a versioned third-party LLM provider API with selected-only loading, bounded configuration, lifecycle wiring, and documentation.
Changes:
- Adds provider-plugin contracts, event validation, and entry-point discovery.
- Threads immutable plugin options through configuration, UI, and rebuild flows.
- Adds packaged fixtures, extensive tests, and author/operator documentation.
File summaries
| File | Description |
|---|---|
src/korvid/agent/provider_plugin.py |
Defines plugin API and event validation. |
src/korvid/agent/provider.py |
Updates provider-boundary documentation. |
src/korvid/agent/setup.py |
Adds immutable plugin options. |
src/korvid/providers/plugin_registry.py |
Implements selected plugin discovery. |
src/korvid/providers/registry.py |
Routes custom providers through plugins. |
src/korvid/providers/configurator.py |
Passes registry and options during probes. |
src/korvid/core/config.py |
Parses and bounds plugin options. |
src/korvid/__main__.py |
Wires registry, warnings, rebuild, and cleanup. |
src/korvid/ui/app.py |
Seeds UI agent options. |
src/korvid/ui/widgets/agent_setup_screen.py |
Preserves options during setup. |
tests/agent/test_provider_plugin.py |
Tests contracts and event validation. |
tests/agent/test_plugin_runtime.py |
Tests runtime integration and isolation. |
tests/core/test_config.py |
Tests option limits and persistence. |
tests/providers/test_plugin_registry.py |
Tests discovery, validation, and caching. |
tests/providers/test_registry.py |
Tests provider factory integration. |
tests/providers/test_configurator.py |
Tests configurator forwarding. |
tests/test_main_wiring.py |
Tests lifecycle and transactional wiring. |
tests/test_optional_extras.py |
Tests lazy discovery and imports. |
tests/ui/test_agent_wiring.py |
Tests option preservation and errors. |
tests/ui/test_agent_setup_screen.py |
Tests option immutability and reconnects. |
tests/fixtures/provider_plugin/company_provider.py |
Provides a valid fixture plugin. |
tests/fixtures/provider_plugin/unselected_provider.py |
Detects unintended imports. |
tests/fixtures/provider_plugin/site_helpers.py |
Builds fixture distributions. |
tests/fixtures/provider_plugin/__init__.py |
Declares the fixture package. |
tests/fixtures/__init__.py |
Declares shared fixtures. |
docs/provider-plugins.md |
Documents the public plugin API. |
docs/agent.md |
Explains plugin selection and trust. |
README.md |
Links the provider-plugin guide. |
Review details
- Files reviewed: 26/28 changed files
- Comments generated: 9
- Review effort level: Balanced
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
1. Gate third-party plugin creation when agent_options_error exists; built-in providers remain usable. Initial startup surfaces warning, rebuild raises ProviderPluginError. 2. ValidatedPluginProvider stream: translate provider exceptions to bounded ProviderPluginContractError (max 2048 UTF-8 byte type name), preserve already-normalized ProviderPluginContractError unchanged. 3. Apply 2048 UTF-8 byte limit to mapping keys before secret normalization and serialized budget check; exact boundary tests added. 4. Deep-freeze tuple elements: AgentSettings options containing tuples with mutable dicts now produce tuples of mapping proxies; nested mutation is rejected. 5. Docs complete adapter: aclose() owns and closes injected CredentialSource in finally; lifecycle text explicit in docs. 6. Replace fixed four pilot.pause() loops in tests/ui/test_agent_wiring.py with existing until() observable condition polling. 7. Translate importlib.metadata discovery/enumeration failures to bounded ProviderPluginError with no exception payload; single broken distribution degrades to warning and continues. 8. Make rebuild fully transactional: create provider, build profile, ToolExecutor, AgentRuntime completely BEFORE swapping provider_box or scheduling old close. If any later step raises, close the new provider exactly once, leave old state live. Failure injection tests added. 9. ASCII-only option keys: reject any non-ASCII key before normalization with bounded error. Cyrillic and Greek lookalike tests added. Values are unaffected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Not ready to approve
Functional normalization gaps and credential/error-boundary leaks remain unresolved.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
Suppressed comments (3)
src/korvid/providers/registry.py:175
- Plugin authentication failures are only logged and converted to
None, so_create_initial_providercannot add the promised TUI-visible startup warning and rebuilds surface only a generic incomplete-configuration error. Raise a payload-freeProviderPluginErrorhere so startup and rebuild use their existing plugin error surfaces; keep the built-in provider behavior unchanged.
except _AuthMisconfigured as exc:
logger.warning("%s — agent disabled", exc)
return None
src/korvid/providers/registry.py:63
- Normalized Copilot variants do not work in the full startup path. This factory maps
github_copilot/case variants togithub-copilot, butload_configdefaults device login only for the exact spelling (core/config.py:163) and_build_agent_wiringloads the stored OAuth token only for that exact spelling (__main__.py:533). Such a configured variant is therefore routed here with the wrong auth and/or no token and the agent remains disabled. Canonicalize the provider before all provider-specific checks, not only inside this factory.
name = normalize_provider_name(provider) if isinstance(provider, str) else ""
tests/ui/test_agent_wiring.py:556
- This new asynchronous notification test reintroduces a scheduler-timing wait: one bare pause need not let the rebuild worker finish on slow CI. Poll the observable notification state with
until, as required bytests/ui/waits.py:1-4,13-36.
await pilot.pause()
- Files reviewed: 26/28 changed files
- Comments generated: 3
- Review effort level: Balanced
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
1. Credential close callback: consume exception with fixed message only, never log exc_info or exception message (may contain secrets). Same fix applied to _close_provider_in_background in __main__. caplog regression test verifies no secret payload leakage. 2. ValidatedPluginProvider.aclose(): translate underlying close exceptions to fixed 'provider plugin close failed' ProviderPluginContractError. Exactly-once guard preserved (second close is still a no-op). 3. Secret key matching: build all consecutive multi-token subsequences from underscore-split parts so multi-token reserved segments like 'api_key' match compound keys like 'client_api_key'. Exact boundary tests added; non-secret compound keys (client_key, api_version) pass. 4. Plugin auth misconfiguration: _AuthMisconfigured on the third-party path now raises payload-free ProviderPluginError so startup warning and rebuild error surfaces work. Built-in providers keep log+None. 5. Provider name canonicalization: config load applies _canonicalize_provider_name (stdlib-only, core-layer) before dispatch, so github_copilot/case variants receive device-login default and composition root loads OAuth token. Parity test confirms identical output to providers.plugin_registry.normalize_provider_name. 6. Replace bare pilot.pause() in test_apply_agent_settings_notifies_on_ plugin_error (line ~556) with until() condition polling. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Round-2 suppressed findings also fixed in 7ad4446: (1) third-party |
There was a problem hiding this comment.
🟡 Not ready to approve
Stream termination and metadata error handling do not yet fully enforce the documented safety contract.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
Suppressed comments (3)
src/korvid/agent/provider_plugin.py:91
- The wrapper validates each event shape but never enforces stream termination. If a plugin yields a
tool_calland then ends withoutdone(or emits more events afterdone),AgentRuntimetreats exhaustion as success and can dispatch that call. Require exactly one terminaldonebefore allowing the stream to complete.
try:
async for event in self._provider.complete(messages, tools, stream=stream):
yield _normalize_event(event)
docs/provider-plugins.md:106
- This block is labeled the exact API-v1 surface, but
completeis documented as a regular method even though the implementation requires an async generator. A plugin author copying this signature would return a synchronous iterator that fails underasync for.
def complete(
src/korvid/agent/provider_plugin.py:185
- The advertised 64 KiB limits are checked with
len(str), which counts characters; non-ASCII text or arguments can therefore reach roughly 256 KiB in UTF-8. Keep ID/name limits character-based, but enforce UTF-8 byte length fortext_delta.textandtool_call.arguments, with matching docs and Unicode boundary tests.
value = _require_str(event, label, key)
if len(value) > max_length:
raise ProviderPluginContractError(f"{label} exceeds max length {max_length}")
- Files reviewed: 26/28 changed files
- Comments generated: 1
- Review effort level: Balanced
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
1. Auth methods validation: never sort/join/include plugin-controlled
values in error messages. Error now reports count of disallowed
values only. Secret+huge auth string test verifies values absent
and message bounded.
2. ValidatedPluginProvider.complete enforces exactly one terminal done:
- zero done on stream exhaustion raises ProviderPluginContractError
- any event after done (including second done) raises before yielding
- normal single done terminal passes unchanged
Wrapper tests and runtime integration test: tool_call then no done
becomes AgentError before dispatch/history corruption.
3. Docs API surface: LLMProvider.complete shown as async generator
(async def + yield) matching the real signature.
4. text_delta.text and tool_call.arguments limits are now 65,536 UTF-8
bytes (not chars). Separate _require_byte_bounded_str helper with
len(value.encode('utf-8')) check. id/name stay char-based 256.
Exact Unicode byte boundary tests (CJK chars at 3 bytes each) for
both text and arguments; docs updated to say 'UTF-8 bytes'.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Round-3 suppressed findings fixed in aa30cc1: (1) |
There was a problem hiding this comment.
🟡 Not ready to approve
Malformed oversized API-version metadata can escape the bounded plugin-error path and crash startup.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
- Files reviewed: 26/28 changed files
- Comments generated: 1
- Review effort level: Balanced
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
Round 4 blocker: ProviderPluginMetadata.api_version with a huge int
(e.g. 10**5000) would trigger Python's int-to-str security limit or
produce an unbounded error message when formatted with f'{value}'.
Fix: replace the format string that included meta.api_version with a
fixed message stating the required value only. No plugin-controlled
value is ever stringified/repr'd in the error path.
Regression test: 5000-digit int constructed via 10**5000 (avoids
parsing-path str-digit limit), asserts ProviderPluginError is raised
with bounded message containing no raw digits from the huge int.
Inspected neighboring metadata field errors: display_name/auth_methods/
supports_generic_setup all use fixed type-check messages that never
embed the supplied value — no analogous hazard found.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Not ready to approve
Common CamelCase credential option keys bypass the new secret-bearing-key rejection.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
Suppressed comments (1)
src/korvid/providers/plugin_registry.py:41
- These reserved names are not actually shared with built-in dispatch:
providers/registry.py:30-40independently defines_OPENAI_COMPAT_ALIASESand separately checks Ollama/Copilot. A future alias update can therefore route a built-in name into plugin loading or leave it unreserved. Derive both dispatch and collision rejection from one canonical definition.
# Centralized reserved names — these are built-in provider identifiers that must
# never be claimed by third-party plugins. Shared with korvid.providers.registry
# to ensure plugin_registry and the built-in dispatch cannot drift.
RESERVED_PROVIDER_NAMES: frozenset[str] = frozenset(
- Files reviewed: 26/28 changed files
- Comments generated: 1
- Review effort level: Balanced
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
Round 5 findings:
1. Secret option key normalization now splits ASCII CamelCase and
acronym-to-word transitions BEFORE casefold. This rejects common
JSON-style keys: apiKey, clientSecret, accessToken, APIKey,
clientAPIKey. Non-secret camelCase keys (clientKey, apiVersion,
modelName, baseURL, maxRetries) remain accepted. ASCII-only policy
unchanged.
2. Built-in provider names are now a true single source:
- plugin_registry.py exports OPENAI_COMPAT_ALIASES, OLLAMA_PROVIDER,
GITHUB_COPILOT_PROVIDER as canonical constants
- RESERVED_PROVIDER_NAMES derives from these via set union
- registry.py imports and uses these exact definitions for dispatch
(no independently defined _OPENAI_COMPAT_ALIASES or string drift)
- Identity/parity tests verify same-object references and that all
reserved names are already in canonical normalized form
- Parametrized variant tests confirm every alias routes to built-in
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Round-5 suppressed provider-name drift finding fixed in 2c33108: OpenAI-compatible aliases plus |
There was a problem hiding this comment.
🟡 Not ready to approve
Authentication defaulting can bypass metadata validation, and the apikey option spelling bypasses secret-key rejection.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
Suppressed comments (1)
src/korvid/providers/registry.py:177
build_credentialsresolves a missing method toapi_key(whenapi_key_envis set) ornone, butProviderPluginConfigstill receives the originalNone. BecauseProviderPluginRegistry.createvalidates only truthy methods, anapi_key-only plugin can be constructed with no credentials, or receive API-key credentials while being told its auth method isNone. Resolve the effective method once and pass that same value to credential construction and plugin config so metadata validation cannot be bypassed.
credentials = build_credentials(name, auth_method, api_key_env)
except _AuthMisconfigured:
from korvid.providers.plugin_registry import ProviderPluginError as _PPE
raise _PPE(f"provider plugin {name!r}: auth misconfigured") from None
- Files reviewed: 26/28 changed files
- Comments generated: 1
- Review effort level: Balanced
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
Round 6 findings:
1. Third-party plugin path now resolves effective auth method ONCE:
`auth_method or ('api_key' if api_key_env else 'none')`. The same
concrete value is passed to both build_credentials and
ProviderPluginConfig, ensuring metadata auth validation always runs
and plugin config agrees with actual credentials. Built-in providers
are unchanged (they already resolve internally).
Tests: api_key-only plugin with method=None + api_key_env gets
method=api_key and credentials; none-only plugin with api_key_env
rejects; api_key-only with no env defaults to none and rejects.
2. Compact lowercase `apikey` is now a reserved secret key segment
equivalent to `api_key`. Added to _SECRET_OPTION_KEY_SEGMENTS and
docs. No other documented multi-word reserved spelling has a
meaningful compact form (secret/password/token/authorization/
credential are all single words). No substring false positives:
the token matching requires exact underscore-boundary matches.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Round-6 suppressed auth finding fixed in f7f6f47: the third-party path resolves one concrete effective auth method ( |
There was a problem hiding this comment.
🟡 Not ready to approve
Plugin-raised contract errors can leak raw payloads, and option-key validation has pathological allocation behavior.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
Suppressed comments (2)
src/korvid/agent/provider_plugin.py:92
- A plugin can raise
ProviderPluginContractErroritself fromcomplete(), and this branch rethrows its raw message.AgentRuntimeexposes that message viaAgentError, so a plugin exception containing credential text or an unbounded payload bypasses the sanitization applied to every other stream exception. Separate errors raised while advancing the plugin iterator from the wrapper's own validation errors, and sanitize the former regardless of exception class.
except ProviderPluginContractError:
raise
src/korvid/core/config.py:618
- Building every contiguous token subsequence materializes O(n²) strings (and O(n³) copied characters) before the 16 KiB serialized-budget check. With several allowed 2 KiB keys containing many separators, config loading can consume hundreds of MB just to test six fixed reserved segments. Compare each fixed segment's token sequence directly instead of constructing the complete subsequence set.
tokens: set[str] = {normalized}
for i in range(len(parts)):
for j in range(i + 1, len(parts) + 1):
tokens.add("_".join(parts[i:j]))
- Files reviewed: 26/28 changed files
- Comments generated: 0 new
- Review effort level: Balanced
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
…ection Round 7 suppressed-but-credible findings: 1. ValidatedPluginProvider.complete refactored: exceptions from advancing the plugin iterator (including ProviderPluginContractError with secret payloads) are always translated to a fixed bounded wrapper error. Only ProviderPluginContractError raised by OUR _normalize_event/done checks is preserved (via separate code path after the try/except). BaseException (CancelledError, GeneratorExit) semantics unchanged. Tests: underlying ProviderPluginContractError with secret payload through both wrapper (unit) and runtime (integration AgentError/ history clean); our own validation errors still produce specific messages. 2. Secret-key detection replaced O(n^2) contiguous subsequence set materialization with bounded sliding-window comparison. For each reserved segment's precomputed token sequence (max 2 tokens), slides over parts in O(len(parts)). Total: O(parts * reserved_patterns). All existing camel/acronym/compact behavior and no false positives. Tests: pathological 2KiB separator-heavy key (1024 parts) passes without error (functional acceptance). All existing secret-key and non-secret-key tests continue passing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Iterator cleanup regression fix (follow-up to 91a3e37): ValidatedPluginProvider.complete now deterministically calls the underlying iterator's aclose() on ALL exit paths: - Normal exhaustion (else branch): _close_iterator_or_raise translates close Exception to fixed ProviderPluginContractError; BaseException propagates unchanged. - Wrapper validation/done errors (except BaseException branch): _close_iterator_suppress swallows all close exceptions to preserve the primary error. No close exception payload ever leaks. - Consumer cancellation/GeneratorExit: same suppress path — the CancelledError/GeneratorExit propagates after cleanup. Iterator close uses getattr/callable (not assume method) since LLMProvider.complete's return type is AsyncIterator which does not guarantee aclose() exists on all implementations. Tests: - Underlying async generator finally/close marker fires on malformed event rejection and after-done rejection (exactly once each). - Consumer cancellation while yielding triggers cleanup (exactly once). - Close-raises-secret with active primary error: primary preserved, no secret payload in error message. - Normal close failure: translated to fixed contract error, no payload. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Round-7 suppressed findings fixed in 91a3e37 + e8aeb00: all underlying plugin iterator exceptions (including plugin-raised ProviderPluginContractError) are sanitized, wrapper-generated contract errors remain useful, secret-key detection is now sliding-window O(parts × fixed patterns), and underlying iterators are deterministically |
There was a problem hiding this comment.
🟡 Not ready to approve
Plugin-controlled exceptions can still bypass the bounded error boundary during iterator creation and event normalization.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
Suppressed comments (2)
src/korvid/agent/provider_plugin.py:100
eventis only required to be aMapping, so calls made by_normalize_event(get, iteration, or field access) can themselves raise. Those exceptions occur outside the translated iterator-advance block and are re-raised raw by the outer handler;AgentRuntimethen surfaces their potentially secret or unbounded message. Translate exceptions raised while reading plugin-controlled mappings while preserving Korvid's own fixed validation diagnostics.
normalized = _normalize_event(event)
src/korvid/agent/provider_plugin.py:84
- The provider's
complete()call occurs before the exception boundary. An invalid plugin can satisfy the ABC with a synchronous override that raises here; its raw exception then reachesAgentRuntime, which exposesstr(exc)as anAgentError, bypassing the bounded/no-secret plugin error surface. Protect the iterator creation itself as well as iteration.
iterator = self._provider.complete(messages, tools, stream=stream)
- Files reviewed: 26/28 changed files
- Comments generated: 0 new
- Review effort level: Balanced
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
…ng reads Round 8 boundary gaps: 1. Iterator creation: self._provider.complete(...) call is now wrapped in try/except Exception. Any synchronous exception (including ProviderPluginContractError with secret payload, or wrong return type causing later protocol error) becomes fixed bounded ProviderPluginContractError. BaseException cancellation semantics unchanged. 2. Hostile Mapping reads: every event.get(key) call during normalization is routed through _safe_mapping_get() which translates any plugin- originated exception to a fixed 'field access' error. Korvid's own validation errors (wrong type/missing/bounds/unknown event/done protocol) still produce their specific diagnostic messages. Tests: hostile Mapping fixture whose get() raises secret+huge payload; wrapper unit tests prove fixed bounded error; runtime integration would get bounded AgentError. Iterator deterministic close confirmed on normalization failure from hostile mapping. Audit: only this wrapper (ValidatedPluginProvider.complete) has plugin-controlled calls. aclose() was already translated in the previous commit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Review-loop limit reached: rounds 7 and 8 contained only suppressed advisory findings and no unresolved blocking threads. Credible boundary findings were still fixed in 91a3e37/e8aeb00/b331321: iterator exceptions and hostile Mapping reads are sanitized, secret-key matching is O(n), and iterators close deterministically on every exit. Final local boundary review approved; focused verification 52 passed; agent full gate reported 3375 passed. Per AGENTS.md §8–9, no further Copilot review is requested; proceeding to fresh full-gate and required-check verification. |
Closes #171
What changed
Public provider API v1
korvid.agent.provider_pluginPROVIDER_PLUGIN_API_VERSION = 1ProviderPluginMetadata/ProviderPluginConfigProviderPluginABCValidatedPluginProviderCredentialSource; no Kubernetes,UI, tool executor, approval, proposal, or audit objects.
text_delta,tool_call,usage,done;strict types and bounds (64 KiB text/arguments, 256-char IDs/names,
non-negative usage <= 1e9), extra fields removed, malformed events fail
before runtime history/tool dispatch is mutated.
Selected-only entry-point loading
ProviderPluginRegistrydiscoverskorvid.providermetadata but calls.load()only for the explicitly selected normalized name..dist-info+entry_points.txt) proves unrelatedentry points are never imported.
(
openai_compat,github_copilot, whitespace/case variants) stay on thebuilt-in path and are rejected directly by the plugin loader.
bounded; import/metadata/factory errors expose no plugin payload or
credential text.
read once.
Bounded plugin options
agent.optionssupports data-only JSON-like config:secret,password,token,api_key,authorization,credential, including Unicode lookalikes)warning; the base TUI remains usable.
AgentSettings.optionsis deeply immutable (nested mapping proxies,lists frozen to tuples) and preserved across reconnect/model flows without
being rewritten by the generic wizard.
Wiring and lifecycle
and rebuild.
failures use the existing setup error surface.
consumes close errors safely.
replace the active provider.
:ai off, and shutdown close the active plugin provider exactlyonce.
importlib.metadata.entry_pointsorimport plugin modules.
Documentation
docs/provider-plugins.md: exact API, complete package/adapter example,event/options limits, lifecycle, compatibility, selected-only behavior, and
trusted-code security warning.
and third-party protocol/auth adapters.
Verification
boundary and reserved-name normalization findings.