package main import ( "bytes" "encoding/json" "fmt" "go/format" "log" "os" "os/exec" "path/filepath" "slices" "strings" "unicode" ) var clientMethods = []string{ "initialize", "thread/start", "thread/resume", "thread/read", "thread/compact/start", "turn/start", "turn/interrupt", } type schema struct { any bool Ref string `json:"$ref"` Type json.RawMessage `json:"type"` Enum []json.RawMessage `json:"enum"` Title string `json:"title"` Properties map[string]*schema `json:"properties"` Required []string `json:"required"` Items *schema `json:"items"` AdditionalProperties *schema `json:"additionalProperties"` OneOf []*schema `json:"oneOf"` AnyOf []*schema `json:"anyOf"` AllOf []*schema `json:"allOf"` } func (s *schema) UnmarshalJSON(b []byte) error { b = bytes.TrimSpace(b) if bytes.Equal(b, []byte("true")) || bytes.Equal(b, []byte("false")) { s.any = true return nil } type plain schema return json.Unmarshal(b, (*plain)(s)) } func (s *schema) types() []string { if len(s.Type) == 0 { return nil } if s.Type[0] == '"' { t := "" err := json.Unmarshal(s.Type, &t) if err != nil { log.Fatalf("type: %v", err) } return []string{t} } ts := []string{} err := json.Unmarshal(s.Type, &ts) if err != nil { log.Fatalf("type: %v", err) } return ts } func (s *schema) isNull() bool { ts := s.types() return len(ts) == 1 && ts[0] == "null" && s.Ref == "" } type bundle struct { Definitions map[string]json.RawMessage `json:"definitions"` } type gen struct { defs map[string]*schema order []string emitted map[string]bool kinds map[string]string out bytes.Buffer consts bytes.Buffer } func main() { dir, err := os.MkdirTemp("", "codex-schema") if err != nil { log.Fatalf("mkdir temp: %v", err) } defer os.RemoveAll(dir) cmd := exec.Command("codex", "app-server", "generate-json-schema", "--experimental", "--out", dir) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err = cmd.Run() if err != nil { log.Fatalf("generate schema: %v", err) } version, err := exec.Command("codex", "--version").Output() if err != nil { log.Fatalf("codex version: %v", err) } raw, err := os.ReadFile(filepath.Join(dir, "codex_app_server_protocol.schemas.json")) if err != nil { log.Fatalf("read schema: %v", err) } b := bundle{} err = json.Unmarshal(raw, &b) if err != nil { log.Fatalf("parse schema: %v", err) } g := &gen{defs: map[string]*schema{}, emitted: map[string]bool{}, kinds: map[string]string{}} g.load(b) src := g.generate(strings.TrimSpace(string(version))) formatted, err := format.Source(src) if err != nil { os.WriteFile("protocol.gen.go", src, 0o644) log.Fatalf("format: %v", err) } err = os.WriteFile("protocol.gen.go", formatted, 0o644) if err != nil { log.Fatalf("write: %v", err) } } func (g *gen) load(b bundle) { top := map[string]json.RawMessage{} for name, raw := range b.Definitions { if name == "v2" { continue } top[name] = raw } v2 := map[string]json.RawMessage{} err := json.Unmarshal(b.Definitions["v2"], &v2) if err != nil { log.Fatalf("parse v2 definitions: %v", err) } for _, set := range []map[string]json.RawMessage{top, v2} { for name, raw := range set { s := &schema{} err := json.Unmarshal(raw, s) if err != nil { log.Fatalf("parse %s: %v", name, err) } prev, ok := g.defs[name] if ok { p, q := *prev, *s p.Title, q.Title = "", "" a, _ := json.Marshal(p) c, _ := json.Marshal(q) if !bytes.Equal(a, c) { log.Fatalf("definition %s differs between the top level and v2", name) } continue } g.defs[name] = s } } } func refName(ref string) string { i := strings.LastIndex(ref, "/") return ref[i+1:] } type method struct { name string params string response string } func (g *gen) variants(def string) []method { s := g.defs[def] if s == nil { log.Fatalf("no definition %s", def) } ms := []method{} for _, v := range s.OneOf { m := v.Properties["method"] if m == nil || len(m.Enum) != 1 { log.Fatalf("%s variant without a single method", def) } name := "" err := json.Unmarshal(m.Enum[0], &name) if err != nil { log.Fatalf("%s method: %v", def, err) } params := "" if p := v.Properties["params"]; p != nil && p.Ref != "" { params = refName(p.Ref) } response := "" if strings.HasSuffix(params, "Params") { r := strings.TrimSuffix(params, "Params") + "Response" if g.defs[r] != nil { response = r } } ms = append(ms, method{name: name, params: params, response: response}) } return ms } func (g *gen) generate(version string) []byte { client := []method{} for _, m := range g.variants("ClientRequest") { if slices.Contains(clientMethods, m.name) { client = append(client, m) } } if len(client) != len(clientMethods) { log.Fatalf("found %d of %d client methods", len(client), len(clientMethods)) } notifications := g.variants("ServerNotification") requests := g.variants("ServerRequest") roots := []string{} for _, set := range [][]method{client, notifications, requests} { for _, m := range set { if m.params != "" { roots = append(roots, m.params) } if m.response != "" { roots = append(roots, m.response) } } } for _, r := range roots { g.named(r) } h := bytes.Buffer{} fmt.Fprintf(&h, "// Code generated by go run ./gen from the %s app-server schema. DO NOT EDIT.\n\n", version) h.WriteString("package codex\n\nimport \"encoding/json\"\n\n") fmt.Fprintf(&h, "const SchemaVersion = %q\n\n", version) h.WriteString("const (\n") seen := map[string]bool{} for _, set := range [][]method{client, notifications, requests} { for _, m := range set { c := "Method" + pascal(m.name) if seen[c] { continue } seen[c] = true fmt.Fprintf(&h, "%s = %q\n", c, m.name) } } h.WriteString(")\n\n") writeTable := func(name string, ms []method, field func(method) string) { fmt.Fprintf(&h, "var %s = map[string]func() any{\n", name) for _, m := range ms { t := field(m) if t == "" { fmt.Fprintf(&h, "Method%s: nil,\n", pascal(m.name)) continue } fmt.Fprintf(&h, "Method%s: func() any { return &%s{} },\n", pascal(m.name), t) } h.WriteString("}\n\n") } writeTable("clientResults", client, func(m method) string { return m.response }) writeTable("notificationParams", notifications, func(m method) string { return m.params }) writeTable("serverRequestParams", requests, func(m method) string { return m.params }) writeTable("serverRequestResponses", requests, func(m method) string { return m.response }) h.Write(g.consts.Bytes()) h.Write(g.out.Bytes()) return h.Bytes() } func (g *gen) named(name string) string { s := g.defs[name] if s == nil { log.Fatalf("no definition %s", name) } if g.emitted[name] { return name } g.emitted[name] = true g.kinds[name] = g.kindOf(s) g.emitType(name, s) return name } func (g *gen) resolve(s *schema) *schema { for s.Ref != "" { s = g.defs[refName(s.Ref)] } return s } func nonNull(list []*schema) []*schema { out := []*schema{} for _, v := range list { if !v.isNull() { out = append(out, v) } } return out } func (g *gen) kindOf(s *schema) string { if s.any { return "raw" } if s.Ref != "" { return g.kindOf(g.resolve(s)) } if len(s.AllOf) == 1 { return g.kindOf(s.AllOf[0]) } alts := nonNull(append(append([]*schema{}, s.OneOf...), s.AnyOf...)) if len(s.Properties) == 0 && len(alts) == 1 { return g.kindOf(alts[0]) } if len(s.Properties) == 0 && len(alts) > 1 { kinds := map[string]bool{} for _, a := range alts { kinds[g.kindOf(a)] = true } if len(kinds) == 1 && (kinds["string"] || kinds["struct"]) { for k := range kinds { return k } } return "raw" } if len(s.Properties) > 0 { return "struct" } ts := []string{} for _, t := range s.types() { if t != "null" { ts = append(ts, t) } } if len(ts) != 1 { return "raw" } switch ts[0] { case "string": return "string" case "integer", "number", "boolean": return "scalar" case "array": return "slice" case "object": return "map" } return "raw" } func (g *gen) emitType(name string, s *schema) { switch g.kindOf(s) { case "struct": fields := g.collectFields(s) g.emitStruct(name, fields) g.emitDiscriminators(name, s) case "string": fmt.Fprintf(&g.out, "type %s string\n\n", name) values := g.enumValues(s) if len(values) > 0 { g.consts.WriteString("const (\n") for _, v := range values { fmt.Fprintf(&g.consts, "%s%s %s = %q\n", name, pascal(v), name, v) } g.consts.WriteString(")\n\n") } default: fmt.Fprintf(&g.out, "type %s = %s\n\n", name, g.goType(s, name)) } } func (g *gen) enumValues(s *schema) []string { if s.Ref != "" { return g.enumValues(g.resolve(s)) } out := []string{} for _, e := range s.Enum { v := "" if json.Unmarshal(e, &v) == nil { out = append(out, v) } } for _, a := range append(append(append([]*schema{}, s.OneOf...), s.AnyOf...), s.AllOf...) { for _, v := range g.enumValues(a) { if !slices.Contains(out, v) { out = append(out, v) } } } return out } type field struct { json string schemas []*schema required int seen int } type objectAlt struct { props map[string]*schema required []string } func (g *gen) objectAlts(s *schema) []objectAlt { if s.Ref != "" { return g.objectAlts(g.resolve(s)) } if len(s.AllOf) == 1 && len(s.Properties) == 0 { return g.objectAlts(s.AllOf[0]) } alts := nonNull(append(append([]*schema{}, s.OneOf...), s.AnyOf...)) base := objectAlt{props: s.Properties, required: s.Required} if len(alts) == 0 { return []objectAlt{base} } out := []objectAlt{} for _, a := range alts { for _, sub := range g.objectAlts(a) { props := map[string]*schema{} for k, v := range base.props { props[k] = v } for k, v := range sub.props { props[k] = v } out = append(out, objectAlt{props: props, required: append(append([]string{}, base.required...), sub.required...)}) } } return out } func (g *gen) collectFields(s *schema) []*field { alts := g.objectAlts(s) byName := map[string]*field{} names := []string{} for _, a := range alts { for k, v := range a.props { f := byName[k] if f == nil { f = &field{json: k} byName[k] = f names = append(names, k) } f.schemas = append(f.schemas, v) f.seen++ if slices.Contains(a.required, k) { f.required++ } } } slices.Sort(names) out := []*field{} for _, n := range names { f := byName[n] if f.seen < len(alts) { f.required = 0 } out = append(out, f) } return out } func (g *gen) emitStruct(name string, fields []*field) { body := bytes.Buffer{} used := map[string]bool{} for _, f := range fields { goName := fieldName(f.json) for used[goName] { goName += "_" } used[goName] = true types := map[string]bool{} nullable := false var first *schema for _, fs := range f.schemas { t, n := g.fieldType(fs, name+goName) types[t] = true nullable = nullable || n if first == nil { first = fs } } t := "" if len(types) == 1 { for k := range types { t = k } } else { t = "json.RawMessage" } required := f.required > 0 && f.required == f.seen && !nullable if !required && g.kindOf(first) == "struct" && len(types) == 1 && !strings.HasPrefix(t, "*") { t = "*" + t } tag := f.json if !required { tag += ",omitempty" } fmt.Fprintf(&body, "%s %s `json:%q`\n", goName, t, tag) } fmt.Fprintf(&g.out, "type %s struct {\n%s}\n\n", name, body.String()) } func (g *gen) emitDiscriminators(name string, s *schema) { r := g.resolve(s) alts := nonNull(append(append([]*schema{}, r.OneOf...), r.AnyOf...)) if len(alts) < 2 { return } values := []string{} for _, a := range alts { a = g.resolve(a) t := a.Properties["type"] if t == nil || len(t.Enum) != 1 { return } v := "" if json.Unmarshal(t.Enum[0], &v) != nil { return } values = append(values, v) } g.consts.WriteString("const (\n") for _, v := range values { fmt.Fprintf(&g.consts, "%sType%s = %q\n", name, pascal(v), v) } g.consts.WriteString(")\n\n") } func (g *gen) fieldType(s *schema, ctx string) (string, bool) { if s.any { return "json.RawMessage", false } if s.Ref != "" { return g.named(refName(s.Ref)), false } if len(s.AllOf) == 1 && len(s.Properties) == 0 { return g.fieldType(s.AllOf[0], ctx) } all := append(append([]*schema{}, s.OneOf...), s.AnyOf...) alts := nonNull(all) nullable := len(alts) < len(all) if len(s.Properties) == 0 && len(alts) == 1 { t, n := g.fieldType(alts[0], ctx) return t, n || nullable } ts := s.types() if slices.Contains(ts, "null") { nullable = true } return g.goType(s, ctx), nullable } func (g *gen) goType(s *schema, ctx string) string { switch g.kindOf(s) { case "raw": return "json.RawMessage" case "struct": if s.Ref != "" { return g.named(refName(s.Ref)) } if !g.emitted[ctx] { g.emitted[ctx] = true g.emitStruct(ctx, g.collectFields(s)) g.emitDiscriminators(ctx, s) } return ctx case "string": if s.Ref != "" { return g.named(refName(s.Ref)) } alts := nonNull(append(append([]*schema{}, s.OneOf...), s.AnyOf...)) if len(alts) == 1 { return g.goType(alts[0], ctx) } return "string" case "scalar": if s.Ref != "" { return g.named(refName(s.Ref)) } for _, t := range s.types() { switch t { case "integer": return "int64" case "number": return "float64" case "boolean": return "bool" } } case "slice": if s.Ref != "" { return g.named(refName(s.Ref)) } if s.Items == nil { return "[]json.RawMessage" } t, _ := g.fieldType(s.Items, ctx+"Item") return "[]" + t case "map": if s.Ref != "" { return g.named(refName(s.Ref)) } if s.AdditionalProperties == nil || s.AdditionalProperties.any { return "map[string]any" } t, _ := g.fieldType(s.AdditionalProperties, ctx+"Value") return "map[string]" + t } if s.Ref != "" { return g.named(refName(s.Ref)) } alts := nonNull(append(append([]*schema{}, s.OneOf...), s.AnyOf...)) if len(alts) == 1 { return g.goType(alts[0], ctx) } return "json.RawMessage" } func pascal(s string) string { b := strings.Builder{} up := true for _, r := range s { if !unicode.IsLetter(r) && !unicode.IsDigit(r) { up = true continue } if up { b.WriteRune(unicode.ToUpper(r)) up = false continue } b.WriteRune(r) } out := b.String() if out == "" || unicode.IsDigit(rune(out[0])) { out = "X" + out } return out } func fieldName(s string) string { p := []rune(pascal(s)) out := []rune{} for i := 0; i < len(p); i++ { end := func(n int) bool { return i+n == len(p) || unicode.IsUpper(p[i+n]) || unicode.IsDigit(p[i+n]) } if i+1 < len(p) && p[i] == 'I' && p[i+1] == 'd' && end(2) { out = append(out, 'I', 'D') i++ continue } if i+2 < len(p) && p[i] == 'I' && p[i+1] == 'd' && p[i+2] == 's' && end(3) { out = append(out, 'I', 'D', 's') i += 2 continue } if i+2 < len(p) && p[i] == 'U' && p[i+1] == 'r' && p[i+2] == 'l' && end(3) { out = append(out, 'U', 'R', 'L') i += 2 continue } out = append(out, p[i]) } return string(out) }