11export * as ConfigAgent from "./agent"
22
3- import { Log } from "../util "
3+ import { Schema } from "effect "
44import z from "zod"
5+ import { Bus } from "@/bus"
6+ import { zod , ZodOverride } from "@/util/effect-zod"
7+ import { Log } from "../util"
58import { NamedError } from "@opencode-ai/shared/util/error"
69import { Glob } from "@opencode-ai/shared/util/glob"
7- import { Bus } from "@/bus"
810import { configEntryNameFromPath } from "./entry-name"
911import { InvalidError } from "./error"
1012import * as ConfigMarkdown from "./markdown"
@@ -13,89 +15,104 @@ import { ConfigPermission } from "./permission"
1315
1416const log = Log . create ( { service : "config" } )
1517
16- export const Info = z
17- . object ( {
18- model : ConfigModelID . zod . optional ( ) ,
19- variant : z
20- . string ( )
21- . optional ( )
22- . describe ( "Default model variant for this agent (applies only when using the agent's configured model)." ) ,
23- temperature : z . number ( ) . optional ( ) ,
24- top_p : z . number ( ) . optional ( ) ,
25- prompt : z . string ( ) . optional ( ) ,
26- tools : z . record ( z . string ( ) , z . boolean ( ) ) . optional ( ) . describe ( "@deprecated Use 'permission' field instead" ) ,
27- disable : z . boolean ( ) . optional ( ) ,
28- description : z . string ( ) . optional ( ) . describe ( "Description of when to use the agent" ) ,
29- mode : z . enum ( [ "subagent" , "primary" , "all" ] ) . optional ( ) ,
30- hidden : z
31- . boolean ( )
32- . optional ( )
33- . describe ( "Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)" ) ,
34- options : z . record ( z . string ( ) , z . any ( ) ) . optional ( ) ,
35- color : z
36- . union ( [
37- z . string ( ) . regex ( / ^ # [ 0 - 9 a - f A - F ] { 6 } $ / , "Invalid hex color format" ) ,
38- z . enum ( [ "primary" , "secondary" , "accent" , "success" , "warning" , "error" , "info" ] ) ,
39- ] )
40- . optional ( )
41- . describe ( "Hex color code (e.g., #FF5733) or theme color (e.g., primary)" ) ,
42- steps : z
43- . number ( )
44- . int ( )
45- . positive ( )
46- . optional ( )
47- . describe ( "Maximum number of agentic iterations before forcing text-only response" ) ,
48- maxSteps : z . number ( ) . int ( ) . positive ( ) . optional ( ) . describe ( "@deprecated Use 'steps' field instead." ) ,
49- permission : ConfigPermission . Info . optional ( ) ,
50- } )
51- . catchall ( z . any ( ) )
52- . transform ( ( agent , _ctx ) => {
53- const knownKeys = new Set ( [
54- "name" ,
55- "model" ,
56- "variant" ,
57- "prompt" ,
58- "description" ,
59- "temperature" ,
60- "top_p" ,
61- "mode" ,
62- "hidden" ,
63- "color" ,
64- "steps" ,
65- "maxSteps" ,
66- "options" ,
67- "permission" ,
68- "disable" ,
69- "tools" ,
70- ] )
71-
72- const options : Record < string , unknown > = { ...agent . options }
73- for ( const [ key , value ] of Object . entries ( agent ) ) {
74- if ( ! knownKeys . has ( key ) ) options [ key ] = value
75- }
18+ const PositiveInt = Schema . Number . check ( Schema . isInt ( ) ) . check ( Schema . isGreaterThan ( 0 ) )
7619
77- const permission : ConfigPermission . Info = { }
78- for ( const [ tool , enabled ] of Object . entries ( agent . tools ?? { } ) ) {
79- const action = enabled ? "allow" : "deny"
80- if ( tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit" ) {
81- permission . edit = action
82- continue
83- }
84- permission [ tool ] = action
85- }
86- Object . assign ( permission , agent . permission )
20+ const Color = Schema . Union ( [
21+ Schema . String . check ( Schema . isPattern ( / ^ # [ 0 - 9 a - f A - F ] { 6 } $ / ) ) ,
22+ Schema . Literals ( [ "primary" , "secondary" , "accent" , "success" , "warning" , "error" , "info" ] ) ,
23+ ] )
24+
25+ // ConfigPermission.Info is a zod schema (its `.preprocess(...).transform(...)`
26+ // shape lives outside the Effect Schema type system), so the walker reaches it
27+ // via ZodOverride rather than a pure Schema reference. This preserves the
28+ // `$ref: PermissionConfig` emitted in openapi.json.
29+ const PermissionRef = Schema . Any . annotate ( { [ ZodOverride ] : ConfigPermission . Info } )
30+
31+ const AgentSchema = Schema . StructWithRest (
32+ Schema . Struct ( {
33+ model : Schema . optional ( ConfigModelID ) ,
34+ variant : Schema . optional ( Schema . String ) . annotate ( {
35+ description : "Default model variant for this agent (applies only when using the agent's configured model)." ,
36+ } ) ,
37+ temperature : Schema . optional ( Schema . Number ) ,
38+ top_p : Schema . optional ( Schema . Number ) ,
39+ prompt : Schema . optional ( Schema . String ) ,
40+ tools : Schema . optional ( Schema . Record ( Schema . String , Schema . Boolean ) ) . annotate ( {
41+ description : "@deprecated Use 'permission' field instead" ,
42+ } ) ,
43+ disable : Schema . optional ( Schema . Boolean ) ,
44+ description : Schema . optional ( Schema . String ) . annotate ( { description : "Description of when to use the agent" } ) ,
45+ mode : Schema . optional ( Schema . Literals ( [ "subagent" , "primary" , "all" ] ) ) ,
46+ hidden : Schema . optional ( Schema . Boolean ) . annotate ( {
47+ description : "Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)" ,
48+ } ) ,
49+ options : Schema . optional ( Schema . Record ( Schema . String , Schema . Any ) ) ,
50+ color : Schema . optional ( Color ) . annotate ( {
51+ description : "Hex color code (e.g., #FF5733) or theme color (e.g., primary)" ,
52+ } ) ,
53+ steps : Schema . optional ( PositiveInt ) . annotate ( {
54+ description : "Maximum number of agentic iterations before forcing text-only response" ,
55+ } ) ,
56+ maxSteps : Schema . optional ( PositiveInt ) . annotate ( { description : "@deprecated Use 'steps' field instead." } ) ,
57+ permission : Schema . optional ( PermissionRef ) ,
58+ } ) ,
59+ [ Schema . Record ( Schema . String , Schema . Any ) ] ,
60+ )
8761
88- const steps = agent . steps ?? agent . maxSteps
62+ const KNOWN_KEYS = new Set ( [
63+ "name" ,
64+ "model" ,
65+ "variant" ,
66+ "prompt" ,
67+ "description" ,
68+ "temperature" ,
69+ "top_p" ,
70+ "mode" ,
71+ "hidden" ,
72+ "color" ,
73+ "steps" ,
74+ "maxSteps" ,
75+ "options" ,
76+ "permission" ,
77+ "disable" ,
78+ "tools" ,
79+ ] )
8980
90- return { ...agent , options, permission, steps } as typeof agent & {
91- options ?: Record < string , unknown >
92- permission ?: ConfigPermission . Info
93- steps ?: number
81+ // Post-parse normalisation:
82+ // - Promote any unknown-but-present keys into `options` so they survive the
83+ // round-trip in a well-known field.
84+ // - Translate the deprecated `tools: { name: boolean }` map into the new
85+ // `permission` shape (write-adjacent tools collapse into `permission.edit`).
86+ // - Coalesce `steps ?? maxSteps` so downstream can ignore the deprecated alias.
87+ const normalize = ( agent : z . infer < typeof Info > ) => {
88+ const options : Record < string , unknown > = { ...agent . options }
89+ for ( const [ key , value ] of Object . entries ( agent ) ) {
90+ if ( ! KNOWN_KEYS . has ( key ) ) options [ key ] = value
91+ }
92+
93+ const permission : ConfigPermission . Info = { }
94+ for ( const [ tool , enabled ] of Object . entries ( agent . tools ?? { } ) ) {
95+ const action = enabled ? "allow" : "deny"
96+ if ( tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit" ) {
97+ permission . edit = action
98+ continue
9499 }
95- } )
96- . meta ( {
97- ref : "AgentConfig" ,
98- } )
100+ permission [ tool ] = action
101+ }
102+ globalThis . Object . assign ( permission , agent . permission )
103+
104+ return { ...agent , options, permission, steps : agent . steps ?? agent . maxSteps }
105+ }
106+
107+ export const Info = zod ( AgentSchema )
108+ . transform ( normalize )
109+ . meta ( { ref : "AgentConfig" } ) as unknown as z . ZodType <
110+ Omit < z . infer < ReturnType < typeof zod < typeof AgentSchema > > > , "options" | "permission" | "steps" > & {
111+ options ?: Record < string , unknown >
112+ permission ?: ConfigPermission . Info
113+ steps ?: number
114+ }
115+ >
99116export type Info = z . infer < typeof Info >
100117
101118export async function load ( dir : string ) {
0 commit comments