diff options
| author | Adam Malczewski <[email protected]> | 2026-06-10 09:51:00 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-10 09:51:00 +0900 |
| commit | a980aba97aed34692dd655e716747c4ff53fb186 (patch) | |
| tree | 56b32ed9db8f66f9f6141eda923f5d1f7c1ba6d9 /packages/host-bin/src/load-external.ts | |
| parent | 52ce1bb8b25cc6ba4ba7d2734c35c95e0a08d723 (diff) | |
| download | dispatch-a980aba97aed34692dd655e716747c4ff53fb186.tar.gz dispatch-a980aba97aed34692dd655e716747c4ff53fb186.zip | |
host-bin: external-extension loader + claude credential wiring
Add loadExternalExtensions(): fault-isolated dynamic import of out-of-repo
extensions declared via DISPATCH_EXTERNAL_EXTENSIONS. main.ts assembles the
credential-store in boot() so a 'claude' credential is registered when an
external anthropic provider is loaded; config.ts surfaces the anthropic model /
credential-key settings those extensions read.
Diffstat (limited to 'packages/host-bin/src/load-external.ts')
| -rw-r--r-- | packages/host-bin/src/load-external.ts | 47 |
1 files changed, 47 insertions, 0 deletions
diff --git a/packages/host-bin/src/load-external.ts b/packages/host-bin/src/load-external.ts new file mode 100644 index 0000000..34b8bce --- /dev/null +++ b/packages/host-bin/src/load-external.ts @@ -0,0 +1,47 @@ +import type { Extension, Logger } from "@dispatch/kernel"; + +/** + * Load external (out-of-repo) extensions by dynamic import. + * + * Each specifier is a module the host imports at boot: an absolute path to a + * built/source entry, or a package name resolvable from the host's working + * directory. The module must expose an `Extension` as `export const extension` + * (or as its default export). The host's own loader (`createHost`) then applies + * apiVersion gating, dependency ordering, the capability gate, and per-extension + * fault isolation — external extensions get exactly the same treatment as + * bundled ones. + * + * Fault-isolated by design: a specifier that fails to import or exports no valid + * extension is logged and SKIPPED — a broken external extension must never crash + * boot (defend faults, not adversaries; never leave the system broken). + */ +export async function loadExternalExtensions( + specifiers: readonly string[], + logger: Logger, +): Promise<Extension[]> { + const loaded: Extension[] = []; + for (const spec of specifiers) { + try { + const mod = (await import(spec)) as Record<string, unknown>; + const candidate = mod.extension ?? mod.default ?? mod; + if (isExtension(candidate)) { + loaded.push(candidate); + logger.info(`Loaded external extension "${candidate.manifest.id}" from ${spec}`); + } else { + logger.warn(`External module "${spec}" has no valid extension export; skipped`); + } + } catch (err) { + logger.error(`Failed to load external extension "${spec}"; skipped`, { err }); + } + } + return loaded; +} + +/** Structural check that a dynamically-imported value is an `Extension`. */ +function isExtension(value: unknown): value is Extension { + if (!value || typeof value !== "object") return false; + const e = value as { manifest?: unknown; activate?: unknown }; + if (typeof e.activate !== "function") return false; + const m = e.manifest as { id?: unknown } | undefined; + return !!m && typeof m.id === "string"; +} |
