@@ -21,9 +21,10 @@ import { isRecord } from "@/util/record"
2121import type { ConsoleState } from "./console-state"
2222import { AppFileSystem } from "@opencode-ai/shared/filesystem"
2323import { InstanceState } from "@/effect"
24- import { Context , Duration , Effect , Exit , Fiber , Layer , Option } from "effect"
24+ import { Context , Duration , Effect , Exit , Fiber , Layer , Option , Schema } from "effect"
2525import { EffectFlock } from "@opencode-ai/shared/util/effect-flock"
2626import { InstanceRef } from "@/effect/instance-ref"
27+ import { zod , ZodOverride } from "@/util/effect-zod"
2728import { ConfigAgent } from "./agent"
2829import { ConfigCommand } from "./command"
2930import { ConfigFormatter } from "./formatter"
@@ -79,152 +80,182 @@ export const Server = ConfigServer.Server.zod
7980export const Layout = ConfigLayout . Layout . zod
8081export type Layout = ConfigLayout . Layout
8182
82- export const Info = z
83- . object ( {
84- $schema : z . string ( ) . optional ( ) . describe ( "JSON schema reference for configuration validation" ) ,
85- logLevel : Log . Level . optional ( ) . describe ( "Log level" ) ,
86- server : Server . optional ( ) . describe ( "Server configuration for opencode serve and web commands" ) ,
87- command : z
88- . record ( z . string ( ) , ConfigCommand . Info . zod )
89- . optional ( )
90- . describe ( "Command configuration, see https://opencode.ai/docs/commands" ) ,
91- skills : ConfigSkills . Info . zod . optional ( ) . describe ( "Additional skill folder paths" ) ,
92- watcher : z
93- . object ( {
94- ignore : z . array ( z . string ( ) ) . optional ( ) ,
95- } )
96- . optional ( ) ,
97- snapshot : z
98- . boolean ( )
99- . optional ( )
100- . describe (
101- "Enable or disable snapshot tracking. When false, filesystem snapshots are not recorded and undoing or reverting will not undo/redo file changes. Defaults to true." ,
102- ) ,
103- // User-facing plugin config is stored as Specs; provenance gets attached later while configs are merged.
104- plugin : ConfigPlugin . Spec . zod . array ( ) . optional ( ) ,
105- share : z
106- . enum ( [ "manual" , "auto" , "disabled" ] )
107- . optional ( )
108- . describe (
109- "Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing" ,
110- ) ,
111- autoshare : z
112- . boolean ( )
113- . optional ( )
114- . describe ( "@deprecated Use 'share' field instead. Share newly created sessions automatically" ) ,
115- autoupdate : z
116- . union ( [ z . boolean ( ) , z . literal ( "notify" ) ] )
117- . optional ( )
118- . describe (
119- "Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications" ,
120- ) ,
121- disabled_providers : z . array ( z . string ( ) ) . optional ( ) . describe ( "Disable providers that are loaded automatically" ) ,
122- enabled_providers : z
123- . array ( z . string ( ) )
124- . optional ( )
125- . describe ( "When set, ONLY these providers will be enabled. All other providers will be ignored" ) ,
126- model : ConfigModelID . zod . describe ( "Model to use in the format of provider/model, eg anthropic/claude-2" ) . optional ( ) ,
127- small_model : ConfigModelID . zod
128- . describe ( "Small model to use for tasks like title generation in the format of provider/model" )
129- . optional ( ) ,
130- default_agent : z
131- . string ( )
132- . optional ( )
133- . describe (
134- "Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid." ,
135- ) ,
136- username : z . string ( ) . optional ( ) . describe ( "Custom username to display in conversations instead of system username" ) ,
137- mode : z
138- . object ( {
139- build : ConfigAgent . Info . optional ( ) ,
140- plan : ConfigAgent . Info . optional ( ) ,
141- } )
142- . catchall ( ConfigAgent . Info )
143- . optional ( )
144- . describe ( "@deprecated Use `agent` field instead." ) ,
145- agent : z
146- . object ( {
83+ // Schemas that still live at the zod layer (have .transform / .preprocess /
84+ // .meta not expressible in current Effect Schema) get referenced via a
85+ // ZodOverride-annotated Schema.Any. Walker sees the annotation and emits the
86+ // exact zod directly, preserving component $refs.
87+ const AgentRef = Schema . Any . annotate ( { [ ZodOverride ] : ConfigAgent . Info } )
88+ const PermissionRef = Schema . Any . annotate ( { [ ZodOverride ] : ConfigPermission . Info } )
89+ const LogLevelRef = Schema . Any . annotate ( { [ ZodOverride ] : Log . Level } )
90+
91+ const PositiveInt = Schema . Number . check ( Schema . isInt ( ) ) . check ( Schema . isGreaterThan ( 0 ) )
92+ const NonNegativeInt = Schema . Number . check ( Schema . isInt ( ) ) . check ( Schema . isGreaterThanOrEqualTo ( 0 ) )
93+
94+ const InfoSchema = Schema . Struct ( {
95+ $schema : Schema . optional ( Schema . String ) . annotate ( {
96+ description : "JSON schema reference for configuration validation" ,
97+ } ) ,
98+ logLevel : Schema . optional ( LogLevelRef ) . annotate ( { description : "Log level" } ) ,
99+ server : Schema . optional ( ConfigServer . Server ) . annotate ( {
100+ description : "Server configuration for opencode serve and web commands" ,
101+ } ) ,
102+ command : Schema . optional ( Schema . Record ( Schema . String , ConfigCommand . Info ) ) . annotate ( {
103+ description : "Command configuration, see https://opencode.ai/docs/commands" ,
104+ } ) ,
105+ skills : Schema . optional ( ConfigSkills . Info ) . annotate ( { description : "Additional skill folder paths" } ) ,
106+ watcher : Schema . optional (
107+ Schema . Struct ( {
108+ ignore : Schema . optional ( Schema . mutable ( Schema . Array ( Schema . String ) ) ) ,
109+ } ) ,
110+ ) ,
111+ snapshot : Schema . optional ( Schema . Boolean ) . annotate ( {
112+ description :
113+ "Enable or disable snapshot tracking. When false, filesystem snapshots are not recorded and undoing or reverting will not undo/redo file changes. Defaults to true." ,
114+ } ) ,
115+ // User-facing plugin config is stored as Specs; provenance gets attached later while configs are merged.
116+ plugin : Schema . optional ( Schema . mutable ( Schema . Array ( ConfigPlugin . Spec ) ) ) ,
117+ share : Schema . optional ( Schema . Literals ( [ "manual" , "auto" , "disabled" ] ) ) . annotate ( {
118+ description :
119+ "Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing" ,
120+ } ) ,
121+ autoshare : Schema . optional ( Schema . Boolean ) . annotate ( {
122+ description : "@deprecated Use 'share' field instead. Share newly created sessions automatically" ,
123+ } ) ,
124+ autoupdate : Schema . optional ( Schema . Union ( [ Schema . Boolean , Schema . Literal ( "notify" ) ] ) ) . annotate ( {
125+ description :
126+ "Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications" ,
127+ } ) ,
128+ disabled_providers : Schema . optional ( Schema . mutable ( Schema . Array ( Schema . String ) ) ) . annotate ( {
129+ description : "Disable providers that are loaded automatically" ,
130+ } ) ,
131+ enabled_providers : Schema . optional ( Schema . mutable ( Schema . Array ( Schema . String ) ) ) . annotate ( {
132+ description : "When set, ONLY these providers will be enabled. All other providers will be ignored" ,
133+ } ) ,
134+ model : Schema . optional ( ConfigModelID ) . annotate ( {
135+ description : "Model to use in the format of provider/model, eg anthropic/claude-2" ,
136+ } ) ,
137+ small_model : Schema . optional ( ConfigModelID ) . annotate ( {
138+ description : "Small model to use for tasks like title generation in the format of provider/model" ,
139+ } ) ,
140+ default_agent : Schema . optional ( Schema . String ) . annotate ( {
141+ description :
142+ "Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid." ,
143+ } ) ,
144+ username : Schema . optional ( Schema . String ) . annotate ( {
145+ description : "Custom username to display in conversations instead of system username" ,
146+ } ) ,
147+ mode : Schema . optional (
148+ Schema . StructWithRest (
149+ Schema . Struct ( {
150+ build : Schema . optional ( AgentRef ) ,
151+ plan : Schema . optional ( AgentRef ) ,
152+ } ) ,
153+ [ Schema . Record ( Schema . String , AgentRef ) ] ,
154+ ) ,
155+ ) . annotate ( { description : "@deprecated Use `agent` field instead." } ) ,
156+ agent : Schema . optional (
157+ Schema . StructWithRest (
158+ Schema . Struct ( {
147159 // primary
148- plan : ConfigAgent . Info . optional ( ) ,
149- build : ConfigAgent . Info . optional ( ) ,
160+ plan : Schema . optional ( AgentRef ) ,
161+ build : Schema . optional ( AgentRef ) ,
150162 // subagent
151- general : ConfigAgent . Info . optional ( ) ,
152- explore : ConfigAgent . Info . optional ( ) ,
163+ general : Schema . optional ( AgentRef ) ,
164+ explore : Schema . optional ( AgentRef ) ,
153165 // specialized
154- title : ConfigAgent . Info . optional ( ) ,
155- summary : ConfigAgent . Info . optional ( ) ,
156- compaction : ConfigAgent . Info . optional ( ) ,
157- } )
158- . catchall ( ConfigAgent . Info )
159- . optional ( )
160- . describe ( "Agent configuration, see https://opencode.ai/docs/agents" ) ,
161- provider : z
162- . record ( z . string ( ) , ConfigProvider . Info . zod )
163- . optional ( )
164- . describe ( "Custom provider configurations and model overrides" ) ,
165- mcp : z
166- . record (
167- z . string ( ) ,
168- z . union ( [
169- ConfigMCP . Info . zod ,
170- z
171- . object ( {
172- enabled : z . boolean ( ) ,
173- } )
174- . strict ( ) ,
175- ] ) ,
176- )
177- . optional ( )
178- . describe ( "MCP (Model Context Protocol) server configurations" ) ,
179- formatter : ConfigFormatter . Info . zod . optional ( ) ,
180- lsp : ConfigLSP . Info . zod . optional ( ) ,
181- instructions : z . array ( z . string ( ) ) . optional ( ) . describe ( "Additional instruction files or patterns to include" ) ,
182- layout : Layout . optional ( ) . describe ( "@deprecated Always uses stretch layout." ) ,
183- permission : ConfigPermission . Info . optional ( ) ,
184- tools : z . record ( z . string ( ) , z . boolean ( ) ) . optional ( ) ,
185- enterprise : z
186- . object ( {
187- url : z . string ( ) . optional ( ) . describe ( "Enterprise URL" ) ,
188- } )
189- . optional ( ) ,
190- compaction : z
191- . object ( {
192- auto : z . boolean ( ) . optional ( ) . describe ( "Enable automatic compaction when context is full (default: true)" ) ,
193- prune : z . boolean ( ) . optional ( ) . describe ( "Enable pruning of old tool outputs (default: true)" ) ,
194- reserved : z
195- . number ( )
196- . int ( )
197- . min ( 0 )
198- . optional ( )
199- . describe ( "Token buffer for compaction. Leaves enough window to avoid overflow during compaction." ) ,
200- } )
201- . optional ( ) ,
202- experimental : z
203- . object ( {
204- disable_paste_summary : z . boolean ( ) . optional ( ) ,
205- batch_tool : z . boolean ( ) . optional ( ) . describe ( "Enable the batch tool" ) ,
206- openTelemetry : z
207- . boolean ( )
208- . optional ( )
209- . describe ( "Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)" ) ,
210- primary_tools : z
211- . array ( z . string ( ) )
212- . optional ( )
213- . describe ( "Tools that should only be available to primary agents." ) ,
214- continue_loop_on_deny : z . boolean ( ) . optional ( ) . describe ( "Continue the agent loop when a tool call is denied" ) ,
215- mcp_timeout : z
216- . number ( )
217- . int ( )
218- . positive ( )
219- . optional ( )
220- . describe ( "Timeout in milliseconds for model context protocol (MCP) requests" ) ,
221- } )
222- . optional ( ) ,
223- } )
166+ title : Schema . optional ( AgentRef ) ,
167+ summary : Schema . optional ( AgentRef ) ,
168+ compaction : Schema . optional ( AgentRef ) ,
169+ } ) ,
170+ [ Schema . Record ( Schema . String , AgentRef ) ] ,
171+ ) ,
172+ ) . annotate ( { description : "Agent configuration, see https://opencode.ai/docs/agents" } ) ,
173+ provider : Schema . optional ( Schema . Record ( Schema . String , ConfigProvider . Info ) ) . annotate ( {
174+ description : "Custom provider configurations and model overrides" ,
175+ } ) ,
176+ mcp : Schema . optional (
177+ Schema . Record (
178+ Schema . String ,
179+ Schema . Union ( [
180+ ConfigMCP . Info ,
181+ // Matches the legacy `{ enabled: false }` form used to disable a server.
182+ Schema . Any . annotate ( { [ ZodOverride ] : z . object ( { enabled : z . boolean ( ) } ) . strict ( ) } ) ,
183+ ] ) ,
184+ ) ,
185+ ) . annotate ( { description : "MCP (Model Context Protocol) server configurations" } ) ,
186+ formatter : Schema . optional ( ConfigFormatter . Info ) ,
187+ lsp : Schema . optional ( ConfigLSP . Info ) ,
188+ instructions : Schema . optional ( Schema . mutable ( Schema . Array ( Schema . String ) ) ) . annotate ( {
189+ description : "Additional instruction files or patterns to include" ,
190+ } ) ,
191+ layout : Schema . optional ( ConfigLayout . Layout ) . annotate ( { description : "@deprecated Always uses stretch layout." } ) ,
192+ permission : Schema . optional ( PermissionRef ) ,
193+ tools : Schema . optional ( Schema . Record ( Schema . String , Schema . Boolean ) ) ,
194+ enterprise : Schema . optional (
195+ Schema . Struct ( {
196+ url : Schema . optional ( Schema . String ) . annotate ( { description : "Enterprise URL" } ) ,
197+ } ) ,
198+ ) ,
199+ compaction : Schema . optional (
200+ Schema . Struct ( {
201+ auto : Schema . optional ( Schema . Boolean ) . annotate ( {
202+ description : "Enable automatic compaction when context is full (default: true)" ,
203+ } ) ,
204+ prune : Schema . optional ( Schema . Boolean ) . annotate ( {
205+ description : "Enable pruning of old tool outputs (default: true)" ,
206+ } ) ,
207+ reserved : Schema . optional ( NonNegativeInt ) . annotate ( {
208+ description : "Token buffer for compaction. Leaves enough window to avoid overflow during compaction." ,
209+ } ) ,
210+ } ) ,
211+ ) ,
212+ experimental : Schema . optional (
213+ Schema . Struct ( {
214+ disable_paste_summary : Schema . optional ( Schema . Boolean ) ,
215+ batch_tool : Schema . optional ( Schema . Boolean ) . annotate ( { description : "Enable the batch tool" } ) ,
216+ openTelemetry : Schema . optional ( Schema . Boolean ) . annotate ( {
217+ description : "Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)" ,
218+ } ) ,
219+ primary_tools : Schema . optional ( Schema . mutable ( Schema . Array ( Schema . String ) ) ) . annotate ( {
220+ description : "Tools that should only be available to primary agents." ,
221+ } ) ,
222+ continue_loop_on_deny : Schema . optional ( Schema . Boolean ) . annotate ( {
223+ description : "Continue the agent loop when a tool call is denied" ,
224+ } ) ,
225+ mcp_timeout : Schema . optional ( PositiveInt ) . annotate ( {
226+ description : "Timeout in milliseconds for model context protocol (MCP) requests" ,
227+ } ) ,
228+ } ) ,
229+ ) ,
230+ } )
231+
232+ // Schema.Struct produces readonly types by default, but the service code
233+ // below mutates Info objects directly (e.g. `config.mode = ...`). Strip the
234+ // readonly recursively so callers get the same mutable shape zod inferred.
235+ //
236+ // `Types.DeepMutable` from effect-smol would be a drop-in, but its fallback
237+ // branch `{ -readonly [K in keyof T]: ... }` collapses `unknown` to `{}`
238+ // (since `keyof unknown = never`), which widens `Record<string, unknown>`
239+ // fields like `ConfigPlugin.Options`. The local version gates on
240+ // `extends object` so `unknown` passes through.
241+ //
242+ // Tuple branch preserves `ConfigPlugin.Spec`'s `readonly [string, Options]`
243+ // shape (otherwise the general array branch widens it to an array).
244+ type DeepMutable < T > = T extends readonly [ unknown , ...unknown [ ] ]
245+ ? { - readonly [ K in keyof T ] : DeepMutable < T [ K ] > }
246+ : T extends readonly ( infer U ) [ ]
247+ ? DeepMutable < U > [ ]
248+ : T extends object
249+ ? { - readonly [ K in keyof T ] : DeepMutable < T [ K ] > }
250+ : T
251+
252+ // The walker emits `z.object({...})` which is non-strict by default. Config
253+ // historically uses `.strict()` (additionalProperties: false in openapi.json),
254+ // so layer that on after derivation. Re-apply the Config ref afterward
255+ // since `.strict()` strips the walker's meta annotation.
256+ export const Info = ( zod ( InfoSchema ) as unknown as z . ZodObject < any > )
224257 . strict ( )
225- . meta ( {
226- ref : "Config" ,
227- } )
258+ . meta ( { ref : "Config" } ) as unknown as z . ZodType < DeepMutable < Schema . Schema . Type < typeof InfoSchema > > >
228259
229260export type Info = z . output < typeof Info > & {
230261 // plugin_origins is derived state, not a persisted config field. It keeps each winning plugin spec together
0 commit comments