fix: harden include confinement edge cases (#18652) (thanks @aether-ai-agent)

This commit is contained in:
Peter Steinberger
2026-02-18 03:26:36 +01:00
parent b5f551d716
commit d1c00dbb7c
6 changed files with 126 additions and 12 deletions

View File

@@ -0,0 +1,38 @@
import fs from "node:fs/promises";
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import { withTempHome } from "../../test/helpers/temp-home.js";
const { noteSpy } = vi.hoisted(() => ({
noteSpy: vi.fn(),
}));
vi.mock("../terminal/note.js", () => ({
note: noteSpy,
}));
import { loadAndMaybeMigrateDoctorConfig } from "./doctor-config-flow.js";
describe("doctor include warning", () => {
it("surfaces include confinement hint for escaped include paths", async () => {
await withTempHome(async (home) => {
const configDir = path.join(home, ".openclaw");
await fs.mkdir(configDir, { recursive: true });
await fs.writeFile(
path.join(configDir, "openclaw.json"),
JSON.stringify({ $include: "/etc/passwd" }, null, 2),
"utf-8",
);
await loadAndMaybeMigrateDoctorConfig({
options: { nonInteractive: true },
confirm: async () => false,
});
});
expect(noteSpy).toHaveBeenCalledWith(
expect.stringContaining("$include paths must stay under:"),
"Doctor warnings",
);
});
});

View File

@@ -147,6 +147,30 @@ function noteOpencodeProviderOverrides(cfg: OpenClawConfig) {
note(lines.join("\n"), "OpenCode Zen");
}
function noteIncludeConfinementWarning(snapshot: {
path?: string | null;
issues?: Array<{ message: string }>;
}): void {
const issues = snapshot.issues ?? [];
const includeIssue = issues.find(
(issue) =>
issue.message.includes("Include path escapes config directory") ||
issue.message.includes("Include path resolves outside config directory"),
);
if (!includeIssue) {
return;
}
const configRoot = path.dirname(snapshot.path ?? CONFIG_PATH);
note(
[
`- $include paths must stay under: ${configRoot}`,
'- Move shared include files under that directory and update to relative paths like "./shared/common.json".',
`- Error: ${includeIssue.message}`,
].join("\n"),
"Doctor warnings",
);
}
type TelegramAllowFromUsernameHit = { path: string; entry: string };
type TelegramAllowFromListRef = {
@@ -758,6 +782,7 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
const fixHints: string[] = [];
if (snapshot.exists && !snapshot.valid && snapshot.legacyIssues.length === 0) {
note("Config invalid; doctor will run with best-effort config.", "Config");
noteIncludeConfinementWarning(snapshot);
}
const warnings = snapshot.warnings ?? [];
if (warnings.length > 0) {