From b3882ecceff59e87163e3fd295a263762abfd51a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 14 Feb 2026 13:34:46 +0000 Subject: [PATCH] refactor(config): share include scan helper --- src/config/includes-scan.ts | 87 +++++++++++++++++++++++++++++++ src/security/audit-extra.async.ts | 85 +----------------------------- src/security/fix.ts | 85 +----------------------------- 3 files changed, 89 insertions(+), 168 deletions(-) create mode 100644 src/config/includes-scan.ts diff --git a/src/config/includes-scan.ts b/src/config/includes-scan.ts new file mode 100644 index 00000000000..28ee377c852 --- /dev/null +++ b/src/config/includes-scan.ts @@ -0,0 +1,87 @@ +import JSON5 from "json5"; +import * as fs from "node:fs/promises"; +import path from "node:path"; +import { INCLUDE_KEY, MAX_INCLUDE_DEPTH } from "./includes.js"; + +function listDirectIncludes(parsed: unknown): string[] { + const out: string[] = []; + const visit = (value: unknown) => { + if (!value) { + return; + } + if (Array.isArray(value)) { + for (const item of value) { + visit(item); + } + return; + } + if (typeof value !== "object") { + return; + } + const rec = value as Record; + const includeVal = rec[INCLUDE_KEY]; + if (typeof includeVal === "string") { + out.push(includeVal); + } else if (Array.isArray(includeVal)) { + for (const item of includeVal) { + if (typeof item === "string") { + out.push(item); + } + } + } + for (const v of Object.values(rec)) { + visit(v); + } + }; + visit(parsed); + return out; +} + +function resolveIncludePath(baseConfigPath: string, includePath: string): string { + return path.normalize( + path.isAbsolute(includePath) + ? includePath + : path.resolve(path.dirname(baseConfigPath), includePath), + ); +} + +export async function collectIncludePathsRecursive(params: { + configPath: string; + parsed: unknown; +}): Promise { + const visited = new Set(); + const result: string[] = []; + + const walk = async (basePath: string, parsed: unknown, depth: number): Promise => { + if (depth > MAX_INCLUDE_DEPTH) { + return; + } + for (const raw of listDirectIncludes(parsed)) { + const resolved = resolveIncludePath(basePath, raw); + if (visited.has(resolved)) { + continue; + } + visited.add(resolved); + result.push(resolved); + + const rawText = await fs.readFile(resolved, "utf-8").catch(() => null); + if (!rawText) { + continue; + } + const nestedParsed = (() => { + try { + return JSON5.parse(rawText); + } catch { + return null; + } + })(); + if (nestedParsed) { + // eslint-disable-next-line no-await-in-loop + await walk(resolved, nestedParsed, depth + 1); + } + } + }; + + await walk(params.configPath, params.parsed, 0); + return result; +} diff --git a/src/security/audit-extra.async.ts b/src/security/audit-extra.async.ts index 55533862939..197a1c98229 100644 --- a/src/security/audit-extra.async.ts +++ b/src/security/audit-extra.async.ts @@ -3,7 +3,6 @@ * * These functions perform I/O (filesystem, config reads) to detect security issues. */ -import JSON5 from "json5"; import fs from "node:fs/promises"; import path from "node:path"; import type { SandboxToolPolicy } from "../agents/sandbox/types.js"; @@ -22,7 +21,7 @@ import { resolveToolProfilePolicy } from "../agents/tool-policy.js"; import { MANIFEST_KEY } from "../compat/legacy-names.js"; import { resolveNativeSkillsEnabled } from "../config/commands.js"; import { createConfigIO } from "../config/config.js"; -import { INCLUDE_KEY, MAX_INCLUDE_DEPTH } from "../config/includes.js"; +import { collectIncludePathsRecursive } from "../config/includes-scan.js"; import { resolveOAuthDir } from "../config/paths.js"; import { normalizePluginsConfig } from "../plugins/config-state.js"; import { normalizeAgentId } from "../routing/session-key.js"; @@ -63,88 +62,6 @@ function expandTilde(p: string, env: NodeJS.ProcessEnv): string | null { return null; } -function resolveIncludePath(baseConfigPath: string, includePath: string): string { - return path.normalize( - path.isAbsolute(includePath) - ? includePath - : path.resolve(path.dirname(baseConfigPath), includePath), - ); -} - -function listDirectIncludes(parsed: unknown): string[] { - const out: string[] = []; - const visit = (value: unknown) => { - if (!value) { - return; - } - if (Array.isArray(value)) { - for (const item of value) { - visit(item); - } - return; - } - if (typeof value !== "object") { - return; - } - const rec = value as Record; - const includeVal = rec[INCLUDE_KEY]; - if (typeof includeVal === "string") { - out.push(includeVal); - } else if (Array.isArray(includeVal)) { - for (const item of includeVal) { - if (typeof item === "string") { - out.push(item); - } - } - } - for (const v of Object.values(rec)) { - visit(v); - } - }; - visit(parsed); - return out; -} - -async function collectIncludePathsRecursive(params: { - configPath: string; - parsed: unknown; -}): Promise { - const visited = new Set(); - const result: string[] = []; - - const walk = async (basePath: string, parsed: unknown, depth: number): Promise => { - if (depth > MAX_INCLUDE_DEPTH) { - return; - } - for (const raw of listDirectIncludes(parsed)) { - const resolved = resolveIncludePath(basePath, raw); - if (visited.has(resolved)) { - continue; - } - visited.add(resolved); - result.push(resolved); - const rawText = await fs.readFile(resolved, "utf-8").catch(() => null); - if (!rawText) { - continue; - } - const nestedParsed = (() => { - try { - return JSON5.parse(rawText); - } catch { - return null; - } - })(); - if (nestedParsed) { - // eslint-disable-next-line no-await-in-loop - await walk(resolved, nestedParsed, depth + 1); - } - } - }; - - await walk(params.configPath, params.parsed, 0); - return result; -} - function isPathInside(basePath: string, candidatePath: string): boolean { const base = path.resolve(basePath); const candidate = path.resolve(candidatePath); diff --git a/src/security/fix.ts b/src/security/fix.ts index 0ecfc1e7d00..f3a2e88cf82 100644 --- a/src/security/fix.ts +++ b/src/security/fix.ts @@ -1,10 +1,9 @@ -import JSON5 from "json5"; import fs from "node:fs/promises"; import path from "node:path"; import type { OpenClawConfig } from "../config/config.js"; import { resolveDefaultAgentId } from "../agents/agent-scope.js"; import { createConfigIO } from "../config/config.js"; -import { INCLUDE_KEY, MAX_INCLUDE_DEPTH } from "../config/includes.js"; +import { collectIncludePathsRecursive } from "../config/includes-scan.js"; import { resolveConfigPath, resolveOAuthDir, resolveStateDir } from "../config/paths.js"; import { readChannelAllowFromStore } from "../pairing/pairing-store.js"; import { runExec } from "../process/exec.js"; @@ -303,88 +302,6 @@ function applyConfigFixes(params: { cfg: OpenClawConfig; env: NodeJS.ProcessEnv return { cfg: next, changes, policyFlips }; } -function listDirectIncludes(parsed: unknown): string[] { - const out: string[] = []; - const visit = (value: unknown) => { - if (!value) { - return; - } - if (Array.isArray(value)) { - for (const item of value) { - visit(item); - } - return; - } - if (typeof value !== "object") { - return; - } - const rec = value as Record; - const includeVal = rec[INCLUDE_KEY]; - if (typeof includeVal === "string") { - out.push(includeVal); - } else if (Array.isArray(includeVal)) { - for (const item of includeVal) { - if (typeof item === "string") { - out.push(item); - } - } - } - for (const v of Object.values(rec)) { - visit(v); - } - }; - visit(parsed); - return out; -} - -function resolveIncludePath(baseConfigPath: string, includePath: string): string { - return path.normalize( - path.isAbsolute(includePath) - ? includePath - : path.resolve(path.dirname(baseConfigPath), includePath), - ); -} - -async function collectIncludePathsRecursive(params: { - configPath: string; - parsed: unknown; -}): Promise { - const visited = new Set(); - const result: string[] = []; - - const walk = async (basePath: string, parsed: unknown, depth: number): Promise => { - if (depth > MAX_INCLUDE_DEPTH) { - return; - } - for (const raw of listDirectIncludes(parsed)) { - const resolved = resolveIncludePath(basePath, raw); - if (visited.has(resolved)) { - continue; - } - visited.add(resolved); - result.push(resolved); - const rawText = await fs.readFile(resolved, "utf-8").catch(() => null); - if (!rawText) { - continue; - } - const nestedParsed = (() => { - try { - return JSON5.parse(rawText); - } catch { - return null; - } - })(); - if (nestedParsed) { - // eslint-disable-next-line no-await-in-loop - await walk(resolved, nestedParsed, depth + 1); - } - } - }; - - await walk(params.configPath, params.parsed, 0); - return result; -} - async function chmodCredentialsAndAgentState(params: { env: NodeJS.ProcessEnv; stateDir: string;