[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
397
electron/plugins/hostService.cjs
Normal file
397
electron/plugins/hostService.cjs
Normal file
@@ -0,0 +1,397 @@
|
||||
"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 };
|
||||
Reference in New Issue
Block a user