Files
openclaw/src/commands/runtime-plugin-install.ts
Peter Steinberger a789b92b39 fix(macos): harden fresh AI onboarding (#102637)
* fix(macos): bootstrap Codex before onboarding test

* fix(plugins): activate deferred runtimes on demand

* fix(macos): stage Codex runtime for onboarding probe

* fix(macos): expose sanitized onboarding errors

* fix(codex): reuse native CLI auth during onboarding

* fix(macos): hide Crestodian without inference

* fix(codex): honor explicit runtime policy during setup

* fix(onboarding): redact verification errors

* fix(macos): reset onboarding state with gateway route

* fix(onboarding): persist Codex plugin install metadata

* chore: refresh onboarding inventories

* fix(macos): restart AI setup after gateway change

* chore: refresh native onboarding inventory

* fix(onboarding): defer gateway restart until setup persists

* fix(macos): reset Crestodian on gateway changes

* chore(macos): refresh native i18n inventory

* fix(onboarding): harden inference setup state

* docs(skills): reuse pristine Parallels snapshots

* chore(macos): refresh onboarding i18n inventory

* fix(onboarding): prevent stale gateway and install state

* docs(skills): preserve saved Parallels sessions

* fix(macos): balance AI onboarding layout

* fix(parallels): parse npm workspace pack output

* chore(macos): refresh onboarding i18n inventory

* fix(parallels): bundle workspace runtime in package artifact

* fix(parallels): isolate canonical package helper

* fix(parallels): pack self-contained workspace artifact

* fix(packaging): exclude macOS app bundle

* fix(macos): restart inference checks after route changes

* docs(changelog): defer onboarding note to release

* fix(onboarding): enforce inference-first gateway setup
2026-07-10 04:59:15 +01:00

210 lines
6.8 KiB
TypeScript

/**
* Runtime plugin install helpers for model selection.
*
* Model choices can require runtime plugins such as Codex or Copilot; this
* module installs, enables, or repairs those plugins from a shared descriptor.
*/
import { existsSync } from "node:fs";
import path from "node:path";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { PluginInstallRecord } from "../config/types.plugins.js";
import { enablePluginInConfig } from "../plugins/enable.js";
import { loadInstalledPluginIndexInstallRecords } from "../plugins/installed-plugin-index-records.js";
import type { RuntimeEnv } from "../runtime.js";
import { resolveUserPath } from "../utils.js";
import type { WizardPrompter } from "../wizard/prompts.js";
/** Static install metadata for a runtime plugin required by model selection. */
export type RuntimePluginInstallDescriptor = {
pluginId: string;
label: string;
npmSpec: string;
warningLabel: string;
};
/** Result returned after ensuring a runtime plugin for a selected model. */
export type RuntimePluginInstallResult = {
cfg: OpenClawConfig;
required: boolean;
installed: boolean;
status?: "installed" | "skipped" | "failed" | "timed_out";
reason?: string;
};
/** Predicate that decides whether a config/model pair needs the runtime plugin. */
export type RuntimePluginSelection = (params: {
cfg: OpenClawConfig;
model?: string;
agentId?: string;
}) => boolean;
/** Parameters for installing or enabling a runtime plugin during setup. */
export type RuntimePluginEnsureParams = {
cfg: OpenClawConfig;
model?: string;
agentId?: string;
prompter: WizardPrompter;
runtime: RuntimeEnv;
workspaceDir?: string;
};
/** Parameters for doctor-style runtime plugin repair. */
export type RuntimePluginRepairParams = {
cfg: OpenClawConfig;
model?: string;
agentId?: string;
env?: NodeJS.ProcessEnv;
};
/** Convenience helpers bound to one runtime plugin descriptor. */
export type RuntimePluginModelSelectionHelpers = {
ensure: (params: RuntimePluginEnsureParams) => Promise<RuntimePluginInstallResult>;
repair: (
params: RuntimePluginRepairParams,
) => Promise<{ required: boolean; changes: string[]; warnings: string[] }>;
};
function isInstalledRecordPresentOnDisk(
record: PluginInstallRecord | undefined,
env: NodeJS.ProcessEnv,
): boolean {
const installPath = record?.installPath?.trim();
if (!installPath) {
return false;
}
return existsSync(path.join(resolveUserPath(installPath, env), "package.json"));
}
/** Ensures the runtime plugin required by the selected model is installed and enabled. */
async function ensureRuntimePluginForModelSelection(params: {
cfg: OpenClawConfig;
model?: string;
agentId?: string;
prompter: WizardPrompter;
runtime: RuntimeEnv;
workspaceDir?: string;
descriptor: RuntimePluginInstallDescriptor;
shouldEnsure: RuntimePluginSelection;
}): Promise<RuntimePluginInstallResult> {
if (
!params.shouldEnsure({
cfg: params.cfg,
model: params.model,
agentId: params.agentId,
})
) {
return {
cfg: params.cfg,
required: false,
installed: false,
};
}
const existingRecords = await loadInstalledPluginIndexInstallRecords({ env: process.env });
if (isInstalledRecordPresentOnDisk(existingRecords[params.descriptor.pluginId], process.env)) {
// A recorded install with package.json on disk can be repaired/enabled
// without re-downloading the plugin during setup.
const repair = await repairRuntimePluginInstallForModelSelection({
cfg: params.cfg,
model: params.model,
agentId: params.agentId,
env: process.env,
descriptor: params.descriptor,
shouldEnsure: params.shouldEnsure,
});
for (const change of repair.changes) {
params.runtime.log?.(change);
}
for (const warning of repair.warnings) {
params.runtime.log?.(`${params.descriptor.warningLabel} update warning: ${warning}`);
}
const enableResult = enablePluginInConfig(params.cfg, params.descriptor.pluginId);
return {
cfg: enableResult.config,
required: true,
installed: enableResult.enabled,
status: enableResult.enabled ? "installed" : "failed",
...(enableResult.reason ? { reason: enableResult.reason } : {}),
};
}
const { ensureOnboardingPluginInstalled } = await import("./onboarding-plugin-install.js");
// Defer to the onboarding plugin installer so runtime plugin installs get the
// same trust, record, timeout, and progress handling as channel/provider setup.
const result = await ensureOnboardingPluginInstalled({
cfg: params.cfg,
entry: {
pluginId: params.descriptor.pluginId,
label: params.descriptor.label,
install: {
npmSpec: params.descriptor.npmSpec,
defaultChoice: "npm",
},
trustedSourceLinkedOfficialInstall: true,
preferRemoteInstall: true,
},
prompter: params.prompter,
runtime: params.runtime,
...(params.workspaceDir !== undefined ? { workspaceDir: params.workspaceDir } : {}),
promptInstall: false,
autoConfirmSingleSource: true,
});
return {
cfg: result.cfg,
required: true,
installed: result.installed,
status: result.status,
};
}
/** Repairs missing install records for runtime plugins required by model selection. */
async function repairRuntimePluginInstallForModelSelection(params: {
cfg: OpenClawConfig;
model?: string;
agentId?: string;
env?: NodeJS.ProcessEnv;
descriptor: RuntimePluginInstallDescriptor;
shouldEnsure: RuntimePluginSelection;
}): Promise<{ required: boolean; changes: string[]; warnings: string[] }> {
if (
!params.shouldEnsure({
cfg: params.cfg,
model: params.model,
agentId: params.agentId,
})
) {
return { required: false, changes: [], warnings: [] };
}
const { repairMissingPluginInstallsForIds } =
await import("./doctor/shared/missing-configured-plugin-install.js");
const result = await repairMissingPluginInstallsForIds({
cfg: params.cfg,
pluginIds: [params.descriptor.pluginId],
...(params.env !== undefined ? { env: params.env } : {}),
});
return {
required: true,
changes: result.changes,
warnings: [...result.warnings, ...(result.notices ?? [])],
};
}
/** Creates ensure/repair helpers pre-bound to a runtime plugin descriptor. */
export function createRuntimePluginModelSelectionHelpers(params: {
descriptor: RuntimePluginInstallDescriptor;
shouldEnsure: RuntimePluginSelection;
}): RuntimePluginModelSelectionHelpers {
return {
ensure: (ensureParams) =>
ensureRuntimePluginForModelSelection({
...ensureParams,
descriptor: params.descriptor,
shouldEnsure: params.shouldEnsure,
}),
repair: (repairParams) =>
repairRuntimePluginInstallForModelSelection({
...repairParams,
descriptor: params.descriptor,
shouldEnsure: params.shouldEnsure,
}),
};
}