fix(config): enforce resolved runtime channel config

This commit is contained in:
Peter Steinberger
2026-04-22 06:14:00 +01:00
parent b70531bf24
commit e1897419de
5 changed files with 119 additions and 0 deletions

View File

@@ -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.

View File

@@ -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",

View File

@@ -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();

View File

@@ -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"] },

View File

@@ -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,