From 47a40fe7f78c524d3b97973c71b166fe4bbde832 Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Mon, 6 Jul 2026 10:02:29 +0200 Subject: [PATCH] fix(cli): tolerate unsafe project config in settings overlay KilocodeConfigOverlay.load() propagated InvalidError from untrusted {env:}/out-of-scope {file:} substitutions through Promise.all, breaking the whole /config/overlay instead of skipping the offending file. Skip failed files (log + return {}) so the settings overlay still shows remaining config, matching the main config loader's degrade-gracefully behavior. --- .../opencode/src/kilocode/config/overlay.ts | 14 +++++++++++++ .../kilocode/server/config-overlay.test.ts | 20 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/packages/opencode/src/kilocode/config/overlay.ts b/packages/opencode/src/kilocode/config/overlay.ts index ef6aae17ba7..4d67a66e851 100644 --- a/packages/opencode/src/kilocode/config/overlay.ts +++ b/packages/opencode/src/kilocode/config/overlay.ts @@ -2,6 +2,7 @@ import path from "path" import { existsSync } from "fs" import { Schema } from "effect" import z from "zod" +import * as Log from "@opencode-ai/core/util/log" import { Global } from "@opencode-ai/core/global" import { ConfigAgent } from "@/config/agent" import { Config } from "@/config/config" @@ -13,6 +14,8 @@ import { KilocodeConfig } from "./config" import { KilocodeConfigSources } from "./sources" export namespace KilocodeConfigOverlay { + const log = Log.create({ service: "kilocode.config.overlay" }) + export const Scope = z.enum(["global", "project"]) export type Scope = z.infer @@ -204,6 +207,17 @@ export namespace KilocodeConfigOverlay { // kilocode_change end async function load(file: string, fileScope?: ConfigVariable.FileScope): Promise { + // kilocode_change start - a single unsafe/invalid project config file must not break the settings overlay; + // untrusted {env:} and out-of-scope {file:} throw InvalidError here, so skip the offending file like the + // main config loader does rather than failing the whole overlay. + return await loadUnsafe(file, fileScope).catch((err) => { + log.warn("skipping unreadable project config in overlay", { file, err }) + return {} as Config.Info + }) + } + + async function loadUnsafe(file: string, fileScope?: ConfigVariable.FileScope): Promise { + // kilocode_change end const text = await Bun.file(file).text() // kilocode_change - overlay reads project config files: {env:} rejected, {file:} confined to fileScope.root const expanded = await ConfigVariable.substitute({ text, type: "path", path: file, trusted: false, fileScope }) diff --git a/packages/opencode/test/kilocode/server/config-overlay.test.ts b/packages/opencode/test/kilocode/server/config-overlay.test.ts index 6767b9fa01b..89fa6da213e 100644 --- a/packages/opencode/test/kilocode/server/config-overlay.test.ts +++ b/packages/opencode/test/kilocode/server/config-overlay.test.ts @@ -144,6 +144,26 @@ describe("config overlay routes", () => { expect(body.targets.project).toBe(path.join(project.path, ".kilo", "kilo.json")) }) + test.serial("tolerates unsafe project config instead of failing the overlay", async () => { + await using project = await tmpdir() + // A project config that references a file outside the project root throws during substitution. + // The overlay must skip it and still resolve, rather than rejecting the whole request. + await Filesystem.write( + path.join(project.path, ".kilo", "kilo.json"), + JSON.stringify({ username: "{file:/etc/passwd}" }), + ) + + const body = await KilocodeConfigOverlay.resolve({ + directory: project.path, + scope: "project", + effective: {}, + global: {}, + sources: [], + }) + + expect(body.project.username ?? "").not.toContain("root:") + }) + test.serial("marks global values inherited in project scope", async () => { await using global = await tmpdir() await using project = await tmpdir()