From 38e10eb1408feb700021b8e8766fb0ab41bf84e2 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2026 14:53:43 -0400 Subject: [PATCH] fix(opencode): ignore unknown config fields (#41312) --- packages/opencode/src/config/parse.ts | 28 ++++---------------- packages/opencode/test/config/config.test.ts | 15 ++++------- 2 files changed, 10 insertions(+), 33 deletions(-) diff --git a/packages/opencode/src/config/parse.ts b/packages/opencode/src/config/parse.ts index 35239086884..538f5113118 100644 --- a/packages/opencode/src/config/parse.ts +++ b/packages/opencode/src/config/parse.ts @@ -37,22 +37,11 @@ export function schema>( data: unknown, source: string, ): DeepMutable { - const extra = topLevelExtraKeys(schema, data) - if (extra.length) { - throw new InvalidError({ - path: source, - issues: [ - { - code: "unrecognized_keys", - keys: extra, - path: [], - message: `Unrecognized key${extra.length === 1 ? "" : "s"}: ${extra.join(", ")}`, - }, - ], - }) - } - - const decoded = EffectSchema.decodeUnknownExit(schema)(data, { errors: "all", propertyOrder: "original" }) + const decoded = EffectSchema.decodeUnknownExit(schema)(data, { + errors: "all", + onExcessProperty: "ignore", + propertyOrder: "original", + }) if (Exit.isSuccess(decoded)) return decoded.value as DeepMutable const error = Cause.squash(decoded.cause) @@ -70,10 +59,3 @@ export function schema>( { cause: error }, ) } - -function topLevelExtraKeys(schema: EffectSchema.Top, data: unknown) { - if (typeof data !== "object" || data === null || Array.isArray(data)) return [] - if (schema.ast._tag !== "Objects" || schema.ast.indexSignatures.length > 0) return [] - const known = new Set(schema.ast.propertySignatures.map((item) => String(item.name))) - return Object.keys(data).filter((key) => !known.has(key)) -} diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index e281831ddfa..8f72c0cb7f6 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -597,12 +597,12 @@ accountTokenIt.instance("resolves env templates in account config with account t }), ) -it.instance("validates config schema and throws on invalid fields", () => +it.instance("validates config schema and throws on invalid values", () => Effect.gen(function* () { const test = yield* TestInstance yield* writeConfigEffect(test.directory, { $schema: "https://opencode.ai/config.json", - invalid_field: "should cause error", + model: 42, }) const exit = yield* Config.use.get().pipe(Effect.exit) expect(Exit.isFailure(exit)).toBe(true) @@ -1331,7 +1331,7 @@ it.instance("permission config preserves user key order", () => }), ) -test("config parser preserves permission order while rejecting unknown top-level keys", () => { +test("config parser preserves permission order while ignoring unknown top-level keys", () => { const config = ConfigParse.schema( ConfigV1.Info, { @@ -1340,18 +1340,13 @@ test("config parser preserves permission order while rejecting unknown top-level "*": "deny", edit: "ask", }, + plugins: ["example"], }, "test", ) expect(Object.keys(config.permission!)).toEqual(["bash", "*", "edit"]) - try { - ConfigParse.schema(ConfigV1.Info, { invalid_field: true }, "test") - throw new Error("expected config parse to fail") - } catch (err) { - const error = err as { data?: { issues?: Array<{ code?: string; keys?: string[]; path?: string[] }> } } - expect(error.data?.issues?.[0]).toMatchObject({ code: "unrecognized_keys", keys: ["invalid_field"], path: [] }) - } + expect(config).not.toHaveProperty("plugins") }) // MCP config merging tests