summaryrefslogtreecommitdiffhomepage
path: root/packages/host-bin/src/load-external.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-10 09:51:00 +0900
committerAdam Malczewski <[email protected]>2026-06-10 09:51:00 +0900
commita980aba97aed34692dd655e716747c4ff53fb186 (patch)
tree56b32ed9db8f66f9f6141eda923f5d1f7c1ba6d9 /packages/host-bin/src/load-external.ts
parent52ce1bb8b25cc6ba4ba7d2734c35c95e0a08d723 (diff)
downloaddispatch-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.ts47
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";
+}