Skip to content

fix(host): isolate any per-plugin bootstrap failure — one failed plugin must not take down the plugin system - #675

Merged
elliotBraem merged 1 commit into
mainfrom
fix/host-plugin-failure-isolation
Oct 31, 2026
Merged

elliotBraem merged 1 commit into
mainfrom
fix/host-plugin-failure-isolation

Conversation

@elliotBraem

Copy link
Copy Markdown
Contributor

Closes #672.

Problem

initializePlugins isolated the common failure (typed PluginBootstrapError from loadRemoteWithRetry) per plugin, but three other error classes escaped the loop's catchTag and hit the outer Effect.catch → unavailableResult(...) — discarding all plugins, including ones that had already loaded, and leaving the host without auth/api:

  • ConfigError from secrets resolution (Config.Redacted reads) — a typed failure the catchTag never matched
  • a defect (die) from buildAuthBaseVariables's production "No reachable origin" throw (and any other throw in the per-plugin path) — dies are not catchable by Effect.catch at all, so the fiber died and the whole plugin layer failed, preventing the host from starting
  • runtime-creation errors from registerAppSharedDeps (all-or-nothing by construction, kept global intentionally)

Fix

  • Secrets resolution and auth base-variable derivation moved inside loadPluginEntryEffect, under the guarded pipeline.
  • The loop's Effect.catchTag is now Effect.catchCauseIf: any failing cause (typed PluginBootstrapError, foreign ConfigError, thrown defect) is normalized via the new exported pluginFailureCause and recorded through recordPluginFailure — already-loaded plugins are kept.
  • Interrupts-only causes are re-raised (hasInterruptsOnly predicate) rather than swallowed.
  • A failed plugin of any kind (auth/api included) now feeds dependents a failedPluginsClientEntry, so dependents fail loudly with a clear message instead of the loop aborting.
  • Global failures (plugin runtime creation via createPluginRuntime/registerAppSharedDeps) keep the previous degraded-all behavior — nothing can load without the runtime.

Behavioral check

With this change, booting a config whose auth plugin dies mid-bootstrap (production origin misconfig — previously a fatal boot failure) now logs the failure, records it in /health's status.failures, and continues to load every other configured plugin. Host consumers were already null-safe (createSessionMiddleware serves without sessions, /api/* 503s without the API, SSR compose is plugin-independent), so the host still runs and exposes the UI.

Tests

  • host/tests/unit/plugins-failure-cause.test.ts — normalization: typed errors pass through unchanged; defects and foreign failures synthesize a stage-init PluginBootstrapError naming the plugin and URL.
  • host/tests/integration/plugin-failure-isolation.test.ts — drives initializePlugins with a stubbed plugin runtime (real DAG + real classification/retry machinery): a plugin whose bundle fails to load doesn't prevent a healthy sibling from loading; a plugin whose bootstrap dies fatally (production origin misconfig) is recorded (failed: auth, No reachable origin) while votes still loads.
  • Verified: host tsc --noEmit clean, full host suite green (247 unit+integration tests), biome check and oxlint --type-aware clean on touched files.

…fig errors no longer take down the whole plugin system (#672)

- secrets resolution and auth base-variable derivation moved inside the
  guarded per-plugin load; failures and thrown defects are normalized into
  PluginBootstrapError (pluginFailureCause) and recorded per plugin
- failure entries now feed every kind (auth/api included) a failedPlugins
  client entry so dependents fail loudly instead of the loop dying
- interrupts-only causes are re-raised; a caught defect names the plugin and
  its failing stage
- regression coverage: pluginFailureCause unit tests + an initializePlugins
  integration suite booting a failing plugin beside a healthy one
@elliotBraem
elliotBraem merged commit caa2d3b into main Oct 31, 2026
20 checks passed
@elliotBraem
elliotBraem deleted the fix/host-plugin-failure-isolation branch October 31, 2026 03:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

One failed plugin must not take down the whole plugin system (host/src/services/plugins.ts)

1 participant