From e1897419de48d99cb6b2bee873f291c03b0e0611 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 22 Apr 2026 06:14:00 +0100 Subject: [PATCH] fix(config): enforce resolved runtime channel config --- CHANGELOG.md | 2 + package.json | 1 + .../check-no-runtime-action-load-config.mjs | 104 ++++++++++++++++++ scripts/check.mjs | 1 + src/plugin-sdk/config-runtime.ts | 11 ++ 5 files changed, 119 insertions(+) create mode 100644 scripts/check-no-runtime-action-load-config.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 94708fa7bbf..2361a9b2513 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ Docs: https://docs.openclaw.ai ### Fixes +- Channels/config: require resolved runtime config on channel send/action/client helpers and block runtime helper `loadConfig()` calls, so SecretRefs are resolved at startup/boundaries instead of being re-read during sends. +- Telegram: keep the sent-message ownership cache isolated per configured session store, so own-message reaction filtering remains correct with custom `session.store` paths. - Ollama: forward OpenClaw thinking control to native `/api/chat` requests as top-level `think`, so `/think off` and `openclaw agent --thinking off` suppress thinking on models such as qwen3 instead of idling until the watchdog fires. Fixes #69902. (#69967) Thanks @WZH8898. - Memory-core/dreaming: suppress the startup-only managed dreaming cron unavailable warning when the cron service is still attaching, while preserving the runtime warning if cron genuinely remains unavailable. Fixes #69939. (#69941) Thanks @Sanjays2402. - Mattermost: suppress reasoning-only payloads even when they arrive as blockquoted `> Reasoning:` text, preventing `/reasoning on` from leaking thinking into channel posts. (#69927) Thanks @lawrence3699. diff --git a/package.json b/package.json index 8b662272c37..d91f7ea773a 100644 --- a/package.json +++ b/package.json @@ -1264,6 +1264,7 @@ "check:loc": "node --import tsx scripts/check-ts-max-loc.ts --max 500", "check:madge-import-cycles": "node --import tsx scripts/check-madge-import-cycles.ts", "check:no-conflict-markers": "node scripts/check-no-conflict-markers.mjs", + "check:no-runtime-action-load-config": "node scripts/check-no-runtime-action-load-config.mjs", "check:static-import-sccs": "pnpm check:madge-import-cycles", "check:temp-path-guardrails": "node --import tsx scripts/check-temp-path-guardrails.ts", "check:test-types": "pnpm tsgo:test", diff --git a/scripts/check-no-runtime-action-load-config.mjs b/scripts/check-no-runtime-action-load-config.mjs new file mode 100644 index 00000000000..9feeb709b4d --- /dev/null +++ b/scripts/check-no-runtime-action-load-config.mjs @@ -0,0 +1,104 @@ +#!/usr/bin/env node +import fs from "node:fs"; +import path from "node:path"; + +const repoRoot = path.resolve(new URL("..", import.meta.url).pathname); + +const CHANNEL_EXTENSION_IDS = new Set([ + "discord", + "imessage", + "irc", + "line", + "matrix", + "mattermost", + "nextcloud-talk", + "signal", + "slack", + "telegram", + "whatsapp", +]); + +const HELPER_BASENAME_PATTERNS = [ + /^action-runtime\.ts$/, + /^actions(?:\..*)?\.ts$/, + /^active-listener\.ts$/, + /^access-control\.ts$/, + /^channel\.ts$/, + /^client(?:[-.].*)?\.ts$/, + /^recipient-resolution\.ts$/, + /^rich-menu\.ts$/, + /^send(?:[-.].*)?\.ts$/, + /^sent-message-cache\.ts$/, + /^thread-bindings\.ts$/, +]; + +const FORBIDDEN_PATTERNS = [/\bloadConfig\s*\(/, /\.config\.loadConfig\s*\(/]; + +function* walk(dir) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (entry.name === "node_modules" || entry.name === "dist") { + continue; + } + const absolute = path.join(dir, entry.name); + if (entry.isDirectory()) { + yield* walk(absolute); + continue; + } + yield absolute; + } +} + +function isCandidate(relativePath) { + const parts = relativePath.split(path.sep); + if (parts[0] !== "extensions" || parts[2] !== "src") { + return false; + } + if (!CHANNEL_EXTENSION_IDS.has(parts[1])) { + return false; + } + if ( + relativePath.endsWith(".test.ts") || + relativePath.endsWith(".test-harness.ts") || + relativePath.endsWith(".d.ts") + ) { + return false; + } + if (parts.includes("monitor") || parts.includes("cli")) { + return false; + } + if (parts.includes("actions")) { + return true; + } + const basename = path.basename(relativePath); + return HELPER_BASENAME_PATTERNS.some((pattern) => pattern.test(basename)); +} + +function main() { + const violations = []; + for (const absolute of walk(path.join(repoRoot, "extensions"))) { + const relativePath = path.relative(repoRoot, absolute); + if (!isCandidate(relativePath)) { + continue; + } + const lines = fs.readFileSync(absolute, "utf8").split(/\r?\n/); + lines.forEach((line, index) => { + if (FORBIDDEN_PATTERNS.some((pattern) => pattern.test(line))) { + violations.push(`${relativePath}:${index + 1}: ${line.trim()}`); + } + }); + } + if (violations.length === 0) { + return; + } + console.error( + [ + "Runtime channel send/action/client/pairing helpers must not call loadConfig().", + "Load and resolve config at the command/gateway/monitor boundary, then pass cfg through.", + "", + ...violations, + ].join("\n"), + ); + process.exitCode = 1; +} + +main(); diff --git a/scripts/check.mjs b/scripts/check.mjs index e2f94c1b01b..f8eff5e13f6 100644 --- a/scripts/check.mjs +++ b/scripts/check.mjs @@ -9,6 +9,7 @@ export async function main(argv = process.argv.slice(2)) { const tailChecks = [ { name: "webhook body guard", args: ["lint:webhook:no-low-level-body-read"] }, + { name: "runtime action config guard", args: ["check:no-runtime-action-load-config"] }, { name: "temp path guard", args: ["check:temp-path-guardrails"] }, { name: "pairing store guard", args: ["lint:auth:no-pairing-store-group"] }, { name: "pairing account guard", args: ["lint:auth:pairing-account-scope"] }, diff --git a/src/plugin-sdk/config-runtime.ts b/src/plugin-sdk/config-runtime.ts index 02d049c170e..c66588a943a 100644 --- a/src/plugin-sdk/config-runtime.ts +++ b/src/plugin-sdk/config-runtime.ts @@ -1,6 +1,17 @@ // Shared config/runtime boundary for plugins that need config loading, // config writes, or session-store helpers without importing src internals. +import type { OpenClawConfig } from "../config/types.js"; + +export function requireRuntimeConfig(config: OpenClawConfig, context: string): OpenClawConfig { + if (config) { + return config; + } + throw new Error( + `${context} requires a resolved runtime config. Load and resolve config at the command or gateway boundary, then pass cfg through the runtime path.`, + ); +} + export { resolveDefaultAgentId } from "../agents/agent-scope.js"; export { clearRuntimeConfigSnapshot,