perf: lazy-load heavy test imports

This commit is contained in:
Peter Steinberger
2026-04-25 19:23:34 +01:00
parent 31456e3326
commit 75fcb8c56d
5 changed files with 144 additions and 36 deletions

View File

@@ -4,7 +4,6 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite
import type { OpenClawConfig } from "../../config/config.js";
import type { AcpSessionRuntimeOptions, SessionAcpMeta } from "../../config/sessions/types.js";
import { resetHeartbeatWakeStateForTests } from "../../infra/heartbeat-wake.js";
import { resetSystemEventsForTest } from "../../infra/system-events.js";
import { withTempDir } from "../../test-helpers/temp-dir.js";
import type { AcpRuntime, AcpRuntimeCapabilities } from "../runtime/types.js";
@@ -237,7 +236,6 @@ describe("AcpSessionManager", () => {
} else {
process.env.OPENCLAW_STATE_DIR = ORIGINAL_STATE_DIR;
}
resetSystemEventsForTest();
resetHeartbeatWakeStateForTests();
resetTaskRegistryForTests({ persist: false });
resetTaskFlowRegistryForTests({ persist: false });

View File

@@ -26,7 +26,6 @@ import type {
ToolCallLocation,
ToolKind,
} from "@agentclientprotocol/sdk";
import { PROTOCOL_VERSION } from "@agentclientprotocol/sdk";
import { listThinkingLevels } from "../auto-reply/thinking.js";
import type { GatewayClient } from "../gateway/client.js";
import type { EventFrame } from "../gateway/protocol/index.js";
@@ -37,7 +36,6 @@ import {
} from "../infra/fixed-window-rate-limit.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { shortenHomePath } from "../utils.js";
import { getAvailableCommands } from "./commands.js";
import {
extractAttachmentsFromPrompt,
extractToolCallContent,
@@ -63,6 +61,21 @@ const ACP_ELEVATED_LEVEL_CONFIG_ID = "elevated_level";
const ACP_LOAD_SESSION_REPLAY_LIMIT = 1_000_000;
const ACP_GATEWAY_DISCONNECT_GRACE_MS = 5_000;
let acpCommandsModulePromise: Promise<typeof import("./commands.js")> | undefined;
let acpSdkModulePromise: Promise<typeof import("@agentclientprotocol/sdk")> | undefined;
async function getAvailableCommandsForAcp() {
acpCommandsModulePromise ??= import("./commands.js");
const { getAvailableCommands } = await acpCommandsModulePromise;
return getAvailableCommands();
}
async function getAcpProtocolVersion() {
acpSdkModulePromise ??= import("@agentclientprotocol/sdk");
const { PROTOCOL_VERSION } = await acpSdkModulePromise;
return PROTOCOL_VERSION;
}
type DisconnectContext = {
generation: number;
reason: string;
@@ -502,7 +515,7 @@ export class AcpGatewayAgent implements Agent {
async initialize(_params: InitializeRequest): Promise<InitializeResponse> {
return {
protocolVersion: PROTOCOL_VERSION,
protocolVersion: await getAcpProtocolVersion(),
agentCapabilities: {
loadSession: true,
promptCapabilities: {
@@ -1212,7 +1225,7 @@ export class AcpGatewayAgent implements Agent {
sessionId,
update: {
sessionUpdate: "available_commands_update",
availableCommands: getAvailableCommands(),
availableCommands: await getAvailableCommandsForAcp(),
},
});
}

View File

@@ -1,5 +1,4 @@
import path from "node:path";
import { fileTypeFromBuffer } from "file-type";
import { type MediaKind, mediaKindFromMime } from "./constants.js";
/** @internal */
@@ -66,6 +65,8 @@ const AUDIO_FILE_EXTENSIONS = new Set([
".wav",
]);
let fileTypeModulePromise: Promise<typeof import("file-type")> | undefined;
export function normalizeMimeType(mime?: string | null): string | undefined {
if (!mime) {
return undefined;
@@ -87,6 +88,8 @@ async function sniffMime(buffer?: Buffer): Promise<string | undefined> {
return undefined;
}
try {
fileTypeModulePromise ??= import("file-type");
const { fileTypeFromBuffer } = await fileTypeModulePromise;
const type = await fileTypeFromBuffer(sliceMimeSniffBuffer(buffer));
return type?.mime ?? undefined;
} catch {

View File

@@ -5,11 +5,6 @@
*/
import fs from "node:fs/promises";
import path from "node:path";
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
import { SANDBOX_BROWSER_SECURITY_HASH_EPOCH } from "../agents/sandbox/constants.js";
import { execDockerRaw, type ExecDockerRawResult } from "../agents/sandbox/docker.js";
import { resolveSkillSource } from "../agents/skills/source.js";
import { listAgentWorkspaceDirs } from "../agents/workspace-dirs.js";
import { formatCliCommand } from "../cli/command-format.js";
import { MANIFEST_KEY } from "../compat/legacy-names.js";
import type { OpenClawConfig, ConfigFileSnapshot } from "../config/config.js";
@@ -20,19 +15,10 @@ import {
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "../shared/string-coerce.js";
import {
formatPermissionDetail,
formatPermissionRemediation,
inspectPathPermissions,
safeStat,
} from "./audit-fs.js";
import { extensionUsesSkippedScannerPath, isPathInside } from "./scan-paths.js";
import type { SkillScanFinding } from "./skill-scanner.js";
import * as skillScanner from "./skill-scanner.js";
import type { ExecFn } from "./windows-acl.js";
export { collectPluginsTrustFindings } from "./audit-plugins-trust.js";
export type SecurityAuditFinding = {
checkId: string;
severity: "info" | "warn" | "critical";
@@ -41,10 +27,16 @@ export type SecurityAuditFinding = {
remediation?: string;
};
type CollectPluginsTrustFindingsParams = Parameters<
typeof import("./audit-plugins-trust.js").collectPluginsTrustFindings
>[0];
type SkillScanSummary = Awaited<
ReturnType<typeof import("./skill-scanner.js").scanDirectoryWithSummary>
>;
type ExecDockerRawFn = (
args: string[],
opts?: { allowFailure?: boolean; input?: Buffer | string; signal?: AbortSignal },
) => Promise<ExecDockerRawResult>;
) => Promise<import("../agents/sandbox/docker.js").ExecDockerRawResult>;
type CodeSafetySummaryCache = Map<string, Promise<unknown>>;
type WorkspaceSkillScanLimits = {
@@ -91,6 +83,18 @@ function realpathWithTimeout(p: string, timeoutMs = 2000): Promise<string | null
}
let skillsModulePromise: Promise<typeof import("../agents/skills.js")> | undefined;
let configModulePromise: Promise<typeof import("../config/config.js")> | undefined;
let agentScopeModulePromise: Promise<typeof import("../agents/agent-scope.js")> | undefined;
let agentWorkspaceDirsModulePromise:
| Promise<typeof import("../agents/workspace-dirs.js")>
| undefined;
let skillSourceModulePromise: Promise<typeof import("../agents/skills/source.js")> | undefined;
let sandboxDockerModulePromise: Promise<typeof import("../agents/sandbox/docker.js")> | undefined;
let sandboxConstantsModulePromise:
| Promise<typeof import("../agents/sandbox/constants.js")>
| undefined;
let auditPluginsTrustModulePromise: Promise<typeof import("./audit-plugins-trust.js")> | undefined;
let auditFsModulePromise: Promise<typeof import("./audit-fs.js")> | undefined;
let skillScannerModulePromise: Promise<typeof import("./skill-scanner.js")> | undefined;
function loadSkillsModule() {
skillsModulePromise ??= import("../agents/skills.js");
@@ -102,10 +106,87 @@ function loadConfigModule() {
return configModulePromise;
}
function loadAuditFsModule() {
auditFsModulePromise ??= import("./audit-fs.js");
return auditFsModulePromise;
}
function loadAgentScopeModule() {
agentScopeModulePromise ??= import("../agents/agent-scope.js");
return agentScopeModulePromise;
}
function loadAgentWorkspaceDirsModule() {
agentWorkspaceDirsModulePromise ??= import("../agents/workspace-dirs.js");
return agentWorkspaceDirsModulePromise;
}
function loadSkillSourceModule() {
skillSourceModulePromise ??= import("../agents/skills/source.js");
return skillSourceModulePromise;
}
function loadSkillScannerModule() {
skillScannerModulePromise ??= import("./skill-scanner.js");
return skillScannerModulePromise;
}
async function loadExecDockerRaw(): Promise<ExecDockerRawFn> {
sandboxDockerModulePromise ??= import("../agents/sandbox/docker.js");
const { execDockerRaw } = await sandboxDockerModulePromise;
return execDockerRaw;
}
async function loadSandboxBrowserSecurityHashEpoch(): Promise<string> {
sandboxConstantsModulePromise ??= import("../agents/sandbox/constants.js");
const { SANDBOX_BROWSER_SECURITY_HASH_EPOCH } = await sandboxConstantsModulePromise;
return SANDBOX_BROWSER_SECURITY_HASH_EPOCH;
}
export async function collectPluginsTrustFindings(
params: CollectPluginsTrustFindingsParams,
): Promise<SecurityAuditFinding[]> {
auditPluginsTrustModulePromise ??= import("./audit-plugins-trust.js");
const { collectPluginsTrustFindings: collect } = await auditPluginsTrustModulePromise;
return await collect(params);
}
// --------------------------------------------------------------------------
// Helpers
// --------------------------------------------------------------------------
async function safeStat(targetPath: string): Promise<{
ok: boolean;
isSymlink: boolean;
isDir: boolean;
mode: number | null;
uid: number | null;
gid: number | null;
error?: string;
}> {
try {
const lst = await fs.lstat(targetPath);
return {
ok: true,
isSymlink: lst.isSymbolicLink(),
isDir: lst.isDirectory(),
mode: typeof lst.mode === "number" ? lst.mode : null,
uid: typeof lst.uid === "number" ? lst.uid : null,
gid: typeof lst.gid === "number" ? lst.gid : null,
};
} catch (err) {
return {
ok: false,
isSymlink: false,
isDir: false,
mode: null,
uid: null,
gid: null,
error: String(err),
};
}
}
function expandTilde(p: string, env: NodeJS.ProcessEnv): string | null {
if (!p.startsWith("~")) {
return p;
@@ -197,7 +278,7 @@ async function getCodeSafetySummary(params: {
dirPath: string;
includeFiles?: string[];
summaryCache?: CodeSafetySummaryCache;
}): Promise<Awaited<ReturnType<typeof skillScanner.scanDirectoryWithSummary>>> {
}): Promise<SkillScanSummary> {
const cacheKey = buildCodeSafetySummaryCacheKey({
dirPath: params.dirPath,
includeFiles: params.includeFiles,
@@ -206,14 +287,16 @@ async function getCodeSafetySummary(params: {
if (cache) {
const hit = cache.get(cacheKey);
if (hit) {
return (await hit) as Awaited<ReturnType<typeof skillScanner.scanDirectoryWithSummary>>;
return (await hit) as SkillScanSummary;
}
const skillScanner = await loadSkillScannerModule();
const pending = skillScanner.scanDirectoryWithSummary(params.dirPath, {
includeFiles: params.includeFiles,
});
cache.set(cacheKey, pending);
return await pending;
}
const skillScanner = await loadSkillScannerModule();
return await skillScanner.scanDirectoryWithSummary(params.dirPath, {
includeFiles: params.includeFiles,
});
@@ -388,7 +471,10 @@ export async function collectSandboxBrowserHashLabelFindings(params?: {
execDockerRawFn?: ExecDockerRawFn;
}): Promise<SecurityAuditFinding[]> {
const findings: SecurityAuditFinding[] = [];
const execFn = params?.execDockerRawFn ?? execDockerRaw;
const [execFn, browserHashEpoch] = await Promise.all([
params?.execDockerRawFn ? Promise.resolve(params.execDockerRawFn) : loadExecDockerRaw(),
loadSandboxBrowserSecurityHashEpoch(),
]);
const containers = await listSandboxBrowserContainers(execFn);
if (!containers || containers.length === 0) {
return findings;
@@ -406,7 +492,7 @@ export async function collectSandboxBrowserHashLabelFindings(params?: {
if (!labels.configHash) {
missingHash.push(containerName);
}
if (labels.epoch !== SANDBOX_BROWSER_SECURITY_HASH_EPOCH) {
if (labels.epoch !== browserHashEpoch) {
staleEpoch.push(containerName);
}
const portMappings = await readSandboxBrowserPortMappings({
@@ -444,7 +530,7 @@ export async function collectSandboxBrowserHashLabelFindings(params?: {
title: "Sandbox browser container hash epoch is stale",
detail:
`Containers: ${staleEpoch.join(", ")}. ` +
`Expected openclaw.browserConfigEpoch=${SANDBOX_BROWSER_SECURITY_HASH_EPOCH}.`,
`Expected openclaw.browserConfigEpoch=${browserHashEpoch}.`,
remediation: `${formatCliCommand("openclaw sandbox recreate --browser --all")} (add --force to skip prompt).`,
});
}
@@ -471,6 +557,7 @@ export async function collectWorkspaceSkillSymlinkEscapeFindings(params: {
skillScanLimits?: WorkspaceSkillScanLimits;
}): Promise<SecurityAuditFinding[]> {
const findings: SecurityAuditFinding[] = [];
const { listAgentWorkspaceDirs } = await loadAgentWorkspaceDirsModule();
const workspaceDirs = listAgentWorkspaceDirs(params.cfg);
if (workspaceDirs.length === 0) {
return findings;
@@ -589,6 +676,9 @@ export async function collectIncludeFilePermFindings(params: {
return findings;
}
const { formatPermissionDetail, formatPermissionRemediation, inspectPathPermissions } =
await loadAuditFsModule();
for (const p of includePaths) {
const perms = await inspectPathPermissions(p, {
env: params.env,
@@ -655,6 +745,8 @@ export async function collectStateDeepFilesystemFindings(params: {
}): Promise<SecurityAuditFinding[]> {
const findings: SecurityAuditFinding[] = [];
const oauthDir = resolveOAuthDir(params.env, params.stateDir);
const { formatPermissionDetail, formatPermissionRemediation, inspectPathPermissions } =
await loadAuditFsModule();
const oauthPerms = await inspectPathPermissions(oauthDir, {
env: params.env,
@@ -703,6 +795,7 @@ export async function collectStateDeepFilesystemFindings(params: {
)
.filter(Boolean)
: [];
const { resolveDefaultAgentId } = await loadAgentScopeModule();
const defaultAgentId = resolveDefaultAgentId(params.cfg);
const ids = Array.from(new Set([defaultAgentId, ...agentIds])).map((id) => normalizeAgentId(id));
@@ -943,6 +1036,10 @@ export async function collectInstalledSkillsCodeSafetyFindings(params: {
const findings: SecurityAuditFinding[] = [];
const pluginExtensionsDir = path.join(params.stateDir, "extensions");
const scannedSkillDirs = new Set<string>();
const [{ listAgentWorkspaceDirs }, { resolveSkillSource }] = await Promise.all([
loadAgentWorkspaceDirsModule(),
loadSkillSourceModule(),
]);
const workspaceDirs = listAgentWorkspaceDirs(params.cfg);
const { loadWorkspaceSkillEntries } = await loadSkillsModule();

View File

@@ -1,14 +1,5 @@
import { resolveSandboxConfigForAgent } from "../agents/sandbox/config.js";
import { isDangerousNetworkMode, normalizeNetworkMode } from "../agents/sandbox/network-mode.js";
/**
* Synchronous security audit collector functions.
*
* These functions analyze config-based security properties without I/O.
*/
export {
collectAttackSurfaceSummaryFindings,
collectSmallModelRiskFindings,
} from "./audit-extra.summary.js";
import { resolveSandboxToolPolicyForAgent } from "../agents/sandbox/tool-policy.js";
import type { SandboxToolPolicy } from "../agents/sandbox/types.js";
import { getBlockedBindReason } from "../agents/sandbox/validate-sandbox-security.js";
@@ -34,6 +25,12 @@ import {
} from "../shared/string-coerce.js";
import { pickSandboxToolPolicy } from "./audit-tool-policy.js";
/**
* Synchronous security audit collector functions.
*
* These functions analyze config-based security properties without I/O.
*/
export type SecurityAuditFinding = {
checkId: string;
severity: "info" | "warn" | "critical";