"use strict"; const path = require("node:path"); const { PLUGIN_API_VERSION } = require("./constants.cjs"); const { PluginDatabase } = require("./database.cjs"); const { PluginCompanionSupervisor } = require("./companionSupervisor.cjs"); const { PluginCredentialBroker, assertLeaseParams } = require("./credentialBroker.cjs"); const { PluginCredentialCatalog } = require("./credentialCatalog.cjs"); const { PluginContributionService } = require("./contributionService.cjs"); const { PluginContributionIconService } = require("./contributionIconService.cjs"); const { PluginFilesystemBroker } = require("./filesystemBroker.cjs"); const { createDefaultPluginHostRpcRegistry } = require("./hostRpcRegistry.cjs"); const { createDefaultPluginModuleResources, createUtilityModuleMappings, normalizePluginModuleResources, } = require("./moduleResources.cjs"); const { PackageStore } = require("./packageStore.cjs"); const { createPluginPaths } = require("./paths.cjs"); const { PluginManager } = require("./pluginManager.cjs"); const { PluginNetworkBroker } = require("./networkBroker.cjs"); const { PluginPermissionEngine } = require("./permissionEngine.cjs"); const { PluginProtocol } = require("./pluginProtocol.cjs"); const { PluginViewHost } = require("./pluginViewHost.cjs"); const { RuntimeSupervisor, assertStorageParams, resolveDefaultRuntimeKind, } = require("./runtimeSupervisor.cjs"); const { PluginQuotaManager } = require("./quotaManager.cjs"); const { registerSecurePluginCapabilities } = require("./secureCapabilities.cjs"); const { PluginSecretStore } = require("./secretStore.cjs"); const { SecretLeaseStore } = require("./secretLease.cjs"); const { PluginTerminalProviderService } = require("./terminalProviderService.cjs"); const { PluginTerminalDataPipelineService } = require("./terminalDataPipelineService.cjs"); const { PluginExtensionProviderService } = require("./extensionProviderService.cjs"); const { PluginSyncSidecarService } = require("./pluginSyncSidecarService.cjs"); function getElectronProcessMetrics(app, pid) { const metric = app.getAppMetrics?.().find((candidate) => candidate.pid === pid); if (!metric) return null; return { cpuPercent: Number(metric.cpu?.percentCPUUsage ?? 0), memoryBytes: Number(metric.memory?.workingSetSize ?? 0) * 1024, }; } /** * Full installed-manifest catalog of sync providers (active + inactive versions). * Shared by startup and enable-time backfill so ambiguity checks always see * cross-plugin / nested-namespace claims. * @param {{ listInstalledVersions?: () => Array<{ pluginId?: string, manifest?: { contributes?: { providers?: Array<{ kind?: string, id?: string }> } } }> }} database * @returns {Array<{ pluginId: string, provider: { id: string } }>} */ function collectInstalledSyncProviderCatalog(database) { const installedSyncProviders = []; const seen = new Set(); const versions = typeof database.listInstalledVersions === "function" ? database.listInstalledVersions() : []; for (const version of versions) { const pluginId = version?.pluginId; if (typeof pluginId !== "string" || !version.manifest) continue; for (const provider of version.manifest.contributes?.providers ?? []) { if (provider?.kind !== "sync") continue; if (typeof provider.id !== "string" || provider.id.length < 1) continue; const key = `${pluginId}\0${provider.id}`; if (seen.has(key)) continue; seen.add(key); installedSyncProviders.push({ pluginId, provider: { id: provider.id }, }); } } return installedSyncProviders; } function createPluginHostService(options) { const paths = createPluginPaths(options.app.getPath("userData")); const appRoot = options.appRoot ?? options.app.getAppPath(); const runtimeDirectory = options.runtimeDirectory ?? path.join(__dirname, "runtime"); const database = new PluginDatabase(paths.database); try { const packageStore = new PackageStore({ paths, database, netcattyVersion: options.app.getVersion(), apiVersion: PLUGIN_API_VERSION, supportedFeatures: options.supportedFeatures ?? [], }); const contributionIconService = new PluginContributionIconService({ database, packageStore, BrowserWindow: options.electron.BrowserWindow, rasterizeIcon: options.rasterizeContributionIcon, }); const moduleResources = options.moduleResources ? normalizePluginModuleResources(options.moduleResources) : createDefaultPluginModuleResources(appRoot, options.additionalModuleResources ?? []); const protocol = new PluginProtocol({ runtimeDirectory, moduleResources, }); const permissionEngine = new PluginPermissionEngine({ database, requestDecision: options.requestPermissionDecision, }); const quotaManager = new PluginQuotaManager({ getProcessMetrics: options.getProcessMetrics ?? ((pid) => getElectronProcessMetrics(options.app, pid)), quotas: options.quotas, }); const secretStore = new PluginSecretStore({ database, safeStorage: options.safeStorage ?? options.electron.safeStorage, }); const leaseStore = new SecretLeaseStore({ secretStore }); const credentialResolver = options.credentialResolver ?? new PluginCredentialCatalog({ safeStorage: options.safeStorage ?? options.electron.safeStorage, }); const credentialBroker = new PluginCredentialBroker({ secretStore, leaseStore, credentialResolver, }); const filesystemBroker = new PluginFilesystemBroker({ quotaManager, openDirectoryHandle: options.openPluginDirectoryHandle, }); const networkBroker = new PluginNetworkBroker({ fetch: options.fetch ?? (options.electron.net?.fetch ? (...args) => options.electron.net.fetch(...args) : undefined), permissionEngine, quotaManager, }); let runtimeSupervisor; const companionSupervisor = new PluginCompanionSupervisor({ paths, quotaManager, spawn: options.spawnCompanion, onContainmentFailure: (identity, error) => ( runtimeSupervisor?.enforcePolicyViolation(identity, error) ), }); const rpcRegistry = options.rpcRegistry ?? createDefaultPluginHostRpcRegistry({ assertStorageParams, database, }); const runtimeAccess = Object.freeze({ start: (...args) => runtimeSupervisor.start(...args), request: (...args) => runtimeSupervisor.request(...args), notify: (...args) => runtimeSupervisor.notify(...args), getRuntimeIdentity: (...args) => runtimeSupervisor.getRuntimeIdentity(...args), }); const contributionService = new PluginContributionService({ database, runtimeSupervisor: runtimeAccess, secretStore, getLocale: options.getLocale, }); contributionService.registerRpcCapabilities(rpcRegistry); registerSecurePluginCapabilities(rpcRegistry, { assertLeaseParams, companionSupervisor, credentialBroker, filesystemBroker, networkBroker, permissionEngine, quotaManager, secretStore, }); const configuredRegistry = options.configureRpcRegistry?.(rpcRegistry); if (configuredRegistry && typeof configuredRegistry.then === "function") { throw new TypeError("Plugin host RPC registry configuration must be synchronous"); } const requestedRuntimeResolver = options.resolveRuntimeKind ?? resolveDefaultRuntimeKind; const resolveRuntimeKind = async (context) => { const kind = await requestedRuntimeResolver(context); if ( (kind !== "browser" && kind !== "utility") || !context.availableKinds.includes(kind) ) throw new Error(`Plugin runtime selection is unavailable: ${String(kind)}`); await permissionEngine.authorizeRequired(context.plugin, { securityPrincipal: context.securityPrincipal, signal: context.signal, skipPermissions: ["runtime.advanced"], }); if (kind === "utility") { await permissionEngine.authorize({ pluginId: context.plugin.id, pluginVersion: context.plugin.activeVersion, runtimeId: null, manifest: context.plugin.manifest, securityPrincipal: context.securityPrincipal, signal: context.signal, }, { permission: "runtime.advanced", resources: ["*"], reason: "Start an advanced runtime with full Node, filesystem, and network authority", operationId: `runtime.advanced:${context.plugin.activeVersion}`, }); } return kind; }; const runtimeMessageGuard = (identity, message) => { quotaManager.guardMessage(identity, message); return options.runtimeMessageGuard?.(identity, message); }; runtimeSupervisor = new RuntimeSupervisor({ electron: options.electron, database, packageStore, protocol, paths, netcattyVersion: options.app.getVersion(), apiVersion: PLUGIN_API_VERSION, supportedFeatures: options.supportedFeatures ?? [], runtimeDirectory, appRoot, rpcRegistry, resolveRuntimeKind, resolveSecurityPrincipal: options.resolveSecurityPrincipal, runtimeMessageGuard, getInitialEnvironment: () => contributionService.getEnvironment(), runtimeResourceMonitor: quotaManager, runtimeCleanup: async (identity) => { leaseStore.revokeRuntime(identity.runtimeId); await companionSupervisor.releaseRuntime(identity.runtimeId); }, utilityModuleMappings: options.utilityModuleMappings ?? createUtilityModuleMappings(moduleResources), }); runtimeSupervisor.onDidChangeRuntime((event) => contributionService.onRuntimeStateChanged(event)); const terminalProviderService = new PluginTerminalProviderService({ contributionService, permissionEngine, runtimeSupervisor, }); const extensionProviderService = new PluginExtensionProviderService({ contributionService, leaseStore, permissionEngine, rpcRegistry, runtimeSupervisor, }); const syncSidecarService = new PluginSyncSidecarService({ database, contributionService, }); // Replay retained cloud sidecars into plugin_settings before the plugin // runtime starts so onStartupFinished sees hydrated values. const originalOnPluginEnabled = contributionService.onPluginEnabled.bind(contributionService); contributionService.onPluginEnabled = async (pluginId) => { try { await syncSidecarService.hydrateInstalledPluginSettings(pluginId); } catch { // Hydration is best-effort; enable must still succeed. } const enabled = await originalOnPluginEnabled(pluginId); try { // Use the same installed-manifest catalog as startup so cross-plugin / // inactive-version ambiguity checks still apply when only this plugin // was just enabled (Codex P2 on d3a9b94c). if (typeof secretStore.backfillSyncProviderBindingsFromLiveProviders === "function") { secretStore.backfillSyncProviderBindingsFromLiveProviders( collectInstalledSyncProviderCatalog(database), ); } } catch { // Best-effort; enable must still succeed. } return enabled; }; // Live-provider seed must wait until PackageStore.recover() has restored // missing plugin_versions. Construction-time seed can bind a stale owner // against an incomplete catalog; post-recovery seed then skips it as // existing (Codex P2 on f2b1b5d8). syncDeleteSecrets already awaits // resolveManager() → package initialize, so cleanup sees the post-recovery // seed without racing an early incomplete bind. const seedSyncBindings = () => { if (typeof secretStore.backfillSyncProviderBindingsFromLiveProviders !== "function") return; secretStore.backfillSyncProviderBindingsFromLiveProviders( collectInstalledSyncProviderCatalog(database), ); }; // PackageStore.recover() repopulates plugin_versions; only then promote // legacy maps and seed live bindings (Codex P2 on dd7aad70 / f2b1b5d8). const originalPackageInitialize = packageStore.initialize.bind(packageStore); packageStore.initialize = async () => { const result = await originalPackageInitialize(); try { if (typeof database.backfillSyncProviderBindingsFromLegacySecrets === "function") { database.backfillSyncProviderBindingsFromLegacySecrets(); } seedSyncBindings(); } catch { // Best-effort } return result; }; // Startup activation goes through contributionService.initialize() → #startPlugin // without onPluginEnabled. Hydrate every enabled plugin before that path runs. const originalInitialize = contributionService.initialize.bind(contributionService); contributionService.initialize = async () => { for (const plugin of database.listPlugins()) { if (!plugin?.enabled || typeof plugin.id !== "string") continue; try { await syncSidecarService.hydrateInstalledPluginSettings(plugin.id); } catch { // Best-effort; startup must continue. } } // Re-seed after any package catalog changes during init. try { if (typeof secretStore.backfillSyncProviderBindingsFromLiveProviders === "function") { secretStore.backfillSyncProviderBindingsFromLiveProviders( collectInstalledSyncProviderCatalog(database), ); } } catch { // Best-effort; startup must continue. } return originalInitialize(); }; const terminalDataPipelineService = options.electron.MessageChannelMain ? new PluginTerminalDataPipelineService({ contributionService, permissionEngine, runtimeSupervisor, MessageChannelMain: options.electron.MessageChannelMain, requestSelection: options.requestTerminalInterceptorSelection, showWarning: options.showTerminalInterceptorWarning, }) : null; quotaManager.setViolationHandler((identity, error) => ( runtimeSupervisor.enforcePolicyViolation(identity, error) )); const viewHost = options.electron.WebContentsView && options.electron.ipcMain ? new PluginViewHost({ electron: options.electron, protocol, packageStore, database, contributionService, }) : null; const manager = new PluginManager({ database, packageStore, runtimeSupervisor, contributionService, beforeClose: async () => { terminalDataPipelineService?.shutdown(); extensionProviderService.shutdown(); credentialResolver.shutdown?.(); await viewHost?.shutdown(); await companionSupervisor.shutdown(); leaseStore.shutdown(); permissionEngine.shutdown(); quotaManager.shutdown(); }, }); return { companionSupervisor, contributionIconService, contributionService, credentialBroker, credentialResolver, database, filesystemBroker, extensionProviderService, syncSidecarService, leaseStore, manager, moduleResources, packageStore, paths, protocol, networkBroker, permissionEngine, quotaManager, rpcRegistry, runtimeSupervisor, secretStore, terminalProviderService, terminalDataPipelineService, viewHost, }; } catch (error) { database.close(); throw error; } } module.exports = { createPluginHostService, getElectronProcessMetrics };