summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-04-21 14:06:47 -0400
committerGitHub <[email protected]>2026-04-21 14:06:47 -0400
commitecc06a3d8f7783d3759061c3404341b0cdc537ec (patch)
treed6a79970c00b2ac7b4dcff7426790bed5fafcebd /packages
parent3205f122eb7a1c97c63ee18f7069ea2248e2b2b4 (diff)
downloadopencode-ecc06a3d8f7783d3759061c3404341b0cdc537ec.tar.gz
opencode-ecc06a3d8f7783d3759061c3404341b0cdc537ec.zip
refactor(core): make Config.Info canonical Effect Schema (#23716)
Diffstat (limited to 'packages')
-rwxr-xr-xpackages/opencode/script/schema.ts2
-rw-r--r--packages/opencode/src/config/config.ts35
-rw-r--r--packages/opencode/src/server/routes/global.ts6
-rw-r--r--packages/opencode/src/server/routes/instance/config.ts6
-rw-r--r--packages/opencode/src/server/routes/instance/httpapi/config.ts2
-rw-r--r--packages/opencode/test/config/config.test.ts10
-rw-r--r--packages/opencode/test/session/compaction.test.ts2
7 files changed, 36 insertions, 27 deletions
diff --git a/packages/opencode/script/schema.ts b/packages/opencode/script/schema.ts
index c0f302f21..448760ae1 100755
--- a/packages/opencode/script/schema.ts
+++ b/packages/opencode/script/schema.ts
@@ -55,7 +55,7 @@ const configFile = process.argv[2]
const tuiFile = process.argv[3]
console.log(configFile)
-await Bun.write(configFile, JSON.stringify(generate(Config.Info), null, 2))
+await Bun.write(configFile, JSON.stringify(generate(Config.Info.zod), null, 2))
if (tuiFile) {
console.log(tuiFile)
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index 7fe337176..b4f4ace67 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -25,6 +25,7 @@ import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "e
import { EffectFlock } from "@opencode-ai/shared/util/effect-flock"
import { InstanceRef } from "@/effect/instance-ref"
import { zod, ZodOverride } from "@/util/effect-zod"
+import { withStatics } from "@/util/schema"
import { ConfigAgent } from "./agent"
import { ConfigCommand } from "./command"
import { ConfigFormatter } from "./formatter"
@@ -91,7 +92,15 @@ const LogLevelRef = Schema.Any.annotate({ [ZodOverride]: Log.Level })
const PositiveInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0))
const NonNegativeInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0))
-export const InfoSchema = Schema.Struct({
+// The Effect Schema is the canonical source of truth. The `.zod` compatibility
+// surface is derived so existing Hono validators keep working without a parallel
+// Zod definition.
+//
+// The walker emits `z.object({...})` which is non-strict by default. Config
+// historically uses `.strict()` (additionalProperties: false in openapi.json),
+// so layer that on after derivation. Re-apply the Config ref afterward
+// since `.strict()` strips the walker's meta annotation.
+export const Info = Schema.Struct({
$schema: Schema.optional(Schema.String).annotate({
description: "JSON schema reference for configuration validation",
}),
@@ -235,6 +244,14 @@ export const InfoSchema = Schema.Struct({
}),
),
})
+ .annotate({ identifier: "Config" })
+ .pipe(
+ withStatics((s) => ({
+ zod: (zod(s) as unknown as z.ZodObject<any>)
+ .strict()
+ .meta({ ref: "Config" }) as unknown as z.ZodType<DeepMutable<Schema.Schema.Type<typeof s>>>,
+ })),
+ )
// Schema.Struct produces readonly types by default, but the service code
// below mutates Info objects directly (e.g. `config.mode = ...`). Strip the
@@ -256,15 +273,7 @@ type DeepMutable<T> = T extends readonly [unknown, ...unknown[]]
? { -readonly [K in keyof T]: DeepMutable<T[K]> }
: T
-// The walker emits `z.object({...})` which is non-strict by default. Config
-// historically uses `.strict()` (additionalProperties: false in openapi.json),
-// so layer that on after derivation. Re-apply the Config ref afterward
-// since `.strict()` strips the walker's meta annotation.
-export const Info = (zod(InfoSchema) as unknown as z.ZodObject<any>)
- .strict()
- .meta({ ref: "Config" }) as unknown as z.ZodType<DeepMutable<Schema.Schema.Type<typeof InfoSchema>>>
-
-export type Info = z.output<typeof Info> & {
+export type Info = DeepMutable<Schema.Schema.Type<typeof Info>> & {
// plugin_origins is derived state, not a persisted config field. It keeps each winning plugin spec together
// with the file and scope it came from so later runtime code can make location-sensitive decisions.
plugin_origins?: ConfigPlugin.Origin[]
@@ -361,7 +370,7 @@ export const layer = Layer.effect(
),
)
const parsed = ConfigParse.jsonc(expanded, source)
- const data = ConfigParse.schema(Info, normalizeLoadedConfig(parsed, source), source)
+ const data = ConfigParse.schema(Info.zod, normalizeLoadedConfig(parsed, source), source)
if (!("path" in options)) return data
yield* Effect.promise(() => resolveLoadedPlugins(data, options.path))
@@ -753,13 +762,13 @@ export const layer = Layer.effect(
let next: Info
if (!file.endsWith(".jsonc")) {
- const existing = ConfigParse.schema(Info, ConfigParse.jsonc(before, file), file)
+ const existing = ConfigParse.schema(Info.zod, ConfigParse.jsonc(before, file), file)
const merged = mergeDeep(writable(existing), writable(config))
yield* fs.writeFileString(file, JSON.stringify(merged, null, 2)).pipe(Effect.orDie)
next = merged
} else {
const updated = patchJsonc(before, writable(config))
- next = ConfigParse.schema(Info, ConfigParse.jsonc(updated, file), file)
+ next = ConfigParse.schema(Info.zod, ConfigParse.jsonc(updated, file), file)
yield* fs.writeFileString(file, updated).pipe(Effect.orDie)
}
diff --git a/packages/opencode/src/server/routes/global.ts b/packages/opencode/src/server/routes/global.ts
index 8208cf966..54f9972e0 100644
--- a/packages/opencode/src/server/routes/global.ts
+++ b/packages/opencode/src/server/routes/global.ts
@@ -147,7 +147,7 @@ export const GlobalRoutes = lazy(() =>
description: "Get global config info",
content: {
"application/json": {
- schema: resolver(Config.Info),
+ schema: resolver(Config.Info.zod),
},
},
},
@@ -168,14 +168,14 @@ export const GlobalRoutes = lazy(() =>
description: "Successfully updated global config",
content: {
"application/json": {
- schema: resolver(Config.Info),
+ schema: resolver(Config.Info.zod),
},
},
},
...errors(400),
},
}),
- validator("json", Config.Info),
+ validator("json", Config.Info.zod),
async (c) => {
const config = c.req.valid("json")
const next = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.updateGlobal(config)))
diff --git a/packages/opencode/src/server/routes/instance/config.ts b/packages/opencode/src/server/routes/instance/config.ts
index 7f368cd31..88e5feef9 100644
--- a/packages/opencode/src/server/routes/instance/config.ts
+++ b/packages/opencode/src/server/routes/instance/config.ts
@@ -20,7 +20,7 @@ export const ConfigRoutes = lazy(() =>
description: "Get config info",
content: {
"application/json": {
- schema: resolver(Config.Info),
+ schema: resolver(Config.Info.zod),
},
},
},
@@ -43,14 +43,14 @@ export const ConfigRoutes = lazy(() =>
description: "Successfully updated config",
content: {
"application/json": {
- schema: resolver(Config.Info),
+ schema: resolver(Config.Info.zod),
},
},
},
...errors(400),
},
}),
- validator("json", Config.Info),
+ validator("json", Config.Info.zod),
async (c) =>
jsonRequest("ConfigRoutes.update", c, function* () {
const config = c.req.valid("json")
diff --git a/packages/opencode/src/server/routes/instance/httpapi/config.ts b/packages/opencode/src/server/routes/instance/httpapi/config.ts
index 678e96e33..2dfdec172 100644
--- a/packages/opencode/src/server/routes/instance/httpapi/config.ts
+++ b/packages/opencode/src/server/routes/instance/httpapi/config.ts
@@ -10,7 +10,7 @@ export const ConfigApi = HttpApi.make("config")
HttpApiGroup.make("config")
.add(
HttpApiEndpoint.get("get", root, {
- success: Config.InfoSchema,
+ success: Config.Info,
}).annotateMerge(
OpenApi.annotations({
identifier: "config.get",
diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts
index 3fafdadaa..e9b053819 100644
--- a/packages/opencode/test/config/config.test.ts
+++ b/packages/opencode/test/config/config.test.ts
@@ -2221,7 +2221,7 @@ describe("OPENCODE_CONFIG_CONTENT token substitution", () => {
test("parseManagedPlist strips MDM metadata keys", async () => {
const config = ConfigParse.schema(
- Config.Info,
+ Config.Info.zod,
ConfigParse.jsonc(
await ConfigManaged.parseManagedPlist(
JSON.stringify({
@@ -2249,7 +2249,7 @@ test("parseManagedPlist strips MDM metadata keys", async () => {
test("parseManagedPlist parses server settings", async () => {
const config = ConfigParse.schema(
- Config.Info,
+ Config.Info.zod,
ConfigParse.jsonc(
await ConfigManaged.parseManagedPlist(
JSON.stringify({
@@ -2269,7 +2269,7 @@ test("parseManagedPlist parses server settings", async () => {
test("parseManagedPlist parses permission rules", async () => {
const config = ConfigParse.schema(
- Config.Info,
+ Config.Info.zod,
ConfigParse.jsonc(
await ConfigManaged.parseManagedPlist(
JSON.stringify({
@@ -2299,7 +2299,7 @@ test("parseManagedPlist parses permission rules", async () => {
test("parseManagedPlist parses enabled_providers", async () => {
const config = ConfigParse.schema(
- Config.Info,
+ Config.Info.zod,
ConfigParse.jsonc(
await ConfigManaged.parseManagedPlist(
JSON.stringify({
@@ -2316,7 +2316,7 @@ test("parseManagedPlist parses enabled_providers", async () => {
test("parseManagedPlist handles empty config", async () => {
const config = ConfigParse.schema(
- Config.Info,
+ Config.Info.zod,
ConfigParse.jsonc(
await ConfigManaged.parseManagedPlist(JSON.stringify({ $schema: "https://opencode.ai/config.json" })),
"test:mobileconfig",
diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts
index 14b47922b..0e2b179f0 100644
--- a/packages/opencode/test/session/compaction.test.ts
+++ b/packages/opencode/test/session/compaction.test.ts
@@ -168,7 +168,7 @@ function layer(result: "continue" | "compact") {
}
function cfg(compaction?: Config.Info["compaction"]) {
- const base = Config.Info.parse({})
+ const base = Config.Info.zod.parse({})
return Layer.mock(Config.Service)({
get: () => Effect.succeed({ ...base, compaction }),
})