Compare commits

...

2 Commits

Author SHA1 Message Date
Kit Langton 55d4cd19e5 fix(opencode): warn on unknown config fields 2026-08-08 18:58:35 +00:00
opencode-agent[bot] 38e10eb140 fix(opencode): ignore unknown config fields (#41312) 2026-08-08 14:53:43 -04:00
3 changed files with 18 additions and 28 deletions
+6 -1
View File
@@ -224,7 +224,12 @@ const layer = Layer.effect(
),
)
const parsed = ConfigParse.jsonc(expanded, source)
const data = ConfigParse.schema(ConfigV1.Info, normalizeLoadedConfig(parsed), source)
const normalized = normalizeLoadedConfig(parsed)
const unrecognized = ConfigParse.unrecognizedKeys(ConfigV1.Info, normalized)
if (unrecognized.length) {
yield* Effect.logWarning("ignoring unrecognized config fields", { source, fields: unrecognized })
}
const data = ConfigParse.schema(ConfigV1.Info, normalized, source)
if (!("path" in options)) return data
yield* Effect.promise(() => resolveLoadedPlugins(data, options.path))
+6 -17
View File
@@ -37,22 +37,11 @@ export function schema<S extends EffectSchema.Decoder<unknown, never>>(
data: unknown,
source: string,
): DeepMutable<S["Type"]> {
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<S["Type"]>
const error = Cause.squash(decoded.cause)
@@ -71,7 +60,7 @@ export function schema<S extends EffectSchema.Decoder<unknown, never>>(
)
}
function topLevelExtraKeys(schema: EffectSchema.Top, data: unknown) {
export function unrecognizedKeys(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)))
+6 -10
View File
@@ -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,14 @@ 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")
expect(ConfigParse.unrecognizedKeys(ConfigV1.Info, { plugins: ["example"] })).toEqual(["plugins"])
})
// MCP config merging tests