feat: guarded plugin auto-detection — installing @soulcraft/cor is the opt-in
With plugins unset (the default), init() probes for the first-party accelerator: not installed means plain brainy with zero noise; installed and healthy means it loads and announces itself; installed but broken (import failure, invalid shape, failed activation, version mismatch) makes init() THROW. An installed accelerator never silently vanishes behind the JS engines - the anti-drift posture of the explicit list, applied to detection. plugins: []/false stays a true opt-out (no probe); an explicit list keeps its required-and-loud semantics. The import runs through an importPluginPackage seam (variable specifier - bundlers cannot static-resolve the optional package; tests simulate all outcomes without it installed). Also: when a plugin activates but registers zero native providers (e.g. a licensing gate declining to engage), the provider summary now warns loudly instead of leaving every query silently on the JS engines. Supersedes 8.0.8's explicit-opt-in wording; README/PLUGINS/types now document the guarded contract. 7 new tests (tests/unit/plugin-autodetect.test.ts).
This commit is contained in:
parent
b37359e097
commit
588267be7f
6 changed files with 265 additions and 26 deletions
|
|
@ -331,6 +331,10 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
* event-driven `whenMigrationComplete()` signal. See {@link awaitMigrationLock}. */
|
||||
private static readonly MIGRATION_POLL_INTERVAL_MS = 250
|
||||
|
||||
/** First-party accelerator packages probed by guarded auto-detection when
|
||||
* `plugins` is undefined (installing one IS the opt-in). See {@link loadPlugins}. */
|
||||
private static readonly AUTO_DETECT_PLUGIN_PACKAGES = ['@soulcraft/cor']
|
||||
|
||||
// Core components
|
||||
private index!: JsHnswVectorIndex
|
||||
private storage!: BaseStorage
|
||||
|
|
@ -1041,6 +1045,17 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
} else {
|
||||
console.log(`[brainy] Providers: ${native.length}/${wellKnownKeys.length} native (${plugins}) | default: ${fallback.join(', ')}`)
|
||||
}
|
||||
// An activated accelerator that registered NOTHING is running as pure
|
||||
// decoration — every query silently serves from the JS engines. Most
|
||||
// often a licensing gate declining to engage. Say so, loudly, so an
|
||||
// evaluator never concludes the accelerator "does nothing".
|
||||
if (native.length === 0) {
|
||||
console.warn(
|
||||
`[brainy] ⚠ ${plugins} activated but registered 0 native providers — all queries are ` +
|
||||
`running on the default JS engines. Check the plugin's requirements (e.g. a license ` +
|
||||
`key) and its logs; see docs/PLUGINS.md.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Mark as initialized BEFORE VFS init
|
||||
|
|
@ -13870,23 +13885,59 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
* Auto-detect and activate plugins.
|
||||
* Called internally during init().
|
||||
*/
|
||||
/**
|
||||
* Import a plugin package by name. Isolated as a seam so tests can simulate
|
||||
* the three auto-detect outcomes (not installed / broken install / valid)
|
||||
* without the package being present. The specifier is a variable, so
|
||||
* bundlers cannot statically resolve — and cannot force-include — the
|
||||
* optional accelerator.
|
||||
*/
|
||||
private async importPluginPackage(pkg: string): Promise<any> {
|
||||
return import(pkg)
|
||||
}
|
||||
|
||||
/**
|
||||
* True when a dynamic-import failure means "the package itself is not
|
||||
* installed" (the silent free path), as opposed to "the package is present
|
||||
* but broken" (which must fail loud). Node and Bun both name the missing
|
||||
* package in the resolution error; a failure naming anything else — an
|
||||
* internal file, a dependency of the plugin, a syntax error — is a broken
|
||||
* install, never a not-installed.
|
||||
*/
|
||||
private static isPackageNotInstalledError(error: unknown, pkg: string): boolean {
|
||||
const code = (error as { code?: string })?.code
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
const namesPackage =
|
||||
message.includes(`'${pkg}'`) || message.includes(`"${pkg}"`) || message.includes(` ${pkg}`)
|
||||
const isResolutionFailure =
|
||||
code === 'ERR_MODULE_NOT_FOUND' ||
|
||||
code === 'MODULE_NOT_FOUND' ||
|
||||
/cannot find (package|module)/i.test(message) ||
|
||||
/failed to resolve/i.test(message) // Bun's resolver phrasing
|
||||
return isResolutionFailure && namesPackage
|
||||
}
|
||||
|
||||
private async loadPlugins(): Promise<void> {
|
||||
// plugins config:
|
||||
// undefined (default) → no auto-detection (safe default)
|
||||
// false → no auto-detection
|
||||
// [] → no auto-detection
|
||||
// ['@soulcraft/cor'] → load only these explicitly listed packages
|
||||
// undefined (default) → guarded auto-detection of the first-party
|
||||
// accelerator: installing @soulcraft/cor IS the
|
||||
// opt-in. Not installed → plain brainy, silently.
|
||||
// Installed → it loads and announces itself; if it
|
||||
// is installed but broken, init() THROWS — an
|
||||
// installed accelerator never silently vanishes.
|
||||
// false / [] → no plugins, no detection (explicit opt-out)
|
||||
// ['@soulcraft/cor'] → load only these explicitly listed packages
|
||||
// Note: plugins registered via brain.use() are always activated regardless of config
|
||||
const pluginConfig = this.config.plugins
|
||||
if (Array.isArray(pluginConfig) && pluginConfig.length > 0) {
|
||||
// Explicit list: import and register the specified packages. A package
|
||||
// listed here is REQUIRED — brainy does no auto-detection, so a missing or
|
||||
// invalid plugin must fail LOUD, never silently fall back to the default
|
||||
// engine (the cross-repo drift this whole guard exists to prevent).
|
||||
// listed here is REQUIRED — a missing or invalid plugin must fail LOUD,
|
||||
// never silently fall back to the default engine (the cross-repo drift
|
||||
// this whole guard exists to prevent).
|
||||
for (const pkg of pluginConfig) {
|
||||
let mod: any
|
||||
try {
|
||||
mod = await import(pkg)
|
||||
mod = await this.importPluginPackage(pkg)
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`[brainy] Plugin "${pkg}" is listed in config.plugins but could not be loaded: ` +
|
||||
|
|
@ -13904,6 +13955,38 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
)
|
||||
}
|
||||
}
|
||||
} else if (pluginConfig === undefined) {
|
||||
// Guarded auto-detection (default). Installing the first-party
|
||||
// accelerator is the opt-in — probe for it, and apply the SAME loud
|
||||
// posture as the explicit branch to everything except "not installed":
|
||||
// a present-but-broken accelerator must never silently degrade to JS.
|
||||
for (const pkg of Brainy.AUTO_DETECT_PLUGIN_PACKAGES) {
|
||||
let mod: any
|
||||
try {
|
||||
mod = await this.importPluginPackage(pkg)
|
||||
} catch (error) {
|
||||
if (Brainy.isPackageNotInstalledError(error, pkg)) {
|
||||
continue // the free path: not installed, nothing to load, no noise
|
||||
}
|
||||
throw new Error(
|
||||
`[brainy] The accelerator "${pkg}" is installed but failed to load: ` +
|
||||
`${error instanceof Error ? error.message : String(error)}. ` +
|
||||
`brainy will NOT silently run the default JS engines in its place — ` +
|
||||
`fix the install (npm i ${pkg}) or disable detection with plugins: [].`
|
||||
)
|
||||
}
|
||||
const plugin: BrainyPlugin = mod.default || mod
|
||||
if (plugin && typeof plugin.activate === 'function' && plugin.name) {
|
||||
this.pluginRegistry.register(plugin)
|
||||
} else {
|
||||
throw new Error(
|
||||
`[brainy] The installed accelerator "${pkg}" is not a valid Brainy plugin ` +
|
||||
`(missing { name, activate }) — a broken or incompatible install. ` +
|
||||
`brainy will NOT silently run the default JS engines in its place — ` +
|
||||
`fix the install (npm i ${pkg}) or disable detection with plugins: [].`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create plugin context
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue