mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:20:43 +00:00
perf: defer unconfigured gateway hooks
This commit is contained in:
@@ -8,7 +8,7 @@ title: "Hooks"
|
||||
|
||||
# Hooks
|
||||
|
||||
Hooks are small scripts that run when something happens inside the Gateway. They are automatically discovered from directories and can be inspected with `openclaw hooks`.
|
||||
Hooks are small scripts that run when something happens inside the Gateway. They can be discovered from directories and inspected with `openclaw hooks`. The Gateway loads internal hooks only after you enable hooks or configure at least one hook entry, hook pack, legacy handler, or extra hook directory.
|
||||
|
||||
There are two kinds of hooks in OpenClaw:
|
||||
|
||||
@@ -139,6 +139,8 @@ Hooks are discovered from these directories, in order of increasing override pre
|
||||
|
||||
Workspace hooks can add new hook names but cannot override bundled, managed, or plugin-provided hooks with the same name.
|
||||
|
||||
The Gateway skips internal hook discovery on startup until internal hooks are configured. Enable a bundled or managed hook with `openclaw hooks enable <name>`, install a hook pack, or set `hooks.internal.enabled=true` to opt in.
|
||||
|
||||
### Hook packs
|
||||
|
||||
Hook packs are npm packages that export hooks via `openclaw.hooks` in `package.json`. Install with:
|
||||
|
||||
@@ -24,6 +24,7 @@ openclaw hooks list
|
||||
```
|
||||
|
||||
List all discovered hooks from workspace, managed, extra, and bundled directories.
|
||||
Gateway startup does not load internal hook handlers until at least one internal hook is configured.
|
||||
|
||||
**Options:**
|
||||
|
||||
|
||||
@@ -70,10 +70,6 @@ export const BUILD_ALL_STEPS = [
|
||||
label: "copy-hook-metadata",
|
||||
kind: "node",
|
||||
args: ["--import", "tsx", "scripts/copy-hook-metadata.ts"],
|
||||
cache: {
|
||||
inputs: ["scripts/copy-hook-metadata.ts", "scripts/lib/copy-assets.ts", "src/hooks/bundled"],
|
||||
outputs: ["dist/bundled"],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "copy-export-html-templates",
|
||||
|
||||
@@ -596,15 +596,25 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
|
||||
};
|
||||
|
||||
const startChannels = async () => {
|
||||
for (const plugin of listChannelPlugins()) {
|
||||
try {
|
||||
await startChannel(plugin.id);
|
||||
} catch (err) {
|
||||
channelLogs[plugin.id]?.error?.(
|
||||
`[${plugin.id}] channel startup failed: ${formatErrorMessage(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const pending = [...listChannelPlugins()];
|
||||
const workerCount = Math.min(8, pending.length);
|
||||
await Promise.all(
|
||||
Array.from({ length: workerCount }, async () => {
|
||||
for (;;) {
|
||||
const plugin = pending.shift();
|
||||
if (!plugin) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await startChannel(plugin.id);
|
||||
} catch (err) {
|
||||
channelLogs[plugin.id]?.error?.(
|
||||
`[${plugin.id}] channel startup failed: ${formatErrorMessage(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const markChannelLoggedOut = (channelId: ChannelId, cleared: boolean, accountId?: string) => {
|
||||
|
||||
@@ -138,7 +138,8 @@ describe("startGatewayPostAttachRuntime", () => {
|
||||
|
||||
expect([...unavailableGatewayMethods]).toEqual([]);
|
||||
expect(hoisted.startPluginServices).toHaveBeenCalledTimes(1);
|
||||
expect(hoisted.setInternalHooksEnabled).toHaveBeenCalledWith(false);
|
||||
expect(hoisted.loadInternalHooks).not.toHaveBeenCalled();
|
||||
expect(hoisted.setInternalHooksEnabled).not.toHaveBeenCalled();
|
||||
expect(hoisted.logGatewayStartup).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ loadedPluginIds: ["beta", "alpha"] }),
|
||||
);
|
||||
|
||||
@@ -1,50 +1,19 @@
|
||||
import { getAcpSessionManager } from "../acp/control-plane/manager.js";
|
||||
import { ACP_SESSION_IDENTITY_RENDERER_VERSION } from "../acp/runtime/session-identifiers.js";
|
||||
import { resolveOpenClawAgentDir } from "../agents/agent-paths.js";
|
||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
|
||||
import { selectAgentHarness } from "../agents/harness/selection.js";
|
||||
import { loadModelCatalog } from "../agents/model-catalog.js";
|
||||
import {
|
||||
getModelRefStatus,
|
||||
isCliProvider,
|
||||
resolveConfiguredModelRef,
|
||||
resolveHooksGmailModel,
|
||||
} from "../agents/model-selection.js";
|
||||
import { ensureOpenClawModelsJson } from "../agents/models-config.js";
|
||||
import { resolveModel } from "../agents/pi-embedded-runner/model.js";
|
||||
import { resolveEmbeddedAgentRuntime } from "../agents/pi-embedded-runner/runtime.js";
|
||||
import { resolveAgentSessionDirs } from "../agents/session-dirs.js";
|
||||
import { cleanStaleLockFiles } from "../agents/session-write-lock.js";
|
||||
import { scheduleSubagentOrphanRecovery } from "../agents/subagent-registry.js";
|
||||
import type { CliDeps } from "../cli/deps.types.js";
|
||||
import { resolveAgentModelPrimaryValue } from "../config/model-input.js";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import type { GatewayTailscaleMode } from "../config/types.gateway.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { startGmailWatcherWithLogs } from "../hooks/gmail-watcher-lifecycle.js";
|
||||
import {
|
||||
createInternalHookEvent,
|
||||
setInternalHooksEnabled,
|
||||
triggerInternalHook,
|
||||
} from "../hooks/internal-hooks.js";
|
||||
import { loadInternalHooks } from "../hooks/loader.js";
|
||||
import { hasConfiguredInternalHooks } from "../hooks/configured.js";
|
||||
import { isTruthyEnvValue } from "../infra/env.js";
|
||||
import { scheduleGatewayUpdateCheck } from "../infra/update-startup.js";
|
||||
import { getGlobalHookRunner } from "../plugins/hook-runner-global.js";
|
||||
import type { scheduleGatewayUpdateCheck } from "../infra/update-startup.js";
|
||||
import type { getGlobalHookRunner } from "../plugins/hook-runner-global.js";
|
||||
import type { loadOpenClawPlugins } from "../plugins/loader.js";
|
||||
import { type PluginServicesHandle, startPluginServices } from "../plugins/services.js";
|
||||
import type { PluginServicesHandle } from "../plugins/services.js";
|
||||
import {
|
||||
GATEWAY_EVENT_UPDATE_AVAILABLE,
|
||||
type GatewayUpdateAvailableEventPayload,
|
||||
} from "./events.js";
|
||||
import {
|
||||
scheduleRestartSentinelWake,
|
||||
shouldWakeFromRestartSentinel,
|
||||
} from "./server-restart-sentinel.js";
|
||||
import { logGatewayStartup } from "./server-startup-log.js";
|
||||
import { startGatewayMemoryBackend } from "./server-startup-memory.js";
|
||||
import { STARTUP_UNAVAILABLE_GATEWAY_METHODS } from "./server-startup-unavailable-methods.js";
|
||||
import { startGatewayTailscaleExposure } from "./server-tailscale.js";
|
||||
import type { startGatewayTailscaleExposure } from "./server-tailscale.js";
|
||||
|
||||
const SESSION_LOCK_STALE_MS = 30 * 60 * 1000;
|
||||
|
||||
@@ -52,10 +21,28 @@ async function prewarmConfiguredPrimaryModel(params: {
|
||||
cfg: OpenClawConfig;
|
||||
log: { warn: (msg: string) => void };
|
||||
}): Promise<void> {
|
||||
const { resolveAgentModelPrimaryValue } = await import("../config/model-input.js");
|
||||
const explicitPrimary = resolveAgentModelPrimaryValue(params.cfg.agents?.defaults?.model)?.trim();
|
||||
if (!explicitPrimary) {
|
||||
return;
|
||||
}
|
||||
const [
|
||||
{ resolveOpenClawAgentDir },
|
||||
{ DEFAULT_MODEL, DEFAULT_PROVIDER },
|
||||
{ selectAgentHarness },
|
||||
{ isCliProvider, resolveConfiguredModelRef },
|
||||
{ ensureOpenClawModelsJson },
|
||||
{ resolveModel },
|
||||
{ resolveEmbeddedAgentRuntime },
|
||||
] = await Promise.all([
|
||||
import("../agents/agent-paths.js"),
|
||||
import("../agents/defaults.js"),
|
||||
import("../agents/harness/selection.js"),
|
||||
import("../agents/model-selection.js"),
|
||||
import("../agents/models-config.js"),
|
||||
import("../agents/pi-embedded-runner/model.js"),
|
||||
import("../agents/pi-embedded-runner/runtime.js"),
|
||||
]);
|
||||
const { provider, model } = resolveConfiguredModelRef({
|
||||
cfg: params.cfg,
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
@@ -103,6 +90,12 @@ export async function startGatewaySidecars(params: {
|
||||
logChannels: { info: (msg: string) => void; error: (msg: string) => void };
|
||||
}) {
|
||||
try {
|
||||
const [{ resolveStateDir }, { resolveAgentSessionDirs }, { cleanStaleLockFiles }] =
|
||||
await Promise.all([
|
||||
import("../config/paths.js"),
|
||||
import("../agents/session-dirs.js"),
|
||||
import("../agents/session-write-lock.js"),
|
||||
]);
|
||||
const stateDir = resolveStateDir(process.env);
|
||||
const sessionDirs = await resolveAgentSessionDirs(stateDir);
|
||||
for (const sessionsDir of sessionDirs) {
|
||||
@@ -117,12 +110,24 @@ export async function startGatewaySidecars(params: {
|
||||
params.log.warn(`session lock cleanup failed on startup: ${String(err)}`);
|
||||
}
|
||||
|
||||
await startGmailWatcherWithLogs({
|
||||
cfg: params.cfg,
|
||||
log: params.logHooks,
|
||||
});
|
||||
if (params.cfg.hooks?.enabled && params.cfg.hooks.gmail?.account) {
|
||||
const { startGmailWatcherWithLogs } = await import("../hooks/gmail-watcher-lifecycle.js");
|
||||
await startGmailWatcherWithLogs({
|
||||
cfg: params.cfg,
|
||||
log: params.logHooks,
|
||||
});
|
||||
}
|
||||
|
||||
if (params.cfg.hooks?.gmail?.model) {
|
||||
const [
|
||||
{ DEFAULT_MODEL, DEFAULT_PROVIDER },
|
||||
{ loadModelCatalog },
|
||||
{ getModelRefStatus, resolveConfiguredModelRef, resolveHooksGmailModel },
|
||||
] = await Promise.all([
|
||||
import("../agents/defaults.js"),
|
||||
import("../agents/model-catalog.js"),
|
||||
import("../agents/model-selection.js"),
|
||||
]);
|
||||
const hooksModelRef = resolveHooksGmailModel({
|
||||
cfg: params.cfg,
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
@@ -154,13 +159,20 @@ export async function startGatewaySidecars(params: {
|
||||
}
|
||||
}
|
||||
|
||||
const internalHooksConfigured = hasConfiguredInternalHooks(params.cfg);
|
||||
try {
|
||||
setInternalHooksEnabled(params.cfg.hooks?.internal?.enabled !== false);
|
||||
const loadedCount = await loadInternalHooks(params.cfg, params.defaultWorkspaceDir);
|
||||
if (loadedCount > 0) {
|
||||
params.logHooks.info(
|
||||
`loaded ${loadedCount} internal hook handler${loadedCount > 1 ? "s" : ""}`,
|
||||
);
|
||||
if (internalHooksConfigured) {
|
||||
const [{ setInternalHooksEnabled }, { loadInternalHooks }] = await Promise.all([
|
||||
import("../hooks/internal-hooks.js"),
|
||||
import("../hooks/loader.js"),
|
||||
]);
|
||||
setInternalHooksEnabled(params.cfg.hooks?.internal?.enabled !== false);
|
||||
const loadedCount = await loadInternalHooks(params.cfg, params.defaultWorkspaceDir);
|
||||
if (loadedCount > 0) {
|
||||
params.logHooks.info(
|
||||
`loaded ${loadedCount} internal hook handler${loadedCount > 1 ? "s" : ""}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
params.logHooks.error(`failed to load hooks: ${String(err)}`);
|
||||
@@ -185,19 +197,24 @@ export async function startGatewaySidecars(params: {
|
||||
);
|
||||
}
|
||||
|
||||
if (params.cfg.hooks?.internal?.enabled !== false) {
|
||||
if (internalHooksConfigured) {
|
||||
setTimeout(() => {
|
||||
const hookEvent = createInternalHookEvent("gateway", "startup", "gateway:startup", {
|
||||
cfg: params.cfg,
|
||||
deps: params.deps,
|
||||
workspaceDir: params.defaultWorkspaceDir,
|
||||
});
|
||||
void triggerInternalHook(hookEvent);
|
||||
void import("../hooks/internal-hooks.js").then(
|
||||
({ createInternalHookEvent, triggerInternalHook }) => {
|
||||
const hookEvent = createInternalHookEvent("gateway", "startup", "gateway:startup", {
|
||||
cfg: params.cfg,
|
||||
deps: params.deps,
|
||||
workspaceDir: params.defaultWorkspaceDir,
|
||||
});
|
||||
void triggerInternalHook(hookEvent);
|
||||
},
|
||||
);
|
||||
}, 250);
|
||||
}
|
||||
|
||||
let pluginServices: PluginServicesHandle | null = null;
|
||||
try {
|
||||
const { startPluginServices } = await import("../plugins/services.js");
|
||||
pluginServices = await startPluginServices({
|
||||
registry: params.pluginRegistry,
|
||||
config: params.cfg,
|
||||
@@ -208,6 +225,9 @@ export async function startGatewaySidecars(params: {
|
||||
}
|
||||
|
||||
if (params.cfg.acp?.enabled) {
|
||||
const [{ getAcpSessionManager }, { ACP_SESSION_IDENTITY_RENDERER_VERSION }] = await Promise.all(
|
||||
[import("../acp/control-plane/manager.js"), import("../acp/runtime/session-identifiers.js")],
|
||||
);
|
||||
void getAcpSessionManager()
|
||||
.reconcilePendingSessionIdentities({ cfg: params.cfg })
|
||||
.then((result) => {
|
||||
@@ -223,35 +243,51 @@ export async function startGatewaySidecars(params: {
|
||||
});
|
||||
}
|
||||
|
||||
void startGatewayMemoryBackend({ cfg: params.cfg, log: params.log }).catch((err) => {
|
||||
params.log.warn(`qmd memory startup initialization failed: ${String(err)}`);
|
||||
});
|
||||
void import("./server-startup-memory.js")
|
||||
.then(({ startGatewayMemoryBackend }) =>
|
||||
startGatewayMemoryBackend({ cfg: params.cfg, log: params.log }),
|
||||
)
|
||||
.catch((err) => {
|
||||
params.log.warn(`qmd memory startup initialization failed: ${String(err)}`);
|
||||
});
|
||||
|
||||
const { shouldWakeFromRestartSentinel, scheduleRestartSentinelWake } =
|
||||
await import("./server-restart-sentinel.js");
|
||||
if (shouldWakeFromRestartSentinel()) {
|
||||
setTimeout(() => {
|
||||
void scheduleRestartSentinelWake({ deps: params.deps });
|
||||
}, 750);
|
||||
}
|
||||
|
||||
const { scheduleSubagentOrphanRecovery } = await import("../agents/subagent-registry.js");
|
||||
scheduleSubagentOrphanRecovery();
|
||||
|
||||
return { pluginServices };
|
||||
}
|
||||
|
||||
type Awaitable<T> = T | Promise<T>;
|
||||
|
||||
type GatewayPostAttachRuntimeDeps = {
|
||||
getGlobalHookRunner: typeof getGlobalHookRunner;
|
||||
getGlobalHookRunner: () => Awaitable<ReturnType<typeof getGlobalHookRunner>>;
|
||||
logGatewayStartup: typeof logGatewayStartup;
|
||||
scheduleGatewayUpdateCheck: typeof scheduleGatewayUpdateCheck;
|
||||
scheduleGatewayUpdateCheck: (
|
||||
...args: Parameters<typeof scheduleGatewayUpdateCheck>
|
||||
) => Awaitable<ReturnType<typeof scheduleGatewayUpdateCheck>>;
|
||||
startGatewaySidecars: typeof startGatewaySidecars;
|
||||
startGatewayTailscaleExposure: typeof startGatewayTailscaleExposure;
|
||||
startGatewayTailscaleExposure: (
|
||||
...args: Parameters<typeof startGatewayTailscaleExposure>
|
||||
) => ReturnType<typeof startGatewayTailscaleExposure>;
|
||||
};
|
||||
|
||||
const defaultGatewayPostAttachRuntimeDeps: GatewayPostAttachRuntimeDeps = {
|
||||
getGlobalHookRunner,
|
||||
getGlobalHookRunner: async () =>
|
||||
(await import("../plugins/hook-runner-global.js")).getGlobalHookRunner(),
|
||||
logGatewayStartup,
|
||||
scheduleGatewayUpdateCheck,
|
||||
scheduleGatewayUpdateCheck: async (...args) =>
|
||||
(await import("../infra/update-startup.js")).scheduleGatewayUpdateCheck(...args),
|
||||
startGatewaySidecars,
|
||||
startGatewayTailscaleExposure,
|
||||
startGatewayTailscaleExposure: async (...args) =>
|
||||
(await import("./server-tailscale.js")).startGatewayTailscaleExposure(...args),
|
||||
};
|
||||
|
||||
export async function startGatewayPostAttachRuntime(
|
||||
@@ -307,48 +343,60 @@ export async function startGatewayPostAttachRuntime(
|
||||
startupStartedAt: params.startupStartedAt,
|
||||
});
|
||||
|
||||
const stopGatewayUpdateCheck = params.minimalTestGateway
|
||||
? () => {}
|
||||
: runtimeDeps.scheduleGatewayUpdateCheck({
|
||||
cfg: params.cfgAtStart,
|
||||
log: params.log,
|
||||
isNixMode: params.isNixMode,
|
||||
onUpdateAvailableChange: (updateAvailable) => {
|
||||
const payload: GatewayUpdateAvailableEventPayload = { updateAvailable };
|
||||
params.broadcast(GATEWAY_EVENT_UPDATE_AVAILABLE, payload, { dropIfSlow: true });
|
||||
},
|
||||
});
|
||||
const stopGatewayUpdateCheckPromise = params.minimalTestGateway
|
||||
? Promise.resolve(() => {})
|
||||
: Promise.resolve(
|
||||
runtimeDeps.scheduleGatewayUpdateCheck({
|
||||
cfg: params.cfgAtStart,
|
||||
log: params.log,
|
||||
isNixMode: params.isNixMode,
|
||||
onUpdateAvailableChange: (updateAvailable) => {
|
||||
const payload: GatewayUpdateAvailableEventPayload = { updateAvailable };
|
||||
params.broadcast(GATEWAY_EVENT_UPDATE_AVAILABLE, payload, { dropIfSlow: true });
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const tailscaleCleanup = params.minimalTestGateway
|
||||
? null
|
||||
: await runtimeDeps.startGatewayTailscaleExposure({
|
||||
tailscaleMode: params.tailscaleMode,
|
||||
resetOnExit: params.resetOnExit,
|
||||
port: params.port,
|
||||
controlUiBasePath: params.controlUiBasePath,
|
||||
logTailscale: params.logTailscale,
|
||||
});
|
||||
const tailscaleCleanupPromise = params.minimalTestGateway
|
||||
? Promise.resolve(null)
|
||||
: Promise.resolve(
|
||||
runtimeDeps.startGatewayTailscaleExposure({
|
||||
tailscaleMode: params.tailscaleMode,
|
||||
resetOnExit: params.resetOnExit,
|
||||
port: params.port,
|
||||
controlUiBasePath: params.controlUiBasePath,
|
||||
logTailscale: params.logTailscale,
|
||||
}),
|
||||
);
|
||||
|
||||
let pluginServices: PluginServicesHandle | null = null;
|
||||
if (!params.minimalTestGateway) {
|
||||
params.log.info("starting channels and sidecars...");
|
||||
({ pluginServices } = await runtimeDeps.startGatewaySidecars({
|
||||
cfg: params.gatewayPluginConfigAtStart,
|
||||
pluginRegistry: params.pluginRegistry,
|
||||
defaultWorkspaceDir: params.defaultWorkspaceDir,
|
||||
deps: params.deps,
|
||||
startChannels: params.startChannels,
|
||||
log: params.log,
|
||||
logHooks: params.logHooks,
|
||||
logChannels: params.logChannels,
|
||||
}));
|
||||
for (const method of STARTUP_UNAVAILABLE_GATEWAY_METHODS) {
|
||||
params.unavailableGatewayMethods.delete(method);
|
||||
}
|
||||
}
|
||||
const sidecarsPromise = params.minimalTestGateway
|
||||
? Promise.resolve({ pluginServices: null })
|
||||
: (async () => {
|
||||
params.log.info("starting channels and sidecars...");
|
||||
const result = await runtimeDeps.startGatewaySidecars({
|
||||
cfg: params.gatewayPluginConfigAtStart,
|
||||
pluginRegistry: params.pluginRegistry,
|
||||
defaultWorkspaceDir: params.defaultWorkspaceDir,
|
||||
deps: params.deps,
|
||||
startChannels: params.startChannels,
|
||||
log: params.log,
|
||||
logHooks: params.logHooks,
|
||||
logChannels: params.logChannels,
|
||||
});
|
||||
for (const method of STARTUP_UNAVAILABLE_GATEWAY_METHODS) {
|
||||
params.unavailableGatewayMethods.delete(method);
|
||||
}
|
||||
return result;
|
||||
})();
|
||||
|
||||
const [stopGatewayUpdateCheck, tailscaleCleanup, { pluginServices }] = await Promise.all([
|
||||
stopGatewayUpdateCheckPromise,
|
||||
tailscaleCleanupPromise,
|
||||
sidecarsPromise,
|
||||
]);
|
||||
|
||||
if (!params.minimalTestGateway) {
|
||||
const hookRunner = runtimeDeps.getGlobalHookRunner();
|
||||
const hookRunner = await runtimeDeps.getGlobalHookRunner();
|
||||
if (hookRunner?.hasHooks("gateway_start")) {
|
||||
void hookRunner.runGatewayStart({ port: params.port }, { port: params.port }).catch((err) => {
|
||||
params.log.warn(`gateway_start hook failed: ${String(err)}`);
|
||||
|
||||
@@ -20,7 +20,7 @@ import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
|
||||
import { resolveMainSessionKey } from "../config/sessions.js";
|
||||
import { clearAgentRunContext } from "../infra/agent-events.js";
|
||||
import { isDiagnosticsEnabled } from "../infra/diagnostic-events.js";
|
||||
import { isVitestRuntimeEnv, logAcceptedEnvOption } from "../infra/env.js";
|
||||
import { isTruthyEnvValue, isVitestRuntimeEnv, logAcceptedEnvOption } from "../infra/env.js";
|
||||
import { ensureOpenClawCliOnPath } from "../infra/path-env.js";
|
||||
import { setGatewaySigusr1RestartPolicy, setPreRestartDeferralCheck } from "../infra/restart.js";
|
||||
import { enqueueSystemEvent } from "../infra/system-events.js";
|
||||
@@ -131,6 +131,34 @@ const logSecrets = log.child("secrets");
|
||||
const gatewayRuntime = runtimeForLogger(log);
|
||||
const canvasRuntime = runtimeForLogger(logCanvas);
|
||||
|
||||
function createGatewayStartupTrace() {
|
||||
const enabled = isTruthyEnvValue(process.env.OPENCLAW_GATEWAY_STARTUP_TRACE);
|
||||
const started = performance.now();
|
||||
let last = started;
|
||||
const emit = (name: string, durationMs: number, totalMs: number) => {
|
||||
if (enabled) {
|
||||
log.info(`startup trace: ${name} ${durationMs.toFixed(1)}ms total=${totalMs.toFixed(1)}ms`);
|
||||
}
|
||||
};
|
||||
return {
|
||||
mark(name: string) {
|
||||
const now = performance.now();
|
||||
emit(name, now - last, now - started);
|
||||
last = now;
|
||||
},
|
||||
async measure<T>(name: string, run: () => Promise<T> | T): Promise<T> {
|
||||
const before = performance.now();
|
||||
try {
|
||||
return await run();
|
||||
} finally {
|
||||
const now = performance.now();
|
||||
emit(name, now - before, now - started);
|
||||
last = now;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
type AuthRateLimitConfig = Parameters<typeof createAuthRateLimiter>[0];
|
||||
|
||||
function createGatewayAuthRateLimiters(rateLimitConfig: AuthRateLimitConfig | undefined): {
|
||||
@@ -222,11 +250,14 @@ export async function startGatewayServer(
|
||||
key: "OPENCLAW_RAW_STREAM_PATH",
|
||||
description: "raw stream log path override",
|
||||
});
|
||||
const startupTrace = createGatewayStartupTrace();
|
||||
|
||||
const configSnapshot = await loadGatewayStartupConfigSnapshot({
|
||||
minimalTestGateway,
|
||||
log,
|
||||
});
|
||||
const configSnapshot = await startupTrace.measure("config.snapshot", () =>
|
||||
loadGatewayStartupConfigSnapshot({
|
||||
minimalTestGateway,
|
||||
log,
|
||||
}),
|
||||
);
|
||||
|
||||
const emitSecretsStateEvent = (
|
||||
code: "SECRETS_RELOADER_DEGRADED" | "SECRETS_RELOADER_RECOVERED",
|
||||
@@ -247,12 +278,14 @@ export async function startGatewayServer(
|
||||
let startupInternalWriteHash: string | null = null;
|
||||
let startupLastGoodSnapshot = configSnapshot;
|
||||
const startupRuntimeConfig = applyConfigOverrides(configSnapshot.config);
|
||||
const authBootstrap = await prepareGatewayStartupConfig({
|
||||
configSnapshot,
|
||||
authOverride: opts.auth,
|
||||
tailscaleOverride: opts.tailscale,
|
||||
activateRuntimeSecrets,
|
||||
});
|
||||
const authBootstrap = await startupTrace.measure("config.auth", () =>
|
||||
prepareGatewayStartupConfig({
|
||||
configSnapshot,
|
||||
authOverride: opts.auth,
|
||||
tailscaleOverride: opts.tailscale,
|
||||
activateRuntimeSecrets,
|
||||
}),
|
||||
);
|
||||
cfgAtStart = authBootstrap.cfg;
|
||||
if (authBootstrap.generatedToken) {
|
||||
if (authBootstrap.persistedGeneratedToken) {
|
||||
@@ -281,11 +314,13 @@ export async function startGatewayServer(
|
||||
// non-loopback installs that upgraded to v2026.2.26+ without required origins.
|
||||
const controlUiSeed = minimalTestGateway
|
||||
? { config: cfgAtStart, persistedAllowedOriginsSeed: false }
|
||||
: await maybeSeedControlUiAllowedOriginsAtStartup({
|
||||
config: cfgAtStart,
|
||||
writeConfig: writeConfigFile,
|
||||
log,
|
||||
});
|
||||
: await startupTrace.measure("control-ui.seed", () =>
|
||||
maybeSeedControlUiAllowedOriginsAtStartup({
|
||||
config: cfgAtStart,
|
||||
writeConfig: writeConfigFile,
|
||||
log,
|
||||
}),
|
||||
);
|
||||
cfgAtStart = controlUiSeed.config;
|
||||
// Always capture the final config hash after all startup writes (plugin
|
||||
// auto-enable, auth token generation, control-UI origin seeding) so the
|
||||
@@ -295,16 +330,20 @@ export async function startGatewayServer(
|
||||
// changes, missing the plugin auto-enable write performed earlier inside
|
||||
// loadGatewayStartupConfigSnapshot(). See #67436.
|
||||
{
|
||||
const startupSnapshot = await readConfigFileSnapshot();
|
||||
const startupSnapshot = await startupTrace.measure("config.final-snapshot", () =>
|
||||
readConfigFileSnapshot(),
|
||||
);
|
||||
startupInternalWriteHash = startupSnapshot.hash ?? null;
|
||||
startupLastGoodSnapshot = startupSnapshot;
|
||||
}
|
||||
const pluginBootstrap = await prepareGatewayPluginBootstrap({
|
||||
cfgAtStart,
|
||||
startupRuntimeConfig,
|
||||
minimalTestGateway,
|
||||
log,
|
||||
});
|
||||
const pluginBootstrap = await startupTrace.measure("plugins.bootstrap", () =>
|
||||
prepareGatewayPluginBootstrap({
|
||||
cfgAtStart,
|
||||
startupRuntimeConfig,
|
||||
minimalTestGateway,
|
||||
log,
|
||||
}),
|
||||
);
|
||||
const {
|
||||
gatewayPluginConfigAtStart,
|
||||
defaultWorkspaceDir,
|
||||
@@ -326,17 +365,19 @@ export async function startGatewayServer(
|
||||
...listChannelPlugins().flatMap((plugin) => plugin.gatewayMethods ?? []),
|
||||
]),
|
||||
);
|
||||
const runtimeConfig = await resolveGatewayRuntimeConfig({
|
||||
cfg: cfgAtStart,
|
||||
port,
|
||||
bind: opts.bind,
|
||||
host: opts.host,
|
||||
controlUiEnabled: opts.controlUiEnabled,
|
||||
openAiChatCompletionsEnabled: opts.openAiChatCompletionsEnabled,
|
||||
openResponsesEnabled: opts.openResponsesEnabled,
|
||||
auth: opts.auth,
|
||||
tailscale: opts.tailscale,
|
||||
});
|
||||
const runtimeConfig = await startupTrace.measure("runtime.config", () =>
|
||||
resolveGatewayRuntimeConfig({
|
||||
cfg: cfgAtStart,
|
||||
port,
|
||||
bind: opts.bind,
|
||||
host: opts.host,
|
||||
controlUiEnabled: opts.controlUiEnabled,
|
||||
openAiChatCompletionsEnabled: opts.openAiChatCompletionsEnabled,
|
||||
openResponsesEnabled: opts.openResponsesEnabled,
|
||||
auth: opts.auth,
|
||||
tailscale: opts.tailscale,
|
||||
}),
|
||||
);
|
||||
const {
|
||||
bindHost,
|
||||
controlUiEnabled,
|
||||
@@ -392,12 +433,14 @@ export async function startGatewayServer(
|
||||
const { rateLimiter: authRateLimiter, browserRateLimiter: browserAuthRateLimiter } =
|
||||
createGatewayAuthRateLimiters(rateLimitConfig);
|
||||
|
||||
const controlUiRootState = await resolveGatewayControlUiRootState({
|
||||
controlUiRootOverride,
|
||||
controlUiEnabled,
|
||||
gatewayRuntime,
|
||||
log,
|
||||
});
|
||||
const controlUiRootState = await startupTrace.measure("control-ui.root", () =>
|
||||
resolveGatewayControlUiRootState({
|
||||
controlUiRootOverride,
|
||||
controlUiEnabled,
|
||||
gatewayRuntime,
|
||||
log,
|
||||
}),
|
||||
);
|
||||
|
||||
const wizardRunner = opts.wizardRunner ?? runSetupWizard;
|
||||
const { wizardSessions, findRunningWizard, purgeWizardSession } = createWizardSessionTracker();
|
||||
@@ -405,7 +448,9 @@ export async function startGatewayServer(
|
||||
const deps = createDefaultDeps();
|
||||
let runtimeState: GatewayServerLiveState | null = null;
|
||||
let canvasHostServer: CanvasHostServer | null = null;
|
||||
const gatewayTls = await loadGatewayTlsRuntime(cfgAtStart.gateway?.tls, log.child("tls"));
|
||||
const gatewayTls = await startupTrace.measure("tls.runtime", () =>
|
||||
loadGatewayTlsRuntime(cfgAtStart.gateway?.tls, log.child("tls")),
|
||||
);
|
||||
if (cfgAtStart.gateway?.tls?.enabled && !gatewayTls.enabled) {
|
||||
throw new Error(gatewayTls.error ?? "gateway tls: failed to enable");
|
||||
}
|
||||
@@ -446,36 +491,39 @@ export async function startGatewayServer(
|
||||
removeChatRun,
|
||||
chatAbortControllers,
|
||||
toolEventRecipients,
|
||||
} = await createGatewayRuntimeState({
|
||||
cfg: cfgAtStart,
|
||||
bindHost,
|
||||
port,
|
||||
controlUiEnabled,
|
||||
controlUiBasePath,
|
||||
controlUiRoot: controlUiRootState,
|
||||
openAiChatCompletionsEnabled,
|
||||
openAiChatCompletionsConfig,
|
||||
openResponsesEnabled,
|
||||
openResponsesConfig,
|
||||
strictTransportSecurityHeader,
|
||||
resolvedAuth,
|
||||
rateLimiter: authRateLimiter,
|
||||
gatewayTls,
|
||||
getResolvedAuth,
|
||||
hooksConfig: () => runtimeState?.hooksConfig ?? initialHooksConfig,
|
||||
getHookClientIpConfig: () => runtimeState?.hookClientIpConfig ?? initialHookClientIpConfig,
|
||||
pluginRegistry,
|
||||
pinChannelRegistry: !minimalTestGateway,
|
||||
deps,
|
||||
canvasRuntime,
|
||||
canvasHostEnabled,
|
||||
allowCanvasHostInTests: opts.allowCanvasHostInTests,
|
||||
logCanvas,
|
||||
log,
|
||||
logHooks,
|
||||
logPlugins,
|
||||
getReadiness,
|
||||
});
|
||||
} = await startupTrace.measure("runtime.state", () =>
|
||||
createGatewayRuntimeState({
|
||||
cfg: cfgAtStart,
|
||||
bindHost,
|
||||
port,
|
||||
controlUiEnabled,
|
||||
controlUiBasePath,
|
||||
controlUiRoot: controlUiRootState,
|
||||
openAiChatCompletionsEnabled,
|
||||
openAiChatCompletionsConfig,
|
||||
openResponsesEnabled,
|
||||
openResponsesConfig,
|
||||
strictTransportSecurityHeader,
|
||||
resolvedAuth,
|
||||
rateLimiter: authRateLimiter,
|
||||
gatewayTls,
|
||||
getResolvedAuth,
|
||||
hooksConfig: () => runtimeState?.hooksConfig ?? initialHooksConfig,
|
||||
getHookClientIpConfig: () => runtimeState?.hookClientIpConfig ?? initialHookClientIpConfig,
|
||||
pluginRegistry,
|
||||
pinChannelRegistry: !minimalTestGateway,
|
||||
deps,
|
||||
canvasRuntime,
|
||||
canvasHostEnabled,
|
||||
allowCanvasHostInTests: opts.allowCanvasHostInTests,
|
||||
logCanvas,
|
||||
log,
|
||||
logHooks,
|
||||
logPlugins,
|
||||
getReadiness,
|
||||
}),
|
||||
);
|
||||
startupTrace.mark("http.bound");
|
||||
const {
|
||||
nodeRegistry,
|
||||
nodePresenceTimers,
|
||||
@@ -559,40 +607,42 @@ export async function startGatewayServer(
|
||||
};
|
||||
|
||||
try {
|
||||
const earlyRuntime = await startGatewayEarlyRuntime({
|
||||
minimalTestGateway,
|
||||
cfgAtStart,
|
||||
port,
|
||||
gatewayTls,
|
||||
tailscaleMode,
|
||||
log,
|
||||
logDiscovery,
|
||||
nodeRegistry,
|
||||
broadcast,
|
||||
nodeSendToAllSubscribed,
|
||||
getPresenceVersion,
|
||||
getHealthVersion,
|
||||
refreshGatewayHealthSnapshot,
|
||||
logHealth,
|
||||
dedupe,
|
||||
chatAbortControllers,
|
||||
chatRunState,
|
||||
chatRunBuffers,
|
||||
chatDeltaSentAt,
|
||||
chatDeltaLastBroadcastLen,
|
||||
removeChatRun,
|
||||
agentRunSeq,
|
||||
nodeSendToSession,
|
||||
...(typeof cfgAtStart.media?.ttlHours === "number"
|
||||
? { mediaCleanupTtlMs: resolveMediaCleanupTtlMs(cfgAtStart.media.ttlHours) }
|
||||
: {}),
|
||||
skillsRefreshDelayMs: runtimeState.skillsRefreshDelayMs,
|
||||
getSkillsRefreshTimer: () => runtimeState.skillsRefreshTimer,
|
||||
setSkillsRefreshTimer: (timer) => {
|
||||
runtimeState.skillsRefreshTimer = timer;
|
||||
},
|
||||
loadConfig,
|
||||
});
|
||||
const earlyRuntime = await startupTrace.measure("runtime.early", () =>
|
||||
startGatewayEarlyRuntime({
|
||||
minimalTestGateway,
|
||||
cfgAtStart,
|
||||
port,
|
||||
gatewayTls,
|
||||
tailscaleMode,
|
||||
log,
|
||||
logDiscovery,
|
||||
nodeRegistry,
|
||||
broadcast,
|
||||
nodeSendToAllSubscribed,
|
||||
getPresenceVersion,
|
||||
getHealthVersion,
|
||||
refreshGatewayHealthSnapshot,
|
||||
logHealth,
|
||||
dedupe,
|
||||
chatAbortControllers,
|
||||
chatRunState,
|
||||
chatRunBuffers,
|
||||
chatDeltaSentAt,
|
||||
chatDeltaLastBroadcastLen,
|
||||
removeChatRun,
|
||||
agentRunSeq,
|
||||
nodeSendToSession,
|
||||
...(typeof cfgAtStart.media?.ttlHours === "number"
|
||||
? { mediaCleanupTtlMs: resolveMediaCleanupTtlMs(cfgAtStart.media.ttlHours) }
|
||||
: {}),
|
||||
skillsRefreshDelayMs: runtimeState.skillsRefreshDelayMs,
|
||||
getSkillsRefreshTimer: () => runtimeState.skillsRefreshTimer,
|
||||
setSkillsRefreshTimer: (timer) => {
|
||||
runtimeState.skillsRefreshTimer = timer;
|
||||
},
|
||||
loadConfig,
|
||||
}),
|
||||
);
|
||||
runtimeState.bonjourStop = earlyRuntime.bonjourStop;
|
||||
runtimeState.skillsChangeUnsub = earlyRuntime.skillsChangeUnsub;
|
||||
if (earlyRuntime.maintenance) {
|
||||
@@ -747,30 +797,33 @@ export async function startGatewayServer(
|
||||
stopGatewayUpdateCheck: runtimeState.stopGatewayUpdateCheck,
|
||||
tailscaleCleanup: runtimeState.tailscaleCleanup,
|
||||
pluginServices: runtimeState.pluginServices,
|
||||
} = await startGatewayPostAttachRuntime({
|
||||
minimalTestGateway,
|
||||
cfgAtStart,
|
||||
bindHost,
|
||||
bindHosts: httpBindHosts,
|
||||
port,
|
||||
tlsEnabled: gatewayTls.enabled,
|
||||
log,
|
||||
isNixMode,
|
||||
startupStartedAt: opts.startupStartedAt,
|
||||
broadcast,
|
||||
tailscaleMode,
|
||||
resetOnExit: tailscaleConfig.resetOnExit ?? false,
|
||||
controlUiBasePath,
|
||||
logTailscale,
|
||||
gatewayPluginConfigAtStart,
|
||||
pluginRegistry,
|
||||
defaultWorkspaceDir,
|
||||
deps,
|
||||
startChannels,
|
||||
logHooks,
|
||||
logChannels,
|
||||
unavailableGatewayMethods,
|
||||
}));
|
||||
} = await startupTrace.measure("runtime.post-attach", () =>
|
||||
startGatewayPostAttachRuntime({
|
||||
minimalTestGateway,
|
||||
cfgAtStart,
|
||||
bindHost,
|
||||
bindHosts: httpBindHosts,
|
||||
port,
|
||||
tlsEnabled: gatewayTls.enabled,
|
||||
log,
|
||||
isNixMode,
|
||||
startupStartedAt: opts.startupStartedAt,
|
||||
broadcast,
|
||||
tailscaleMode,
|
||||
resetOnExit: tailscaleConfig.resetOnExit ?? false,
|
||||
controlUiBasePath,
|
||||
logTailscale,
|
||||
gatewayPluginConfigAtStart,
|
||||
pluginRegistry,
|
||||
defaultWorkspaceDir,
|
||||
deps,
|
||||
startChannels,
|
||||
logHooks,
|
||||
logChannels,
|
||||
unavailableGatewayMethods,
|
||||
}),
|
||||
));
|
||||
startupTrace.mark("ready");
|
||||
|
||||
// Keep scheduled work inert until post-attach sidecars finish.
|
||||
const activated = activateGatewayScheduledServices({
|
||||
|
||||
34
src/hooks/configured.ts
Normal file
34
src/hooks/configured.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { HookConfig, HookInstallRecord } from "../config/types.hooks.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { getLegacyInternalHookHandlers } from "./legacy-config.js";
|
||||
|
||||
function hasEnabledEntry(entries: Record<string, HookConfig> | undefined): boolean {
|
||||
if (!entries) {
|
||||
return false;
|
||||
}
|
||||
return Object.values(entries).some((entry) => entry?.enabled !== false);
|
||||
}
|
||||
|
||||
function hasConfiguredInstalls(installs: Record<string, HookInstallRecord> | undefined): boolean {
|
||||
return installs ? Object.keys(installs).length > 0 : false;
|
||||
}
|
||||
|
||||
export function hasConfiguredInternalHooks(config: OpenClawConfig): boolean {
|
||||
const internal = config.hooks?.internal;
|
||||
if (!internal || internal.enabled === false) {
|
||||
return false;
|
||||
}
|
||||
if (internal.enabled === true) {
|
||||
return true;
|
||||
}
|
||||
if (hasEnabledEntry(internal.entries)) {
|
||||
return true;
|
||||
}
|
||||
if ((internal.load?.extraDirs ?? []).some((dir) => dir.trim().length > 0)) {
|
||||
return true;
|
||||
}
|
||||
if (hasConfiguredInstalls(internal.installs)) {
|
||||
return true;
|
||||
}
|
||||
return getLegacyInternalHookHandlers(config).length > 0;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { setLoggerOverride } from "../logging/logger.js";
|
||||
import { loggingState } from "../logging/state.js";
|
||||
import { stripAnsi } from "../terminal/ansi.js";
|
||||
import { captureEnv } from "../test-utils/env.js";
|
||||
import { hasConfiguredInternalHooks } from "./configured.js";
|
||||
import {
|
||||
clearInternalHooks,
|
||||
getRegisteredEventKeys,
|
||||
@@ -133,6 +134,25 @@ describe("loader", () => {
|
||||
});
|
||||
|
||||
describe("loadInternalHooks", () => {
|
||||
it("detects configured internal hook surfaces", () => {
|
||||
expect(hasConfiguredInternalHooks({} satisfies OpenClawConfig)).toBe(false);
|
||||
expect(
|
||||
hasConfiguredInternalHooks({
|
||||
hooks: { internal: { entries: { "session-memory": { enabled: true } } } },
|
||||
} satisfies OpenClawConfig),
|
||||
).toBe(true);
|
||||
expect(
|
||||
hasConfiguredInternalHooks({
|
||||
hooks: { internal: { entries: { "session-memory": { enabled: false } } } },
|
||||
} satisfies OpenClawConfig),
|
||||
).toBe(false);
|
||||
expect(
|
||||
hasConfiguredInternalHooks({
|
||||
hooks: { internal: { load: { extraDirs: ["/tmp/hooks"] } } },
|
||||
} satisfies OpenClawConfig),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
const createLegacyHandlerConfig = () =>
|
||||
createEnabledHooksConfig([
|
||||
{
|
||||
@@ -172,10 +192,7 @@ describe("loader", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("should treat missing hooks.internal.enabled as enabled (default-on)", async () => {
|
||||
// Empty config should NOT skip loading — it should attempt discovery.
|
||||
// With no discoverable hooks in the temp dir (bundled dir is overridden
|
||||
// to /nonexistent), this returns 0 but does NOT bail at the guard.
|
||||
it("skips hook discovery until internal hooks are configured", async () => {
|
||||
for (const cfg of [
|
||||
{} satisfies OpenClawConfig,
|
||||
{ hooks: {} } satisfies OpenClawConfig,
|
||||
|
||||
@@ -14,6 +14,7 @@ import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { resolveGlobalSingleton } from "../shared/global-singleton.js";
|
||||
import { sanitizeForLog } from "../terminal/ansi.js";
|
||||
import { shouldIncludeHook } from "./config.js";
|
||||
import { hasConfiguredInternalHooks } from "./configured.js";
|
||||
import { buildImportUrl } from "./import-url.js";
|
||||
import type { InternalHookHandler } from "./internal-hooks.js";
|
||||
import { registerInternalHook, unregisterInternalHook } from "./internal-hooks.js";
|
||||
@@ -86,8 +87,7 @@ export async function loadInternalHooks(
|
||||
): Promise<number> {
|
||||
resetLoadedInternalHooks();
|
||||
|
||||
// Hooks are on by default; only skip when explicitly disabled.
|
||||
if (cfg.hooks?.internal?.enabled === false) {
|
||||
if (!hasConfiguredInternalHooks(cfg)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -320,6 +320,21 @@ function hasPluginSdkSubpathArtifact(packageRoot: string, subpath: string) {
|
||||
);
|
||||
}
|
||||
|
||||
function listDistPluginSdkArtifactSubpaths(packageRoot: string): Set<string> {
|
||||
try {
|
||||
const distPluginSdkDir = path.join(packageRoot, "dist", "plugin-sdk");
|
||||
return new Set(
|
||||
fs
|
||||
.readdirSync(distPluginSdkDir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith(".js"))
|
||||
.map((entry) => entry.name.slice(0, -".js".length))
|
||||
.filter((subpath) => isSafePluginSdkSubpathSegment(subpath)),
|
||||
);
|
||||
} catch {
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
|
||||
function listPrivateLocalOnlyPluginSdkSubpaths(packageRoot: string): string[] {
|
||||
if (!shouldIncludePrivateLocalOnlyPluginSdkSubpaths()) {
|
||||
return [];
|
||||
@@ -389,6 +404,9 @@ export function resolvePluginSdkScopedAliasMap(
|
||||
return cached;
|
||||
}
|
||||
const aliasMap: Record<string, string> = {};
|
||||
const distPluginSdkArtifacts = orderedKinds.includes("dist")
|
||||
? listDistPluginSdkArtifactSubpaths(packageRoot)
|
||||
: new Set<string>();
|
||||
for (const subpath of listPluginSdkExportedSubpaths({
|
||||
modulePath,
|
||||
argv1: params.argv1,
|
||||
@@ -397,6 +415,9 @@ export function resolvePluginSdkScopedAliasMap(
|
||||
})) {
|
||||
for (const kind of orderedKinds) {
|
||||
if (kind === "dist") {
|
||||
if (!distPluginSdkArtifacts.has(subpath)) {
|
||||
continue;
|
||||
}
|
||||
const candidate = path.join(packageRoot, "dist", "plugin-sdk", `${subpath}.js`);
|
||||
if (isUsableDistPluginSdkArtifact(candidate)) {
|
||||
for (const packageName of PLUGIN_SDK_PACKAGE_NAMES) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "../config/model-input.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { AgentToolsConfig } from "../config/types.tools.js";
|
||||
import { hasConfiguredInternalHooks } from "../hooks/configured.js";
|
||||
import { hasConfiguredWebSearchCredential } from "../plugins/web-search-credential-presence.js";
|
||||
import { inferParamBFromIdOrName } from "../shared/model-param-b.js";
|
||||
import { pickSandboxToolPolicy } from "./audit-tool-policy.js";
|
||||
@@ -178,7 +179,7 @@ export function collectAttackSurfaceSummaryFindings(cfg: OpenClawConfig): Securi
|
||||
const group = summarizeGroupPolicy(cfg);
|
||||
const elevated = cfg.tools?.elevated?.enabled !== false;
|
||||
const webhooksEnabled = cfg.hooks?.enabled === true;
|
||||
const internalHooksEnabled = cfg.hooks?.internal?.enabled !== false;
|
||||
const internalHooksEnabled = hasConfiguredInternalHooks(cfg);
|
||||
const browserEnabled = cfg.browser?.enabled ?? true;
|
||||
|
||||
const detail =
|
||||
|
||||
@@ -27,9 +27,9 @@ describe("collectAttackSurfaceSummaryFindings", () => {
|
||||
expectedDetail: ["hooks.webhooks: enabled", "hooks.internal: enabled"],
|
||||
},
|
||||
{
|
||||
name: "reports internal hooks as enabled by default and webhooks as disabled when neither is configured",
|
||||
name: "reports internal hooks as disabled until configured",
|
||||
cfg: {} satisfies OpenClawConfig,
|
||||
expectedDetail: ["hooks.webhooks: disabled", "hooks.internal: enabled"],
|
||||
expectedDetail: ["hooks.webhooks: disabled", "hooks.internal: disabled"],
|
||||
},
|
||||
{
|
||||
name: "reports internal hooks as disabled when explicitly set to false",
|
||||
|
||||
@@ -149,6 +149,12 @@ describe("resolveBuildAllSteps", () => {
|
||||
expect(step?.cache).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not cache hook metadata over compiled hook handlers", () => {
|
||||
const step = BUILD_ALL_STEPS.find((entry) => entry.label === "copy-hook-metadata");
|
||||
expect(step).toBeTruthy();
|
||||
expect(step?.cache).toBeUndefined();
|
||||
});
|
||||
|
||||
it("rejects unknown build profiles", () => {
|
||||
expect(() => resolveBuildAllSteps("wat")).toThrow("Unknown build profile: wat");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user