import type { Bus } from "../bus/bus.js"; import type { AuthContract } from "../contracts/auth.js"; import type { ConfigAccess, EventsEmitter, Extension, HostAPI, Manifest, PermissionGate, ScheduledJob, SecretsAccess, StorageNamespace, } from "../contracts/extension.js"; import type { EventHandler, EventHookDescriptor, FilterDescriptor, FilterHandler, ServiceHandle, } from "../contracts/hooks.js"; import type { LogDeps, Logger, LogSink } from "../contracts/logging.js"; import type { ProviderContract } from "../contracts/provider.js"; import type { ToolContract } from "../contracts/tool.js"; import { createLogger } from "../logging/logger.js"; import { resolveActivationOrder } from "./dag.js"; import { isApiVersionCompatible } from "./version.js"; export const KERNEL_API_VERSION = "0.1.0"; export interface DisabledExtension { readonly manifest: Manifest; readonly reason: string; } export interface HostDeps { readonly logger: Logger; readonly config: ConfigAccess; readonly storageFactory: (namespace: string) => StorageNamespace; readonly secrets: SecretsAccess; readonly permissions: PermissionGate; readonly scheduler: { readonly register: (job: ScheduledJob) => void }; readonly bus: Bus; readonly events: EventsEmitter; readonly logSink: LogSink; readonly logDeps: LogDeps; } export interface Host { readonly activate: () => Promise; readonly deactivate: () => Promise; readonly getTools: () => ReadonlyMap; readonly getTool: (name: string) => ToolContract | undefined; readonly getProviders: () => ReadonlyMap; readonly getProvider: (id: string) => ProviderContract | undefined; readonly getAuthProviders: () => ReadonlyMap; readonly getAuthProvider: (id: string) => AuthContract | undefined; readonly getScheduledJobs: () => readonly ScheduledJob[]; readonly getMigrations: () => readonly string[]; readonly getDisabled: () => readonly DisabledExtension[]; readonly getExtensions: () => readonly Manifest[]; readonly getHostAPI: () => HostAPI; } export function createHost(extensions: readonly Extension[], deps: HostDeps): Host { const tools = new Map(); const providers = new Map(); const authProviders = new Map(); const scheduledJobs: ScheduledJob[] = []; const migrations: string[] = []; const disabled: DisabledExtension[] = []; const activated: Extension[] = []; const ordered = resolveActivationOrder(extensions.map((e) => e.manifest)); const extById = new Map(); for (const ext of extensions) { extById.set(ext.manifest.id, ext); } const compatible: Extension[] = []; for (const m of ordered) { const ext = extById.get(m.id); if (ext === undefined) continue; if (isApiVersionCompatible(m.apiVersion, KERNEL_API_VERSION)) { compatible.push(ext); } else { disabled.push({ manifest: m, reason: `apiVersion "${m.apiVersion}" is incompatible with kernel API ${KERNEL_API_VERSION}`, }); deps.logger.warn(`Extension "${m.id}" disabled: apiVersion incompatible`); } } for (const ext of compatible) { const extMigrations = ext.manifest.contributes?.migrations; if (extMigrations) { for (const migration of extMigrations) { migrations.push(migration); } } } function buildHostAPI( extensionId: string, opts?: { readonly registrationClosed?: boolean }, ): HostAPI { const closed = opts?.registrationClosed ?? false; const extLogger = createLogger({ extensionId }, deps.logSink, deps.logDeps); return { defineTool(tool: ToolContract) { if (closed) throw new Error("Registration not available after activation"); tools.set(tool.name, tool); }, defineProvider(provider: ProviderContract) { if (closed) throw new Error("Registration not available after activation"); providers.set(provider.id, provider); }, defineAuth(auth: AuthContract) { if (closed) throw new Error("Registration not available after activation"); authProviders.set(auth.id, auth); }, on(hook: EventHookDescriptor, handler: EventHandler) { return deps.bus.on(hook, handler); }, emit(hook: EventHookDescriptor, payload: TPayload) { deps.bus.emit(hook, payload); }, addFilter(hook: FilterDescriptor, fn: FilterHandler) { return deps.bus.addFilter(hook, fn); }, async applyFilters( hook: FilterDescriptor, value: TValue, opts?: { readonly failClosed?: boolean }, ): Promise { return deps.bus.applyFilters(hook, value, opts); }, provideService(handle: ServiceHandle, impl: T) { deps.bus.provideService(handle, impl); }, getService(handle: ServiceHandle): T { return deps.bus.getService(handle); }, storage(namespace: string): StorageNamespace { return deps.storageFactory(namespace); }, config: deps.config, secrets: deps.secrets, permissions: deps.permissions, events: deps.events, logger: extLogger, getProviders() { return providers; }, getTools() { return tools; }, getAuthProviders() { return authProviders; }, getAuthProvider(id: string) { return authProviders.get(id); }, getExtensions() { return Object.freeze(activated.map((e) => e.manifest)); }, scheduler: { register(job: ScheduledJob) { scheduledJobs.push(job); deps.scheduler.register(job); }, }, }; } return { async activate() { for (const ext of compatible) { try { await ext.activate(buildHostAPI(ext.manifest.id)); activated.push(ext); deps.logger.info(`Extension "${ext.manifest.id}" activated`); } catch (err) { disabled.push({ manifest: ext.manifest, reason: `Activation failed: ${err instanceof Error ? err.message : String(err)}`, }); deps.logger.error(`Extension "${ext.manifest.id}" failed to activate`, { err }); } } }, async deactivate() { for (let i = activated.length - 1; i >= 0; i--) { const ext = activated[i]; if (ext === undefined || ext.deactivate === undefined) continue; try { await ext.deactivate(); } catch (err) { deps.logger.error(`Extension "${ext.manifest.id}" failed to deactivate`, { err }); } } }, getTools() { return tools; }, getTool(name: string) { return tools.get(name); }, getProviders() { return providers; }, getProvider(id: string) { return providers.get(id); }, getAuthProviders() { return authProviders; }, getAuthProvider(id: string) { return authProviders.get(id); }, getScheduledJobs() { return scheduledJobs; }, getMigrations() { return migrations; }, getDisabled() { return disabled; }, getExtensions() { return Object.freeze(activated.map((e) => e.manifest)); }, getHostAPI() { return buildHostAPI("__host__", { registrationClosed: true }); }, }; }