Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
972 lines
28 KiB
Go
972 lines
28 KiB
Go
//revive:disable:file-length-limit
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"go/format"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/atombender/go-jsonschema/pkg/generator"
|
|
)
|
|
|
|
const (
|
|
schemaBundleFile = "codex_app_server_protocol.schemas.json"
|
|
generatedHeaderBase = "// Code generated by runtime/codexproto/internal/codegen. DO NOT EDIT.\n"
|
|
codexSourceCommit = "6d2168f06ae275d5e1f73cabf935d2bcc8549998"
|
|
codexSourceVersion = "0.0.0"
|
|
)
|
|
|
|
func main() {
|
|
packageRoot, err := repoRoot()
|
|
if err != nil {
|
|
fatal(err)
|
|
}
|
|
|
|
schemaDir := filepath.Join(packageRoot, "schema", "json")
|
|
if err := generateProtocolTypes(schemaDir, packageRoot, codexSourceCommit, codexSourceVersion); err != nil {
|
|
fatal(err)
|
|
}
|
|
|
|
if err := generateRPCStubs(schemaDir, packageRoot, codexSourceCommit); err != nil {
|
|
fatal(err)
|
|
}
|
|
}
|
|
|
|
func generateProtocolTypes(schemaDir, repoRoot, codexCommit, codexVersion string) error {
|
|
cfg := generator.Config{
|
|
DefaultPackageName: "codexproto",
|
|
DefaultOutputName: "types_gen.go",
|
|
Tags: []string{"json"},
|
|
Capitalizations: []string{"ID", "JSON", "RPC", "MCP", "HTTP", "URL", "SSE", "UUID"},
|
|
OnlyModels: true,
|
|
}
|
|
gen, err := generator.New(cfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
schemaFiles, err := findSchemaFiles(schemaDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
skip := skipSchemaFiles()
|
|
var fallbacks []string
|
|
var titles []string
|
|
for _, file := range schemaFiles {
|
|
if filepath.Base(file) == schemaBundleFile {
|
|
continue
|
|
}
|
|
if skip[filepath.Base(file)] {
|
|
continue
|
|
}
|
|
title, titleErr := schemaTitle(file)
|
|
if titleErr != nil {
|
|
return titleErr
|
|
}
|
|
if title != "" {
|
|
titles = append(titles, title)
|
|
}
|
|
if err := safeDoFile(gen, file); err != nil {
|
|
if title == "" {
|
|
return fmt.Errorf("generate types from %s: %w", file, err)
|
|
}
|
|
fallbacks = append(fallbacks, title)
|
|
}
|
|
}
|
|
|
|
sources, err := gen.Sources()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
manualTypes := manualProtocolTypes()
|
|
for name, src := range sources {
|
|
sources[name] = filterManualTypeDeclarations(src, manualTypes)
|
|
}
|
|
|
|
outDir := repoRoot
|
|
|
|
generated := collectGeneratedTypes(sources)
|
|
for name := range manualTypes {
|
|
generated[name] = struct{}{}
|
|
}
|
|
for _, title := range titles {
|
|
if title == "" {
|
|
continue
|
|
}
|
|
if _, ok := generated[title]; ok {
|
|
continue
|
|
}
|
|
if _, ok := generated[title+"JSON"]; ok {
|
|
continue
|
|
}
|
|
fallbacks = append(fallbacks, title)
|
|
}
|
|
|
|
for name, src := range sources {
|
|
normalized := normalizeGeneratedHeader(src, codexCommit)
|
|
if err := os.WriteFile(filepath.Join(outDir, name), normalized, 0o644); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := writeFallbackTypes(outDir, fallbacks, generated, codexCommit); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := writeProtocolAliases(outDir, generated, fallbacks, codexCommit); err != nil {
|
|
return err
|
|
}
|
|
|
|
return writeProtocolMetadata(outDir, codexCommit, codexVersion)
|
|
}
|
|
|
|
func filterManualTypeDeclarations(src []byte, manualTypes map[string]struct{}) []byte {
|
|
lines := strings.SplitAfter(string(src), "\n")
|
|
out := make([]string, 0, len(lines))
|
|
for i := 0; i < len(lines); {
|
|
line := lines[i]
|
|
trimmed := strings.TrimSpace(line)
|
|
if strings.HasPrefix(trimmed, "type ") {
|
|
fields := strings.Fields(trimmed)
|
|
if len(fields) >= 2 {
|
|
if _, ok := manualTypes[fields[1]]; ok {
|
|
i++
|
|
balance := strings.Count(line, "{") - strings.Count(line, "}")
|
|
for i < len(lines) && balance > 0 {
|
|
balance += strings.Count(lines[i], "{") - strings.Count(lines[i], "}")
|
|
i++
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
out = append(out, line)
|
|
i++
|
|
}
|
|
return []byte(strings.Join(out, ""))
|
|
}
|
|
|
|
type schemaVariant struct {
|
|
Properties map[string]schemaProperty `json:"properties"`
|
|
}
|
|
|
|
type schemaProperty struct {
|
|
Enum []string `json:"enum"`
|
|
Ref string `json:"$ref"`
|
|
Type string `json:"type"`
|
|
Items *struct {
|
|
Ref string `json:"$ref"`
|
|
} `json:"items"`
|
|
}
|
|
|
|
type oneOfSchema struct {
|
|
OneOf []schemaVariant `json:"oneOf"`
|
|
}
|
|
|
|
func generateRPCStubs(schemaDir, repoRoot, codexCommit string) error {
|
|
defs, err := parseDefinitionNames(schemaDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
clientMethods, err := loadMethods(filepath.Join(schemaDir, "ClientRequest.json"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
clientMethods = append(clientMethods, manualClientMethods()...)
|
|
sort.Slice(clientMethods, func(i, j int) bool { return clientMethods[i].Method < clientMethods[j].Method })
|
|
|
|
serverMethods, err := loadMethods(filepath.Join(schemaDir, "ServerRequest.json"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
notifications, err := loadNotifications(filepath.Join(schemaDir, "ServerNotification.json"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
clientRPC, err := buildRPCMethods(clientMethods, defs, clientResponseOverrides())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
serverRPC, err := buildRPCMethods(serverMethods, defs, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
outDir := repoRoot
|
|
|
|
if err := writeFile(outDir, "client_requests_gen.go", renderClientRequests(clientRPC, codexCommit)); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := writeFile(outDir, "server_requests_gen.go", renderServerRequests(serverRPC, codexCommit)); err != nil {
|
|
return err
|
|
}
|
|
|
|
return writeFile(outDir, "notifications_gen.go", renderNotifications(notifications, codexCommit))
|
|
}
|
|
|
|
func parseDefinitionNames(schemaDir string) (map[string]struct{}, error) {
|
|
files, err := findSchemaFiles(schemaDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defs := make(map[string]struct{})
|
|
for _, file := range files {
|
|
data, err := os.ReadFile(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var doc struct {
|
|
Title string `json:"title"`
|
|
Definitions map[string]json.RawMessage `json:"definitions"`
|
|
}
|
|
if err := json.Unmarshal(data, &doc); err != nil {
|
|
return nil, err
|
|
}
|
|
if doc.Title != "" {
|
|
defs[doc.Title] = struct{}{}
|
|
}
|
|
for name := range doc.Definitions {
|
|
defs[name] = struct{}{}
|
|
}
|
|
}
|
|
return defs, nil
|
|
}
|
|
|
|
func schemaTitle(path string) (string, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
var doc struct {
|
|
Title string `json:"title"`
|
|
}
|
|
if err := json.Unmarshal(data, &doc); err != nil {
|
|
return "", err
|
|
}
|
|
return doc.Title, nil
|
|
}
|
|
|
|
func writeFallbackTypes(outDir string, titles []string, generated map[string]struct{}, codexCommit string) error {
|
|
fallbackPath := filepath.Join(outDir, "fallback_gen.go")
|
|
if len(titles) == 0 {
|
|
_ = os.Remove(fallbackPath)
|
|
return nil
|
|
}
|
|
|
|
unique := map[string]struct{}{}
|
|
for _, title := range titles {
|
|
if title != "" {
|
|
unique[title] = struct{}{}
|
|
}
|
|
}
|
|
|
|
aliases := map[string]string{}
|
|
var names []string
|
|
for name := range unique {
|
|
if _, exists := generated[name]; exists {
|
|
continue
|
|
}
|
|
if target, ok := sanitizedFallbackTarget(name, generated); ok {
|
|
aliases[name] = target
|
|
continue
|
|
}
|
|
names = append(names, name)
|
|
}
|
|
if len(names) == 0 && len(aliases) == 0 {
|
|
_ = os.Remove(fallbackPath)
|
|
return nil
|
|
}
|
|
sort.Strings(names)
|
|
aliasNames := make([]string, 0, len(aliases))
|
|
for name := range aliases {
|
|
aliasNames = append(aliasNames, name)
|
|
}
|
|
sort.Strings(aliasNames)
|
|
|
|
var b strings.Builder
|
|
b.WriteString(generatedHeader(codexCommit))
|
|
b.WriteString("package codexproto\n\n")
|
|
b.WriteString("// Fallback types for schemas that failed to generate.\n")
|
|
for _, name := range aliasNames {
|
|
b.WriteString("type ")
|
|
b.WriteString(name)
|
|
b.WriteString(" = ")
|
|
b.WriteString(aliases[name])
|
|
b.WriteString("\n")
|
|
}
|
|
for _, name := range names {
|
|
b.WriteString("type ")
|
|
b.WriteString(name)
|
|
b.WriteString(" interface{}\n")
|
|
}
|
|
|
|
return os.WriteFile(fallbackPath, []byte(b.String()), 0o644)
|
|
}
|
|
|
|
func sanitizedFallbackTarget(name string, generated map[string]struct{}) (string, bool) {
|
|
target := "Sanitized" + name + "JSON"
|
|
if _, ok := generated[target]; ok {
|
|
return target, true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func collectGeneratedTypes(sources map[string][]byte) map[string]struct{} {
|
|
generated := map[string]struct{}{}
|
|
for _, src := range sources {
|
|
lines := strings.Split(string(src), "\n")
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
if strings.HasPrefix(line, "type ") {
|
|
fields := strings.Fields(line)
|
|
if len(fields) >= 2 {
|
|
generated[fields[1]] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return generated
|
|
}
|
|
|
|
func writeProtocolAliases(outDir string, generated map[string]struct{}, fallbacks []string, codexCommit string) error {
|
|
existing := map[string]struct{}{}
|
|
for name := range generated {
|
|
existing[name] = struct{}{}
|
|
}
|
|
for _, name := range fallbacks {
|
|
if name != "" {
|
|
existing[name] = struct{}{}
|
|
}
|
|
}
|
|
|
|
aliases := map[string]string{}
|
|
for name := range generated {
|
|
if !strings.HasSuffix(name, "JSON") {
|
|
continue
|
|
}
|
|
base := strings.TrimSuffix(name, "JSON")
|
|
if base == "" {
|
|
continue
|
|
}
|
|
if _, ok := existing[base]; ok {
|
|
continue
|
|
}
|
|
aliases[base] = name
|
|
}
|
|
|
|
aliasPath := filepath.Join(outDir, "aliases_gen.go")
|
|
if len(aliases) == 0 {
|
|
_ = os.Remove(aliasPath)
|
|
return nil
|
|
}
|
|
|
|
names := make([]string, 0, len(aliases))
|
|
for name := range aliases {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
|
|
var b strings.Builder
|
|
b.WriteString(generatedHeader(codexCommit))
|
|
b.WriteString("package codexproto\n\n")
|
|
b.WriteString("// Aliases for JSON-suffixed schema types.\n")
|
|
for _, name := range names {
|
|
b.WriteString("type ")
|
|
b.WriteString(name)
|
|
b.WriteString(" = ")
|
|
b.WriteString(aliases[name])
|
|
b.WriteString("\n")
|
|
}
|
|
|
|
return os.WriteFile(aliasPath, []byte(b.String()), 0o644)
|
|
}
|
|
|
|
func skipSchemaFiles() map[string]bool {
|
|
return map[string]bool{
|
|
"JSONRPCMessage.json": true,
|
|
"JSONRPCRequest.json": true,
|
|
"JSONRPCResponse.json": true,
|
|
"JSONRPCNotification.json": true,
|
|
"JSONRPCError.json": true,
|
|
"JSONRPCErrorError.json": true,
|
|
"EventMsg.json": true,
|
|
"RequestId.json": true,
|
|
}
|
|
}
|
|
|
|
func loadMethods(path string) ([]rpcMethod, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var doc oneOfSchema
|
|
if err := json.Unmarshal(data, &doc); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
methods := make([]rpcMethod, 0, len(doc.OneOf))
|
|
for _, variant := range doc.OneOf {
|
|
method := ""
|
|
if prop, ok := variant.Properties["method"]; ok && len(prop.Enum) == 1 {
|
|
method = prop.Enum[0]
|
|
}
|
|
if method == "" {
|
|
continue
|
|
}
|
|
|
|
paramsType := ""
|
|
if prop, ok := variant.Properties["params"]; ok {
|
|
if prop.Ref != "" {
|
|
paramsType = refName(prop.Ref)
|
|
} else if prop.Type == "null" {
|
|
paramsType = ""
|
|
}
|
|
}
|
|
|
|
methods = append(methods, rpcMethod{
|
|
Method: method,
|
|
ParamsType: paramsType,
|
|
})
|
|
}
|
|
|
|
sort.Slice(methods, func(i, j int) bool { return methods[i].Method < methods[j].Method })
|
|
|
|
return methods, nil
|
|
}
|
|
|
|
func loadNotifications(path string) ([]rpcNotification, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var doc oneOfSchema
|
|
if err := json.Unmarshal(data, &doc); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
notifications := make([]rpcNotification, 0, len(doc.OneOf))
|
|
for _, variant := range doc.OneOf {
|
|
method := ""
|
|
if prop, ok := variant.Properties["method"]; ok && len(prop.Enum) == 1 {
|
|
method = prop.Enum[0]
|
|
}
|
|
if method == "" {
|
|
continue
|
|
}
|
|
|
|
paramsType := ""
|
|
if prop, ok := variant.Properties["params"]; ok {
|
|
if prop.Ref != "" {
|
|
paramsType = refName(prop.Ref)
|
|
} else if prop.Type == "null" {
|
|
paramsType = ""
|
|
}
|
|
}
|
|
|
|
notifications = append(notifications, rpcNotification{
|
|
Method: method,
|
|
ParamsType: paramsType,
|
|
})
|
|
}
|
|
|
|
sort.Slice(notifications, func(i, j int) bool { return notifications[i].Method < notifications[j].Method })
|
|
|
|
return notifications, nil
|
|
}
|
|
|
|
func refName(ref string) string {
|
|
const prefix = "#/definitions/"
|
|
if strings.HasPrefix(ref, prefix) {
|
|
return strings.TrimPrefix(ref, prefix)
|
|
}
|
|
return ref
|
|
}
|
|
|
|
type rpcMethod struct {
|
|
Method string
|
|
ParamsType string
|
|
ResponseType string
|
|
}
|
|
|
|
type rpcNotification struct {
|
|
Method string
|
|
ParamsType string
|
|
}
|
|
|
|
func buildRPCMethods(methods []rpcMethod, defs map[string]struct{}, overrides map[string]string) ([]rpcMethod, error) {
|
|
resolved := make([]rpcMethod, 0, len(methods))
|
|
for _, method := range methods {
|
|
if method.ResponseType != "" {
|
|
resolved = append(resolved, method)
|
|
continue
|
|
}
|
|
responseType, err := resolveResponseType(method, defs, overrides)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
method.ResponseType = responseType
|
|
resolved = append(resolved, method)
|
|
}
|
|
return resolved, nil
|
|
}
|
|
|
|
func resolveResponseType(method rpcMethod, defs map[string]struct{}, overrides map[string]string) (string, error) {
|
|
if overrides != nil {
|
|
if override, ok := overrides[method.Method]; ok {
|
|
return override, nil
|
|
}
|
|
}
|
|
|
|
if method.ParamsType != "" {
|
|
base := strings.TrimSuffix(method.ParamsType, "Params")
|
|
candidate := base + "Response"
|
|
if _, ok := defs[candidate]; ok {
|
|
return candidate, nil
|
|
}
|
|
}
|
|
|
|
candidate := methodBaseName(method.Method) + "Response"
|
|
if _, ok := defs[candidate]; ok {
|
|
return candidate, nil
|
|
}
|
|
|
|
return "", fmt.Errorf("unable to resolve response type for method %q", method.Method)
|
|
}
|
|
|
|
func methodBaseName(method string) string {
|
|
parts := strings.FieldsFunc(method, func(r rune) bool {
|
|
return (r < 'a' || r > 'z') && (r < 'A' || r > 'Z') && (r < '0' || r > '9')
|
|
})
|
|
var b strings.Builder
|
|
for _, part := range parts {
|
|
if part == "" {
|
|
continue
|
|
}
|
|
b.WriteString(strings.ToUpper(part[:1]))
|
|
if len(part) > 1 {
|
|
b.WriteString(part[1:])
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func clientResponseOverrides() map[string]string {
|
|
return map[string]string{
|
|
"account/logout": "LogoutAccountResponse",
|
|
"account/rateLimits/read": "GetAccountRateLimitsResponse",
|
|
"account/usage/read": "GetAccountTokenUsageResponse",
|
|
"account/workspaceMessages/read": "GetWorkspaceMessagesResponse",
|
|
"addConversationListener": "AddConversationSubscriptionResponse",
|
|
"config/value/write": "ConfigWriteResponse",
|
|
"config/batchWrite": "ConfigWriteResponse",
|
|
"config/mcpServer/reload": "McpServerRefreshResponse",
|
|
"externalAgentConfig/import/readHistories": "ExternalAgentConfigImportHistoriesReadResponse",
|
|
"removeConversationListener": "RemoveConversationSubscriptionResponse",
|
|
}
|
|
}
|
|
|
|
func manualClientMethods() []rpcMethod {
|
|
return []rpcMethod{
|
|
{
|
|
Method: "collaborationMode/list",
|
|
ParamsType: "CollaborationModeListParams",
|
|
ResponseType: "CollaborationModeListResponse",
|
|
},
|
|
}
|
|
}
|
|
|
|
func renderClientRequests(methods []rpcMethod, codexCommit string) []byte {
|
|
var b strings.Builder
|
|
b.WriteString(generatedHeader(codexCommit))
|
|
b.WriteString("package codexproto\n\n")
|
|
b.WriteString("import (\n\t\"context\"\n\t\"encoding/json\"\n)\n\n")
|
|
|
|
b.WriteString("// ClientRequests exposes typed JSON-RPC calls supported by the app-server.\n")
|
|
b.WriteString("type ClientRequests interface {\n")
|
|
for _, method := range methods {
|
|
if method.ParamsType == "" {
|
|
fmt.Fprintf(&b, "\t%s(ctx context.Context) (*%s, error)\n", methodName(method.Method), method.ResponseType)
|
|
} else {
|
|
fmt.Fprintf(&b, "\t%s(ctx context.Context, params %s) (*%s, error)\n", methodName(method.Method), paramsType(method.ParamsType), method.ResponseType)
|
|
}
|
|
}
|
|
b.WriteString("}\n\n")
|
|
|
|
b.WriteString("// Client implements ClientRequests using JSON-RPC.\n")
|
|
for _, method := range methods {
|
|
if method.ParamsType == "" {
|
|
fmt.Fprintf(&b, "func (c *Client) %s(ctx context.Context) (*%s, error) {\n", methodName(method.Method), method.ResponseType)
|
|
} else {
|
|
fmt.Fprintf(&b, "func (c *Client) %s(ctx context.Context, params %s) (*%s, error) {\n", methodName(method.Method), paramsType(method.ParamsType), method.ResponseType)
|
|
}
|
|
b.WriteString("\tvar result ")
|
|
b.WriteString(method.ResponseType)
|
|
b.WriteString("\n")
|
|
if method.ParamsType == "" {
|
|
fmt.Fprintf(&b, "\tif err := c.Call(ctx, %q, nil, &result); err != nil {\n", method.Method)
|
|
} else {
|
|
fmt.Fprintf(&b, "\tif err := c.Call(ctx, %q, params, &result); err != nil {\n", method.Method)
|
|
}
|
|
b.WriteString("\t\treturn nil, err\n\t}\n\treturn &result, nil\n}\n\n")
|
|
}
|
|
|
|
b.WriteString("// BuildClientRequest builds a JSON-RPC request for a client method.\n")
|
|
b.WriteString("func BuildClientRequest(method string, params any, id RequestID) (JSONRPCRequest, error) {\n")
|
|
b.WriteString("\tvar raw json.RawMessage\n\tif params != nil {\n\t\tdata, err := json.Marshal(params)\n\t\tif err != nil {\n\t\t\treturn JSONRPCRequest{}, err\n\t\t}\n\t\traw = data\n\t}\n\treturn JSONRPCRequest{ID: id, Method: method, Params: raw}, nil\n}\n")
|
|
|
|
return []byte(b.String())
|
|
}
|
|
|
|
func renderServerRequests(methods []rpcMethod, codexCommit string) []byte {
|
|
var b strings.Builder
|
|
b.WriteString(generatedHeader(codexCommit))
|
|
b.WriteString("package codexproto\n\n")
|
|
b.WriteString("import (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n)\n\n")
|
|
|
|
b.WriteString("// ServerRequestHandler handles requests initiated by the app-server.\n")
|
|
b.WriteString("type ServerRequestHandler interface {\n")
|
|
for _, method := range methods {
|
|
if method.ParamsType == "" {
|
|
fmt.Fprintf(&b, "\t%s(ctx context.Context) (*%s, error)\n", methodName(method.Method), method.ResponseType)
|
|
} else {
|
|
fmt.Fprintf(&b, "\t%s(ctx context.Context, params %s) (*%s, error)\n", methodName(method.Method), paramsType(method.ParamsType), method.ResponseType)
|
|
}
|
|
}
|
|
b.WriteString("}\n\n")
|
|
|
|
b.WriteString("// ParsedServerRequest represents a typed server request envelope.\n")
|
|
b.WriteString("type ParsedServerRequest struct {\n\tMethod string\n\tParams any\n\tRaw json.RawMessage\n}\n\n")
|
|
|
|
b.WriteString("type serverRequestParser func(json.RawMessage) (ParsedServerRequest, error)\n\n")
|
|
b.WriteString("var serverRequestParsers = map[string]serverRequestParser{\n")
|
|
for _, method := range methods {
|
|
fmt.Fprintf(&b, "\t%q: func(params json.RawMessage) (ParsedServerRequest, error) {\n", method.Method)
|
|
if method.ParamsType == "" {
|
|
fmt.Fprintf(&b, "\t\treturn ParsedServerRequest{Method: %q, Params: nil, Raw: params}, nil\n", method.Method)
|
|
} else {
|
|
b.WriteString("\t\tvar payload ")
|
|
b.WriteString(method.ParamsType)
|
|
b.WriteString("\n")
|
|
b.WriteString("\t\tif len(params) > 0 {\n\t\t\tif err := json.Unmarshal(params, &payload); err != nil {\n\t\t\t\treturn ParsedServerRequest{Method: ")
|
|
fmt.Fprintf(&b, "%q", method.Method)
|
|
b.WriteString(", Raw: params}, err\n\t\t\t}\n\t\t}\n")
|
|
b.WriteString("\t\treturn ParsedServerRequest{Method: ")
|
|
fmt.Fprintf(&b, "%q", method.Method)
|
|
b.WriteString(", Params: payload, Raw: params}, nil\n")
|
|
}
|
|
b.WriteString("\t},\n")
|
|
}
|
|
b.WriteString("}\n\n")
|
|
|
|
b.WriteString("// ParseServerRequest parses a server request by method.\n")
|
|
b.WriteString("func ParseServerRequest(method string, params json.RawMessage) (ParsedServerRequest, error) {\n")
|
|
b.WriteString("\tif parser, ok := serverRequestParsers[method]; ok {\n\t\treturn parser(params)\n\t}\n")
|
|
b.WriteString("\treturn ParsedServerRequest{Method: method, Raw: params}, nil\n}\n\n")
|
|
|
|
b.WriteString("// IsKnownServerRequestMethod reports whether method is in the generated server request surface.\n")
|
|
b.WriteString("func IsKnownServerRequestMethod(method string) bool {\n")
|
|
b.WriteString("\t_, ok := serverRequestParsers[method]\n\treturn ok\n}\n\n")
|
|
|
|
b.WriteString("// ServerRequestMethods returns the generated server request surface in stable order.\n")
|
|
b.WriteString("func ServerRequestMethods() []string {\n")
|
|
b.WriteString("\treturn []string{\n")
|
|
for _, method := range methods {
|
|
fmt.Fprintf(&b, "\t\t%q,\n", method.Method)
|
|
}
|
|
b.WriteString("\t}\n}\n\n")
|
|
|
|
b.WriteString("func dispatchServerRequest(ctx context.Context, handler ServerRequestHandler, req JSONRPCRequest) (any, error) {\n")
|
|
b.WriteString("\tswitch req.Method {\n")
|
|
for _, method := range methods {
|
|
fmt.Fprintf(&b, "\tcase %q:\n", method.Method)
|
|
if method.ParamsType == "" {
|
|
b.WriteString("\t\treturn handler.")
|
|
b.WriteString(methodName(method.Method))
|
|
b.WriteString("(ctx)\n")
|
|
} else {
|
|
b.WriteString("\t\tvar params ")
|
|
b.WriteString(method.ParamsType)
|
|
b.WriteString("\n")
|
|
b.WriteString("\t\tif len(req.Params) > 0 {\n\t\t\tif err := json.Unmarshal(req.Params, ¶ms); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n")
|
|
b.WriteString("\t\treturn handler.")
|
|
b.WriteString(methodName(method.Method))
|
|
b.WriteString("(ctx, params)\n")
|
|
}
|
|
}
|
|
b.WriteString("\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported server request %q\", req.Method)\n\t}\n}\n")
|
|
|
|
return []byte(b.String())
|
|
}
|
|
|
|
func renderNotifications(notifications []rpcNotification, codexCommit string) []byte {
|
|
var b strings.Builder
|
|
b.WriteString(generatedHeader(codexCommit))
|
|
b.WriteString("package codexproto\n\n")
|
|
b.WriteString("import \"encoding/json\"\n\n")
|
|
|
|
b.WriteString("// Notification represents a typed server notification.\n")
|
|
b.WriteString("type Notification struct {\n\tMethod string\n\tParams any\n\tRaw json.RawMessage\n}\n\n")
|
|
|
|
b.WriteString("type notificationParser func(json.RawMessage) (Notification, error)\n\n")
|
|
b.WriteString("var notificationParsers = map[string]notificationParser{\n")
|
|
for _, notification := range notifications {
|
|
fmt.Fprintf(&b, "\t%q: func(params json.RawMessage) (Notification, error) {\n", notification.Method)
|
|
if notification.ParamsType == "" {
|
|
fmt.Fprintf(&b, "\t\treturn Notification{Method: %q, Params: nil, Raw: params}, nil\n", notification.Method)
|
|
} else {
|
|
b.WriteString("\t\tvar payload ")
|
|
b.WriteString(notification.ParamsType)
|
|
b.WriteString("\n")
|
|
b.WriteString("\t\tif len(params) > 0 {\n\t\t\tif err := json.Unmarshal(params, &payload); err != nil {\n\t\t\t\treturn Notification{Method: ")
|
|
fmt.Fprintf(&b, "%q", notification.Method)
|
|
b.WriteString(", Raw: params}, err\n\t\t\t}\n\t\t}\n")
|
|
b.WriteString("\t\treturn Notification{Method: ")
|
|
fmt.Fprintf(&b, "%q", notification.Method)
|
|
b.WriteString(", Params: payload, Raw: params}, nil\n")
|
|
}
|
|
b.WriteString("\t},\n")
|
|
}
|
|
b.WriteString("}\n\n")
|
|
|
|
b.WriteString("// ParseServerNotification parses a server notification by method.\n")
|
|
b.WriteString("func ParseServerNotification(method string, params json.RawMessage) (Notification, error) {\n")
|
|
b.WriteString("\tif handler, ok := notificationParsers[method]; ok {\n\t\treturn handler(params)\n\t}\n")
|
|
b.WriteString("\treturn Notification{Method: method, Raw: params}, nil\n}\n")
|
|
|
|
b.WriteString("\n// IsKnownServerNotificationMethod reports whether method is in the generated server notification surface.\n")
|
|
b.WriteString("func IsKnownServerNotificationMethod(method string) bool {\n")
|
|
b.WriteString("\t_, ok := notificationParsers[method]\n\treturn ok\n}\n")
|
|
|
|
return []byte(b.String())
|
|
}
|
|
|
|
func paramsType(name string) string {
|
|
if name == "" {
|
|
return "struct{}"
|
|
}
|
|
return name
|
|
}
|
|
|
|
func manualProtocolTypes() map[string]struct{} {
|
|
return map[string]struct{}{
|
|
"ApplyPatchApprovalParams": {},
|
|
"ApplyPatchApprovalResponse": {},
|
|
"CommandExecutionRequestApprovalParams": {},
|
|
"CommandExecutionRequestApprovalResponse": {},
|
|
"ErrorNotification": {},
|
|
"ExecCommandApprovalParams": {},
|
|
"ExecCommandApprovalResponse": {},
|
|
"FileChangeRequestApprovalParams": {},
|
|
"FileChangeRequestApprovalResponse": {},
|
|
"ItemCompletedNotification": {},
|
|
"PermissionsRequestApprovalParams": {},
|
|
"PermissionsRequestApprovalResponse": {},
|
|
"ReviewStartParams": {},
|
|
"ThreadResumeResponse": {},
|
|
"ThreadGoal": {},
|
|
"ThreadGoalUpdatedNotification": {},
|
|
"ThreadForkParams": {},
|
|
"ThreadForkResponse": {},
|
|
"ThreadResumeParams": {},
|
|
"ThreadStartParams": {},
|
|
"ThreadStartResponse": {},
|
|
"ToolRequestUserInputParams": {},
|
|
"ToolRequestUserInputResponse": {},
|
|
"TurnStartParams": {},
|
|
"TurnStartParamsInputElem": {},
|
|
"TurnSteerParamsInputElem": {},
|
|
"TurnCompletedNotification": {},
|
|
"TurnStartedNotification": {},
|
|
}
|
|
}
|
|
|
|
func methodName(method string) string {
|
|
base := methodBaseName(method)
|
|
if base == "" {
|
|
return "Unknown"
|
|
}
|
|
return base
|
|
}
|
|
|
|
func normalizeGeneratedHeader(src []byte, codexCommit string) []byte {
|
|
if len(src) == 0 {
|
|
return src
|
|
}
|
|
parts := strings.SplitN(string(src), "\n", 2)
|
|
if len(parts) == 0 {
|
|
return src
|
|
}
|
|
header := strings.TrimSpace(parts[0])
|
|
headerLower := strings.ToLower(header)
|
|
if !strings.HasPrefix(headerLower, "// code generated") && !strings.HasPrefix(headerLower, "// generated") {
|
|
return src
|
|
}
|
|
rest := ""
|
|
if len(parts) > 1 {
|
|
rest = strings.TrimLeft(parts[1], "\n")
|
|
}
|
|
for strings.Contains(rest, "\n\n\n") {
|
|
rest = strings.ReplaceAll(rest, "\n\n\n", "\n\n")
|
|
}
|
|
return []byte(generatedHeader(codexCommit) + rest)
|
|
}
|
|
|
|
func generatedHeader(codexCommit string) string {
|
|
return generatedHeaderBase + "// Source codex commit: " + codexCommit + "\n\n"
|
|
}
|
|
|
|
func writeProtocolMetadata(outDir, codexCommit, codexVersion string) error {
|
|
var b strings.Builder
|
|
b.WriteString(generatedHeader(codexCommit))
|
|
b.WriteString("package codexproto\n\n")
|
|
b.WriteString("// GeneratedCodexCommit is the Codex source commit used to generate this protocol package.\n")
|
|
fmt.Fprintf(&b, "const GeneratedCodexCommit = %q\n\n", codexCommit)
|
|
b.WriteString("// GeneratedCodexVersion is the Codex package version used to generate this protocol package.\n")
|
|
fmt.Fprintf(&b, "const GeneratedCodexVersion = %q\n", codexVersion)
|
|
return writeFile(outDir, "metadata_gen.go", []byte(b.String()))
|
|
}
|
|
|
|
func writeFile(dir, name string, contents []byte) error {
|
|
path := filepath.Join(dir, name)
|
|
if filepath.Ext(name) == ".go" {
|
|
formatted, err := format.Source(contents)
|
|
if err != nil {
|
|
return fmt.Errorf("format %s: %w", name, err)
|
|
}
|
|
contents = formatted
|
|
}
|
|
return os.WriteFile(path, contents, 0o644)
|
|
}
|
|
|
|
func findSchemaFiles(schemaDir string) ([]string, error) {
|
|
var files []string
|
|
err := filepath.WalkDir(schemaDir, func(path string, d os.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
if filepath.Ext(path) != ".json" {
|
|
return nil
|
|
}
|
|
files = append(files, path)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sort.Strings(files)
|
|
return files, nil
|
|
}
|
|
|
|
func safeDoFile(gen *generator.Generator, file string) (err error) {
|
|
sanitized, cleanup, err := sanitizeSchemaFile(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cleanup()
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
err = fmt.Errorf("panic: %v", r)
|
|
}
|
|
}()
|
|
return gen.DoFile(sanitized)
|
|
}
|
|
|
|
func sanitizeSchemaFile(path string) (string, func(), error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
var node any
|
|
if err := json.Unmarshal(data, &node); err != nil {
|
|
return "", nil, err
|
|
}
|
|
stripSubschemas(node)
|
|
out, err := json.Marshal(node)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
tempDir, err := os.MkdirTemp("", "tutti-codexproto-sanitized-*")
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
sanitizedName := "sanitized-" + filepath.Base(path)
|
|
sanitizedPath := filepath.Join(tempDir, sanitizedName)
|
|
if err := os.WriteFile(sanitizedPath, out, 0o644); err != nil {
|
|
_ = os.RemoveAll(tempDir)
|
|
return "", nil, err
|
|
}
|
|
cleanup := func() {
|
|
_ = os.RemoveAll(tempDir)
|
|
}
|
|
return sanitizedPath, cleanup, nil
|
|
}
|
|
|
|
func stripSubschemas(node any) {
|
|
switch value := node.(type) {
|
|
case map[string]any:
|
|
delete(value, "oneOf")
|
|
delete(value, "anyOf")
|
|
for _, child := range value {
|
|
stripSubschemas(child)
|
|
}
|
|
case []any:
|
|
for _, child := range value {
|
|
stripSubschemas(child)
|
|
}
|
|
}
|
|
}
|
|
|
|
func repoRoot() (string, error) {
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if packageRoot := filepath.Join(cwd, "runtime", "codexproto"); isPackageRoot(packageRoot) {
|
|
return packageRoot, nil
|
|
}
|
|
|
|
// Walk up until we find the codexproto package root.
|
|
current := cwd
|
|
for {
|
|
if isPackageRoot(current) {
|
|
return current, nil
|
|
}
|
|
parent := filepath.Dir(current)
|
|
if parent == current {
|
|
break
|
|
}
|
|
current = parent
|
|
}
|
|
|
|
return "", fmt.Errorf("unable to locate codexproto package root from %s", cwd)
|
|
}
|
|
|
|
func isPackageRoot(path string) bool {
|
|
return exists(filepath.Join(path, "schema", "json", schemaBundleFile)) &&
|
|
exists(filepath.Join(path, "internal", "codegen", "main.go"))
|
|
}
|
|
|
|
func exists(path string) bool {
|
|
_, err := os.Stat(path)
|
|
return err == nil
|
|
}
|
|
|
|
func fatal(err error) {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|