From bf6402f9da686178948864444bf20d488c402452 Mon Sep 17 00:00:00 2001 From: akazwz <50396286+akazwz@users.noreply.github.com> Date: Sun, 30 Aug 2026 22:05:59 -0700 Subject: [PATCH] feat(agent): add direct Codex runtime --- cmd/agent/http_providers.go | 4 + cmd/agent/module.go | 1 + cmd/gen-codex-protocol/main.go | 66 + cmd/internal/core/module.go | 2 + cmd/internal/core/providers.go | 29 +- internal/agent/runtime/codex/appserver.go | 421 + internal/agent/runtime/codex/auth.go | 108 + internal/agent/runtime/codex/config.go | 57 + internal/agent/runtime/codex/conn.go | 255 + internal/agent/runtime/codex/driver.go | 722 + internal/agent/runtime/codex/elicitation.go | 360 + internal/agent/runtime/codex/lifecycle.go | 227 + .../agent/runtime/codex/lifecycle_test.go | 230 + internal/agent/runtime/codex/process.go | 33 + .../runtime/codex/protocol/methods.gen.go | 238 + .../agent/runtime/codex/protocol/protocol.go | 253 + .../runtime/codex/protocol/roundtrip_test.go | 401 + .../agent/runtime/codex/protocol/types.gen.go | 1638 ++ .../runtime/codex/protocol/unions.gen.go | 2001 ++ .../agent/runtime/codex/protocolgen/emit.go | 302 + .../runtime/codex/protocolgen/emit_methods.go | 77 + .../runtime/codex/protocolgen/emit_unions.go | 148 + .../runtime/codex/protocolgen/generate.go | 113 + .../agent/runtime/codex/protocolgen/naming.go | 86 + .../runtime/codex/protocolgen/resolve.go | 320 + .../agent/runtime/codex/protocolgen/schema.go | 282 + .../ChatgptAuthTokensRefreshResponse.json | 23 + .../schema/ClientNotification.json | 22 + ...mmandExecutionRequestApprovalResponse.json | 116 + .../FileChangeRequestApprovalResponse.json | 47 + .../McpServerElicitationRequestResponse.json | 29 + .../PermissionsRequestApprovalResponse.json | 322 + .../protocolgen/schema/ServerRequest.json | 2079 ++ .../schema/ToolRequestUserInputResponse.json | 34 + .../codex/protocolgen/schema/VERSION.json | 16 + .../codex_app_server_protocol.v2.schemas.json | 22847 ++++++++++++++++ .../agent/runtime/codex/protocolgen/subset.go | 82 + internal/agent/runtime/codex/tools.go | 155 + internal/agent/runtime/codex/turn.go | 717 + internal/agent/runtime/codex/userinput.go | 179 + internal/handlers/external_agent_codex.go | 184 + mise.toml | 8 + scripts/codex-schema-sync.sh | 52 + 43 files changed, 35284 insertions(+), 2 deletions(-) create mode 100644 cmd/gen-codex-protocol/main.go create mode 100644 internal/agent/runtime/codex/appserver.go create mode 100644 internal/agent/runtime/codex/auth.go create mode 100644 internal/agent/runtime/codex/config.go create mode 100644 internal/agent/runtime/codex/conn.go create mode 100644 internal/agent/runtime/codex/driver.go create mode 100644 internal/agent/runtime/codex/elicitation.go create mode 100644 internal/agent/runtime/codex/lifecycle.go create mode 100644 internal/agent/runtime/codex/lifecycle_test.go create mode 100644 internal/agent/runtime/codex/process.go create mode 100644 internal/agent/runtime/codex/protocol/methods.gen.go create mode 100644 internal/agent/runtime/codex/protocol/protocol.go create mode 100644 internal/agent/runtime/codex/protocol/roundtrip_test.go create mode 100644 internal/agent/runtime/codex/protocol/types.gen.go create mode 100644 internal/agent/runtime/codex/protocol/unions.gen.go create mode 100644 internal/agent/runtime/codex/protocolgen/emit.go create mode 100644 internal/agent/runtime/codex/protocolgen/emit_methods.go create mode 100644 internal/agent/runtime/codex/protocolgen/emit_unions.go create mode 100644 internal/agent/runtime/codex/protocolgen/generate.go create mode 100644 internal/agent/runtime/codex/protocolgen/naming.go create mode 100644 internal/agent/runtime/codex/protocolgen/resolve.go create mode 100644 internal/agent/runtime/codex/protocolgen/schema.go create mode 100644 internal/agent/runtime/codex/protocolgen/schema/ChatgptAuthTokensRefreshResponse.json create mode 100644 internal/agent/runtime/codex/protocolgen/schema/ClientNotification.json create mode 100644 internal/agent/runtime/codex/protocolgen/schema/CommandExecutionRequestApprovalResponse.json create mode 100644 internal/agent/runtime/codex/protocolgen/schema/FileChangeRequestApprovalResponse.json create mode 100644 internal/agent/runtime/codex/protocolgen/schema/McpServerElicitationRequestResponse.json create mode 100644 internal/agent/runtime/codex/protocolgen/schema/PermissionsRequestApprovalResponse.json create mode 100644 internal/agent/runtime/codex/protocolgen/schema/ServerRequest.json create mode 100644 internal/agent/runtime/codex/protocolgen/schema/ToolRequestUserInputResponse.json create mode 100644 internal/agent/runtime/codex/protocolgen/schema/VERSION.json create mode 100644 internal/agent/runtime/codex/protocolgen/schema/codex_app_server_protocol.v2.schemas.json create mode 100644 internal/agent/runtime/codex/protocolgen/subset.go create mode 100644 internal/agent/runtime/codex/tools.go create mode 100644 internal/agent/runtime/codex/turn.go create mode 100644 internal/agent/runtime/codex/userinput.go create mode 100644 internal/handlers/external_agent_codex.go create mode 100755 scripts/codex-schema-sync.sh diff --git a/cmd/agent/http_providers.go b/cmd/agent/http_providers.go index 43cb787ef9..75fd17d216 100644 --- a/cmd/agent/http_providers.go +++ b/cmd/agent/http_providers.go @@ -123,6 +123,10 @@ func (r botRuntimeResets) BeginBotHistoryReset(ctx context.Context, botID string }, nil } +func provideExternalAgentCodexServerHandler(handler *handlers.ExternalAgentCodexHandler) *handlers.ExternalAgentCodexHandler { + return handler +} + func provideProviderOAuthHandler(providersService *providers.Service) *handlers.ProviderOAuthHandler { return handlers.NewProviderOAuthHandler(providersService) } diff --git a/cmd/agent/module.go b/cmd/agent/module.go index 80aaac049a..72f0bd4610 100644 --- a/cmd/agent/module.go +++ b/cmd/agent/module.go @@ -94,6 +94,7 @@ func commonOptions() fx.Option { provideServerHandler(handlers.NewProvidersHandler), provideServerHandler(handlers.NewProviderTemplatesHandler), provideServerHandler(provideProviderOAuthHandler), + provideServerHandler(provideExternalAgentCodexServerHandler), provideServerHandler(handlers.NewFetchProvidersHandler), provideServerHandler(handlers.NewSearchProvidersHandler), provideServerHandler(handlers.NewModelsHandler), diff --git a/cmd/gen-codex-protocol/main.go b/cmd/gen-codex-protocol/main.go new file mode 100644 index 0000000000..efebe811f1 --- /dev/null +++ b/cmd/gen-codex-protocol/main.go @@ -0,0 +1,66 @@ +// Command gen-codex-protocol regenerates the Go types for the codex +// app-server v2 protocol from the vendored JSON Schema snapshot in +// internal/agent/runtime/codex/protocolgen/schema. +package main + +import ( + "flag" + "fmt" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/felinics/memoh/internal/agent/runtime/codex/protocolgen" +) + +func main() { + out := flag.String("out", "internal/agent/runtime/codex/protocol", "output directory for generated files") + flag.Parse() + + files, err := protocolgen.Generate() + if err != nil { + fmt.Fprintln(os.Stderr, "gen-codex-protocol:", err) + os.Exit(1) + } + + if err := os.MkdirAll(*out, 0o750); err != nil { + fmt.Fprintln(os.Stderr, "gen-codex-protocol:", err) + os.Exit(1) + } + + names := make([]string, 0, len(files)) + for name := range files { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + path := filepath.Join(*out, name) + if err := os.WriteFile(path, files[name], 0o600); err != nil { + fmt.Fprintln(os.Stderr, "gen-codex-protocol:", err) + os.Exit(1) + } + fmt.Println("wrote", path) + } + + // Remove stale generated files no longer produced. + entries, err := os.ReadDir(*out) + if err != nil { + fmt.Fprintln(os.Stderr, "gen-codex-protocol:", err) + os.Exit(1) + } + for _, entry := range entries { + name := entry.Name() + if !strings.HasSuffix(name, ".gen.go") { + continue + } + if _, ok := files[name]; !ok { + path := filepath.Join(*out, name) + if err := os.Remove(path); err != nil { + fmt.Fprintln(os.Stderr, "gen-codex-protocol:", err) + os.Exit(1) + } + fmt.Println("removed stale", path) + } + } +} diff --git a/cmd/internal/core/module.go b/cmd/internal/core/module.go index b8d9514cf9..bea9ceadbe 100644 --- a/cmd/internal/core/module.go +++ b/cmd/internal/core/module.go @@ -88,7 +88,9 @@ func ServerModule() fx.Option { agentcredential.NewService, provideACPRunner, provideACPSessionPool, + provideCodexDriver, provideDirectAgentDrivers, + provideExternalAgentCodexHandler, provideHooksService, provideProvidersService, providertemplates.NewService, diff --git a/cmd/internal/core/providers.go b/cmd/internal/core/providers.go index 4cad092516..feccb11736 100644 --- a/cmd/internal/core/providers.go +++ b/cmd/internal/core/providers.go @@ -35,9 +35,11 @@ import ( agentpayload "github.com/felinics/memoh/internal/agent/event/payload" acpagent "github.com/felinics/memoh/internal/agent/runtime/acp" acpclient "github.com/felinics/memoh/internal/agent/runtime/acp/client" + codexruntime "github.com/felinics/memoh/internal/agent/runtime/codex" "github.com/felinics/memoh/internal/agent/runtime/external" "github.com/felinics/memoh/internal/agent/runtime/native" sessionruntime "github.com/felinics/memoh/internal/agent/runtime/session" + "github.com/felinics/memoh/internal/agent/runtime/toolmount" agenttools "github.com/felinics/memoh/internal/agent/tool" "github.com/felinics/memoh/internal/agent/turn" "github.com/felinics/memoh/internal/agentcredential" @@ -574,8 +576,31 @@ func provideACPSessionPool(lc fx.Lifecycle, log *slog.Logger, runner *acpclient. return pool } -func provideDirectAgentDrivers() external.Drivers { - return nil +func provideCodexDriver(lc fx.Lifecycle, log *slog.Logger, workspaceManager *workspace.Manager, botAgents *botagents.Service, credentials *agentcredential.Service, toolApproval *toolapproval.Service, userInput *userinput.Service, toolGateway *mcp.ToolGatewayService, toolContexts *mcp.ToolSessionContextStore) *codexruntime.Driver { + driver := codexruntime.NewDriver( + workspaceManager, + botAgents, + credentials, + toolApproval, + userInput, + toolmount.Gateway{Tools: toolGateway, Contexts: toolContexts, Logger: log}, + log, + ) + lc.Append(fx.Hook{ + OnStop: func(context.Context) error { + driver.CloseAll() + return nil + }, + }) + return driver +} + +func provideDirectAgentDrivers(codex *codexruntime.Driver) external.Drivers { + return external.Drivers{codex} +} + +func provideExternalAgentCodexHandler(log *slog.Logger, driver *codexruntime.Driver, botAgents *botagents.Service, botService *bots.Service, accountService *accounts.Service) *handlers.ExternalAgentCodexHandler { + return handlers.NewExternalAgentCodexHandler(log, driver, botAgents, botService, accountService) } func provideAgentService(log *slog.Logger, a *native.Agent, modelsService *models.Service, queries dbstore.Queries, msgService *message.DBService, settingsService *settings.Service, accountService *accounts.Service, botService *bots.Service, mediaService *media.Service, containerdHandler *handlers.ContainerdHandler, workspaceManager *workspace.Manager, memoryRegistry *memprovider.Registry, channelStore *channel.Store, _ *route.DBService, sessionService *sessionpkg.Service, eventHub *event.Hub, compactionService *compaction.Service, pipeline *timeline.Pipeline, rc *boot.RuntimeConfig, bgManager *background.Manager, toolApproval *toolapproval.Service, userInput *userinput.Service, acpPool *acpagent.SessionPool, directAgents external.Drivers, hookService *hookspkg.Service, sessionRuntime *sessionruntime.Manager, workdirService *workdir.Service, cfg config.Config) *application.Service { diff --git a/internal/agent/runtime/codex/appserver.go b/internal/agent/runtime/codex/appserver.go new file mode 100644 index 0000000000..bb6f966524 --- /dev/null +++ b/internal/agent/runtime/codex/appserver.go @@ -0,0 +1,421 @@ +package codex + +import ( + "context" + "errors" + "fmt" + "log/slog" + "strings" + "sync" + "time" + + "github.com/felinics/memoh/internal/agent/runtime/codex/protocol" + "github.com/felinics/memoh/internal/agent/runtime/toolmount" + "github.com/felinics/memoh/internal/version" + "github.com/felinics/memoh/internal/workspace/bridge" +) + +// appServer is one long-lived `codex app-server` process for one Bot Agent. +type appServer struct { + botID string + botAgentID string + proc *appServerProcess + conn *conn + logger *slog.Logger + // client is the workspace bridge the process runs over; tool-gateway + // mounts reuse it. + client *bridge.Client + // workspaceInfo captures the workspace backend and its container-local + // tools proxy address for tool-gateway mounts. + workspaceInfo bridge.WorkspaceInfo + // mountCtx bounds thread tool-gateway mounts to the server's lifetime. + mountCtx context.Context + mountCancel context.CancelFunc + + codexVersion string + + mu sync.Mutex + turns map[string]*turnState // active turn per thread id + // loadedThreads tracks thread ids this process has started or resumed; + // resuming an already-loaded thread is a no-op server-side but tracking + // avoids redundant calls. + loadedThreads map[string]bool + // toollessThreads marks threads whose start-time config carried no Memoh + // tool gateway; the driver re-emits a notice for them every turn. + toollessThreads map[string]bool + // toolLookup reports whether a tool name is served by the Memoh gateway + // for this bot; the MCP consent path uses it turn-independently. + toolLookup func(context.Context, string) bool + authReady bool + // toolMounts holds each thread's live gateway route (see tools.go). + toolMounts map[string]*toolmount.Mount + // logins tracks in-flight device-code logins by login id; outcomes arrive + // via the account/login/completed notification. + logins map[string]*loginOutcome +} + +// loginOutcome is the terminal state of one device-code login. +type loginOutcome struct { + Done bool + Success bool + Error string +} + +// ErrAuthRequired identifies a configured Codex runtime that still needs the +// user to complete account authorization in the Bot's workspace. +var ErrAuthRequired = errors.New("codex runtime authentication is required") + +// handshakeTimeout bounds initialize plus auth setup on a fresh process. +const handshakeTimeout = 60 * time.Second + +func startAppServerSession(ctx context.Context, botID, botAgentID string, client *bridge.Client, cfg Config, logger *slog.Logger) (*appServer, error) { + proc, err := startAppServer(ctx, client, defaultProjectPath, codexHome(botAgentID), cfg) + if err != nil { + return nil, fmt.Errorf("start codex app-server: %w", err) + } + mountCtx, mountCancel := context.WithCancel(ctx) + srv := &appServer{ + botID: botID, + botAgentID: botAgentID, + proc: proc, + logger: logger, + client: client, + mountCtx: mountCtx, + mountCancel: mountCancel, + turns: map[string]*turnState{}, + loadedThreads: map[string]bool{}, + toollessThreads: map[string]bool{}, + logins: map[string]*loginOutcome{}, + toolMounts: map[string]*toolmount.Mount{}, + } + srv.conn = newConn(proc, srv, logger) + + handshakeCtx, cancel := context.WithTimeout(ctx, handshakeTimeout) + defer cancel() + var initResp protocol.InitializeResponse + err = srv.conn.Call(handshakeCtx, "initialize", protocol.InitializeParams{ + ClientInfo: protocol.ClientInfo{ + Name: "memoh", + Version: version.ShortCommitHash(), + }, + }, &initResp) + if err != nil { + _ = srv.conn.Close() + return nil, fmt.Errorf("codex initialize failed: %w (stderr: %s)", err, proc.StderrTail()) + } + if err := srv.conn.Notify(protocol.MethodInitialized, nil); err != nil { + _ = srv.conn.Close() + return nil, err + } + srv.codexVersion = codexVersionFromUserAgent(initResp.UserAgent) + if srv.codexVersion != protocol.PinnedCodexVersion { + // The generated protocol types match the pinned CLI exactly. A drifted + // binary usually still speaks a compatible superset (unknown fields + // and methods are tolerated by design), so warn loudly instead of + // refusing service; the toolkit pin and this check must converge. + logger.Warn("codex CLI version differs from the pinned protocol snapshot", + slog.String("bot_id", botID), + slog.String("cli_version", srv.codexVersion), + slog.String("pinned", protocol.PinnedCodexVersion), + ) + } + return srv, nil +} + +// codexVersionFromUserAgent extracts the CLI version from the initialize +// userAgent, e.g. "memoh/0.151.0 (Mac OS 27.0; arm64) …" → "0.151.0". +func codexVersionFromUserAgent(userAgent string) string { + head, _, _ := strings.Cut(userAgent, " ") + _, ver, ok := strings.Cut(head, "/") + if !ok { + return "" + } + return ver +} + +// ensureAuth verifies codex is authenticated, logging in with the bot's API +// key when needed. ChatGPT-mode credentials must already exist in CODEX_HOME +// (established via a login flow); their refresh is codex's own job. +func (s *appServer) ensureAuth(ctx context.Context, cfg Config) error { + if cfg.Auth == AuthAPIKey && cfg.APIKey == "" { + return ErrAuthRequired + } + s.mu.Lock() + ready := s.authReady + s.mu.Unlock() + if ready { + return nil + } + var account protocol.GetAccountResponse + if err := s.conn.Call(ctx, protocol.MethodAccountRead, protocol.GetAccountParams{}, &account); err != nil { + return fmt.Errorf("codex account/read: %w", err) + } + needsLogin := account.Account == nil + if account.Account != nil { + switch cfg.Auth { + case AuthAPIKey: + // account/read only reveals THAT an API-key login exists, never + // which key it holds — CODEX_HOME may still carry a rotated or + // revoked predecessor. The login below is a cheap local auth.json + // write, so always re-login with the configured key: this app + // server starts at most once per configuration (a metadata change + // recycles it), making the login effectively once per key. + needsLogin = true + case AuthChatGPT: + // Stored credentials must match the configured mode: a leftover + // api-key login on a ChatGPT bot silently bills the wrong account. + if account.Account.Chatgpt == nil { + return fmt.Errorf("%w: account/read returned no ChatGPT credentials", ErrAuthRequired) + } + } + } + if needsLogin { + switch cfg.Auth { + case AuthAPIKey: + var login protocol.LoginAccountResponse + err := s.conn.Call(ctx, protocol.MethodAccountLoginStart, protocol.LoginAccountParams{ + APIKey: &protocol.APIKeyLoginAccountParams{APIKey: cfg.APIKey}, + }, &login) + if err != nil { + return fmt.Errorf("codex api-key login: %w", err) + } + case AuthChatGPT: + return fmt.Errorf("%w: CODEX_HOME has no stored ChatGPT credentials", ErrAuthRequired) + default: + return ErrNotConfigured + } + } + s.mu.Lock() + s.authReady = true + s.mu.Unlock() + return nil +} + +// registerTurn claims the thread's turn slot for routing inbound traffic. +func (s *appServer) registerTurn(threadID string, turn *turnState) { + s.mu.Lock() + s.turns[threadID] = turn + s.mu.Unlock() +} + +func (s *appServer) unregisterTurn(threadID string, turn *turnState) { + s.mu.Lock() + if s.turns[threadID] == turn { + delete(s.turns, threadID) + } + s.mu.Unlock() +} + +func (s *appServer) turnForThread(threadID string) *turnState { + s.mu.Lock() + defer s.mu.Unlock() + return s.turns[threadID] +} + +func (s *appServer) hasActiveTurns() bool { + s.mu.Lock() + defer s.mu.Unlock() + return len(s.turns) > 0 +} + +func (s *appServer) markThreadLoaded(threadID string) { + s.mu.Lock() + s.loadedThreads[threadID] = true + s.mu.Unlock() +} + +func (s *appServer) threadLoaded(threadID string) bool { + s.mu.Lock() + defer s.mu.Unlock() + return s.loadedThreads[threadID] +} + +func (s *appServer) setThreadToolless(threadID string, toolless bool) { + s.mu.Lock() + defer s.mu.Unlock() + if toolless { + s.toollessThreads[threadID] = true + } else { + delete(s.toollessThreads, threadID) + } +} + +func (s *appServer) threadToolless(threadID string) bool { + s.mu.Lock() + defer s.mu.Unlock() + return s.toollessThreads[threadID] +} + +// HandleServerRequest routes app-server → Memoh requests to the owning turn. +// It runs on the read loop, so decisions are dispatched to goroutines. +func (s *appServer) HandleServerRequest(_ context.Context, req *protocol.Inbound) { + decoded, known, err := protocol.DecodeServerRequestParams(req.Method, req.Params) + if err != nil { + s.logger.Error("codex: undecodable server request", slog.String("method", req.Method), slog.Any("error", err)) + _ = s.conn.RespondError(req.ID, -32602, "memoh could not decode this request") + return + } + if !known { + s.logger.Warn("codex: unhandled server request method", slog.String("method", req.Method)) + _ = s.conn.RespondError(req.ID, -32601, "memoh does not handle this request") + return + } + switch params := decoded.(type) { + case *protocol.McpServerElicitationRequestParams: + go s.dispatchElicitation(req, params) //nolint:contextcheck // consent lookup and turn decisions own their lifetimes + return + case *protocol.ChatgptAuthTokensRefreshParams: + // Token injection mode is not used: codex owns auth.json refresh. + _ = s.conn.RespondError(req.ID, -32000, "memoh does not inject ChatGPT auth tokens") + return + } + threadID := serverRequestThreadID(decoded) + turn := s.turnForThread(threadID) + if turn == nil { + // Fail closed: an approval with no live turn has nobody to decide it. + s.logger.Warn("codex: server request for idle thread", slog.String("method", req.Method), slog.String("thread_id", threadID)) + _ = s.conn.RespondError(req.ID, -32000, "no active turn for this thread") + return + } + // The decision runs on the turn-scoped context, not the read loop's; the + // turn owns its lifetime. + go turn.handleServerRequest(s.conn, req, decoded) //nolint:contextcheck // turnState.ctx bounds the decision +} + +// HandleNotification routes app-server notifications to the owning turn. +func (s *appServer) HandleNotification(_ context.Context, note *protocol.Inbound) { + decoded, known, err := protocol.DecodeServerNotificationParams(note.Method, note.Params) + if err != nil { + s.logger.Warn("codex: undecodable notification", slog.String("method", note.Method), slog.Any("error", err)) + return + } + if !known { + return + } + threadID := notificationThreadID(decoded) + if threadID == "" { + s.handleGlobalNotification(note.Method, decoded) + return + } + if turn := s.turnForThread(threadID); turn != nil { + turn.handleNotification(decoded) + } +} + +func (s *appServer) handleGlobalNotification(method string, decoded any) { + switch method { + case protocol.MethodDeprecationNotice: + s.logger.Warn("codex deprecation notice", slog.Any("notice", decoded)) + case protocol.MethodWarning, protocol.MethodConfigWarning: + s.logger.Warn("codex warning", slog.String("method", method), slog.Any("payload", decoded)) + case protocol.MethodAccountLoginCompleted: + completed, ok := decoded.(*protocol.AccountLoginCompletedNotification) + if !ok || completed.LoginID == nil { + return + } + outcome := &loginOutcome{Done: true, Success: completed.Success} + if completed.Error != nil { + outcome.Error = *completed.Error + } + s.mu.Lock() + if _, tracked := s.logins[*completed.LoginID]; tracked { + s.logins[*completed.LoginID] = outcome + } + if completed.Success { + // Fresh credentials just landed in CODEX_HOME. + s.authReady = true + } + s.mu.Unlock() + } +} + +// trackLogin registers a device-code login for completion tracking. +func (s *appServer) trackLogin(loginID string) { + s.mu.Lock() + s.logins[loginID] = &loginOutcome{} + s.mu.Unlock() +} + +// loginStatus returns the tracked outcome; ok is false for unknown logins. +func (s *appServer) loginStatus(loginID string) (loginOutcome, bool) { + s.mu.Lock() + defer s.mu.Unlock() + outcome, ok := s.logins[loginID] + if !ok { + return loginOutcome{}, false + } + return *outcome, true +} + +func (s *appServer) forgetLogin(loginID string) { + s.mu.Lock() + delete(s.logins, loginID) + s.mu.Unlock() +} + +// Done reports process exit; the server lifecycle table watches it. +func (s *appServer) Done() <-chan struct{} { return s.proc.Done() } + +func (s *appServer) Close() error { + if s.mountCancel != nil { + s.mountCancel() + } + s.stopToolMounts() + return s.conn.Close() +} + +// serverRequestThreadID extracts the thread id from a typed server request. +func serverRequestThreadID(decoded any) string { + switch params := decoded.(type) { + case *protocol.CommandExecutionRequestApprovalParams: + return params.ThreadID + case *protocol.FileChangeRequestApprovalParams: + return params.ThreadID + case *protocol.PermissionsRequestApprovalParams: + return params.ThreadID + case *protocol.ToolRequestUserInputParams: + return params.ThreadID + } + return "" +} + +// notificationThreadID extracts the thread id from a typed notification. +func notificationThreadID(decoded any) string { + switch params := decoded.(type) { + case *protocol.ThreadStartedNotification: + return params.Thread.ID + case *protocol.ThreadStatusChangedNotification: + return params.ThreadID + case *protocol.ThreadTokenUsageUpdatedNotification: + return params.ThreadID + case *protocol.ContextCompactedNotification: //nolint:staticcheck // still the wire shape for thread/compacted at the pinned version + return params.ThreadID + case *protocol.TurnStartedNotification: + return params.ThreadID + case *protocol.TurnCompletedNotification: + return params.ThreadID + case *protocol.TurnPlanUpdatedNotification: + return params.ThreadID + case *protocol.ItemStartedNotification: + return params.ThreadID + case *protocol.ItemCompletedNotification: + return params.ThreadID + case *protocol.AgentMessageDeltaNotification: + return params.ThreadID + case *protocol.ReasoningTextDeltaNotification: + return params.ThreadID + case *protocol.ReasoningSummaryTextDeltaNotification: + return params.ThreadID + case *protocol.ReasoningSummaryPartAddedNotification: + return params.ThreadID + case *protocol.CommandExecutionOutputDeltaNotification: + return params.ThreadID + case *protocol.FileChangeOutputDeltaNotification: + return params.ThreadID + case *protocol.ErrorNotification: + return params.ThreadID + case *protocol.ServerRequestResolvedNotification: + return params.ThreadID + } + return "" +} diff --git a/internal/agent/runtime/codex/auth.go b/internal/agent/runtime/codex/auth.go new file mode 100644 index 0000000000..aeac8a1423 --- /dev/null +++ b/internal/agent/runtime/codex/auth.go @@ -0,0 +1,108 @@ +package codex + +import ( + "context" + "encoding/json" + "errors" + "log/slog" + "path" + "strings" + "time" + + "github.com/felinics/memoh/internal/agent/runtime/external" + "github.com/felinics/memoh/internal/agentcredential" + "github.com/felinics/memoh/internal/workspace/bridge" +) + +type chatGPTCredential struct { + accessToken string + idToken string + refreshToken string + accountID string + lastRefresh time.Time +} + +func materializeChatGPTCredential(ctx context.Context, client *bridge.Client, botAgentID string, credential agentcredential.ResolvedCredential) error { + if credential.AuthKind != agentcredential.AuthKindOpenAICodexOAuth { + return agentcredential.ErrIncompatible + } + lastRefresh, _ := time.Parse(time.RFC3339Nano, metadataString(credential.AccountMetadata, "last_refresh")) + payload, err := json.Marshal(map[string]any{ + "auth_mode": "chatgpt", + "tokens": map[string]string{ + "access_token": credential.Secret["access_token"], + "id_token": credential.Secret["id_token"], + "refresh_token": credential.Secret["refresh_token"], + "account_id": credential.Secret["account_id"], + }, + "last_refresh": lastRefresh.UTC().Format(time.RFC3339Nano), + }) + if err != nil { + return err + } + home := codexHome(botAgentID) + if err := client.Mkdir(ctx, home); err != nil { + return err + } + return client.WriteFile(ctx, path.Join(home, "auth.json"), append(payload, '\n')) +} + +func readChatGPTCredential(ctx context.Context, client *bridge.Client, botAgentID string) (chatGPTCredential, error) { + response, err := client.ReadFile(ctx, path.Join(codexHome(botAgentID), "auth.json"), 0, 0) + if err != nil { + return chatGPTCredential{}, err + } + var payload struct { + AuthMode string `json:"auth_mode"` + Tokens map[string]string `json:"tokens"` + LastRefresh string `json:"last_refresh"` + } + if err := json.Unmarshal([]byte(response.GetContent()), &payload); err != nil { + return chatGPTCredential{}, err + } + credential := chatGPTCredential{ + accessToken: strings.TrimSpace(payload.Tokens["access_token"]), + idToken: strings.TrimSpace(payload.Tokens["id_token"]), + refreshToken: strings.TrimSpace(payload.Tokens["refresh_token"]), + accountID: strings.TrimSpace(payload.Tokens["account_id"]), + } + credential.lastRefresh, _ = time.Parse(time.RFC3339Nano, strings.TrimSpace(payload.LastRefresh)) + if payload.AuthMode != "chatgpt" || credential.accessToken == "" || credential.idToken == "" || credential.refreshToken == "" || credential.accountID == "" { + return chatGPTCredential{}, errors.New("codex auth.json does not contain a complete ChatGPT credential") + } + return credential, nil +} + +func (d *Driver) persistChatGPTCredential(ctx context.Context, client *bridge.Client, input external.PromptInput, stored agentcredential.ResolvedCredential) { + current, err := readChatGPTCredential(context.WithoutCancel(ctx), client, input.BotAgentID) + if err != nil { + d.logger.Warn("read refreshed codex credential failed", slog.Any("error", err)) + return + } + if current.accessToken == stored.Secret["access_token"] && + current.idToken == stored.Secret["id_token"] && + current.refreshToken == stored.Secret["refresh_token"] && + current.accountID == stored.Secret["account_id"] { + return + } + metadata := map[string]any{ + "account_id": current.accountID, + "last_refresh": current.lastRefresh.UTC().Format(time.RFC3339Nano), + } + _, err = d.credentials.UpdateSecretCAS( + context.WithoutCancel(ctx), + stored.ID, + stored.CredentialVersion, + map[string]string{ + "access_token": current.accessToken, + "id_token": current.idToken, + "refresh_token": current.refreshToken, + "account_id": current.accountID, + }, + metadata, + stored.ExpiresAt, + ) + if err != nil { + d.logger.Warn("persist refreshed codex credential failed", slog.Any("error", err)) + } +} diff --git a/internal/agent/runtime/codex/config.go b/internal/agent/runtime/codex/config.go new file mode 100644 index 0000000000..4be6046672 --- /dev/null +++ b/internal/agent/runtime/codex/config.go @@ -0,0 +1,57 @@ +// Package codex implements the direct codex app-server runtime driver: it +// speaks the v2 protocol (internal/agent/runtime/codex/protocol) to a pinned +// codex CLI running inside the bot workspace, with no ACP adapter in between. +// +// Session state is owned by codex itself under CODEX_HOME on the bot's +// persistent data volume; Memoh keeps only the thread id in session runtime +// metadata and projects the turn transcript into its own history. +package codex + +import ( + "path" + + "github.com/felinics/memoh/internal/agent/runtime/codex/codexcfg" + "github.com/felinics/memoh/internal/runtimekind" +) + +const ( + // RuntimeType is the thread runtime type this driver serves. + RuntimeType = string(runtimekind.Codex) + + // metadataThreadIDKey stores the codex thread id in session runtime + // metadata. Losing it starts a fresh codex thread on the next turn. + metadataThreadIDKey = "codex_thread_id" + + codexHomeRoot = "/data/.codex/agents" + // launcherPath is the pinned toolkit launcher for the codex CLI. + launcherPath = "/opt/memoh/toolkit/bin/codex" + // defaultProjectPath matches the workspace data volume root. + defaultProjectPath = "/data" +) + +func codexHome(botAgentID string) string { + return path.Join(codexHomeRoot, botAgentID) +} + +// Configuration lives in the codexcfg leaf package so validators can import +// it without the driver's dependency tree; these aliases keep driver-side +// call sites short. +type ( + AuthMode = codexcfg.AuthMode + Config = codexcfg.Config +) + +const ( + AuthAPIKey = codexcfg.AuthAPIKey + AuthChatGPT = codexcfg.AuthChatGPT +) + +var ( + ErrNotConfigured = codexcfg.ErrNotConfigured + ParseAgentConfig = codexcfg.ParseAgentConfig +) + +func metadataString(meta map[string]any, key string) string { + value, _ := meta[key].(string) + return value +} diff --git a/internal/agent/runtime/codex/conn.go b/internal/agent/runtime/codex/conn.go new file mode 100644 index 0000000000..2c3d1457d8 --- /dev/null +++ b/internal/agent/runtime/codex/conn.go @@ -0,0 +1,255 @@ +package codex + +import ( + "bufio" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "log/slog" + "sync" + "sync/atomic" + "time" + + "github.com/felinics/memoh/internal/agent/runtime/codex/protocol" +) + +// maxLineBytes bounds one NDJSON line from the app-server. Turn payloads can +// carry whole file diffs; 32MiB is far above anything observed and still a +// hard stop against a runaway stream. +const maxLineBytes = 32 * 1024 * 1024 + +// ErrConnClosed reports that the app-server connection is gone. +var ErrConnClosed = errors.New("codex app-server connection closed") + +// inboundHandler receives server → client requests and notifications. Calls +// are serialized by the read loop; implementations must not block on it — +// long work (approvals) must move to its own goroutine. +type inboundHandler interface { + HandleServerRequest(ctx context.Context, req *protocol.Inbound) + HandleNotification(ctx context.Context, note *protocol.Inbound) +} + +// procIO is the stdio surface conn drives; appServerProcess implements it, +// and tests substitute an in-memory pipe. +type procIO interface { + io.ReadWriter + Close() error +} + +// conn multiplexes JSON-RPC over one app-server process: correlated request / +// response pairs out, server requests and notifications in. +type conn struct { + proc procIO + logger *slog.Logger + handler inboundHandler + + writeMu sync.Mutex + nextID atomic.Uint64 + + mu sync.Mutex + pending map[string]chan *protocol.Inbound + closed bool + err error +} + +//nolint:contextcheck // the read loop outlives any caller context by design +func newConn(proc procIO, handler inboundHandler, logger *slog.Logger) *conn { + c := &conn{ + proc: proc, + logger: logger, + handler: handler, + pending: map[string]chan *protocol.Inbound{}, + } + go c.readLoop() + return c +} + +// Call sends a request and decodes the response result into result (skipped +// when result is nil). It returns *protocol.RPCError for server-side errors. +func (c *conn) Call(ctx context.Context, method string, params any, result any) error { + id := protocol.NewRequestID(c.nextID.Add(1)) + respCh := make(chan *protocol.Inbound, 1) + if err := c.registerPending(id.Key(), respCh); err != nil { + return err + } + if err := c.writeLine(protocol.Request{ID: id, Method: method, Params: params}); err != nil { + c.unregisterPending(id.Key()) + return err + } + select { + case <-ctx.Done(): + c.unregisterPending(id.Key()) + return ctx.Err() + case resp, ok := <-respCh: + if !ok { + return c.closeErr() + } + if resp.Err != nil { + return resp.Err + } + if result == nil { + return nil + } + if err := json.Unmarshal(resp.Result, result); err != nil { + return fmt.Errorf("codex: decoding %s response: %w", method, err) + } + return nil + } +} + +// Notify sends a fire-and-forget notification. +func (c *conn) Notify(method string, params any) error { + return c.writeLine(protocol.Notification{Method: method, Params: params}) +} + +// Respond answers a server → client request. +func (c *conn) Respond(id protocol.RequestID, result any) error { + return c.writeLine(protocol.Response{ID: id, Result: result}) +} + +// RespondError rejects a server → client request. +func (c *conn) RespondError(id protocol.RequestID, code int64, message string) error { + return c.writeLine(protocol.ErrorResponse{ID: id, Error: &protocol.RPCError{Code: code, Message: message}}) +} + +// writeTimeout bounds one stdio write. A bridge stream that cannot accept a +// small line within it is wedged; the connection (and process) come down +// rather than chaining every caller behind the write mutex forever. +const writeTimeout = 30 * time.Second + +func (c *conn) writeLine(payload any) error { + encoded, err := json.Marshal(payload) + if err != nil { + return err + } + c.writeMu.Lock() + defer c.writeMu.Unlock() + done := make(chan error, 1) + go func() { + _, writeErr := c.proc.Write(append(encoded, '\n')) + done <- writeErr + }() + select { + case writeErr := <-done: + if writeErr != nil { + return errors.Join(ErrConnClosed, writeErr) + } + return nil + case <-time.After(writeTimeout): + c.shutdown(errors.New("app-server stdio write stalled")) + return c.closeErr() + } +} + +func (c *conn) registerPending(key string, ch chan *protocol.Inbound) error { + c.mu.Lock() + defer c.mu.Unlock() + if c.closed { + return c.errLocked() + } + c.pending[key] = ch + return nil +} + +func (c *conn) unregisterPending(key string) { + c.mu.Lock() + delete(c.pending, key) + c.mu.Unlock() +} + +func (c *conn) closeErr() error { + c.mu.Lock() + defer c.mu.Unlock() + return c.errLocked() +} + +func (c *conn) errLocked() error { + if c.err != nil { + return errors.Join(ErrConnClosed, c.err) + } + return ErrConnClosed +} + +func (c *conn) readLoop() { + scanner := bufio.NewScanner(c.proc) + scanner.Buffer(make([]byte, 64*1024), maxLineBytes) + ctx := context.Background() + for scanner.Scan() { + line := scanner.Bytes() + if len(line) == 0 { + continue + } + inbound, err := protocol.DecodeInbound(line) + if err != nil { + // Non-JSON output on stdout is unexpected but survivable. + c.logger.Warn("codex: undecodable app-server line", slog.String("line", truncateForLog(line)), slog.Any("error", err)) + continue + } + switch inbound.Kind { + case protocol.InboundResponse: + if inbound.ID.IsZero() { + c.logger.Warn("codex: orphan null-id response", slog.String("line", truncateForLog(line))) + continue + } + c.mu.Lock() + ch, ok := c.pending[inbound.ID.Key()] + if ok { + delete(c.pending, inbound.ID.Key()) + } + c.mu.Unlock() + if !ok { + c.logger.Warn("codex: response for unknown request id", slog.String("id", inbound.ID.String())) + continue + } + ch <- inbound + case protocol.InboundRequest: + c.handler.HandleServerRequest(ctx, inbound) + case protocol.InboundNotification: + c.handler.HandleNotification(ctx, inbound) + } + } + scanErr := scanner.Err() + if scanErr == nil { + scanErr = io.EOF + } + c.shutdown(scanErr) +} + +// shutdown fails all pending calls and marks the connection closed. The +// process goes down with the connection: a server nobody can talk to is a +// zombie that would otherwise pass liveness checks forever. +func (c *conn) shutdown(cause error) { + c.mu.Lock() + if c.closed { + c.mu.Unlock() + return + } + c.closed = true + if !errors.Is(cause, io.EOF) { + c.err = cause + } + pending := c.pending + c.pending = map[string]chan *protocol.Inbound{} + c.mu.Unlock() + for _, ch := range pending { + close(ch) + } + _ = c.proc.Close() +} + +// Close tears the process down and fails pending calls. +func (c *conn) Close() error { + err := c.proc.Close() + c.shutdown(io.EOF) + return err +} + +func truncateForLog(line []byte) string { + const limit = 512 + if len(line) <= limit { + return string(line) + } + return string(line[:limit]) + "…" +} diff --git a/internal/agent/runtime/codex/driver.go b/internal/agent/runtime/codex/driver.go new file mode 100644 index 0000000000..abd7873a16 --- /dev/null +++ b/internal/agent/runtime/codex/driver.go @@ -0,0 +1,722 @@ +package codex + +import ( + "context" + "errors" + "fmt" + "log/slog" + "path" + "strings" + "time" + + "github.com/felinics/memoh/internal/agent/decision/approval" + "github.com/felinics/memoh/internal/agent/runtime/codex/protocol" + "github.com/felinics/memoh/internal/agent/runtime/external" + "github.com/felinics/memoh/internal/agent/runtime/toolmount" + "github.com/felinics/memoh/internal/agent/sessionmode" + "github.com/felinics/memoh/internal/agentcredential" + "github.com/felinics/memoh/internal/apperror" + "github.com/felinics/memoh/internal/botagents" + "github.com/felinics/memoh/internal/mcp" + "github.com/felinics/memoh/internal/workspace/bridge" +) + +// BridgeSource resolves the workspace bridge client for a bot; the workspace +// manager implements it. +type BridgeSource interface { + MCPClient(ctx context.Context, botID string) (*bridge.Client, error) + WorkspaceInfo(ctx context.Context, botID string) (bridge.WorkspaceInfo, error) +} + +// ApprovalService is the decision flow the driver routes approvals through. +type ApprovalService interface { + approval.FlowService + RegisterWaiter(approvalID string) func() +} + +// Driver runs codex turns over the app-server protocol. +type Driver struct { + bridges BridgeSource + agents *botagents.Service + credentials *agentcredential.Service + approval ApprovalService + userInput UserInputService + toolGateway toolmount.Gateway + logger *slog.Logger + + // servers owns the shared per-Agent app-server lifecycle: reference + // counting for concurrent users, drain-on-recycle instead of kill-by-bot. + servers *serverTable +} + +// NewDriver constructs the codex runtime driver. +func NewDriver( + bridges BridgeSource, + agents *botagents.Service, + credentials *agentcredential.Service, + approvalSvc ApprovalService, + userInput UserInputService, + toolGateway toolmount.Gateway, + logger *slog.Logger, +) *Driver { + d := &Driver{ + bridges: bridges, + agents: agents, + credentials: credentials, + approval: approvalSvc, + userInput: userInput, + toolGateway: toolGateway, + logger: logger.With(slog.String("runtime", RuntimeType)), + } + d.servers = newServerTable(d.startServer, d.logger) + return d +} + +// RuntimeType implements external.Driver. +func (*Driver) RuntimeType() string { return RuntimeType } + +func (d *Driver) ResetBot(botID string) { d.CloseBot(botID) } + +func (d *Driver) ResetBotAgent(botID, botAgentID string) { + d.servers.recycle(serverKey(botID, botAgentID)) +} + +func serverKey(botID, botAgentID string) string { + return botID + "\x00" + botAgentID +} + +func splitServerKey(key string) (string, string) { + botID, botAgentID, _ := strings.Cut(key, "\x00") + return botID, botAgentID +} + +func (d *Driver) resolveAgentConfig(ctx context.Context, botID, botAgentID string, credentialRequired bool) (Config, agentcredential.ResolvedCredential, error) { + agent, err := d.agents.Get(ctx, botID, botAgentID) + if err != nil { + return Config{}, agentcredential.ResolvedCredential{}, err + } + cfg, err := ParseAgentConfig(agent.Metadata) + if err != nil { + return Config{}, agentcredential.ResolvedCredential{}, err + } + credential, err := d.credentials.ResolveForBotAgent(ctx, botID, botAgentID) + if errors.Is(err, agentcredential.ErrNotFound) && !credentialRequired { + return cfg, agentcredential.ResolvedCredential{}, nil + } + if err != nil { + return Config{}, agentcredential.ResolvedCredential{}, external.CredentialError(err) + } + switch cfg.Auth { + case AuthAPIKey: + if credential.AuthKind != agentcredential.AuthKindOpenAIAPIKey { + return Config{}, agentcredential.ResolvedCredential{}, external.CredentialError(agentcredential.ErrIncompatible) + } + cfg.APIKey = credential.Secret["api_key"] + case AuthChatGPT: + if credential.AuthKind != agentcredential.AuthKindOpenAICodexOAuth { + return Config{}, agentcredential.ResolvedCredential{}, external.CredentialError(agentcredential.ErrIncompatible) + } + } + return cfg, credential, nil +} + +// ModelCatalog returns the live model vocabulary advertised by the bot's +// pinned codex app-server. Authentication is checked first because ChatGPT +// plans and API-key accounts may expose different catalogs. +func (d *Driver) ModelCatalog(ctx context.Context, botID, botAgentID string) (external.ModelCatalog, error) { + cfg, _, err := d.resolveAgentConfig(ctx, botID, botAgentID, true) + if err != nil { + return external.ModelCatalog{}, err + } + srv, releaseServer, err := d.acquireServer(ctx, botID, botAgentID) + if err != nil { + return external.ModelCatalog{}, err + } + defer releaseServer() + if err := srv.ensureAuth(ctx, cfg); err != nil { + if errors.Is(err, ErrAuthRequired) { + return external.ModelCatalog{}, apperror.Wrap(apperror.CodeExternalRuntimeAuthRequired, err, nil) + } + return external.ModelCatalog{}, err + } + + const pageSize = uint64(100) + includeHidden := false + limit := pageSize + var cursor *string + models := make([]external.ModelOption, 0, 16) + seenCursors := map[string]struct{}{} + for { + var response protocol.ModelListResponse + if err := srv.conn.Call(ctx, protocol.MethodModelList, protocol.ModelListParams{ + Cursor: cursor, + IncludeHidden: &includeHidden, + Limit: &limit, + }, &response); err != nil { + return external.ModelCatalog{}, fmt.Errorf("codex model/list: %w", err) + } + for _, model := range response.Data { + if model.Hidden { + continue + } + // turn/start accepts the catalog's concrete `model` value. `id` is + // the app-server row identity and is only a fallback for older + // catalog responses where both happened to be the same string. + modelID := firstNonEmpty(model.Model, model.ID) + if modelID == "" { + continue + } + efforts := make([]external.ReasoningEffortOption, 0, len(model.SupportedReasoningEfforts)) + for _, effort := range model.SupportedReasoningEfforts { + id := strings.TrimSpace(effort.ReasoningEffort) + if id == "" { + continue + } + efforts = append(efforts, external.ReasoningEffortOption{ + ID: id, + Name: id, + Description: effort.Description, + }) + } + models = append(models, external.ModelOption{ + ID: modelID, + Name: firstNonEmpty(model.DisplayName, modelID), + Description: model.Description, + Default: model.IsDefault, + DefaultReasoningEffort: strings.TrimSpace(model.DefaultReasoningEffort), + ReasoningEfforts: efforts, + }) + } + if response.NextCursor == nil || strings.TrimSpace(*response.NextCursor) == "" { + break + } + next := strings.TrimSpace(*response.NextCursor) + if _, exists := seenCursors[next]; exists { + return external.ModelCatalog{}, errors.New("codex model/list returned a repeated cursor") + } + seenCursors[next] = struct{}{} + cursor = &next + } + + return external.ModelCatalog{ + Models: models, + ConfiguredModelID: cfg.Model, + ConfiguredReasoningEffort: cfg.ReasoningEffort, + }, nil +} + +// Prompt implements external.Driver: it runs one turn on the bot's +// app-server, streaming events through the sink. +func (d *Driver) Prompt(ctx context.Context, input external.PromptInput) (external.PromptResult, error) { + cfg, credential, err := d.resolveAgentConfig(ctx, input.BotID, input.BotAgentID, true) + if err != nil { + if apperror.CodeOf(err) != "" { + return external.PromptResult{}, err + } + return external.PromptResult{}, apperror.Wrap(apperror.CodeExternalRuntimeUnavailable, err, map[string]string{"runtime": RuntimeType}) + } + + srv, releaseServer, err := d.acquireServer(ctx, input.BotID, input.BotAgentID) + if err != nil { + return external.PromptResult{}, apperror.Wrap(apperror.CodeExternalRuntimeUnavailable, err, map[string]string{"runtime": RuntimeType}) + } + defer releaseServer() + if err := srv.ensureAuth(ctx, cfg); err != nil { + if errors.Is(err, ErrAuthRequired) { + return external.PromptResult{}, apperror.Wrap(apperror.CodeExternalRuntimeAuthRequired, err, nil) + } + return external.PromptResult{}, apperror.Wrap(apperror.CodeExternalRuntimeUnavailable, err, map[string]string{"runtime": RuntimeType}) + } + + threadID, isNewThread, err := d.ensureThread(ctx, srv, cfg, input) + if err != nil { + return external.PromptResult{}, err + } + // Thread config is fixed at start, so a thread that began without a tool + // gateway stays toolless for the app-server's life. Re-notice every turn: + // silent capability loss is exactly what this channel exists to prevent. + if srv.threadToolless(threadID) { + toolmount.EmitUnavailableNotice(input.Sink, "this conversation started without Memoh tools; start a new session to restore them") + } + + turn := newTurnState(ctx, input, threadID, d.approval, d.approval.RegisterWaiter, d.userInput, srv.toolLookup, d.logger) + defer turn.close() + srv.registerTurn(threadID, turn) + defer srv.unregisterTurn(threadID, turn) + unregisterToolEvents := toolmount.RegisterTurnSink(d.toolGateway.Contexts, input.BotID, input.ThreadID, input.RunID, turn.emit) + defer unregisterToolEvents() + + turnParams := protocol.TurnStartParams{ + ThreadID: threadID, + Input: buildTurnInput(input), + } + if model := firstNonEmpty(input.ModelID, cfg.Model); model != "" { + turnParams.Model = &model + } + if effort := firstNonEmpty(input.ReasoningEffort, cfg.ReasoningEffort); effort != "" { + turnParams.Effort = &effort + } + + var turnResp protocol.TurnStartResponse + if err := srv.conn.Call(ctx, protocol.MethodTurnStart, turnParams, &turnResp); err != nil { + if ctx.Err() != nil { + // The server may have accepted the turn even though the response + // never reached us; interrupt by the id from turn/started so no + // orphan keeps running unsupervised. When even that id has not + // arrived there is nothing to address the interrupt to — recycle + // the server: siblings holding references finish on the draining + // process (which dies at their last release), new work builds a + // fresh one, and the unidentified turn is confined to a process + // with a bounded life instead of sharing one with future turns. + if turnID := turn.currentTurnID(); turnID != "" { + d.interruptTurn(srv, threadID, turnID) + } else { + d.logger.Warn("codex turn/start cancelled before a turn id arrived; draining app-server", + slog.String("thread_id", threadID)) + d.ResetBotAgent(input.BotID, input.BotAgentID) + } + } + return d.turnResultAfterError(turn, isNewThread, threadID, err) + } + turn.setTurnID(turnResp.Turn.ID) + + select { + case <-turn.done: + case <-ctx.Done(): + d.interruptTurn(srv, threadID, firstNonEmpty(turnResp.Turn.ID, turn.currentTurnID())) + select { + case <-turn.done: + case <-time.After(interruptSettleTimeout): + // A turn codex never settled would keep executing — and mutating + // the workspace — or wedge the server busy. Recycle: siblings + // finish on the draining process, new work cold-starts a fresh + // one, and the wedged turn dies with the displaced process once + // its last user releases it. + d.logger.Warn("codex turn did not settle after interrupt; draining app-server", + slog.String("thread_id", threadID)) + d.ResetBotAgent(input.BotID, input.BotAgentID) + case <-srv.proc.Done(): + } + case <-srv.proc.Done(): + result, _ := turn.result(newThreadMetadata(isNewThread, threadID)) + return result, fmt.Errorf("codex app-server exited mid-turn: %s", srv.proc.StderrTail()) + } + + result, resultErr := turn.result(newThreadMetadata(isNewThread, threadID)) + if cfg.Auth == AuthChatGPT { + d.persistChatGPTCredential(ctx, srv.client, input, credential) + } + if ctx.Err() != nil && resultErr == nil { + // The application layer distinguishes stop from failure by context + // state; an interrupted turn is not an error. + return result, nil + } + return result, resultErr +} + +// newThreadMetadata reports the thread id to persist when it was just created. +func newThreadMetadata(isNew bool, threadID string) string { + if isNew { + return threadID + } + return "" +} + +func (*Driver) turnResultAfterError(turn *turnState, isNewThread bool, threadID string, err error) (external.PromptResult, error) { + result, _ := turn.result(newThreadMetadata(isNewThread, threadID)) + var rpcErr *protocol.RPCError + if errors.As(err, &rpcErr) { + return result, fmt.Errorf("codex turn/start rejected: %s", rpcErr.Message) + } + return result, err +} + +// ensureThread starts or resumes the session's codex thread. +func (d *Driver) ensureThread(ctx context.Context, srv *appServer, cfg Config, input external.PromptInput) (threadID string, isNew bool, err error) { + threadID = strings.TrimSpace(metadataString(input.RuntimeMetadata, metadataThreadIDKey)) + if input.ForceFreshRuntime { + // Discuss turns re-inject the full composed context every round; + // resuming the stored thread would duplicate it on top of codex's + // own saved history. + threadID = "" + } + cwd := strings.TrimSpace(metadataString(input.RuntimeMetadata, "project_path")) + if cwd == "" { + cwd = defaultProjectPath + } + approvalPolicy := protocol.AskForApproval{Unit: protocol.AskForApprovalUnitOnRequest} + + if threadID == "" { + toolsConfig, bindTools, err := d.prepareThreadTools(srv, input) + if err != nil { + return "", false, apperror.Wrap(apperror.CodeExternalRuntimeUnavailable, err, map[string]string{"runtime": RuntimeType}) + } + params := protocol.ThreadStartParams{ + Cwd: &cwd, + ApprovalPolicy: &approvalPolicy, + Config: toolsConfig, + } + if cfg.Model != "" { + params.Model = &cfg.Model + } + var resp protocol.ThreadStartResponse + if err := srv.conn.Call(ctx, protocol.MethodThreadStart, params, &resp); err != nil { + bindTools("") + return "", false, fmt.Errorf("codex thread/start: %w", err) + } + threadID = resp.Thread.ID + if threadID == "" { + bindTools("") + return "", false, errors.New("codex thread/start returned no thread id") + } + bindTools(threadID) + srv.markThreadLoaded(threadID) + srv.setThreadToolless(threadID, toolsConfig == nil) + return threadID, true, nil + } + + if srv.threadLoaded(threadID) { + return threadID, false, nil + } + toolsConfig, bindTools, err := d.prepareThreadTools(srv, input) + if err != nil { + return "", false, apperror.Wrap(apperror.CodeExternalRuntimeUnavailable, err, map[string]string{"runtime": RuntimeType}) + } + params := protocol.ThreadResumeParams{ + ThreadID: threadID, + Cwd: &cwd, + ApprovalPolicy: &approvalPolicy, + Config: toolsConfig, + } + var resp protocol.ThreadResumeResponse + err = srv.conn.Call(ctx, protocol.MethodThreadResume, params, &resp) + if err == nil { + bindTools(threadID) + srv.markThreadLoaded(threadID) + srv.setThreadToolless(threadID, toolsConfig == nil) + return threadID, false, nil + } + bindTools("") + if ctx.Err() != nil { + return "", false, fmt.Errorf("codex thread/resume: %w", err) + } + // The stored thread no longer exists on the codex side (wiped state, + // version change). A session that can never run again is worse than one + // that lost its runtime-side context: fall back to a fresh thread and + // persist the new id. + d.logger.Warn("codex thread/resume failed; starting a fresh thread", + slog.String("thread_id", threadID), slog.Any("error", err)) + freshConfig, bindFresh, err2 := d.prepareThreadTools(srv, input) + if err2 != nil { + return "", false, apperror.Wrap(apperror.CodeExternalRuntimeUnavailable, err2, map[string]string{"runtime": RuntimeType}) + } + startParams := protocol.ThreadStartParams{ + Cwd: &cwd, + ApprovalPolicy: &approvalPolicy, + Config: freshConfig, + } + if cfg.Model != "" { + startParams.Model = &cfg.Model + } + var startResp protocol.ThreadStartResponse + if startErr := srv.conn.Call(ctx, protocol.MethodThreadStart, startParams, &startResp); startErr != nil { + bindFresh("") + return "", false, fmt.Errorf("codex thread/start after failed resume: %w", errors.Join(startErr, err)) + } + if startResp.Thread.ID == "" { + bindFresh("") + return "", false, errors.New("codex thread/start returned no thread id") + } + bindFresh(startResp.Thread.ID) + srv.markThreadLoaded(startResp.Thread.ID) + srv.setThreadToolless(startResp.Thread.ID, freshConfig == nil) + return startResp.Thread.ID, true, nil +} + +// interruptTurn asks the app-server to stop the running turn; it runs on a +// background context because the turn context is already cancelled. +// +//nolint:contextcheck // the turn context is cancelled; the interrupt must still go out +func (d *Driver) interruptTurn(srv *appServer, threadID, turnID string) { + if turnID == "" { + return + } + interruptCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + err := srv.conn.Call(interruptCtx, protocol.MethodTurnInterrupt, protocol.TurnInterruptParams{ + ThreadID: threadID, + TurnID: turnID, + }, nil) + if err != nil { + d.logger.Warn("codex turn/interrupt failed", slog.String("thread_id", threadID), slog.Any("error", err)) + } +} + +// botGatewayToolLookup reports whether a tool name is served by the Memoh +// tool gateway for this bot. It uses the same base session as a thread with +// no active turn, so the consent-time lookup matches the registry the +// execution will see; misses and errors report false (fail closed). +func (d *Driver) botGatewayToolLookup(botID string) func(context.Context, string) bool { + return func(ctx context.Context, toolName string) bool { + _, ok, err := d.toolGateway.Tools.LookupTool(ctx, mcp.ToolSessionContext{ + BotID: botID, + ChatID: botID, + SessionType: sessionmode.Chat, + CanListUserInput: true, + }, toolName) + if err != nil { + d.logger.Warn("codex: gateway tool lookup failed", slog.String("tool", toolName), slog.Any("error", err)) + return false + } + return ok + } +} + +// startServer is the server table's start hook. +func (d *Driver) startServer(ctx context.Context, key string) (recyclable, error) { + botID, botAgentID := splitServerKey(key) + cfg, credential, err := d.resolveAgentConfig(ctx, botID, botAgentID, false) + if err != nil { + return nil, err + } + client, err := d.bridges.MCPClient(ctx, botID) + if err != nil { + return nil, fmt.Errorf("workspace bridge for bot %s: %w", botID, err) + } + info, err := d.bridges.WorkspaceInfo(ctx, botID) + if err != nil { + return nil, fmt.Errorf("workspace info for bot %s: %w", botID, err) + } + if cfg.Auth == AuthChatGPT && credential.ID != "" { + if err := materializeChatGPTCredential(ctx, client, botAgentID, credential); err != nil { + return nil, external.CredentialError(err) + } + } + srv, err := startAppServerSession(context.WithoutCancel(ctx), botID, botAgentID, client, cfg, d.logger) + if err != nil { + return nil, err + } + srv.workspaceInfo = info + srv.toolLookup = d.botGatewayToolLookup(botID) + return srv, nil +} + +// acquireServer returns the bot's live app-server plus a release the caller +// must defer; the reference keeps the server alive across a concurrent +// recycle (which drains instead of killing). +func (d *Driver) acquireServer(ctx context.Context, botID, botAgentID string) (*appServer, func(), error) { + resource, release, err := d.servers.acquire(ctx, serverKey(botID, botAgentID)) + if err != nil { + return nil, nil, err + } + return resource.(*appServer), release, nil +} + +// CloseAll tears down every app-server (server shutdown). +func (d *Driver) CloseAll() { + d.servers.closeAll() +} + +// CloseBot recycles the bot's app-server: new work builds a fresh server on +// current configuration, while in-flight turns finish on the displaced one, +// which dies at their last release. +func (d *Driver) CloseBot(botID string) { + prefix := botID + "\x00" + d.servers.recycleWhere(func(key string) bool { return strings.HasPrefix(key, prefix) }) +} + +// ForkThread implements external.ThreadForker: it derives a new codex thread +// sharing history up to lastTurnID (inclusive; empty forks at the head) and +// returns the runtime-metadata delta naming the forked thread. The session's +// runtime metadata supplies the source thread id and working directory. +func (d *Driver) ForkThread(ctx context.Context, botID, botAgentID string, runtimeMetadata map[string]any, lastTurnID string) (map[string]any, error) { + threadID := strings.TrimSpace(metadataString(runtimeMetadata, metadataThreadIDKey)) + if threadID == "" { + return nil, apperror.Wrap(apperror.CodeExternalRuntimeUnavailable, errors.New("session has no codex thread to fork"), map[string]string{"runtime": RuntimeType}) + } + srv, releaseServer, err := d.acquireServer(ctx, botID, botAgentID) + if err != nil { + return nil, apperror.Wrap(apperror.CodeExternalRuntimeUnavailable, err, map[string]string{"runtime": RuntimeType}) + } + defer releaseServer() + cwd := strings.TrimSpace(metadataString(runtimeMetadata, "project_path")) + if cwd == "" { + cwd = defaultProjectPath + } + approvalPolicy := protocol.AskForApproval{Unit: protocol.AskForApprovalUnitOnRequest} + params := protocol.ThreadForkParams{ + ThreadID: threadID, + Cwd: &cwd, + ApprovalPolicy: &approvalPolicy, + } + if trimmed := strings.TrimSpace(lastTurnID); trimmed != "" { + params.LastTurnID = &trimmed + } + var resp protocol.ThreadForkResponse + if err := srv.conn.Call(ctx, protocol.MethodThreadFork, params, &resp); err != nil { + return nil, fmt.Errorf("codex thread/fork: %w", err) + } + if resp.Thread.ID == "" { + return nil, errors.New("codex thread/fork returned no thread id") + } + // Deliberately not marked loaded: the forked session's first prompt goes + // through thread/resume, which applies the per-thread tool-gateway config. + return map[string]any{metadataThreadIDKey: resp.Thread.ID}, nil +} + +func firstNonEmpty(values ...string) string { + for _, v := range values { + if strings.TrimSpace(v) != "" { + return strings.TrimSpace(v) + } + } + return "" +} + +// DeviceLoginStart describes a pending ChatGPT device-code login. +type DeviceLoginStart struct { + LoginID string + UserCode string + VerificationURL string +} + +// DeviceLoginStatus is the poll answer for a pending device-code login. +type DeviceLoginStatus struct { + // Status is pending, success, error, or unknown (login not tracked, e.g. + // the app-server restarted). + Status string + Error string +} + +// StartChatGPTDeviceLogin begins a device-code login on the bot's codex, +// returning the code the user must enter. Completion lands in CODEX_HOME and +// is observable via PollDeviceLogin. +func (d *Driver) StartChatGPTDeviceLogin(ctx context.Context, botID, botAgentID string) (DeviceLoginStart, error) { + cfg, _, err := d.resolveAgentConfig(ctx, botID, botAgentID, false) + if err != nil { + return DeviceLoginStart{}, err + } + if cfg.Auth != AuthChatGPT { + return DeviceLoginStart{}, external.CredentialError(agentcredential.ErrIncompatible) + } + srv, releaseServer, err := d.acquireServer(ctx, botID, botAgentID) + if err != nil { + return DeviceLoginStart{}, err + } + defer releaseServer() + var resp protocol.LoginAccountResponse + err = srv.conn.Call(ctx, protocol.MethodAccountLoginStart, protocol.LoginAccountParams{ + ChatgptDeviceCode: &protocol.ChatgptDeviceCodeLoginAccountParams{}, + }, &resp) + if err != nil { + return DeviceLoginStart{}, err + } + device := resp.ChatgptDeviceCode + if device == nil || device.LoginID == "" || device.UserCode == "" || device.VerificationURL == "" { + return DeviceLoginStart{}, errors.New("codex device login response is incomplete") + } + srv.trackLogin(device.LoginID) + return DeviceLoginStart{ + LoginID: device.LoginID, + UserCode: device.UserCode, + VerificationURL: device.VerificationURL, + }, nil +} + +// PollDeviceLogin reports a tracked login's state. +func (d *Driver) PollDeviceLogin(botID, botAgentID, loginID string) DeviceLoginStatus { + resource := d.servers.peek(serverKey(botID, botAgentID)) + if resource == nil { + return DeviceLoginStatus{Status: "unknown"} + } + srv := resource.(*appServer) + outcome, ok := srv.loginStatus(loginID) + switch { + case !ok: + return DeviceLoginStatus{Status: "unknown"} + case !outcome.Done: + return DeviceLoginStatus{Status: "pending"} + case outcome.Success: + return DeviceLoginStatus{Status: "success"} + default: + srv.forgetLogin(loginID) + message := outcome.Error + if message == "" { + message = "codex login failed" + } + return DeviceLoginStatus{Status: "error", Error: message} + } +} + +func (d *Driver) CompleteChatGPTDeviceLogin(ctx context.Context, ownerUserID, botID, botAgentID, loginID string) error { + srv, releaseServer, err := d.acquireServer(ctx, botID, botAgentID) + if err != nil { + return err + } + defer releaseServer() + outcome, ok := srv.loginStatus(loginID) + if !ok || !outcome.Done || !outcome.Success { + return errors.New("codex device login is not complete") + } + credential, err := readChatGPTCredential(ctx, srv.client, botAgentID) + if err != nil { + return err + } + _, err = d.credentials.AttachToBotAgent(ctx, ownerUserID, botID, botAgentID, agentcredential.CreateRequest{ + Provider: agentcredential.ProviderOpenAI, + AuthKind: agentcredential.AuthKindOpenAICodexOAuth, + Secret: map[string]string{ + "access_token": credential.accessToken, + "id_token": credential.idToken, + "refresh_token": credential.refreshToken, + "account_id": credential.accountID, + }, + AccountMetadata: map[string]any{ + "account_id": credential.accountID, + "last_refresh": credential.lastRefresh.UTC().Format(time.RFC3339Nano), + }, + }) + if err != nil { + return external.CredentialError(err) + } + srv.forgetLogin(loginID) + return nil +} + +func (d *Driver) PurgeBotAgentAuth(ctx context.Context, botID, botAgentID string) error { + if resource := d.servers.peek(serverKey(botID, botAgentID)); resource != nil && resource.(*appServer).hasActiveTurns() { + return apperror.New(apperror.CodeAgentCredentialRuntimeBusy, nil) + } + d.ResetBotAgent(botID, botAgentID) + client, err := d.bridges.MCPClient(ctx, botID) + if err != nil { + return err + } + err = client.DeleteFile(ctx, path.Join(codexHome(botAgentID), "auth.json"), false) + if errors.Is(err, bridge.ErrNotFound) { + return nil + } + return err +} + +// CancelDeviceLogin aborts a pending device-code login. +func (d *Driver) CancelDeviceLogin(ctx context.Context, botID, botAgentID, loginID string) error { + resource := d.servers.peek(serverKey(botID, botAgentID)) + if resource == nil { + return nil + } + srv := resource.(*appServer) + // The cancel RPC needs the connection live for its duration; peek holds + // no reference, so pin one for the call. + pinned, releaseServer, err := d.acquireServer(ctx, botID, botAgentID) + if err != nil || pinned != srv { + if err == nil { + releaseServer() + } + return nil + } + defer releaseServer() + srv.forgetLogin(loginID) + var resp protocol.CancelLoginAccountResponse + return srv.conn.Call(ctx, protocol.MethodAccountLoginCancel, protocol.CancelLoginAccountParams{LoginID: loginID}, &resp) +} diff --git a/internal/agent/runtime/codex/elicitation.go b/internal/agent/runtime/codex/elicitation.go new file mode 100644 index 0000000000..5d517cffbb --- /dev/null +++ b/internal/agent/runtime/codex/elicitation.go @@ -0,0 +1,360 @@ +// Bridges codex MCP server elicitation to Memoh's ask_user decision flow. +// Form-mode schemas reuse the shared elicitation core (the same mapping the +// ACP runtime uses); url mode renders as a single confirm card. The protocol +// carries no thread id, so the app-server routes an elicitation to the bot's +// sole active turn and declines when ownership is ambiguous. +package codex + +import ( + "context" + "encoding/json" + "log/slog" + "regexp" + "strings" + "time" + + "github.com/google/uuid" + + "github.com/felinics/memoh/internal/agent/decision/approval" + userinput "github.com/felinics/memoh/internal/agent/decision/input" + "github.com/felinics/memoh/internal/agent/event" + "github.com/felinics/memoh/internal/agent/runtime/codex/protocol" +) + +// dispatchElicitation answers one MCP elicitation. A Memoh tool-call consent +// is decided at the app-server level — it needs only the bot-scoped gateway +// lookup, so concurrent turns cannot break it. Everything that needs a user +// (forms, third-party consents) routes to the bot's sole active turn; with +// zero or several candidates the owner is unknowable and declining is the +// only safe answer. +func (s *appServer) dispatchElicitation(req *protocol.Inbound, params *protocol.McpServerElicitationRequestParams) { + if toolName, ok := autoAcceptedConsentTool(s.mountCtx, params, s.toolLookup); ok { + s.logger.Debug("codex: auto-accepting Memoh MCP tool consent; the gateway enforces the real policy", + slog.String("tool", toolName)) + _ = s.conn.Respond(req.ID, protocol.McpServerElicitationRequestResponse{ + Action: protocol.McpServerElicitationActionAccept, + }) + return + } + turn := s.soleActiveTurn() + if turn == nil { + s.logger.Warn("codex: declining MCP elicitation without a unique active turn", slog.String("mode", params.Tag)) + _ = s.conn.Respond(req.ID, protocol.McpServerElicitationRequestResponse{ + Action: protocol.McpServerElicitationActionDecline, + }) + return + } + turn.handleElicitation(s.conn, req, params) +} + +// autoAcceptedConsentTool reports whether the elicitation is a consent for a +// tool the Memoh gateway itself serves. The gateway enforces the real +// approval policy on the actual call; a codex-side card here would +// double-approve every gateway tool. This trusts the workspace-owned codex +// config not to alias a foreign server as "memoh" — anyone who can edit that +// config already runs arbitrary commands as the agent, so the codex consent +// layer is not a security boundary against them. Everything else fails +// closed to the user-facing path. +func autoAcceptedConsentTool(ctx context.Context, params *protocol.McpServerElicitationRequestParams, lookup func(context.Context, string) bool) (string, bool) { + message, _, ok := elicitationConsentEnvelope(params) + if !ok || lookup == nil { + return "", false + } + serverName, toolName, parsed := mcpConsentTarget(message) + if !parsed || serverName != memohMCPServerName || !lookup(ctx, toolName) { + return "", false + } + return toolName, true +} + +// elicitationConsentEnvelope reports whether the elicitation is a codex MCP +// tool-call consent, returning its message and _meta. +func elicitationConsentEnvelope(params *protocol.McpServerElicitationRequestParams) (string, map[string]any, bool) { + var message string + var meta map[string]any + switch params.Tag { + case protocol.McpServerElicitationRequestParamsTagForm: + if params.Form == nil { + return "", nil, false + } + message, meta = params.Form.Message, anyToSchemaMap(params.Form.Meta) + case protocol.McpServerElicitationRequestParamsTagOpenaiForm: + if params.OpenaiForm == nil { + return "", nil, false + } + message, meta = params.OpenaiForm.Message, anyToSchemaMap(params.OpenaiForm.Meta) + default: + return "", nil, false + } + if kind, _ := meta["codex_approval_kind"].(string); kind == "mcp_tool_call" { + return message, meta, true + } + return "", nil, false +} + +// soleActiveTurn returns the bot's only running turn, or nil when zero or +// several turns are active. +func (s *appServer) soleActiveTurn() *turnState { + s.mu.Lock() + defer s.mu.Unlock() + var sole *turnState + for _, turn := range s.turns { + if turn == nil { + continue + } + if sole != nil { + return nil + } + sole = turn + } + return sole +} + +// handleElicitation runs a user-facing elicitation through this turn. It +// mirrors handleServerRequest's bookkeeping: the decision is bounded by the +// turn context, registered in inflight so serverRequest/resolved can cancel +// it, and refused outright once the turn has closed. +func (t *turnState) handleElicitation(c *conn, req *protocol.Inbound, params *protocol.McpServerElicitationRequestParams) { + ctx, cancel := context.WithCancel(t.ctx) + defer cancel() + key := req.ID.Key() + t.mu.Lock() + if t.closed { + t.mu.Unlock() + _ = c.Respond(req.ID, protocol.McpServerElicitationRequestResponse{ + Action: protocol.McpServerElicitationActionCancel, + }) + return + } + t.inflight[key] = cancel + t.mu.Unlock() + defer func() { + t.mu.Lock() + delete(t.inflight, key) + t.mu.Unlock() + }() + _ = c.Respond(req.ID, t.runElicitation(ctx, params)) +} + +func (t *turnState) runElicitation(ctx context.Context, params *protocol.McpServerElicitationRequestParams) protocol.McpServerElicitationRequestResponse { + decline := protocol.McpServerElicitationRequestResponse{Action: protocol.McpServerElicitationActionDecline} + cancel := protocol.McpServerElicitationRequestResponse{Action: protocol.McpServerElicitationActionCancel} + if t == nil || t.userInput == nil || !t.input.CanRequestUserInput { + return decline + } + + var message string + var schema map[string]any + var meta map[string]any + switch params.Tag { + case protocol.McpServerElicitationRequestParamsTagForm: + if params.Form == nil { + return decline + } + message = params.Form.Message + schema = anyToSchemaMap(params.Form.RequestedSchema) + meta = anyToSchemaMap(params.Form.Meta) + case protocol.McpServerElicitationRequestParamsTagOpenaiForm: + if params.OpenaiForm == nil { + return decline + } + message = params.OpenaiForm.Message + schema = anyToSchemaMap(params.OpenaiForm.RequestedSchema) + meta = anyToSchemaMap(params.OpenaiForm.Meta) + case protocol.McpServerElicitationRequestParamsTagURL: + if params.URL == nil { + return decline + } + return t.runURLElicitation(ctx, params.URL) + default: + t.logger.Warn("codex: declining MCP elicitation with unsupported mode", slog.String("mode", params.Tag)) + t.emitElicitationDeclinedNotice("the requested interaction format is not supported") + return decline + } + + // Codex marks MCP tool-call consent in _meta. That shape is permission, + // not data: it routes through the approval path, never the form mapper + // (whose empty-properties schema it would fail anyway). + if approvalKind, _ := meta["codex_approval_kind"].(string); approvalKind == "mcp_tool_call" { + return t.runMCPToolConsent(ctx, message, meta) + } + if schema == nil { + t.logger.Warn("codex: declining MCP elicitation with unreadable schema", slog.String("mode", params.Tag)) + t.emitElicitationDeclinedNotice("the requested form cannot be read") + return decline + } + + input, mapping, err := userinput.ElicitationFormInput(message, schema) + if err != nil { + t.logger.Warn("codex: declining unsupported MCP elicitation form", + slog.String("mode", params.Tag), slog.Any("error", err)) + t.emitElicitationDeclinedNotice("the requested form cannot be rendered safely") + return decline + } + flow, ok := t.runElicitationFlow(ctx, input) + if !ok { + return cancel + } + switch flow.Status { + case userinput.StatusSubmitted: + content, err := mapping.Content(flow) + if err != nil { + t.logger.Warn("codex: elicitation answers did not satisfy the form schema", slog.Any("error", err)) + return cancel + } + return protocol.McpServerElicitationRequestResponse{ + Action: protocol.McpServerElicitationActionAccept, + Content: content, + } + case userinput.StatusCanceled: + if reason, _ := flow.Result["reason"].(string); strings.TrimSpace(reason) == "user_canceled" { + return decline + } + return cancel + default: + return cancel + } +} + +// mcpConsentTarget extracts the server and tool a consent asks about from +// codex's consent message. The template is hard-coded in the pinned codex +// release ("Allow the MCP server to run tool \"\"?"); a +// non-matching message reports no target and the consent falls through to +// the user's approval card (fail closed, never a false allow). +var mcpConsentMessagePattern = regexp.MustCompile(`^Allow the (\S+) MCP server to run tool "([^"]+)"\?$`) + +func mcpConsentTarget(message string) (serverName, toolName string, ok bool) { + match := mcpConsentMessagePattern.FindStringSubmatch(strings.TrimSpace(message)) + if match == nil { + return "", "", false + } + return match[1], match[2], true +} + +// runMCPToolConsent asks the user about an MCP tool-call consent the +// dispatch layer could not auto-accept — unknown servers, unknown tools, +// unparseable messages — as a permission approval. +func (t *turnState) runMCPToolConsent(ctx context.Context, message string, meta map[string]any) protocol.McpServerElicitationRequestResponse { + decline := protocol.McpServerElicitationRequestResponse{Action: protocol.McpServerElicitationActionDecline} + cancel := protocol.McpServerElicitationRequestResponse{Action: protocol.McpServerElicitationActionCancel} + + input := map[string]any{"message": strings.TrimSpace(message)} + if params, ok := meta["tool_params"].(map[string]any); ok && len(params) > 0 { + input["tool_params"] = params + } + if description, ok := meta["tool_description"].(string); ok && strings.TrimSpace(description) != "" { + input["tool_description"] = strings.TrimSpace(description) + } + result := t.decide(ctx, "codex-consent-"+uuid.NewString(), "permission", input, nil) + switch { + case result.Approved: + return protocol.McpServerElicitationRequestResponse{Action: protocol.McpServerElicitationActionAccept} + case strings.EqualFold(result.Status, approval.StatusRejected): + return decline + default: + return cancel + } +} + +// runURLElicitation asks the user to complete an out-of-band browser step. +// The MCP url mode carries no form content; the response is accept once the +// user confirms, decline when they cancel. +func (t *turnState) runURLElicitation(ctx context.Context, params *protocol.URLMcpServerElicitationRequestParams) protocol.McpServerElicitationRequestResponse { + decline := protocol.McpServerElicitationRequestResponse{Action: protocol.McpServerElicitationActionDecline} + url := strings.TrimSpace(params.URL) + if url == "" { + return decline + } + text := strings.TrimSpace(params.Message) + if text == "" { + text = "The agent needs you to complete a step in your browser" + } + input := map[string]any{"questions": []map[string]any{{ + "text": text + ": " + url, + "kind": userinput.QuestionKindSingleSelect, + "options": []map[string]any{ + {"label": "Done", "description": "I completed the step"}, + {"label": "Cancel", "description": "Do not continue"}, + }, + }}} + flow, ok := t.runElicitationFlow(ctx, input) + if !ok || flow.Status != userinput.StatusSubmitted { + return decline + } + answers := userinput.AnswersFromResult(flow.Result) + if len(answers) == 1 && len(answers[0].Selected) == 1 && answers[0].Selected[0].Label == "Done" { + return protocol.McpServerElicitationRequestResponse{Action: protocol.McpServerElicitationActionAccept} + } + return decline +} + +func (t *turnState) runElicitationFlow(ctx context.Context, input map[string]any) (userinput.Request, bool) { + expiresAt := time.Now().Add(userinput.DefaultWaitTimeout + time.Minute) + flow, err := userinput.RunFlow(ctx, t.userInput, userinput.FlowRequest{ + Input: userinput.CreatePendingInput{ + BotID: t.input.BotID, + SessionID: t.input.ThreadID, + RouteID: t.input.RouteID, + ChannelIdentityID: t.input.ChannelIdentityID, + RequestedByChannelIdentityID: t.input.ChannelIdentityID, + ToolCallID: "codex-elicitation-" + uuid.NewString(), + ToolName: userinput.ToolNameAskUser, + Input: input, + ProviderMetadata: map[string]any{ + "source": userinput.ProviderSourceCodexElicitation, + "thread_id": t.threadID, + "run_id": t.input.RunID, + }, + SourcePlatform: t.input.CurrentPlatform, + ReplyTarget: t.input.ReplyTarget, + ConversationType: t.input.ConversationType, + ExpiresAt: &expiresAt, + }, + ActorChannelIdentityID: t.input.ChannelIdentityID, + // The non-interactive case was rejected at runElicitation's entry. + Interactive: true, + WaitTimeout: userinput.DefaultWaitTimeout, + Emit: t.emitUserInputRequest, + NonInteractiveReason: "codex MCP elicitation requested user input without an interactive stream", + UndeliveredReason: "codex MCP elicitation was not delivered to the interactive stream", + TimeoutReason: "codex MCP elicitation timed out", + AbortReason: "codex MCP elicitation aborted", + }) + if err != nil { + if ctx.Err() == nil { + t.logger.Error("codex MCP elicitation flow failed", slog.String("thread_id", t.threadID), slog.Any("error", err)) + } + return userinput.Request{}, false + } + return flow.Request, true +} + +// emitElicitationDeclinedNotice surfaces a declined MCP elicitation in the +// conversation so the user knows a tool asked for input Memoh could not show. +func (t *turnState) emitElicitationDeclinedNotice(reason string) { + t.emit(event.StreamEvent{ + Type: event.RuntimeNotice, + Code: "elicitation_declined", + Delta: "A tool asked for user input that could not be shown: " + reason, + }) +} + +// anyToSchemaMap coerces a decoded protocol schema (typed struct or free-form +// value) into the generic JSON-schema map the shared elicitation core reads. +func anyToSchemaMap(value any) map[string]any { + if value == nil { + return nil + } + if m, ok := value.(map[string]any); ok { + return m + } + raw, err := json.Marshal(value) + if err != nil { + return nil + } + var out map[string]any + if err := json.Unmarshal(raw, &out); err != nil || out == nil { + return nil + } + return out +} diff --git a/internal/agent/runtime/codex/lifecycle.go b/internal/agent/runtime/codex/lifecycle.go new file mode 100644 index 0000000000..eb7564d3af --- /dev/null +++ b/internal/agent/runtime/codex/lifecycle.go @@ -0,0 +1,227 @@ +package codex + +import ( + "context" + "errors" + "log/slog" + "sync" +) + +// recyclable is the resource shape the server table manages: a process-backed +// handle that can be torn down and reports its own exit. +type recyclable interface { + Close() error + Done() <-chan struct{} +} + +// serverTable owns the lifecycle of one shared resource per bot. It exists +// because the app-server is used concurrently (turns, model catalog, login) +// and recycled asynchronously (configuration updates, wedged-turn recovery), +// and ad-hoc checks over a bare map kept racing each other: kill-by-bot +// aborted sibling turns, an is-anyone-else-active check raced registration, +// and a recycle could miss a server still starting outside the lock. +// +// The model is reference counting plus displacement: +// +// - acquire takes the current entry under the lock and holds a reference +// for the whole use, so a user is visible from the moment it commits to +// the server — there is no window between "decided to use it" and +// "registered". +// - recycle displaces the entry from the table. New acquires build a fresh +// server (a new generation); existing references keep the displaced +// server alive until the last release, which tears it down. A wedged +// turn is thereby confined to a dying process instead of forcing a +// choice between killing siblings and sharing the process forever. +// - a server still starting when displaced discovers it on completion and +// destroys itself; the acquire retries and rebuilds from current state. +type serverTable struct { + mu sync.Mutex + entries map[string]*serverEntry + start func(ctx context.Context, botID string) (recyclable, error) + logger *slog.Logger +} + +type serverEntry struct { + resource recyclable + refs int + draining bool + starting bool + ready chan struct{} +} + +func newServerTable(start func(ctx context.Context, botID string) (recyclable, error), logger *slog.Logger) *serverTable { + return &serverTable{ + entries: map[string]*serverEntry{}, + start: start, + logger: logger, + } +} + +var errServerDisplaced = errors.New("codex app-server was recycled during startup") + +// acquire returns the bot's live server and a release the caller must invoke +// when done with it (idempotent). It waits for a concurrent startup, retries +// once when a startup loses to a recycle, and never returns a displaced or +// dead server. +func (t *serverTable) acquire(ctx context.Context, botID string) (recyclable, func(), error) { + for attempt := 0; attempt < 3; attempt++ { + t.mu.Lock() + entry := t.entries[botID] + if entry != nil { + if entry.starting { + ready := entry.ready + t.mu.Unlock() + select { + case <-ready: + case <-ctx.Done(): + return nil, nil, ctx.Err() + } + continue + } + if entry.resource != nil && resourceAlive(entry.resource) { + entry.refs++ + t.mu.Unlock() + return entry.resource, t.releaseFunc(entry), nil + } + // A dead remnant the exit reaper has not collected yet. + if t.entries[botID] == entry { + delete(t.entries, botID) + } + } + placeholder := &serverEntry{starting: true, ready: make(chan struct{})} + t.entries[botID] = placeholder + t.mu.Unlock() + + resource, err := t.start(ctx, botID) + t.mu.Lock() + displaced := t.entries[botID] != placeholder || placeholder.draining + if err != nil { + if t.entries[botID] == placeholder { + delete(t.entries, botID) + } + close(placeholder.ready) + t.mu.Unlock() + return nil, nil, err + } + if displaced { + // Recycled mid-startup (a configuration update, a shutdown): this + // server was built from pre-recycle state and must not serve. + close(placeholder.ready) + t.mu.Unlock() + _ = resource.Close() + continue + } + placeholder.resource = resource + placeholder.starting = false + placeholder.refs = 1 + close(placeholder.ready) + t.mu.Unlock() + go t.reap(botID, placeholder, resource) + return resource, t.releaseFunc(placeholder), nil + } + return nil, nil, errServerDisplaced +} + +func (t *serverTable) releaseFunc(entry *serverEntry) func() { + var once sync.Once + return func() { + once.Do(func() { + t.mu.Lock() + entry.refs-- + closeNow := entry.draining && entry.refs == 0 && entry.resource != nil + t.mu.Unlock() + if closeNow { + _ = entry.resource.Close() + } + }) + } +} + +// recycle displaces the bot's entry: new acquires build a fresh server, and +// the displaced one dies when idle — immediately if nothing holds it, at the +// last release otherwise, or on startup completion if it was still starting. +func (t *serverTable) recycle(botID string) { + t.mu.Lock() + entry := t.entries[botID] + if entry == nil { + t.mu.Unlock() + return + } + delete(t.entries, botID) + entry.draining = true + closeNow := !entry.starting && entry.refs == 0 && entry.resource != nil + resource := entry.resource + t.mu.Unlock() + if closeNow { + _ = resource.Close() + } +} + +func (t *serverTable) recycleWhere(match func(string) bool) { + t.mu.Lock() + resources := make([]recyclable, 0) + for key, entry := range t.entries { + if !match(key) { + continue + } + delete(t.entries, key) + entry.draining = true + if !entry.starting && entry.refs == 0 && entry.resource != nil { + resources = append(resources, entry.resource) + } + } + t.mu.Unlock() + for _, resource := range resources { + _ = resource.Close() + } +} + +// peek returns the bot's current live server without creating one and +// without taking a reference; callers may only touch in-memory state. +func (t *serverTable) peek(botID string) recyclable { + t.mu.Lock() + defer t.mu.Unlock() + entry := t.entries[botID] + if entry == nil || entry.starting || entry.resource == nil || !resourceAlive(entry.resource) { + return nil + } + return entry.resource +} + +// closeAll hard-tears every entry down for process shutdown. Servers still +// starting discover their displacement on completion and self-destroy. +func (t *serverTable) closeAll() { + t.mu.Lock() + entries := make([]*serverEntry, 0, len(t.entries)) + for _, entry := range t.entries { + entry.draining = true + entries = append(entries, entry) + } + t.entries = map[string]*serverEntry{} + t.mu.Unlock() + for _, entry := range entries { + if entry.resource != nil { + _ = entry.resource.Close() + } + } +} + +// reap collects the entry when its process exits on its own. +func (t *serverTable) reap(botID string, entry *serverEntry, resource recyclable) { + <-resource.Done() + t.mu.Lock() + if t.entries[botID] == entry { + delete(t.entries, botID) + } + t.mu.Unlock() + t.logger.Info("codex app-server exited", slog.String("bot_id", botID)) +} + +func resourceAlive(resource recyclable) bool { + select { + case <-resource.Done(): + return false + default: + return true + } +} diff --git a/internal/agent/runtime/codex/lifecycle_test.go b/internal/agent/runtime/codex/lifecycle_test.go new file mode 100644 index 0000000000..ebfcd1a7d5 --- /dev/null +++ b/internal/agent/runtime/codex/lifecycle_test.go @@ -0,0 +1,230 @@ +package codex + +import ( + "context" + "errors" + "log/slog" + "sync" + "sync/atomic" + "testing" + "time" +) + +type fakeResource struct { + id int + done chan struct{} + closed atomic.Bool +} + +func newFakeResource(id int) *fakeResource { + return &fakeResource{id: id, done: make(chan struct{})} +} + +func (f *fakeResource) Close() error { + if f.closed.CompareAndSwap(false, true) { + close(f.done) + } + return nil +} + +func (f *fakeResource) Done() <-chan struct{} { return f.done } + +type fakeStarter struct { + mu sync.Mutex + next int + started []*fakeResource + // block, when set, is closed by the test to let a startup finish. + block chan struct{} + err error +} + +func (s *fakeStarter) start(context.Context, string) (recyclable, error) { + s.mu.Lock() + block := s.block + err := s.err + s.next++ + resource := newFakeResource(s.next) + if err == nil { + s.started = append(s.started, resource) + } + s.mu.Unlock() + if block != nil { + <-block + } + if err != nil { + return nil, err + } + return resource, nil +} + +func TestServerTableSharesOneServerAcrossAcquires(t *testing.T) { + starter := &fakeStarter{} + table := newServerTable(starter.start, slog.Default()) + + a, releaseA, err := table.acquire(context.Background(), "bot") + if err != nil { + t.Fatalf("acquire a: %v", err) + } + b, releaseB, err := table.acquire(context.Background(), "bot") + if err != nil { + t.Fatalf("acquire b: %v", err) + } + if a != b { + t.Fatal("two acquires built two servers") + } + releaseA() + releaseB() + if a.(*fakeResource).closed.Load() { + t.Fatal("release closed a server that was never recycled") + } +} + +// The core of the fix: recycling while a sibling holds the server must not +// kill it under the sibling — the server drains and dies at the last release, +// while a new acquire gets a fresh generation immediately. +func TestServerTableRecycleDrainsInsteadOfKilling(t *testing.T) { + starter := &fakeStarter{} + table := newServerTable(starter.start, slog.Default()) + + old, releaseOld, err := table.acquire(context.Background(), "bot") + if err != nil { + t.Fatalf("acquire old: %v", err) + } + table.recycle("bot") + if old.(*fakeResource).closed.Load() { + t.Fatal("recycle killed a server a sibling still holds") + } + + fresh, releaseFresh, err := table.acquire(context.Background(), "bot") + if err != nil { + t.Fatalf("acquire fresh: %v", err) + } + if fresh == old { + t.Fatal("acquire after recycle returned the draining server") + } + releaseOld() + if !old.(*fakeResource).closed.Load() { + t.Fatal("last release did not close the drained server") + } + releaseFresh() + if fresh.(*fakeResource).closed.Load() { + t.Fatal("fresh generation closed without being recycled") + } +} + +func TestServerTableRecycleClosesIdleServerImmediately(t *testing.T) { + starter := &fakeStarter{} + table := newServerTable(starter.start, slog.Default()) + srv, release, err := table.acquire(context.Background(), "bot") + if err != nil { + t.Fatalf("acquire: %v", err) + } + release() + table.recycle("bot") + if !srv.(*fakeResource).closed.Load() { + t.Fatal("recycle left an idle server running") + } +} + +// A server displaced while still starting must destroy itself and the +// acquire must rebuild from post-recycle state — the configuration-update +// escape hatch this table exists to close. +func TestServerTableDisplacesServerStillStarting(t *testing.T) { + starter := &fakeStarter{block: make(chan struct{})} + table := newServerTable(starter.start, slog.Default()) + + got := make(chan recyclable, 1) + go func() { + srv, release, err := table.acquire(context.Background(), "bot") + if err != nil { + got <- nil + return + } + defer release() + got <- srv + }() + + // Wait for the placeholder, then recycle while startup is blocked. + deadline := time.Now().Add(2 * time.Second) + for { + table.mu.Lock() + entry := table.entries["bot"] + starting := entry != nil && entry.starting + table.mu.Unlock() + if starting { + break + } + if time.Now().After(deadline) { + t.Fatal("startup placeholder never appeared") + } + time.Sleep(time.Millisecond) + } + table.recycle("bot") + close(starter.block) + + srv := <-got + if srv == nil { + t.Fatal("acquire failed after displacement") + } + starter.mu.Lock() + first := starter.started[0] + total := len(starter.started) + starter.mu.Unlock() + if total < 2 { + t.Fatalf("started %d servers, want the displaced one plus a rebuild", total) + } + if srv == first { + t.Fatal("acquire returned the displaced pre-recycle server") + } + if !first.closed.Load() { + t.Fatal("displaced server was not self-destroyed") + } +} + +func TestServerTableStartFailureIsReturnedAndClearsPlaceholder(t *testing.T) { + starter := &fakeStarter{err: errors.New("bridge unavailable")} + table := newServerTable(starter.start, slog.Default()) + if _, _, err := table.acquire(context.Background(), "bot"); err == nil { + t.Fatal("acquire swallowed a startup failure") + } + starter.mu.Lock() + starter.err = nil + starter.mu.Unlock() + if _, release, err := table.acquire(context.Background(), "bot"); err != nil { + t.Fatalf("acquire after failure: %v", err) + } else { + release() + } +} + +func TestServerTableReapsExitedProcess(t *testing.T) { + starter := &fakeStarter{} + table := newServerTable(starter.start, slog.Default()) + srv, release, err := table.acquire(context.Background(), "bot") + if err != nil { + t.Fatalf("acquire: %v", err) + } + release() + _ = srv.(*fakeResource).Close() // simulate the process dying on its own + deadline := time.Now().Add(2 * time.Second) + for { + table.mu.Lock() + _, present := table.entries["bot"] + table.mu.Unlock() + if !present { + break + } + if time.Now().After(deadline) { + t.Fatal("dead server was never reaped") + } + time.Sleep(time.Millisecond) + } + fresh, releaseFresh, err := table.acquire(context.Background(), "bot") + if err != nil { + t.Fatalf("acquire after death: %v", err) + } + if fresh == srv { + t.Fatal("acquire returned a dead server") + } + releaseFresh() +} diff --git a/internal/agent/runtime/codex/process.go b/internal/agent/runtime/codex/process.go new file mode 100644 index 0000000000..63bddcef9d --- /dev/null +++ b/internal/agent/runtime/codex/process.go @@ -0,0 +1,33 @@ +package codex + +import ( + "context" + "fmt" + "strings" + + "github.com/felinics/memoh/internal/agent/runtime/agentprocess" + "github.com/felinics/memoh/internal/workspace/bridge" +) + +const containerPath = "/opt/memoh/toolkit/bin:/usr/local/bin:/usr/bin:/bin" + +type appServerProcess = agentprocess.Process + +func startAppServer(ctx context.Context, client *bridge.Client, workDir, home string, cfg Config) (*appServerProcess, error) { + workDir = strings.TrimSpace(workDir) + if workDir == "" { + workDir = defaultProjectPath + } + if err := client.Mkdir(ctx, home); err != nil { + return nil, fmt.Errorf("create codex home %s: %w", home, err) + } + env := []string{ + "CODEX_HOME=" + home, + "PATH=" + containerPath, + "RUST_LOG=error", + } + if cfg.Auth == AuthAPIKey && cfg.BaseURL != "" { + env = append(env, "OPENAI_BASE_URL="+cfg.BaseURL) + } + return agentprocess.Start(ctx, client, launcherPath+" app-server", workDir, env) +} diff --git a/internal/agent/runtime/codex/protocol/methods.gen.go b/internal/agent/runtime/codex/protocol/methods.gen.go new file mode 100644 index 0000000000..52f04d42cc --- /dev/null +++ b/internal/agent/runtime/codex/protocol/methods.gen.go @@ -0,0 +1,238 @@ +// Code generated by gen-codex-protocol from the codex app-server v2 JSON +// Schema snapshot (codex-cli 0.151.0). DO NOT EDIT. +// +// Regenerate with `mise run codex-protocol-generate`; refresh the snapshot +// itself with `mise run codex-schema-sync`. + +package protocol + +// PinnedCodexVersion is the codex CLI version the vendored schema snapshot +// (and therefore these generated types) corresponds to. +const PinnedCodexVersion = "0.151.0" + +// Client request methods (Memoh → app-server). +const ( + MethodInitialize = "initialize" + MethodThreadStart = "thread/start" + MethodThreadResume = "thread/resume" + MethodThreadFork = "thread/fork" + MethodThreadRead = "thread/read" + MethodThreadCompactStart = "thread/compact/start" + MethodTurnStart = "turn/start" + MethodTurnSteer = "turn/steer" + MethodTurnInterrupt = "turn/interrupt" + MethodModelList = "model/list" + MethodAccountRead = "account/read" + MethodAccountRateLimitsRead = "account/rateLimits/read" + MethodAccountLoginStart = "account/login/start" + MethodAccountLoginCancel = "account/login/cancel" + MethodAccountLogout = "account/logout" +) + +// Server request methods (app-server → Memoh, expect a response). +const ( + MethodItemCommandExecutionRequestApproval = "item/commandExecution/requestApproval" + MethodItemFileChangeRequestApproval = "item/fileChange/requestApproval" + MethodItemPermissionsRequestApproval = "item/permissions/requestApproval" + MethodItemToolRequestUserInput = "item/tool/requestUserInput" + MethodMCPServerElicitationRequest = "mcpServer/elicitation/request" + MethodAccountChatgptAuthTokensRefresh = "account/chatgptAuthTokens/refresh" +) + +// Server notification methods decoded into typed params. +const ( + MethodError = "error" + MethodWarning = "warning" + MethodConfigWarning = "configWarning" + MethodDeprecationNotice = "deprecationNotice" + MethodThreadStarted = "thread/started" + MethodThreadStatusChanged = "thread/status/changed" + MethodThreadTokenUsageUpdated = "thread/tokenUsage/updated" + MethodThreadCompacted = "thread/compacted" + MethodTurnStarted = "turn/started" + MethodTurnCompleted = "turn/completed" + MethodTurnPlanUpdated = "turn/plan/updated" + MethodItemStarted = "item/started" + MethodItemCompleted = "item/completed" + MethodItemAgentMessageDelta = "item/agentMessage/delta" + MethodItemReasoningTextDelta = "item/reasoning/textDelta" + MethodItemReasoningSummaryTextDelta = "item/reasoning/summaryTextDelta" + MethodItemReasoningSummaryPartAdded = "item/reasoning/summaryPartAdded" + MethodItemCommandExecutionOutputDelta = "item/commandExecution/outputDelta" + MethodItemFileChangeOutputDelta = "item/fileChange/outputDelta" + MethodAccountUpdated = "account/updated" + MethodAccountLoginCompleted = "account/login/completed" + MethodAccountRateLimitsUpdated = "account/rateLimits/updated" + MethodServerRequestResolved = "serverRequest/resolved" +) + +// NewResponseForMethod returns a pointer to the zero response value for a +// client request method, ready for unmarshaling. ok is false for methods +// whose responses are not generated (initialize) or unknown methods. +func NewResponseForMethod(method string) (resp any, ok bool) { + switch method { + case "thread/start": + return new(ThreadStartResponse), true + case "thread/resume": + return new(ThreadResumeResponse), true + case "thread/fork": + return new(ThreadForkResponse), true + case "thread/read": + return new(ThreadReadResponse), true + case "thread/compact/start": + return new(ThreadCompactStartResponse), true + case "turn/start": + return new(TurnStartResponse), true + case "turn/steer": + return new(TurnSteerResponse), true + case "turn/interrupt": + return new(TurnInterruptResponse), true + case "model/list": + return new(ModelListResponse), true + case "account/read": + return new(GetAccountResponse), true + case "account/rateLimits/read": + return new(GetAccountRateLimitsResponse), true + case "account/login/start": + return new(LoginAccountResponse), true + case "account/login/cancel": + return new(CancelLoginAccountResponse), true + case "account/logout": + return new(LogoutAccountResponse), true + } + return nil, false +} + +// DecodeServerRequestParams decodes the params of an app-server → Memoh +// request into its generated type. ok is false for unknown methods; the +// caller keeps the raw envelope in that case. +func DecodeServerRequestParams(method string, params []byte) (decoded any, ok bool, err error) { + switch method { + case "item/commandExecution/requestApproval": + v := new(CommandExecutionRequestApprovalParams) + err = jsonUnmarshal(params, v) + return v, true, err + case "item/fileChange/requestApproval": + v := new(FileChangeRequestApprovalParams) + err = jsonUnmarshal(params, v) + return v, true, err + case "item/permissions/requestApproval": + v := new(PermissionsRequestApprovalParams) + err = jsonUnmarshal(params, v) + return v, true, err + case "item/tool/requestUserInput": + v := new(ToolRequestUserInputParams) + err = jsonUnmarshal(params, v) + return v, true, err + case "mcpServer/elicitation/request": + v := new(McpServerElicitationRequestParams) + err = jsonUnmarshal(params, v) + return v, true, err + case "account/chatgptAuthTokens/refresh": + v := new(ChatgptAuthTokensRefreshParams) + err = jsonUnmarshal(params, v) + return v, true, err + } + return nil, false, nil +} + +// DecodeServerNotificationParams decodes the params of an app-server +// notification into its generated type. ok is false for methods outside the +// typed subset; those still surface to the caller as raw envelopes. +func DecodeServerNotificationParams(method string, params []byte) (decoded any, ok bool, err error) { + switch method { + case "error": + v := new(ErrorNotification) + err = jsonUnmarshal(params, v) + return v, true, err + case "warning": + v := new(WarningNotification) + err = jsonUnmarshal(params, v) + return v, true, err + case "configWarning": + v := new(ConfigWarningNotification) + err = jsonUnmarshal(params, v) + return v, true, err + case "deprecationNotice": + v := new(DeprecationNoticeNotification) + err = jsonUnmarshal(params, v) + return v, true, err + case "thread/started": + v := new(ThreadStartedNotification) + err = jsonUnmarshal(params, v) + return v, true, err + case "thread/status/changed": + v := new(ThreadStatusChangedNotification) + err = jsonUnmarshal(params, v) + return v, true, err + case "thread/tokenUsage/updated": + v := new(ThreadTokenUsageUpdatedNotification) + err = jsonUnmarshal(params, v) + return v, true, err + case "thread/compacted": + v := new(ContextCompactedNotification) + err = jsonUnmarshal(params, v) + return v, true, err + case "turn/started": + v := new(TurnStartedNotification) + err = jsonUnmarshal(params, v) + return v, true, err + case "turn/completed": + v := new(TurnCompletedNotification) + err = jsonUnmarshal(params, v) + return v, true, err + case "turn/plan/updated": + v := new(TurnPlanUpdatedNotification) + err = jsonUnmarshal(params, v) + return v, true, err + case "item/started": + v := new(ItemStartedNotification) + err = jsonUnmarshal(params, v) + return v, true, err + case "item/completed": + v := new(ItemCompletedNotification) + err = jsonUnmarshal(params, v) + return v, true, err + case "item/agentMessage/delta": + v := new(AgentMessageDeltaNotification) + err = jsonUnmarshal(params, v) + return v, true, err + case "item/reasoning/textDelta": + v := new(ReasoningTextDeltaNotification) + err = jsonUnmarshal(params, v) + return v, true, err + case "item/reasoning/summaryTextDelta": + v := new(ReasoningSummaryTextDeltaNotification) + err = jsonUnmarshal(params, v) + return v, true, err + case "item/reasoning/summaryPartAdded": + v := new(ReasoningSummaryPartAddedNotification) + err = jsonUnmarshal(params, v) + return v, true, err + case "item/commandExecution/outputDelta": + v := new(CommandExecutionOutputDeltaNotification) + err = jsonUnmarshal(params, v) + return v, true, err + case "item/fileChange/outputDelta": + v := new(FileChangeOutputDeltaNotification) + err = jsonUnmarshal(params, v) + return v, true, err + case "account/updated": + v := new(AccountUpdatedNotification) + err = jsonUnmarshal(params, v) + return v, true, err + case "account/login/completed": + v := new(AccountLoginCompletedNotification) + err = jsonUnmarshal(params, v) + return v, true, err + case "account/rateLimits/updated": + v := new(AccountRateLimitsUpdatedNotification) + err = jsonUnmarshal(params, v) + return v, true, err + case "serverRequest/resolved": + v := new(ServerRequestResolvedNotification) + err = jsonUnmarshal(params, v) + return v, true, err + } + return nil, false, nil +} diff --git a/internal/agent/runtime/codex/protocol/protocol.go b/internal/agent/runtime/codex/protocol/protocol.go new file mode 100644 index 0000000000..48a26ad951 --- /dev/null +++ b/internal/agent/runtime/codex/protocol/protocol.go @@ -0,0 +1,253 @@ +// Package protocol implements the wire contract Memoh speaks with the codex +// app-server: JSON-RPC 2.0 semantics over newline-delimited JSON on stdio, +// with the `jsonrpc` version field omitted, as the app-server does. +// +// The typed protocol surface (*.gen.go) is generated from the vendored JSON +// Schema snapshot by cmd/gen-codex-protocol; this file holds the +// version-independent core: envelopes, request ids, inbound classification, +// and the union runtime helpers the generated code relies on. +// +// Decoding is deliberately tolerant: unknown methods, notification kinds, and +// union variants never fail — they surface with their raw bytes retained — so +// a pinned CLI upgrade can only add information, not break the stream. +package protocol + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "reflect" + "strconv" +) + +// RequestID mirrors the app-server RequestId, which is a string or a number. +// The original JSON representation is preserved so ids echo back +// byte-for-byte: 42 must not become "42". +type RequestID struct { + raw json.RawMessage +} + +// NewRequestID returns a numeric request id. +func NewRequestID(n uint64) RequestID { + return RequestID{raw: json.RawMessage(strconv.FormatUint(n, 10))} +} + +func (id RequestID) MarshalJSON() ([]byte, error) { + if len(id.raw) == 0 { + return nil, errors.New("codex protocol: marshaling zero RequestID") + } + return id.raw, nil +} + +func (id *RequestID) UnmarshalJSON(data []byte) error { + // Fresh allocation: value copies of a RequestID must not be rewritten by + // a later decode into the original variable. + id.raw = append(json.RawMessage(nil), data...) + return nil +} + +// IsZero reports whether the id is unset. +func (id RequestID) IsZero() bool { return len(id.raw) == 0 } + +// Key returns the exact wire representation, usable as a correlation map key. +func (id RequestID) Key() string { return string(id.raw) } + +func (id RequestID) String() string { return string(id.raw) } + +// Request is an outbound Memoh → app-server request. +type Request struct { + ID RequestID `json:"id"` + Method string `json:"method"` + Params any `json:"params,omitempty"` +} + +func (r Request) MarshalJSON() ([]byte, error) { + type plain Request + p := plain(r) + p.Params = normalizeParams(p.Params) + return json.Marshal(p) +} + +// Notification is an outbound Memoh → app-server notification. +type Notification struct { + Method string `json:"method"` + Params any `json:"params,omitempty"` +} + +func (n Notification) MarshalJSON() ([]byte, error) { + type plain Notification + p := plain(n) + p.Params = normalizeParams(p.Params) + return json.Marshal(p) +} + +// normalizeParams unwraps a typed nil (a nil pointer/map/slice inside a +// non-nil interface) to a plain nil so `params,omitempty` actually omits it +// instead of sending `"params": null`, which serde rejects for required +// params. +func normalizeParams(params any) any { + if params == nil { + return nil + } + v := reflect.ValueOf(params) + switch v.Kind() { + case reflect.Pointer, reflect.Map, reflect.Slice, reflect.Interface: + if v.IsNil() { + return nil + } + } + return params +} + +// Response answers an app-server → Memoh request. +type Response struct { + ID RequestID `json:"id"` + Result any `json:"result"` +} + +// ErrorResponse rejects an app-server → Memoh request. +type ErrorResponse struct { + ID RequestID `json:"id"` + Error *RPCError `json:"error"` +} + +// RPCError is the JSON-RPC error object. Codes are not enumerated by the +// schema; treat them as opaque and rely on CodexErrorInfo where present. +type RPCError struct { + Code int64 `json:"code"` + Message string `json:"message"` + Data json.RawMessage `json:"data,omitempty"` +} + +func (e *RPCError) Error() string { + return fmt.Sprintf("codex app-server error %d: %s", e.Code, e.Message) +} + +// InboundKind classifies a line received from the app-server. +type InboundKind int + +const ( + // InboundResponse answers one of our requests. + InboundResponse InboundKind = iota + 1 + // InboundRequest is a server → client request that expects a Response. + InboundRequest + // InboundNotification is fire-and-forget. + InboundNotification +) + +// Inbound is one decoded line from the app-server stream. +type Inbound struct { + Kind InboundKind + ID RequestID // requests and responses + Method string // requests and notifications + Params json.RawMessage // requests and notifications + Result json.RawMessage // successful responses + Err *RPCError // failed responses + Raw json.RawMessage // the full original line +} + +// DecodeInbound classifies one NDJSON line from the app-server. +func DecodeInbound(line []byte) (*Inbound, error) { + var probe struct { + ID json.RawMessage `json:"id"` + Method string `json:"method"` + Params json.RawMessage `json:"params"` + Result json.RawMessage `json:"result"` + Error *RPCError `json:"error"` + } + if err := json.Unmarshal(line, &probe); err != nil { + return nil, fmt.Errorf("codex protocol: decoding inbound line: %w", err) + } + in := &Inbound{ + Method: probe.Method, + Params: probe.Params, + Result: probe.Result, + Err: probe.Error, + Raw: append(json.RawMessage(nil), line...), + } + // A literal `id: null` (JSON-RPC's marker for "request id unknowable", + // e.g. a parse-error response) is no id: it can never correlate with a + // pending request, and answering a null-id request would be wrong. + if trimmed := bytes.TrimSpace(probe.ID); len(trimmed) > 0 && !bytes.Equal(trimmed, []byte("null")) { + in.ID = RequestID{raw: probe.ID} + } + switch { + case probe.Method != "" && !in.ID.IsZero(): + in.Kind = InboundRequest + case probe.Method != "": + in.Kind = InboundNotification + case !in.ID.IsZero(): + in.Kind = InboundResponse + case probe.Error != nil || len(probe.Result) > 0: + // A null-id response (e.g. a parse-error report): a real response that + // can never correlate. Surface it with a zero ID so the caller can log + // it as an orphan instead of dropping the error on the floor. + in.Kind = InboundResponse + default: + return nil, errors.New("codex protocol: inbound line is neither request, response, nor notification") + } + return in, nil +} + +// MethodInitialized is the client notification completing the handshake. +const MethodInitialized = "initialized" + +// InitializeResponse is the app-server's answer to `initialize`. It belongs to +// the version-independent bootstrap layer, which is why it is not part of the +// generated v2 surface. +type InitializeResponse struct { + UserAgent string `json:"userAgent"` + CodexHome string `json:"codexHome"` + PlatformFamily string `json:"platformFamily"` + PlatformOS string `json:"platformOs"` +} + +// Union runtime helpers referenced by generated code. Generated files use +// these instead of importing packages so they stay import-free. + +type rawMessage = json.RawMessage + +var ( + jsonUnmarshal = json.Unmarshal + jsonMarshal = json.Marshal +) + +func isJSONString(data []byte) bool { + trimmed := bytes.TrimSpace(data) + return len(trimmed) > 0 && trimmed[0] == '"' +} + +// marshalTagged marshals payload and splices the union tag into the object. +func marshalTagged(tagKey, tagValue string, payload any) ([]byte, error) { + encoded, err := json.Marshal(payload) + if err != nil { + return nil, err + } + var obj map[string]json.RawMessage + if err := json.Unmarshal(encoded, &obj); err != nil { + return nil, fmt.Errorf("codex protocol: tagged union payload is not an object: %w", err) + } + if obj == nil { + obj = map[string]json.RawMessage{} + } + tag, err := json.Marshal(tagValue) + if err != nil { + return nil, err + } + obj[tagKey] = tag + return json.Marshal(obj) +} + +// marshalKeyed wraps payload as the single-key object form of a mixed union. +func marshalKeyed(key string, payload any) ([]byte, error) { + encoded, err := json.Marshal(payload) + if err != nil { + return nil, err + } + return json.Marshal(map[string]json.RawMessage{key: encoded}) +} + +func errNoVariant(name string) error { + return fmt.Errorf("codex protocol: %s: no variant set", name) +} diff --git a/internal/agent/runtime/codex/protocol/roundtrip_test.go b/internal/agent/runtime/codex/protocol/roundtrip_test.go new file mode 100644 index 0000000000..e325c7a01d --- /dev/null +++ b/internal/agent/runtime/codex/protocol/roundtrip_test.go @@ -0,0 +1,401 @@ +package protocol + +import ( + "bytes" + "encoding/json" + "os" + "strings" + "testing" + + "github.com/felinics/memoh/internal/agent/runtime/codex/protocolgen" +) + +func TestDecodeInboundClassification(t *testing.T) { + tests := []struct { + name string + line string + kind InboundKind + }{ + { + name: "server request", + line: `{"id":42,"method":"item/commandExecution/requestApproval","params":{"itemId":"item_1","threadId":"th_1","turnId":"turn_1","startedAtMs":1724800000000,"command":"rm -rf build"}}`, + kind: InboundRequest, + }, + { + name: "notification", + line: `{"method":"item/agentMessage/delta","params":{"delta":"hello","itemId":"item_2","threadId":"th_1","turnId":"turn_1"}}`, + kind: InboundNotification, + }, + { + name: "response", + line: `{"id":7,"result":{"thread":{"id":"th_1"}}}`, + kind: InboundResponse, + }, + { + name: "error response", + line: `{"id":"req-9","error":{"code":-32001,"message":"overloaded"}}`, + kind: InboundResponse, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + in, err := DecodeInbound([]byte(tt.line)) + if err != nil { + t.Fatal(err) + } + if in.Kind != tt.kind { + t.Fatalf("kind = %v, want %v", in.Kind, tt.kind) + } + }) + } + + if _, err := DecodeInbound([]byte(`{"jsonrpc":"2.0"}`)); err == nil { + t.Fatal("want error for line with neither method nor id") + } +} + +func TestRequestIDPreservesWireBytes(t *testing.T) { + // A numeric id must echo back as a number, not a string. + in, err := DecodeInbound([]byte(`{"id":42,"method":"item/commandExecution/requestApproval","params":{}}`)) + if err != nil { + t.Fatal(err) + } + resp, err := json.Marshal(Response{ID: in.ID, Result: map[string]any{}}) + if err != nil { + t.Fatal(err) + } + if !bytes.Contains(resp, []byte(`"id":42`)) { + t.Fatalf("numeric id not preserved: %s", resp) + } + + // A string id stays a string. + in, err = DecodeInbound([]byte(`{"id":"probe-1","method":"x","params":{}}`)) + if err != nil { + t.Fatal(err) + } + if in.ID.Key() != `"probe-1"` { + t.Fatalf("string id key = %s", in.ID.Key()) + } + + // A zero id refuses to marshal rather than emitting invalid JSON. + if _, err := json.Marshal(Response{Result: map[string]any{}}); err == nil { + t.Fatal("want error marshaling zero RequestID") + } +} + +func TestServerRequestApprovalRoundTrip(t *testing.T) { + line := `{"id":3,"method":"item/commandExecution/requestApproval","params":{"itemId":"item_1","threadId":"th_1","turnId":"turn_1","startedAtMs":1724800000000,"command":"cargo test","cwd":"/workspace","reason":"needs network","proposedExecpolicyAmendment":["cargo","test"]}}` + in, err := DecodeInbound([]byte(line)) + if err != nil { + t.Fatal(err) + } + decoded, ok, err := DecodeServerRequestParams(in.Method, in.Params) + if err != nil || !ok { + t.Fatalf("decode: ok=%v err=%v", ok, err) + } + params, isTyped := decoded.(*CommandExecutionRequestApprovalParams) + if !isTyped { + t.Fatalf("decoded type %T", decoded) + } + if params.ItemID != "item_1" || params.Command == nil || *params.Command != "cargo test" { + t.Fatalf("unexpected params: %+v", params) + } + if params.StartedAtMs != 1724800000000 { + t.Fatalf("startedAtMs = %d", params.StartedAtMs) + } + + // Unit decision answer. + resp, err := json.Marshal(Response{ID: in.ID, Result: CommandExecutionRequestApprovalResponse{ + Decision: CommandExecutionApprovalDecision{Unit: CommandExecutionApprovalDecisionUnitAccept}, + }}) + if err != nil { + t.Fatal(err) + } + want := `{"id":3,"result":{"decision":"accept"}}` + if string(resp) != want { + t.Fatalf("response = %s, want %s", resp, want) + } +} + +func TestMixedUnionForms(t *testing.T) { + // Bare string form. + var d CommandExecutionApprovalDecision + if err := json.Unmarshal([]byte(`"acceptForSession"`), &d); err != nil { + t.Fatal(err) + } + if d.Unit != "acceptForSession" { + t.Fatalf("unit = %q", d.Unit) + } + + // Object form; note the snake_case payload field on this one type. + var d2 CommandExecutionApprovalDecision + payload := `{"acceptWithExecpolicyAmendment":{"execpolicy_amendment":["cargo","test"]}}` + if err := json.Unmarshal([]byte(payload), &d2); err != nil { + t.Fatal(err) + } + if d2.AcceptWithExecpolicyAmendment == nil || len(d2.AcceptWithExecpolicyAmendment.ExecpolicyAmendment) != 2 { + t.Fatalf("payload variant not decoded: %+v", d2) + } + out, err := json.Marshal(d2) + if err != nil { + t.Fatal(err) + } + var back CommandExecutionApprovalDecision + if err := json.Unmarshal(out, &back); err != nil { + t.Fatal(err) + } + if back.AcceptWithExecpolicyAmendment == nil || len(back.AcceptWithExecpolicyAmendment.ExecpolicyAmendment) != 2 { + t.Fatalf("round trip lost payload: %s", out) + } + + // Unknown object variant is tolerated and re-marshals as its raw bytes. + var d3 CommandExecutionApprovalDecision + unknown := `{"futureDecisionKind":{"x":1}}` + if err := json.Unmarshal([]byte(unknown), &d3); err != nil { + t.Fatal(err) + } + if d3.Unit != "" || d3.AcceptWithExecpolicyAmendment != nil { + t.Fatalf("unknown variant should not populate fields: %+v", d3) + } + out, err = json.Marshal(d3) + if err != nil { + t.Fatal(err) + } + if string(out) != unknown { + t.Fatalf("raw echo = %s, want %s", out, unknown) + } + + // An empty union refuses to marshal. + if _, err := json.Marshal(CommandExecutionApprovalDecision{}); err == nil { + t.Fatal("want error for empty union") + } +} + +func TestTaggedUnionForms(t *testing.T) { + // Known variant. + var item ThreadItem + userMsg := `{"type":"userMessage","id":"item_1","content":[{"type":"text","text":"hi"}]}` + if err := json.Unmarshal([]byte(userMsg), &item); err != nil { + t.Fatal(err) + } + if item.Tag != ThreadItemTagUserMessage || item.UserMessage == nil { + t.Fatalf("decode: %+v", item) + } + if len(item.UserMessage.Content) != 1 || item.UserMessage.Content[0].Text == nil || item.UserMessage.Content[0].Text.Text != "hi" { + t.Fatalf("nested union: %+v", item.UserMessage.Content) + } + + // Marshal splices the tag back in. + out, err := json.Marshal(item) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(out), `"type":"userMessage"`) { + t.Fatalf("tag missing: %s", out) + } + + // Unknown variant keeps tag and raw, and echoes verbatim. + var future ThreadItem + futureJSON := `{"type":"quantumMessage","id":"item_9","payload":{"a":1}}` + if err := json.Unmarshal([]byte(futureJSON), &future); err != nil { + t.Fatal(err) + } + if future.Tag != "quantumMessage" { + t.Fatalf("tag = %q", future.Tag) + } + out, err = json.Marshal(future) + if err != nil { + t.Fatal(err) + } + if string(out) != futureJSON { + t.Fatalf("raw echo = %s", out) + } +} + +func TestDecodeServerNotificationParams(t *testing.T) { + decoded, ok, err := DecodeServerNotificationParams("item/agentMessage/delta", []byte(`{"delta":"hi","itemId":"i","threadId":"t","turnId":"u"}`)) + if err != nil || !ok { + t.Fatalf("ok=%v err=%v", ok, err) + } + delta, isTyped := decoded.(*AgentMessageDeltaNotification) + if !isTyped || delta.Delta != "hi" { + t.Fatalf("decoded: %#v", decoded) + } + + // Methods outside the typed subset are reported, not failed. + _, ok, err = DecodeServerNotificationParams("thread/realtime/sdp", []byte(`{}`)) + if err != nil || ok { + t.Fatalf("unknown method: ok=%v err=%v", ok, err) + } +} + +func TestUnionDecodeIntoReusedValue(t *testing.T) { + // Reusing a variable across decodes must not leak the previous variant. + var item ThreadItem + if err := json.Unmarshal([]byte(`{"type":"agentMessage","id":"a","text":"one"}`), &item); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(`{"type":"reasoning","id":"r"}`), &item); err != nil { + t.Fatal(err) + } + if item.AgentMessage != nil || item.Reasoning == nil || item.Tag != ThreadItemTagReasoning { + t.Fatalf("stale variant survived re-decode: %+v", item) + } + out, err := json.Marshal(item) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(out), `"type":"reasoning"`) { + t.Fatalf("re-marshal produced the old variant: %s", out) + } + + // encoding/json reuses slice elements by capacity without zeroing them. + items := []ThreadItem{} + if err := json.Unmarshal([]byte(`[{"type":"agentMessage","id":"a","text":"one"},{"type":"plan","id":"p","text":"x"}]`), &items); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(`[{"type":"reasoning","id":"r"}]`), &items); err != nil { + t.Fatal(err) + } + if len(items) != 1 || items[0].AgentMessage != nil || items[0].Reasoning == nil { + t.Fatalf("slice element reuse leaked stale variant: %+v", items) + } + + // Mixed union: a stale Unit must not shadow a later object variant. + var d CommandExecutionApprovalDecision + if err := json.Unmarshal([]byte(`"accept"`), &d); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(`{"acceptWithExecpolicyAmendment":{"execpolicy_amendment":["x"]}}`), &d); err != nil { + t.Fatal(err) + } + if d.Unit != "" || d.AcceptWithExecpolicyAmendment == nil { + t.Fatalf("stale unit survived re-decode: %+v", d) + } + out, err = json.Marshal(d) + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(out), "accept\"") && !strings.Contains(string(out), "acceptWithExecpolicyAmendment") { + t.Fatalf("re-marshal produced the stale unit: %s", out) + } +} + +func TestUnionRawIsStable(t *testing.T) { + // Bytes handed out by Raw() must survive later decodes into the variable. + var item ThreadItem + first := `{"type":"futureMessage","id":"f","payload":{"a":1}}` + if err := json.Unmarshal([]byte(first), &item); err != nil { + t.Fatal(err) + } + saved := item.Raw() + if err := json.Unmarshal([]byte(`{"type":"plan","id":"p","text":"x"}`), &item); err != nil { + t.Fatal(err) + } + if string(saved) != first { + t.Fatalf("saved Raw() bytes were rewritten by a later decode: %s", saved) + } +} + +func TestOutboundEnvelopeMarshal(t *testing.T) { + // nil params are omitted entirely. + out, err := json.Marshal(Request{ID: NewRequestID(1), Method: "account/rateLimits/read"}) + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(out), "params") { + t.Fatalf("nil params not omitted: %s", out) + } + + // A typed-nil pointer is also omitted, not sent as `"params": null`. + var p *TurnStartParams + out, err = json.Marshal(Request{ID: NewRequestID(2), Method: MethodTurnStart, Params: p}) + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(out), "params") { + t.Fatalf("typed-nil params not omitted: %s", out) + } + + // Required collection fields normalize nil to empty on the way out. + out, err = json.Marshal(Request{ID: NewRequestID(3), Method: MethodTurnStart, Params: TurnStartParams{ThreadID: "t"}}) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(out), `"input":[]`) { + t.Fatalf("required collection marshaled as null: %s", out) + } + + out, err = json.Marshal(Notification{Method: MethodInitialized}) + if err != nil { + t.Fatal(err) + } + if string(out) != `{"method":"initialized"}` { + t.Fatalf("notification = %s", out) + } +} + +func TestRequestIDValueCopyStable(t *testing.T) { + var id RequestID + if err := json.Unmarshal([]byte(`101`), &id); err != nil { + t.Fatal(err) + } + saved := id + if err := json.Unmarshal([]byte(`202`), &id); err != nil { + t.Fatal(err) + } + if saved.Key() != "101" { + t.Fatalf("value copy drifted to %s", saved.Key()) + } +} + +func TestDecodeInboundNullID(t *testing.T) { + // A parse-error style response with id:null is an orphan response, not a + // correlatable one and not an error. + in, err := DecodeInbound([]byte(`{"id":null,"error":{"code":-32700,"message":"parse error"}}`)) + if err != nil { + t.Fatal(err) + } + if in.Kind != InboundResponse || !in.ID.IsZero() || in.Err == nil { + t.Fatalf("null-id error line: kind=%v id=%q err=%v", in.Kind, in.ID.Key(), in.Err) + } + + // id:null plus a method is a notification — never a request to answer. + in, err = DecodeInbound([]byte(`{"id":null,"method":"m","params":{}}`)) + if err != nil { + t.Fatal(err) + } + if in.Kind != InboundNotification { + t.Fatalf("null-id method line: kind=%v", in.Kind) + } +} + +func TestNewResponseForMethod(t *testing.T) { + resp, ok := NewResponseForMethod(MethodThreadStart) + if !ok { + t.Fatal("thread/start should have a generated response") + } + if _, isTyped := resp.(*ThreadStartResponse); !isTyped { + t.Fatalf("response type %T", resp) + } + if _, ok := NewResponseForMethod("initialize"); ok { + t.Fatal("initialize response is hand-written, not generated") + } +} + +// Keep the checked-in protocol sources pinned to the vendored Codex schema. +func TestGeneratedFilesAreFresh(t *testing.T) { + files, err := protocolgen.Generate() + if err != nil { + t.Fatal(err) + } + for name, want := range files { + got, err := os.ReadFile(name) //nolint:gosec // fixed generated paths owned by this package + if err != nil { + t.Fatalf("%s: %v (run `mise run codex-protocol-generate`)", name, err) + } + if !bytes.Equal(got, want) { + t.Errorf("%s is stale; run `mise run codex-protocol-generate`", name) + } + } +} diff --git a/internal/agent/runtime/codex/protocol/types.gen.go b/internal/agent/runtime/codex/protocol/types.gen.go new file mode 100644 index 0000000000..794b872678 --- /dev/null +++ b/internal/agent/runtime/codex/protocol/types.gen.go @@ -0,0 +1,1638 @@ +// Code generated by gen-codex-protocol from the codex app-server v2 JSON +// Schema snapshot (codex-cli 0.151.0). DO NOT EDIT. +// +// Regenerate with `mise run codex-protocol-generate`; refresh the snapshot +// itself with `mise run codex-schema-sync`. + +package protocol + +// A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem). +type AbsolutePathBuf = string + +type AccountLoginCompletedNotification struct { + Error *string `json:"error,omitempty"` + LoginID *string `json:"loginId,omitempty"` + OnboardingEntrypoint *DesktopOnboardingEntrypoint `json:"onboardingEntrypoint,omitempty"` + Success bool `json:"success"` +} + +// Sparse rolling rate-limit update. +type AccountRateLimitsUpdatedNotification struct { + RateLimits RateLimitSnapshot `json:"rateLimits"` +} + +type AccountUpdatedNotification struct { + AuthMode *AuthMode `json:"authMode,omitempty"` + PlanType *PlanType `json:"planType,omitempty"` +} + +type AdditionalFileSystemPermissions struct { + Entries []FileSystemSandboxEntry `json:"entries,omitempty"` + GlobScanMaxDepth *uint64 `json:"globScanMaxDepth,omitempty"` + // This will be removed in favor of `entries`. + Read []LegacyAppPathString `json:"read,omitempty"` + // This will be removed in favor of `entries`. + Write []LegacyAppPathString `json:"write,omitempty"` +} + +type AdditionalNetworkPermissions struct { + Enabled *bool `json:"enabled,omitempty"` +} + +type AgentMessageDelivery string + +const ( + AgentMessageDeliveryAsync AgentMessageDelivery = "async" +) + +type AgentMessageDeltaNotification struct { + Delta string `json:"delta"` + ItemID string `json:"itemId"` + ThreadID string `json:"threadId"` + TurnID string `json:"turnId"` +} + +type AgentPath = string + +// Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility. +type ApprovalsReviewer string + +const ( + ApprovalsReviewerAutoReview ApprovalsReviewer = "auto_review" + ApprovalsReviewerGuardianSubagent ApprovalsReviewer = "guardian_subagent" + ApprovalsReviewerUser ApprovalsReviewer = "user" +) + +// Authentication mode for OpenAI-backed providers. +type AuthMode string + +const ( + AuthModeAgentIdentity AuthMode = "agentIdentity" + AuthModeApikey AuthMode = "apikey" + AuthModeBedrockAccessKeys AuthMode = "bedrockAccessKeys" + AuthModeBedrockAPIKey AuthMode = "bedrockApiKey" + AuthModeChatgpt AuthMode = "chatgpt" + AuthModeChatgptAuthTokens AuthMode = "chatgptAuthTokens" + AuthModeHeaders AuthMode = "headers" + AuthModePersonalAccessToken AuthMode = "personalAccessToken" +) + +type ByteRange struct { + End uint64 `json:"end"` + Start uint64 `json:"start"` +} + +type CancelLoginAccountParams struct { + LoginID string `json:"loginId"` +} + +type CancelLoginAccountResponse struct { + Status CancelLoginAccountStatus `json:"status"` +} + +type CancelLoginAccountStatus string + +const ( + CancelLoginAccountStatusCanceled CancelLoginAccountStatus = "canceled" + CancelLoginAccountStatusNotFound CancelLoginAccountStatus = "notFound" +) + +type ChatgptAuthTokensRefreshParams struct { + // Workspace/account identifier that Codex was previously using. + PreviousAccountID *string `json:"previousAccountId,omitempty"` + Reason ChatgptAuthTokensRefreshReason `json:"reason"` +} + +type ChatgptAuthTokensRefreshReason string + +const ( + ChatgptAuthTokensRefreshReasonUnauthorized ChatgptAuthTokensRefreshReason = "unauthorized" +) + +type ChatgptAuthTokensRefreshResponse struct { + AccessToken string `json:"accessToken"` + ChatgptAccountID string `json:"chatgptAccountId"` + ChatgptPlanType *string `json:"chatgptPlanType,omitempty"` +} + +type ClientInfo struct { + Name string `json:"name"` + Title *string `json:"title,omitempty"` + Version string `json:"version"` +} + +type CollabAgentState struct { + Message *string `json:"message,omitempty"` + Status CollabAgentStatus `json:"status"` +} + +type CollabAgentStatus string + +const ( + CollabAgentStatusCompleted CollabAgentStatus = "completed" + CollabAgentStatusErrored CollabAgentStatus = "errored" + CollabAgentStatusInterrupted CollabAgentStatus = "interrupted" + CollabAgentStatusNotFound CollabAgentStatus = "notFound" + CollabAgentStatusPendingInit CollabAgentStatus = "pendingInit" + CollabAgentStatusRunning CollabAgentStatus = "running" + CollabAgentStatusShutdown CollabAgentStatus = "shutdown" +) + +type CollabAgentTool string + +const ( + CollabAgentToolCloseAgent CollabAgentTool = "closeAgent" + CollabAgentToolFollowupTask CollabAgentTool = "followupTask" + CollabAgentToolInterruptAgent CollabAgentTool = "interruptAgent" + CollabAgentToolListAgents CollabAgentTool = "listAgents" + CollabAgentToolResumeAgent CollabAgentTool = "resumeAgent" + CollabAgentToolSendInput CollabAgentTool = "sendInput" + CollabAgentToolSendMessage CollabAgentTool = "sendMessage" + CollabAgentToolSpawnAgent CollabAgentTool = "spawnAgent" + CollabAgentToolWait CollabAgentTool = "wait" +) + +type CollabAgentToolCallStatus string + +const ( + CollabAgentToolCallStatusCompleted CollabAgentToolCallStatus = "completed" + CollabAgentToolCallStatusFailed CollabAgentToolCallStatus = "failed" + CollabAgentToolCallStatusInProgress CollabAgentToolCallStatus = "inProgress" + CollabAgentToolCallStatusInterrupted CollabAgentToolCallStatus = "interrupted" +) + +// Distinguishes a command approval from input sent to an existing terminal. +type CommandExecutionApprovalKind string + +const ( + CommandExecutionApprovalKindCommand CommandExecutionApprovalKind = "command" + CommandExecutionApprovalKindWriteStdin CommandExecutionApprovalKind = "writeStdin" +) + +type CommandExecutionOutputDeltaNotification struct { + Delta string `json:"delta"` + ItemID string `json:"itemId"` + ThreadID string `json:"threadId"` + TurnID string `json:"turnId"` +} + +type CommandExecutionRequestApprovalParams struct { + // Unique identifier for this specific approval callback. + ApprovalID *string `json:"approvalId,omitempty"` + // The command to be executed. + Command *string `json:"command,omitempty"` + // Best-effort parsed command actions for friendly display. + CommandActions []CommandAction `json:"commandActions,omitempty"` + // The command's working directory. + Cwd *LegacyAppPathString `json:"cwd,omitempty"` + // Environment in which the command will run. + EnvironmentID *string `json:"environmentId,omitempty"` + ItemID string `json:"itemId"` + // Kind of action under review. Defaults to `command` for older servers. + Kind *CommandExecutionApprovalKind `json:"kind,omitempty"` + // Optional context for a managed-network approval prompt. + NetworkApprovalContext *NetworkApprovalContext `json:"networkApprovalContext,omitempty"` + // Optional proposed execpolicy amendment to allow similar commands without prompting. + ProposedExecpolicyAmendment []string `json:"proposedExecpolicyAmendment,omitempty"` + // Optional proposed network policy amendments (allow/deny host) for future requests. + ProposedNetworkPolicyAmendments []NetworkPolicyAmendment `json:"proposedNetworkPolicyAmendments,omitempty"` + // Optional explanatory reason (e.g. request for network access). + Reason *string `json:"reason,omitempty"` + // Unix timestamp (in milliseconds) when this approval request started. + StartedAtMs int64 `json:"startedAtMs"` + ThreadID string `json:"threadId"` + TurnID string `json:"turnId"` +} + +type CommandExecutionRequestApprovalResponse struct { + Decision CommandExecutionApprovalDecision `json:"decision"` +} + +type CommandExecutionSource string + +const ( + CommandExecutionSourceAgent CommandExecutionSource = "agent" + CommandExecutionSourceUnifiedExecInteraction CommandExecutionSource = "unifiedExecInteraction" + CommandExecutionSourceUnifiedExecStartup CommandExecutionSource = "unifiedExecStartup" + CommandExecutionSourceUserShell CommandExecutionSource = "userShell" +) + +type CommandExecutionStatus string + +const ( + CommandExecutionStatusCompleted CommandExecutionStatus = "completed" + CommandExecutionStatusDeclined CommandExecutionStatus = "declined" + CommandExecutionStatusFailed CommandExecutionStatus = "failed" + CommandExecutionStatusInProgress CommandExecutionStatus = "inProgress" +) + +type ConfigWarningNotification struct { + // Optional extra guidance or error details. + Details *string `json:"details,omitempty"` + // Optional path to the config file that triggered the warning. + Path *string `json:"path,omitempty"` + // Optional range for the error location inside the config file. + Range *TextRange `json:"range,omitempty"` + // Concise summary of the warning. + Summary string `json:"summary"` +} + +// Deprecated: Use `ContextCompaction` item type instead. +type ContextCompactedNotification struct { + ThreadID string `json:"threadId"` + TurnID string `json:"turnId"` +} + +type CreditsSnapshot struct { + Balance *string `json:"balance,omitempty"` + HasCredits bool `json:"hasCredits"` + Unlimited bool `json:"unlimited"` +} + +type DeprecationNoticeNotification struct { + // Optional extra guidance, such as migration steps or rationale. + Details *string `json:"details,omitempty"` + // Concise summary of what is deprecated. + Summary string `json:"summary"` +} + +type DesktopOnboardingEntrypoint string + +const ( + DesktopOnboardingEntrypointLifeSciences DesktopOnboardingEntrypoint = "life_sciences" +) + +type DynamicToolCallStatus string + +const ( + DynamicToolCallStatusCompleted DynamicToolCallStatus = "completed" + DynamicToolCallStatusFailed DynamicToolCallStatus = "failed" + DynamicToolCallStatusInProgress DynamicToolCallStatus = "inProgress" +) + +type ErrorNotification struct { + Error TurnError `json:"error"` + ThreadID string `json:"threadId"` + TurnID string `json:"turnId"` + WillRetry bool `json:"willRetry"` +} + +type FileChangeApprovalDecision string + +const ( + FileChangeApprovalDecisionAccept FileChangeApprovalDecision = "accept" + FileChangeApprovalDecisionAcceptForSession FileChangeApprovalDecision = "acceptForSession" + FileChangeApprovalDecisionCancel FileChangeApprovalDecision = "cancel" + FileChangeApprovalDecisionDecline FileChangeApprovalDecision = "decline" +) + +// Deprecated legacy notification for `apply_patch` textual output. +type FileChangeOutputDeltaNotification struct { + Delta string `json:"delta"` + ItemID string `json:"itemId"` + ThreadID string `json:"threadId"` + TurnID string `json:"turnId"` +} + +type FileChangeRequestApprovalParams struct { + // [UNSTABLE] When set, the agent is asking the user to allow writes under this root for the remainder of the session (unclear if this is honored today). + GrantRoot *string `json:"grantRoot,omitempty"` + ItemID string `json:"itemId"` + // Optional explanatory reason (e.g. request for extra write access). + Reason *string `json:"reason,omitempty"` + // Unix timestamp (in milliseconds) when this approval request started. + StartedAtMs int64 `json:"startedAtMs"` + ThreadID string `json:"threadId"` + TurnID string `json:"turnId"` +} + +type FileChangeRequestApprovalResponse struct { + Decision FileChangeApprovalDecision `json:"decision"` +} + +type FileSystemAccessMode string + +const ( + FileSystemAccessModeDeny FileSystemAccessMode = "deny" + FileSystemAccessModeRead FileSystemAccessMode = "read" + FileSystemAccessModeWrite FileSystemAccessMode = "write" +) + +type FileSystemSandboxEntry struct { + Access FileSystemAccessMode `json:"access"` + Path FileSystemPath `json:"path"` +} + +type FileUpdateChange struct { + Diff string `json:"diff"` + Kind PatchChangeKind `json:"kind"` + Path string `json:"path"` +} + +type GetAccountParams struct { + // When `true`, requests a proactive token refresh before returning. + RefreshToken *bool `json:"refreshToken,omitempty"` +} + +type GetAccountRateLimitsResponse struct { + RateLimitResetCredits *RateLimitResetCreditsSummary `json:"rateLimitResetCredits,omitempty"` + // Backward-compatible single-bucket view; mirrors the historical payload. + RateLimits RateLimitSnapshot `json:"rateLimits"` + // Multi-bucket view keyed by metered `limit_id` (for example, `codex`). + RateLimitsByLimitID map[string]RateLimitSnapshot `json:"rateLimitsByLimitId,omitempty"` +} + +type GetAccountResponse struct { + Account *Account `json:"account,omitempty"` + RequiresOpenaiAuth bool `json:"requiresOpenaiAuth"` +} + +type GitInfo struct { + Branch *string `json:"branch,omitempty"` + OriginURL *string `json:"originUrl,omitempty"` + Sha *string `json:"sha,omitempty"` +} + +type GrantedPermissionProfile struct { + FileSystem *AdditionalFileSystemPermissions `json:"fileSystem,omitempty"` + Network *AdditionalNetworkPermissions `json:"network,omitempty"` +} + +type HookPromptFragment struct { + HookRunID string `json:"hookRunId"` + Text string `json:"text"` +} + +type ImageDetail string + +const ( + ImageDetailAuto ImageDetail = "auto" + ImageDetailHigh ImageDetail = "high" + ImageDetailLow ImageDetail = "low" + ImageDetailOriginal ImageDetail = "original" +) + +// Client-declared capabilities negotiated during initialize. +type InitializeCapabilities struct { + // Opt into receiving experimental API methods and fields. + ExperimentalAPI *bool `json:"experimentalApi,omitempty"` + // MCP extension settings declared by the app-server client. + Extensions map[string]any `json:"extensions,omitempty"` + // Legacy opt-in for the `openai/form` MCP extension. + MCPServerOpenaiFormElicitation *bool `json:"mcpServerOpenaiFormElicitation,omitempty"` + // Exact notification method names that should be suppressed for this connection (for example `thread/started`). + OptOutNotificationMethods []string `json:"optOutNotificationMethods,omitempty"` + // Opt into `attestation/generate` requests for upstream `x-oai-attestation`. + RequestAttestation *bool `json:"requestAttestation,omitempty"` +} + +type InitializeParams struct { + Capabilities *InitializeCapabilities `json:"capabilities,omitempty"` + ClientInfo ClientInfo `json:"clientInfo"` +} + +// Canonical user-input modality tags advertised by a model. +type InputModality string + +const ( + InputModalityAudio InputModality = "audio" + InputModalityImage InputModality = "image" + InputModalityText InputModality = "text" +) + +type ItemCompletedNotification struct { + // Unix timestamp (in milliseconds) when this item lifecycle completed. + CompletedAtMs int64 `json:"completedAtMs"` + Item ThreadItem `json:"item"` + ThreadID string `json:"threadId"` + TurnID string `json:"turnId"` +} + +type ItemStartedNotification struct { + Item ThreadItem `json:"item"` + // Unix timestamp (in milliseconds) when this item lifecycle started. + StartedAtMs int64 `json:"startedAtMs"` + ThreadID string `json:"threadId"` + TurnID string `json:"turnId"` +} + +type LegacyAppPathString = string + +type LoginAppBrand string + +const ( + LoginAppBrandChatgpt LoginAppBrand = "chatgpt" + LoginAppBrandCodex LoginAppBrand = "codex" +) + +type LogoutAccountResponse struct { +} + +type McpElicitationArrayType string + +const ( + McpElicitationArrayTypeArray McpElicitationArrayType = "array" +) + +type McpElicitationBooleanSchema struct { + Default *bool `json:"default,omitempty"` + Description *string `json:"description,omitempty"` + Title *string `json:"title,omitempty"` + Type McpElicitationBooleanType `json:"type"` +} + +type McpElicitationBooleanType string + +const ( + McpElicitationBooleanTypeBoolean McpElicitationBooleanType = "boolean" +) + +type McpElicitationConstOption struct { + Const string `json:"const"` + Title string `json:"title"` +} + +// McpElicitationEnumSchema is an untagged union with no discriminator; decode the raw +// JSON into one of: McpElicitationSingleSelectEnumSchema, McpElicitationMultiSelectEnumSchema, McpElicitationLegacyTitledEnumSchema. +type McpElicitationEnumSchema = rawMessage + +type McpElicitationLegacyTitledEnumSchema struct { + Default *string `json:"default,omitempty"` + Description *string `json:"description,omitempty"` + Enum []string `json:"enum"` + EnumNames []string `json:"enumNames,omitempty"` + Title *string `json:"title,omitempty"` + Type McpElicitationStringType `json:"type"` +} + +func (v McpElicitationLegacyTitledEnumSchema) MarshalJSON() ([]byte, error) { + type plain McpElicitationLegacyTitledEnumSchema + p := plain(v) + if p.Enum == nil { + p.Enum = []string{} + } + return jsonMarshal(p) +} + +// McpElicitationMultiSelectEnumSchema is an untagged union with no discriminator; decode the raw +// JSON into one of: McpElicitationUntitledMultiSelectEnumSchema, McpElicitationTitledMultiSelectEnumSchema. +type McpElicitationMultiSelectEnumSchema = rawMessage + +type McpElicitationNumberSchema struct { + Default *float64 `json:"default,omitempty"` + Description *string `json:"description,omitempty"` + Maximum *float64 `json:"maximum,omitempty"` + Minimum *float64 `json:"minimum,omitempty"` + Title *string `json:"title,omitempty"` + Type McpElicitationNumberType `json:"type"` +} + +type McpElicitationNumberType string + +const ( + McpElicitationNumberTypeInteger McpElicitationNumberType = "integer" + McpElicitationNumberTypeNumber McpElicitationNumberType = "number" +) + +type McpElicitationObjectType string + +const ( + McpElicitationObjectTypeObject McpElicitationObjectType = "object" +) + +// McpElicitationPrimitiveSchema is an untagged union with no discriminator; decode the raw +// JSON into one of: McpElicitationEnumSchema, McpElicitationStringSchema, McpElicitationNumberSchema, McpElicitationBooleanSchema. +type McpElicitationPrimitiveSchema = rawMessage + +// Typed form schema for MCP `elicitation/create` requests. +type McpElicitationSchema struct { + Schema *string `json:"$schema,omitempty"` + Properties map[string]McpElicitationPrimitiveSchema `json:"properties"` + Required []string `json:"required,omitempty"` + Type McpElicitationObjectType `json:"type"` +} + +func (v McpElicitationSchema) MarshalJSON() ([]byte, error) { + type plain McpElicitationSchema + p := plain(v) + if p.Properties == nil { + p.Properties = map[string]McpElicitationPrimitiveSchema{} + } + return jsonMarshal(p) +} + +// McpElicitationSingleSelectEnumSchema is an untagged union with no discriminator; decode the raw +// JSON into one of: McpElicitationUntitledSingleSelectEnumSchema, McpElicitationTitledSingleSelectEnumSchema. +type McpElicitationSingleSelectEnumSchema = rawMessage + +type McpElicitationStringFormat string + +const ( + McpElicitationStringFormatDate McpElicitationStringFormat = "date" + McpElicitationStringFormatDateTime McpElicitationStringFormat = "date-time" + McpElicitationStringFormatEmail McpElicitationStringFormat = "email" + McpElicitationStringFormatURI McpElicitationStringFormat = "uri" +) + +type McpElicitationStringSchema struct { + Default *string `json:"default,omitempty"` + Description *string `json:"description,omitempty"` + Format *McpElicitationStringFormat `json:"format,omitempty"` + MaxLength *uint64 `json:"maxLength,omitempty"` + MinLength *uint64 `json:"minLength,omitempty"` + Title *string `json:"title,omitempty"` + Type McpElicitationStringType `json:"type"` +} + +type McpElicitationStringType string + +const ( + McpElicitationStringTypeString McpElicitationStringType = "string" +) + +type McpElicitationTitledEnumItems struct { + AnyOf []McpElicitationConstOption `json:"anyOf"` +} + +func (v McpElicitationTitledEnumItems) MarshalJSON() ([]byte, error) { + type plain McpElicitationTitledEnumItems + p := plain(v) + if p.AnyOf == nil { + p.AnyOf = []McpElicitationConstOption{} + } + return jsonMarshal(p) +} + +type McpElicitationTitledMultiSelectEnumSchema struct { + Default []string `json:"default,omitempty"` + Description *string `json:"description,omitempty"` + Items McpElicitationTitledEnumItems `json:"items"` + MaxItems *uint64 `json:"maxItems,omitempty"` + MinItems *uint64 `json:"minItems,omitempty"` + Title *string `json:"title,omitempty"` + Type McpElicitationArrayType `json:"type"` +} + +type McpElicitationTitledSingleSelectEnumSchema struct { + Default *string `json:"default,omitempty"` + Description *string `json:"description,omitempty"` + OneOf []McpElicitationConstOption `json:"oneOf"` + Title *string `json:"title,omitempty"` + Type McpElicitationStringType `json:"type"` +} + +func (v McpElicitationTitledSingleSelectEnumSchema) MarshalJSON() ([]byte, error) { + type plain McpElicitationTitledSingleSelectEnumSchema + p := plain(v) + if p.OneOf == nil { + p.OneOf = []McpElicitationConstOption{} + } + return jsonMarshal(p) +} + +type McpElicitationUntitledEnumItems struct { + Enum []string `json:"enum"` + Type McpElicitationStringType `json:"type"` +} + +func (v McpElicitationUntitledEnumItems) MarshalJSON() ([]byte, error) { + type plain McpElicitationUntitledEnumItems + p := plain(v) + if p.Enum == nil { + p.Enum = []string{} + } + return jsonMarshal(p) +} + +type McpElicitationUntitledMultiSelectEnumSchema struct { + Default []string `json:"default,omitempty"` + Description *string `json:"description,omitempty"` + Items McpElicitationUntitledEnumItems `json:"items"` + MaxItems *uint64 `json:"maxItems,omitempty"` + MinItems *uint64 `json:"minItems,omitempty"` + Title *string `json:"title,omitempty"` + Type McpElicitationArrayType `json:"type"` +} + +type McpElicitationUntitledSingleSelectEnumSchema struct { + Default *string `json:"default,omitempty"` + Description *string `json:"description,omitempty"` + Enum []string `json:"enum"` + Title *string `json:"title,omitempty"` + Type McpElicitationStringType `json:"type"` +} + +func (v McpElicitationUntitledSingleSelectEnumSchema) MarshalJSON() ([]byte, error) { + type plain McpElicitationUntitledSingleSelectEnumSchema + p := plain(v) + if p.Enum == nil { + p.Enum = []string{} + } + return jsonMarshal(p) +} + +type McpServerElicitationAction string + +const ( + McpServerElicitationActionAccept McpServerElicitationAction = "accept" + McpServerElicitationActionCancel McpServerElicitationAction = "cancel" + McpServerElicitationActionDecline McpServerElicitationAction = "decline" +) + +type McpServerElicitationRequestResponse struct { + // Optional client metadata for form-mode action handling. + Meta any `json:"_meta,omitempty"` + Action McpServerElicitationAction `json:"action"` + // Structured user input for accepted elicitations, mirroring RMCP `CreateElicitationResult`. + Content any `json:"content,omitempty"` +} + +type McpToolCallAppContext struct { + ActionName *string `json:"actionName,omitempty"` + AppName *string `json:"appName,omitempty"` + ConnectorID string `json:"connectorId"` + LinkID *string `json:"linkId,omitempty"` + ResourceURI *string `json:"resourceUri,omitempty"` +} + +type McpToolCallError struct { + Message string `json:"message"` +} + +type McpToolCallResult struct { + Meta any `json:"_meta,omitempty"` + Content []any `json:"content"` + StructuredContent any `json:"structuredContent,omitempty"` +} + +func (v McpToolCallResult) MarshalJSON() ([]byte, error) { + type plain McpToolCallResult + p := plain(v) + if p.Content == nil { + p.Content = []any{} + } + return jsonMarshal(p) +} + +type McpToolCallStatus string + +const ( + McpToolCallStatusCompleted McpToolCallStatus = "completed" + McpToolCallStatusFailed McpToolCallStatus = "failed" + McpToolCallStatusInProgress McpToolCallStatus = "inProgress" +) + +type MemoryCitation struct { + Entries []MemoryCitationEntry `json:"entries"` + ThreadIds []string `json:"threadIds"` +} + +func (v MemoryCitation) MarshalJSON() ([]byte, error) { + type plain MemoryCitation + p := plain(v) + if p.Entries == nil { + p.Entries = []MemoryCitationEntry{} + } + if p.ThreadIds == nil { + p.ThreadIds = []string{} + } + return jsonMarshal(p) +} + +type MemoryCitationEntry struct { + LineEnd uint64 `json:"lineEnd"` + LineStart uint64 `json:"lineStart"` + Note string `json:"note"` + Path string `json:"path"` +} + +// Classifies an assistant message as interim commentary or final answer text. +type MessagePhase string + +const ( + MessagePhaseCommentary MessagePhase = "commentary" + MessagePhaseFinalAnswer MessagePhase = "final_answer" +) + +type MisalignmentErrorDetails struct { + // A substantive localized explanation is required before offering continuation. + DetailedExplanation *string `json:"detailedExplanation,omitempty"` + // Open-ended classification; clients must accept categories added by Responses. + ErrorType *string `json:"errorType,omitempty"` + // Instruction to submit as the next turn's user input if continuation is confirmed. + Steer *MisalignmentSteer `json:"steer,omitempty"` +} + +type MisalignmentSteer struct { + Message string `json:"message"` +} + +type Model struct { + // Deprecated: use `serviceTiers` instead. + AdditionalSpeedTiers []string `json:"additionalSpeedTiers,omitempty"` + AvailabilityNux *ModelAvailabilityNux `json:"availabilityNux,omitempty"` + DefaultReasoningEffort ReasoningEffort `json:"defaultReasoningEffort"` + // Catalog default service tier id for this model, when one is configured. + DefaultServiceTier *string `json:"defaultServiceTier,omitempty"` + Description string `json:"description"` + DisplayName string `json:"displayName"` + Hidden bool `json:"hidden"` + ID string `json:"id"` + InputModalities []InputModality `json:"inputModalities,omitempty"` + IsDefault bool `json:"isDefault"` + Model string `json:"model"` + ModelSpecialty *string `json:"modelSpecialty,omitempty"` + // Multi-agent runtime declared by this model, when available. + MultiAgentVersion *MultiAgentVersion `json:"multiAgentVersion,omitempty"` + ServiceTiers []ModelServiceTier `json:"serviceTiers,omitempty"` + SupportedReasoningEfforts []ReasoningEffortOption `json:"supportedReasoningEfforts"` + SupportsPersonality *bool `json:"supportsPersonality,omitempty"` + Upgrade *string `json:"upgrade,omitempty"` + UpgradeInfo *ModelUpgradeInfo `json:"upgradeInfo,omitempty"` +} + +func (v Model) MarshalJSON() ([]byte, error) { + type plain Model + p := plain(v) + if p.SupportedReasoningEfforts == nil { + p.SupportedReasoningEfforts = []ReasoningEffortOption{} + } + return jsonMarshal(p) +} + +type ModelAvailabilityNux struct { + Message string `json:"message"` +} + +type ModelListParams struct { + // Opaque pagination cursor returned by a previous call. + Cursor *string `json:"cursor,omitempty"` + // When true, include models that are hidden from the default picker list. + IncludeHidden *bool `json:"includeHidden,omitempty"` + // Optional page size; defaults to a reasonable server-side value. + Limit *uint64 `json:"limit,omitempty"` +} + +type ModelListResponse struct { + Data []Model `json:"data"` + // Opaque cursor to pass to the next call to continue after the last item. If None, there are no more items to return. + NextCursor *string `json:"nextCursor,omitempty"` +} + +func (v ModelListResponse) MarshalJSON() ([]byte, error) { + type plain ModelListResponse + p := plain(v) + if p.Data == nil { + p.Data = []Model{} + } + return jsonMarshal(p) +} + +type ModelServiceTier struct { + Description string `json:"description"` + ID string `json:"id"` + Name string `json:"name"` +} + +type ModelUpgradeInfo struct { + MigrationMarkdown *string `json:"migrationMarkdown,omitempty"` + Model string `json:"model"` + ModelLink *string `json:"modelLink,omitempty"` + // Informational Unix timestamp for this upgrade's scheduled retirement, if known. + RetirementAt *int64 `json:"retirementAt,omitempty"` + UpgradeCopy *string `json:"upgradeCopy,omitempty"` +} + +// Multi-agent runtime supported by a model. +type MultiAgentVersion string + +const ( + MultiAgentVersionDisabled MultiAgentVersion = "disabled" + MultiAgentVersionV1 MultiAgentVersion = "v1" + MultiAgentVersionV2 MultiAgentVersion = "v2" +) + +type NetworkAccess string + +const ( + NetworkAccessEnabled NetworkAccess = "enabled" + NetworkAccessRestricted NetworkAccess = "restricted" +) + +type NetworkApprovalContext struct { + Host string `json:"host"` + Protocol NetworkApprovalProtocol `json:"protocol"` +} + +type NetworkApprovalProtocol string + +const ( + NetworkApprovalProtocolHTTP NetworkApprovalProtocol = "http" + NetworkApprovalProtocolHTTPS NetworkApprovalProtocol = "https" + NetworkApprovalProtocolSocks5Tcp NetworkApprovalProtocol = "socks5Tcp" + NetworkApprovalProtocolSocks5Udp NetworkApprovalProtocol = "socks5Udp" +) + +type NetworkPolicyAmendment struct { + Action NetworkPolicyRuleAction `json:"action"` + Host string `json:"host"` +} + +type NetworkPolicyRuleAction string + +const ( + NetworkPolicyRuleActionAllow NetworkPolicyRuleAction = "allow" + NetworkPolicyRuleActionDeny NetworkPolicyRuleAction = "deny" +) + +type NonSteerableTurnKind string + +const ( + NonSteerableTurnKindCompact NonSteerableTurnKind = "compact" + NonSteerableTurnKindReview NonSteerableTurnKind = "review" +) + +type PatchApplyStatus string + +const ( + PatchApplyStatusCompleted PatchApplyStatus = "completed" + PatchApplyStatusDeclined PatchApplyStatus = "declined" + PatchApplyStatusFailed PatchApplyStatus = "failed" + PatchApplyStatusInProgress PatchApplyStatus = "inProgress" +) + +type PermissionGrantScope string + +const ( + PermissionGrantScopeSession PermissionGrantScope = "session" + PermissionGrantScopeTurn PermissionGrantScope = "turn" +) + +type PermissionsRequestApprovalParams struct { + Cwd AbsolutePathBuf `json:"cwd"` + EnvironmentID *string `json:"environmentId,omitempty"` + ItemID string `json:"itemId"` + Permissions RequestPermissionProfile `json:"permissions"` + Reason *string `json:"reason,omitempty"` + // Unix timestamp (in milliseconds) when this approval request started. + StartedAtMs int64 `json:"startedAtMs"` + ThreadID string `json:"threadId"` + TurnID string `json:"turnId"` +} + +type PermissionsRequestApprovalResponse struct { + Permissions GrantedPermissionProfile `json:"permissions"` + Scope *PermissionGrantScope `json:"scope,omitempty"` + // Review every subsequent command in this turn before normal sandboxed execution. + StrictAutoReview *bool `json:"strictAutoReview,omitempty"` +} + +type Personality string + +const ( + PersonalityFriendly Personality = "friendly" + PersonalityNone Personality = "none" + PersonalityPragmatic Personality = "pragmatic" +) + +type PlanType string + +const ( + PlanTypeBusiness PlanType = "business" + PlanTypeEdu PlanType = "edu" + PlanTypeEduPlus PlanType = "edu_plus" + PlanTypeEduPro PlanType = "edu_pro" + PlanTypeEnt26 PlanType = "ent26" + PlanTypeEnterprise PlanType = "enterprise" + PlanTypeEnterpriseCbpAutomation PlanType = "enterprise_cbp_automation" + PlanTypeEnterpriseCbpUsageBased PlanType = "enterprise_cbp_usage_based" + PlanTypeFree PlanType = "free" + PlanTypeGo PlanType = "go" + PlanTypePlus PlanType = "plus" + PlanTypePro PlanType = "pro" + PlanTypeProlite PlanType = "prolite" + PlanTypeSelfServeBusinessProlite PlanType = "self_serve_business_prolite" + PlanTypeSelfServeBusinessUsageBased PlanType = "self_serve_business_usage_based" + PlanTypeTeam PlanType = "team" + PlanTypeUnknown PlanType = "unknown" +) + +type RateLimitReachedType string + +const ( + RateLimitReachedTypeRateLimitReached RateLimitReachedType = "rate_limit_reached" + RateLimitReachedTypeWorkspaceMemberCreditsDepleted RateLimitReachedType = "workspace_member_credits_depleted" + RateLimitReachedTypeWorkspaceMemberUsageLimitReached RateLimitReachedType = "workspace_member_usage_limit_reached" + RateLimitReachedTypeWorkspaceOwnerCreditsDepleted RateLimitReachedType = "workspace_owner_credits_depleted" + RateLimitReachedTypeWorkspaceOwnerUsageLimitReached RateLimitReachedType = "workspace_owner_usage_limit_reached" +) + +type RateLimitResetCredit struct { + // Backend-provided display description for this credit, or `null` when unavailable. + Description *string `json:"description,omitempty"` + // Unix timestamp in seconds when the credit expires, or `null` if it does not expire. + ExpiresAt *int64 `json:"expiresAt,omitempty"` + // Unix timestamp in seconds when the credit was granted. + GrantedAt int64 `json:"grantedAt"` + // Opaque backend identifier for this reset credit. + ID string `json:"id"` + ResetType RateLimitResetType `json:"resetType"` + Status RateLimitResetCreditStatus `json:"status"` + // Backend-provided display title for this credit, or `null` when unavailable. + Title *string `json:"title,omitempty"` +} + +type RateLimitResetCreditStatus string + +const ( + RateLimitResetCreditStatusAvailable RateLimitResetCreditStatus = "available" + RateLimitResetCreditStatusRedeemed RateLimitResetCreditStatus = "redeemed" + RateLimitResetCreditStatusRedeeming RateLimitResetCreditStatus = "redeeming" + RateLimitResetCreditStatusUnknown RateLimitResetCreditStatus = "unknown" +) + +type RateLimitResetCreditsSummary struct { + AvailableCount int64 `json:"availableCount"` + // Detail rows for available reset credits, when the backend provides them. + Credits []RateLimitResetCredit `json:"credits,omitempty"` +} + +type RateLimitResetType string + +const ( + RateLimitResetTypeCodexRateLimits RateLimitResetType = "codexRateLimits" + RateLimitResetTypeUnknown RateLimitResetType = "unknown" +) + +type RateLimitSnapshot struct { + Credits *CreditsSnapshot `json:"credits,omitempty"` + IndividualLimit *SpendControlLimitSnapshot `json:"individualLimit,omitempty"` + LimitID *string `json:"limitId,omitempty"` + LimitName *string `json:"limitName,omitempty"` + PlanType *PlanType `json:"planType,omitempty"` + Primary *RateLimitWindow `json:"primary,omitempty"` + RateLimitReachedType *RateLimitReachedType `json:"rateLimitReachedType,omitempty"` + Secondary *RateLimitWindow `json:"secondary,omitempty"` + // Backend-reported spend-control state. `None` is unavailable, not a sparse-update recovery. + SpendControlReached *bool `json:"spendControlReached,omitempty"` +} + +type RateLimitWindow struct { + ResetsAt *int64 `json:"resetsAt,omitempty"` + UsedPercent int64 `json:"usedPercent"` + WindowDurationMins *int64 `json:"windowDurationMins,omitempty"` +} + +// A non-empty reasoning effort value advertised by the model. +type ReasoningEffort = string + +type ReasoningEffortOption struct { + Description string `json:"description"` + ReasoningEffort ReasoningEffort `json:"reasoningEffort"` +} + +// A summary of the reasoning performed by the model. This can be useful for debugging and understanding the model's reasoning process. See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#reasoning-summaries +type ReasoningSummary string + +const ( + ReasoningSummaryAuto ReasoningSummary = "auto" + ReasoningSummaryConcise ReasoningSummary = "concise" + ReasoningSummaryDetailed ReasoningSummary = "detailed" + ReasoningSummaryNone ReasoningSummary = "none" +) + +type ReasoningSummaryPartAddedNotification struct { + ItemID string `json:"itemId"` + SummaryIndex int64 `json:"summaryIndex"` + ThreadID string `json:"threadId"` + TurnID string `json:"turnId"` +} + +type ReasoningSummaryTextDeltaNotification struct { + Delta string `json:"delta"` + ItemID string `json:"itemId"` + SummaryIndex int64 `json:"summaryIndex"` + ThreadID string `json:"threadId"` + TurnID string `json:"turnId"` +} + +type ReasoningTextDeltaNotification struct { + ContentIndex int64 `json:"contentIndex"` + Delta string `json:"delta"` + ItemID string `json:"itemId"` + ThreadID string `json:"threadId"` + TurnID string `json:"turnId"` +} + +type RequestPermissionProfile struct { + FileSystem *AdditionalFileSystemPermissions `json:"fileSystem,omitempty"` + Network *AdditionalNetworkPermissions `json:"network,omitempty"` +} + +type SandboxMode string + +const ( + SandboxModeDangerFullAccess SandboxMode = "danger-full-access" + SandboxModeReadOnly SandboxMode = "read-only" + SandboxModeWorkspaceWrite SandboxMode = "workspace-write" +) + +type ServerRequestResolvedNotification struct { + RequestID RequestID `json:"requestId"` + ThreadID string `json:"threadId"` +} + +type SpendControlLimitSnapshot struct { + Limit string `json:"limit"` + RemainingPercent int64 `json:"remainingPercent"` + ResetsAt int64 `json:"resetsAt"` + Used string `json:"used"` +} + +type SubAgentActivityKind string + +const ( + SubAgentActivityKindCompleted SubAgentActivityKind = "completed" + SubAgentActivityKindInteracted SubAgentActivityKind = "interacted" + SubAgentActivityKindInterrupted SubAgentActivityKind = "interrupted" + SubAgentActivityKindStarted SubAgentActivityKind = "started" +) + +type TextElement struct { + // Byte range in the parent `text` buffer that this element occupies. + ByteRange ByteRange `json:"byteRange"` + // Optional human-readable placeholder for the element, displayed in the UI. + Placeholder *string `json:"placeholder,omitempty"` +} + +type TextPosition struct { + // 1-based column number (in Unicode scalar values). + Column uint64 `json:"column"` + // 1-based line number. + Line uint64 `json:"line"` +} + +type TextRange struct { + End TextPosition `json:"end"` + Start TextPosition `json:"start"` +} + +type Thread struct { + // Optional random unique nickname assigned to an AgentControl-spawned sub-agent. + AgentNickname *string `json:"agentNickname,omitempty"` + // Optional role (agent_role) assigned to an AgentControl-spawned sub-agent. + AgentRole *string `json:"agentRole,omitempty"` + // Version of the CLI that created the thread. + CliVersion string `json:"cliVersion"` + // Unix timestamp (in seconds) when the thread was created. + CreatedAt int64 `json:"createdAt"` + // Working directory captured for the thread. + Cwd AbsolutePathBuf `json:"cwd"` + // Whether the thread is ephemeral and should not be materialized on disk. + Ephemeral bool `json:"ephemeral"` + // Source thread id when this thread was created by forking another thread. + ForkedFromID *string `json:"forkedFromId,omitempty"` + // Optional Git metadata captured when the thread was created. + GitInfo *GitInfo `json:"gitInfo,omitempty"` + // Persisted thread history contract selected when this thread was created. + HistoryMode *ThreadHistoryMode `json:"historyMode,omitempty"` + // Identifier for this thread. Codex-generated thread IDs are UUIDv7. + ID string `json:"id"` + // Model provider used for this thread (for example, 'openai'). + ModelProvider string `json:"modelProvider"` + // Optional user-facing thread title. + Name *string `json:"name,omitempty"` + // The ID of the parent thread. This will only be set if this thread is a subagent. + ParentThreadID *string `json:"parentThreadId,omitempty"` + // [UNSTABLE] Path to the thread on disk. + Path *string `json:"path,omitempty"` + // Usually the first user message in the thread, if available. + Preview string `json:"preview"` + // Canonical project assignment owned by app-server, if any. + ProjectID *string `json:"projectId"` + // Unix timestamp (in seconds) used for thread recency ordering. + RecencyAt *int64 `json:"recencyAt,omitempty"` + // The independently persisted section selected for this thread, if any. + Section *ThreadSection `json:"section,omitempty"` + // Unix timestamp in seconds when the thread entered its current section. + SectionEnteredAt *int64 `json:"sectionEnteredAt,omitempty"` + // Session id shared by threads that belong to the same session tree. + SessionID string `json:"sessionId"` + // Origin of the thread (CLI, VSCode, codex exec, codex app-server, etc.). + Source SessionSource `json:"source"` + // Current runtime status for the thread. + Status ThreadStatus `json:"status"` + // Optional analytics source classification for this thread. + ThreadSource *ThreadSource `json:"threadSource,omitempty"` + // Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list. + Turns []Turn `json:"turns"` + // Unix timestamp (in seconds) when the thread was last updated. + UpdatedAt int64 `json:"updatedAt"` +} + +func (v Thread) MarshalJSON() ([]byte, error) { + type plain Thread + p := plain(v) + if p.Turns == nil { + p.Turns = []Turn{} + } + return jsonMarshal(p) +} + +type ThreadActiveFlag string + +const ( + ThreadActiveFlagWaitingOnApproval ThreadActiveFlag = "waitingOnApproval" + ThreadActiveFlagWaitingOnUserInput ThreadActiveFlag = "waitingOnUserInput" +) + +type ThreadCompactStartParams struct { + ThreadID string `json:"threadId"` +} + +type ThreadCompactStartResponse struct { +} + +// There are two ways to fork a thread: 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread. 2. By path: load the thread from disk by path and fork it into a new thread. +type ThreadForkParams struct { + ApprovalPolicy *AskForApproval `json:"approvalPolicy,omitempty"` + // Override where approval requests are routed for review on this thread and subsequent turns. + ApprovalsReviewer *ApprovalsReviewer `json:"approvalsReviewer,omitempty"` + BaseInstructions *string `json:"baseInstructions,omitempty"` + Config map[string]any `json:"config,omitempty"` + Cwd *string `json:"cwd,omitempty"` + DeveloperInstructions *string `json:"developerInstructions,omitempty"` + Ephemeral *bool `json:"ephemeral,omitempty"` + // When true, return only thread metadata and live fork state without populating `thread.turns`. This is useful when the client plans to call `thread/turns/list` immediately after forking. Full-history hydration is deprecated for paginated threads; use this with `thread/turns/list` and `thread/items/list` instead. + ExcludeTurns *bool `json:"excludeTurns,omitempty"` + // Optional last turn id to fork through, inclusive. + LastTurnID *string `json:"lastTurnId,omitempty"` + // Configuration overrides for the forked thread, if any. + Model *string `json:"model,omitempty"` + ModelProvider *string `json:"modelProvider,omitempty"` + Sandbox *SandboxMode `json:"sandbox,omitempty"` + ServiceTier *string `json:"serviceTier,omitempty"` + ThreadID string `json:"threadId"` + // Optional client-supplied analytics source classification for this forked thread. + ThreadSource *ThreadSource `json:"threadSource,omitempty"` +} + +type ThreadForkResponse struct { + ApprovalPolicy AskForApproval `json:"approvalPolicy"` + // Reviewer currently used for approval requests on this thread. + ApprovalsReviewer ApprovalsReviewer `json:"approvalsReviewer"` + Cwd AbsolutePathBuf `json:"cwd"` + // Environment-native paths to instruction source files currently loaded for this thread. + InstructionSources []LegacyAppPathString `json:"instructionSources,omitempty"` + Model string `json:"model"` + ModelProvider string `json:"modelProvider"` + ReasoningEffort *ReasoningEffort `json:"reasoningEffort,omitempty"` + // Legacy sandbox policy retained for compatibility. Experimental clients should prefer `activePermissionProfile` for profile provenance. + Sandbox SandboxPolicy `json:"sandbox"` + ServiceTier *string `json:"serviceTier,omitempty"` + Thread Thread `json:"thread"` +} + +type ThreadHistoryMode string + +const ( + ThreadHistoryModeLegacy ThreadHistoryMode = "legacy" + ThreadHistoryModePaginated ThreadHistoryMode = "paginated" +) + +type ThreadId = string + +type ThreadReadParams struct { + // When true, include turns and their items from rollout history. Full-history hydration is deprecated for paginated threads; prefer a metadata-only read and page with `thread/turns/list` and `thread/items/list`. + IncludeTurns *bool `json:"includeTurns,omitempty"` + ThreadID string `json:"threadId"` +} + +type ThreadReadResponse struct { + Thread Thread `json:"thread"` +} + +// There are three ways to resume a thread: 1. By thread_id: load the thread from disk by thread_id and resume it. 2. By history: instantiate the thread from memory and resume it. 3. By path: load the thread from disk by path and resume it. +type ThreadResumeParams struct { + ApprovalPolicy *AskForApproval `json:"approvalPolicy,omitempty"` + // Override where approval requests are routed for review on this thread and subsequent turns. + ApprovalsReviewer *ApprovalsReviewer `json:"approvalsReviewer,omitempty"` + BaseInstructions *string `json:"baseInstructions,omitempty"` + Config map[string]any `json:"config,omitempty"` + Cwd *string `json:"cwd,omitempty"` + DeveloperInstructions *string `json:"developerInstructions,omitempty"` + // When true, return only thread metadata and live-resume state without populating `thread.turns`. This is useful when the client plans to call `thread/turns/list` immediately after resuming. Full-history hydration is deprecated for paginated threads; use this with `thread/turns/list` and `thread/items/list` instead. + ExcludeTurns *bool `json:"excludeTurns,omitempty"` + // Configuration overrides for the resumed thread, if any. + Model *string `json:"model,omitempty"` + ModelProvider *string `json:"modelProvider,omitempty"` + Personality *Personality `json:"personality,omitempty"` + Sandbox *SandboxMode `json:"sandbox,omitempty"` + ServiceTier *string `json:"serviceTier,omitempty"` + ThreadID string `json:"threadId"` +} + +type ThreadResumeResponse struct { + ApprovalPolicy AskForApproval `json:"approvalPolicy"` + // Reviewer currently used for approval requests on this thread. + ApprovalsReviewer ApprovalsReviewer `json:"approvalsReviewer"` + Cwd AbsolutePathBuf `json:"cwd"` + // Environment-native paths to instruction source files currently loaded for this thread. + InstructionSources []LegacyAppPathString `json:"instructionSources,omitempty"` + // Opaque cursor for hydrating paginated items backwards. + ItemsBackwardsCursor *string `json:"itemsBackwardsCursor,omitempty"` + Model string `json:"model"` + ModelProvider string `json:"modelProvider"` + ReasoningEffort *ReasoningEffort `json:"reasoningEffort,omitempty"` + // Legacy sandbox policy retained for compatibility. Experimental clients should prefer `activePermissionProfile` for profile provenance. + Sandbox SandboxPolicy `json:"sandbox"` + ServiceTier *string `json:"serviceTier,omitempty"` + Thread Thread `json:"thread"` + // Opaque cursor for hydrating paginated turns backwards. + TurnsBackwardsCursor *string `json:"turnsBackwardsCursor,omitempty"` +} + +// An independently persisted, user-visible thread section. +type ThreadSection struct { + // Optional appearance synchronized across clients. + Appearance *ThreadSectionAppearance `json:"appearance,omitempty"` + // Opaque UUIDv7 identity that remains stable when the section is renamed. + ID string `json:"id"` + // The current user-visible section name. + Name string `json:"name"` +} + +// Extensible visual presentation for a custom thread section. +type ThreadSectionAppearance struct { + Color *string `json:"color,omitempty"` + Icon *string `json:"icon,omitempty"` +} + +type ThreadSource = string + +type ThreadStartParams struct { + ApprovalPolicy *AskForApproval `json:"approvalPolicy,omitempty"` + // Override where approval requests are routed for review on this thread and subsequent turns. + ApprovalsReviewer *ApprovalsReviewer `json:"approvalsReviewer,omitempty"` + BaseInstructions *string `json:"baseInstructions,omitempty"` + Config map[string]any `json:"config,omitempty"` + Cwd *string `json:"cwd,omitempty"` + DeveloperInstructions *string `json:"developerInstructions,omitempty"` + Ephemeral *bool `json:"ephemeral,omitempty"` + Model *string `json:"model,omitempty"` + ModelProvider *string `json:"modelProvider,omitempty"` + Personality *Personality `json:"personality,omitempty"` + Sandbox *SandboxMode `json:"sandbox,omitempty"` + ServiceName *string `json:"serviceName,omitempty"` + ServiceTier *string `json:"serviceTier,omitempty"` + SessionStartSource *ThreadStartSource `json:"sessionStartSource,omitempty"` + // Optional client-supplied analytics source classification for this thread. + ThreadSource *ThreadSource `json:"threadSource,omitempty"` +} + +type ThreadStartResponse struct { + ApprovalPolicy AskForApproval `json:"approvalPolicy"` + // Reviewer currently used for approval requests on this thread. + ApprovalsReviewer ApprovalsReviewer `json:"approvalsReviewer"` + Cwd AbsolutePathBuf `json:"cwd"` + // Environment-native paths to instruction source files currently loaded for this thread. + InstructionSources []LegacyAppPathString `json:"instructionSources,omitempty"` + Model string `json:"model"` + ModelProvider string `json:"modelProvider"` + ReasoningEffort *ReasoningEffort `json:"reasoningEffort,omitempty"` + // Legacy sandbox policy retained for compatibility. Experimental clients should prefer `activePermissionProfile` for profile provenance. + Sandbox SandboxPolicy `json:"sandbox"` + ServiceTier *string `json:"serviceTier,omitempty"` + Thread Thread `json:"thread"` +} + +type ThreadStartSource string + +const ( + ThreadStartSourceClear ThreadStartSource = "clear" + ThreadStartSourceStartup ThreadStartSource = "startup" +) + +type ThreadStartedNotification struct { + Thread Thread `json:"thread"` +} + +type ThreadStatusChangedNotification struct { + Status ThreadStatus `json:"status"` + ThreadID string `json:"threadId"` +} + +type ThreadTokenUsage struct { + Last TokenUsageBreakdown `json:"last"` + ModelContextWindow *int64 `json:"modelContextWindow,omitempty"` + Total TokenUsageBreakdown `json:"total"` +} + +type ThreadTokenUsageUpdatedNotification struct { + ThreadID string `json:"threadId"` + TokenUsage ThreadTokenUsage `json:"tokenUsage"` + TurnID string `json:"turnId"` +} + +type TokenUsageBreakdown struct { + CacheWriteInputTokens *int64 `json:"cacheWriteInputTokens,omitempty"` + CachedInputTokens int64 `json:"cachedInputTokens"` + InputTokens int64 `json:"inputTokens"` + OutputTokens int64 `json:"outputTokens"` + ReasoningOutputTokens int64 `json:"reasoningOutputTokens"` + TotalTokens int64 `json:"totalTokens"` +} + +// EXPERIMENTAL. Captures a user's answer to a request_user_input question. +type ToolRequestUserInputAnswer struct { + Answers []string `json:"answers"` +} + +func (v ToolRequestUserInputAnswer) MarshalJSON() ([]byte, error) { + type plain ToolRequestUserInputAnswer + p := plain(v) + if p.Answers == nil { + p.Answers = []string{} + } + return jsonMarshal(p) +} + +// EXPERIMENTAL. Defines a single selectable option for request_user_input. +type ToolRequestUserInputOption struct { + Description string `json:"description"` + Label string `json:"label"` +} + +// EXPERIMENTAL. Params sent with a request_user_input event. +type ToolRequestUserInputParams struct { + // @deprecated Use `isBlocking` to decide whether the request should block. + AutoResolutionMs *uint64 `json:"autoResolutionMs,omitempty"` + IsBlocking bool `json:"isBlocking"` + ItemID string `json:"itemId"` + Questions []ToolRequestUserInputQuestion `json:"questions"` + ThreadID string `json:"threadId"` + TurnID string `json:"turnId"` +} + +func (v ToolRequestUserInputParams) MarshalJSON() ([]byte, error) { + type plain ToolRequestUserInputParams + p := plain(v) + if p.Questions == nil { + p.Questions = []ToolRequestUserInputQuestion{} + } + return jsonMarshal(p) +} + +// EXPERIMENTAL. Represents one request_user_input question and its required options. +type ToolRequestUserInputQuestion struct { + Header string `json:"header"` + ID string `json:"id"` + IsOther *bool `json:"isOther,omitempty"` + IsSecret *bool `json:"isSecret,omitempty"` + Options []ToolRequestUserInputOption `json:"options,omitempty"` + Question string `json:"question"` +} + +// EXPERIMENTAL. Response payload mapping question ids to answers. +type ToolRequestUserInputResponse struct { + Answers map[string]ToolRequestUserInputAnswer `json:"answers"` +} + +func (v ToolRequestUserInputResponse) MarshalJSON() ([]byte, error) { + type plain ToolRequestUserInputResponse + p := plain(v) + if p.Answers == nil { + p.Answers = map[string]ToolRequestUserInputAnswer{} + } + return jsonMarshal(p) +} + +type Turn struct { + // Unix timestamp (in seconds) when the turn completed. + CompletedAt *int64 `json:"completedAt,omitempty"` + // Duration between turn start and completion in milliseconds, if known. + DurationMs *int64 `json:"durationMs,omitempty"` + // Only populated when the Turn's status is failed. + Error *TurnError `json:"error,omitempty"` + // Identifier for this turn. Codex-generated turn IDs are UUIDv7. + ID string `json:"id"` + // Thread items currently included in this turn payload. + Items []ThreadItem `json:"items"` + // Describes how much of `items` has been loaded for this turn. + ItemsView *TurnItemsView `json:"itemsView,omitempty"` + // Unix timestamp (in seconds) when the turn started. + StartedAt *int64 `json:"startedAt,omitempty"` + Status TurnStatus `json:"status"` +} + +func (v Turn) MarshalJSON() ([]byte, error) { + type plain Turn + p := plain(v) + if p.Items == nil { + p.Items = []ThreadItem{} + } + return jsonMarshal(p) +} + +type TurnCompletedNotification struct { + ThreadID string `json:"threadId"` + Turn Turn `json:"turn"` +} + +type TurnError struct { + AdditionalDetails *string `json:"additionalDetails,omitempty"` + CodexErrorInfo *CodexErrorInfo `json:"codexErrorInfo,omitempty"` + Message string `json:"message"` + // Optional public explanation and continuation instruction for a misalignment block. + Misalignment *MisalignmentErrorDetails `json:"misalignment,omitempty"` +} + +type TurnInterruptParams struct { + ThreadID string `json:"threadId"` + TurnID string `json:"turnId"` +} + +type TurnInterruptResponse struct { +} + +type TurnItemsView string + +const ( + TurnItemsViewFull TurnItemsView = "full" + TurnItemsViewNotLoaded TurnItemsView = "notLoaded" + TurnItemsViewSummary TurnItemsView = "summary" +) + +type TurnPlanStep struct { + Status TurnPlanStepStatus `json:"status"` + Step string `json:"step"` +} + +type TurnPlanStepStatus string + +const ( + TurnPlanStepStatusCompleted TurnPlanStepStatus = "completed" + TurnPlanStepStatusInProgress TurnPlanStepStatus = "inProgress" + TurnPlanStepStatusPending TurnPlanStepStatus = "pending" +) + +type TurnPlanUpdatedNotification struct { + Explanation *string `json:"explanation,omitempty"` + Plan []TurnPlanStep `json:"plan"` + ThreadID string `json:"threadId"` + TurnID string `json:"turnId"` +} + +func (v TurnPlanUpdatedNotification) MarshalJSON() ([]byte, error) { + type plain TurnPlanUpdatedNotification + p := plain(v) + if p.Plan == nil { + p.Plan = []TurnPlanStep{} + } + return jsonMarshal(p) +} + +type TurnStartParams struct { + // Override the approval policy for this turn and subsequent turns. + ApprovalPolicy *AskForApproval `json:"approvalPolicy,omitempty"` + // Override where approval requests are routed for review on this turn and subsequent turns. + ApprovalsReviewer *ApprovalsReviewer `json:"approvalsReviewer,omitempty"` + ClientUserMessageID *string `json:"clientUserMessageId,omitempty"` + // Override the working directory for this turn and subsequent turns. + Cwd *string `json:"cwd,omitempty"` + // Override the reasoning effort for this turn and subsequent turns. + Effort *ReasoningEffort `json:"effort,omitempty"` + Input []UserInput `json:"input"` + // Override the model for this turn and subsequent turns. + Model *string `json:"model,omitempty"` + // Optional JSON Schema used to constrain the final assistant message for this turn. + OutputSchema any `json:"outputSchema,omitempty"` + // Override the personality for this turn and subsequent turns. + Personality *Personality `json:"personality,omitempty"` + // Override the sandbox policy for this turn and subsequent turns. + SandboxPolicy *SandboxPolicy `json:"sandboxPolicy,omitempty"` + // Override the service tier for this turn and subsequent turns. + ServiceTier *string `json:"serviceTier,omitempty"` + // Override the service tier only when this request starts a new turn. Use "default" for standard speed. Omitted or null inherits the thread's tier. Does not change the thread's tier or a turn being steered. + ServiceTierForTurn *string `json:"serviceTierForTurn,omitempty"` + // Override the reasoning summary for this turn and subsequent turns. + Summary *ReasoningSummary `json:"summary,omitempty"` + ThreadID string `json:"threadId"` + ToolOutput *TurnToolOutput `json:"toolOutput,omitempty"` + // Optional source classification for the caller that starts this turn. Ignored when this request steers an already-active turn. + TurnTrigger *string `json:"turnTrigger,omitempty"` +} + +func (v TurnStartParams) MarshalJSON() ([]byte, error) { + type plain TurnStartParams + p := plain(v) + if p.Input == nil { + p.Input = []UserInput{} + } + return jsonMarshal(p) +} + +type TurnStartResponse struct { + Turn Turn `json:"turn"` +} + +type TurnStartedNotification struct { + ThreadID string `json:"threadId"` + Turn Turn `json:"turn"` +} + +type TurnStatus string + +const ( + TurnStatusCompleted TurnStatus = "completed" + TurnStatusFailed TurnStatus = "failed" + TurnStatusInProgress TurnStatus = "inProgress" + TurnStatusInterrupted TurnStatus = "interrupted" +) + +type TurnSteerParams struct { + ClientUserMessageID *string `json:"clientUserMessageId,omitempty"` + // Required active turn id precondition. The request fails when it does not match the currently active turn. + ExpectedTurnID string `json:"expectedTurnId"` + Input []UserInput `json:"input"` + ThreadID string `json:"threadId"` +} + +func (v TurnSteerParams) MarshalJSON() ([]byte, error) { + type plain TurnSteerParams + p := plain(v) + if p.Input == nil { + p.Input = []UserInput{} + } + return jsonMarshal(p) +} + +type TurnSteerResponse struct { + TurnID string `json:"turnId"` +} + +type TurnToolOutput struct { + Name string `json:"name"` + Namespace *string `json:"namespace,omitempty"` + Output rawMessage `json:"output"` +} + +type WarningNotification struct { + // Concise warning message for the user. + Message string `json:"message"` + // Optional thread target when the warning applies to a specific thread. + ThreadID *string `json:"threadId,omitempty"` +} + +type AskForApprovalGranular struct { + MCPElicitations bool `json:"mcp_elicitations"` + RequestPermissions *bool `json:"request_permissions,omitempty"` + Rules bool `json:"rules"` + SandboxApproval bool `json:"sandbox_approval"` + SkillApproval *bool `json:"skill_approval,omitempty"` +} + +type CodexErrorInfoHTTPConnectionFailed struct { + HTTPStatusCode *uint64 `json:"httpStatusCode,omitempty"` +} + +type CodexErrorInfoResponseStreamConnectionFailed struct { + HTTPStatusCode *uint64 `json:"httpStatusCode,omitempty"` +} + +type CodexErrorInfoResponseStreamDisconnected struct { + HTTPStatusCode *uint64 `json:"httpStatusCode,omitempty"` +} + +type CodexErrorInfoResponseTooManyFailedAttempts struct { + HTTPStatusCode *uint64 `json:"httpStatusCode,omitempty"` +} + +type CodexErrorInfoActiveTurnNotSteerable struct { + TurnKind NonSteerableTurnKind `json:"turnKind"` +} + +type CommandExecutionApprovalDecisionAcceptWithExecpolicyAmendment struct { + ExecpolicyAmendment []string `json:"execpolicy_amendment"` +} + +func (v CommandExecutionApprovalDecisionAcceptWithExecpolicyAmendment) MarshalJSON() ([]byte, error) { + type plain CommandExecutionApprovalDecisionAcceptWithExecpolicyAmendment + p := plain(v) + if p.ExecpolicyAmendment == nil { + p.ExecpolicyAmendment = []string{} + } + return jsonMarshal(p) +} + +type CommandExecutionApprovalDecisionApplyNetworkPolicyAmendment struct { + NetworkPolicyAmendment NetworkPolicyAmendment `json:"network_policy_amendment"` +} + +type SubAgentSourceThreadSpawn struct { + AgentNickname *string `json:"agent_nickname,omitempty"` + AgentPath *AgentPath `json:"agent_path,omitempty"` + AgentRole *string `json:"agent_role,omitempty"` + Depth int64 `json:"depth"` + ParentThreadID ThreadId `json:"parent_thread_id"` +} diff --git a/internal/agent/runtime/codex/protocol/unions.gen.go b/internal/agent/runtime/codex/protocol/unions.gen.go new file mode 100644 index 0000000000..29a5cf6c6b --- /dev/null +++ b/internal/agent/runtime/codex/protocol/unions.gen.go @@ -0,0 +1,2001 @@ +// Code generated by gen-codex-protocol from the codex app-server v2 JSON +// Schema snapshot (codex-cli 0.151.0). DO NOT EDIT. +// +// Regenerate with `mise run codex-protocol-generate`; refresh the snapshot +// itself with `mise run codex-schema-sync`. + +package protocol + +// Account is an internally-tagged union (tag property "type"). +// Unknown variants decode without error: only Tag and Raw() are populated. +type Account struct { + // Tag is the value of the "type" property observed on decode. + Tag string + AmazonBedrock *AmazonBedrockAccount + APIKey *ApiKeyAccount + Chatgpt *ChatgptAccount + raw rawMessage +} + +const ( + AccountTagAmazonBedrock = "amazonBedrock" + AccountTagAPIKey = "apiKey" + AccountTagChatgpt = "chatgpt" +) + +func (u *Account) UnmarshalJSON(data []byte) error { + *u = Account{} + u.raw = append(rawMessage(nil), data...) + var probe struct { + Tag string `json:"type"` + } + if err := jsonUnmarshal(data, &probe); err != nil { + return err + } + u.Tag = probe.Tag + switch probe.Tag { + case "amazonBedrock": + u.AmazonBedrock = new(AmazonBedrockAccount) + return jsonUnmarshal(data, u.AmazonBedrock) + case "apiKey": + u.APIKey = new(ApiKeyAccount) + return jsonUnmarshal(data, u.APIKey) + case "chatgpt": + u.Chatgpt = new(ChatgptAccount) + return jsonUnmarshal(data, u.Chatgpt) + } + return nil +} + +func (u Account) MarshalJSON() ([]byte, error) { + switch { + case u.AmazonBedrock != nil: + return marshalTagged("type", "amazonBedrock", u.AmazonBedrock) + case u.APIKey != nil: + return marshalTagged("type", "apiKey", u.APIKey) + case u.Chatgpt != nil: + return marshalTagged("type", "chatgpt", u.Chatgpt) + } + if len(u.raw) > 0 { + return u.raw, nil + } + return nil, errNoVariant("Account") +} + +// Raw returns the original JSON for this union value, if it was decoded. +func (u Account) Raw() []byte { return u.raw } + +type AmazonBedrockAccount struct { + UsesCodexManagedCredentials *bool `json:"usesCodexManagedCredentials,omitempty"` +} + +type ApiKeyAccount struct { +} + +type ChatgptAccount struct { + Email *string `json:"email"` + PlanType PlanType `json:"planType"` +} + +// AskForApproval is a mixed union: on the wire it is either a bare string +// (Unit) or a single-key object (one payload field set). Unknown variants +// decode without error and are retained in Raw(). +type AskForApproval struct { + // Unit holds the bare-string variant value, if that form was used. + Unit string + Granular *AskForApprovalGranular + raw rawMessage +} + +const ( + AskForApprovalUnitNever = "never" + AskForApprovalUnitOnRequest = "on-request" + AskForApprovalUnitUntrusted = "untrusted" +) + +func (u *AskForApproval) UnmarshalJSON(data []byte) error { + *u = AskForApproval{} + u.raw = append(rawMessage(nil), data...) + if isJSONString(data) { + return jsonUnmarshal(data, &u.Unit) + } + var obj map[string]rawMessage + if err := jsonUnmarshal(data, &obj); err != nil { + return err + } + if len(obj) != 1 { + return nil + } + for key, payload := range obj { + switch key { + case "granular": + u.Granular = new(AskForApprovalGranular) + return jsonUnmarshal(payload, u.Granular) + } + } + return nil +} + +func (u AskForApproval) MarshalJSON() ([]byte, error) { + if u.Unit != "" { + return jsonMarshal(u.Unit) + } + switch { + case u.Granular != nil: + return marshalKeyed("granular", u.Granular) + } + if len(u.raw) > 0 { + return u.raw, nil + } + return nil, errNoVariant("AskForApproval") +} + +// Raw returns the original JSON for this union value, if it was decoded. +func (u AskForApproval) Raw() []byte { return u.raw } + +// This translation layer make sure that we expose codex error code in camel case. +// CodexErrorInfo is a mixed union: on the wire it is either a bare string +// (Unit) or a single-key object (one payload field set). Unknown variants +// decode without error and are retained in Raw(). +type CodexErrorInfo struct { + // Unit holds the bare-string variant value, if that form was used. + Unit string + ActiveTurnNotSteerable *CodexErrorInfoActiveTurnNotSteerable + HTTPConnectionFailed *CodexErrorInfoHTTPConnectionFailed + ResponseStreamConnectionFailed *CodexErrorInfoResponseStreamConnectionFailed + ResponseStreamDisconnected *CodexErrorInfoResponseStreamDisconnected + ResponseTooManyFailedAttempts *CodexErrorInfoResponseTooManyFailedAttempts + raw rawMessage +} + +const ( + CodexErrorInfoUnitBadRequest = "badRequest" + CodexErrorInfoUnitContextWindowExceeded = "contextWindowExceeded" + CodexErrorInfoUnitCyberPolicy = "cyberPolicy" + CodexErrorInfoUnitInternalServerError = "internalServerError" + CodexErrorInfoUnitMisalignmentPolicyViolation = "misalignmentPolicyViolation" + CodexErrorInfoUnitOther = "other" + CodexErrorInfoUnitRateLimitExceeded = "rateLimitExceeded" + CodexErrorInfoUnitSandboxError = "sandboxError" + CodexErrorInfoUnitServerOverloaded = "serverOverloaded" + CodexErrorInfoUnitSessionBudgetExceeded = "sessionBudgetExceeded" + CodexErrorInfoUnitThreadRollbackFailed = "threadRollbackFailed" + CodexErrorInfoUnitUnauthorized = "unauthorized" + CodexErrorInfoUnitUsageLimitExceeded = "usageLimitExceeded" +) + +func (u *CodexErrorInfo) UnmarshalJSON(data []byte) error { + *u = CodexErrorInfo{} + u.raw = append(rawMessage(nil), data...) + if isJSONString(data) { + return jsonUnmarshal(data, &u.Unit) + } + var obj map[string]rawMessage + if err := jsonUnmarshal(data, &obj); err != nil { + return err + } + if len(obj) != 1 { + return nil + } + for key, payload := range obj { + switch key { + case "activeTurnNotSteerable": + u.ActiveTurnNotSteerable = new(CodexErrorInfoActiveTurnNotSteerable) + return jsonUnmarshal(payload, u.ActiveTurnNotSteerable) + case "httpConnectionFailed": + u.HTTPConnectionFailed = new(CodexErrorInfoHTTPConnectionFailed) + return jsonUnmarshal(payload, u.HTTPConnectionFailed) + case "responseStreamConnectionFailed": + u.ResponseStreamConnectionFailed = new(CodexErrorInfoResponseStreamConnectionFailed) + return jsonUnmarshal(payload, u.ResponseStreamConnectionFailed) + case "responseStreamDisconnected": + u.ResponseStreamDisconnected = new(CodexErrorInfoResponseStreamDisconnected) + return jsonUnmarshal(payload, u.ResponseStreamDisconnected) + case "responseTooManyFailedAttempts": + u.ResponseTooManyFailedAttempts = new(CodexErrorInfoResponseTooManyFailedAttempts) + return jsonUnmarshal(payload, u.ResponseTooManyFailedAttempts) + } + } + return nil +} + +func (u CodexErrorInfo) MarshalJSON() ([]byte, error) { + if u.Unit != "" { + return jsonMarshal(u.Unit) + } + switch { + case u.ActiveTurnNotSteerable != nil: + return marshalKeyed("activeTurnNotSteerable", u.ActiveTurnNotSteerable) + case u.HTTPConnectionFailed != nil: + return marshalKeyed("httpConnectionFailed", u.HTTPConnectionFailed) + case u.ResponseStreamConnectionFailed != nil: + return marshalKeyed("responseStreamConnectionFailed", u.ResponseStreamConnectionFailed) + case u.ResponseStreamDisconnected != nil: + return marshalKeyed("responseStreamDisconnected", u.ResponseStreamDisconnected) + case u.ResponseTooManyFailedAttempts != nil: + return marshalKeyed("responseTooManyFailedAttempts", u.ResponseTooManyFailedAttempts) + } + if len(u.raw) > 0 { + return u.raw, nil + } + return nil, errNoVariant("CodexErrorInfo") +} + +// Raw returns the original JSON for this union value, if it was decoded. +func (u CodexErrorInfo) Raw() []byte { return u.raw } + +// CommandAction is an internally-tagged union (tag property "type"). +// Unknown variants decode without error: only Tag and Raw() are populated. +type CommandAction struct { + // Tag is the value of the "type" property observed on decode. + Tag string + ListFiles *ListFilesCommandAction + Read *ReadCommandAction + Search *SearchCommandAction + Unknown *UnknownCommandAction + raw rawMessage +} + +const ( + CommandActionTagListFiles = "listFiles" + CommandActionTagRead = "read" + CommandActionTagSearch = "search" + CommandActionTagUnknown = "unknown" +) + +func (u *CommandAction) UnmarshalJSON(data []byte) error { + *u = CommandAction{} + u.raw = append(rawMessage(nil), data...) + var probe struct { + Tag string `json:"type"` + } + if err := jsonUnmarshal(data, &probe); err != nil { + return err + } + u.Tag = probe.Tag + switch probe.Tag { + case "listFiles": + u.ListFiles = new(ListFilesCommandAction) + return jsonUnmarshal(data, u.ListFiles) + case "read": + u.Read = new(ReadCommandAction) + return jsonUnmarshal(data, u.Read) + case "search": + u.Search = new(SearchCommandAction) + return jsonUnmarshal(data, u.Search) + case "unknown": + u.Unknown = new(UnknownCommandAction) + return jsonUnmarshal(data, u.Unknown) + } + return nil +} + +func (u CommandAction) MarshalJSON() ([]byte, error) { + switch { + case u.ListFiles != nil: + return marshalTagged("type", "listFiles", u.ListFiles) + case u.Read != nil: + return marshalTagged("type", "read", u.Read) + case u.Search != nil: + return marshalTagged("type", "search", u.Search) + case u.Unknown != nil: + return marshalTagged("type", "unknown", u.Unknown) + } + if len(u.raw) > 0 { + return u.raw, nil + } + return nil, errNoVariant("CommandAction") +} + +// Raw returns the original JSON for this union value, if it was decoded. +func (u CommandAction) Raw() []byte { return u.raw } + +type ListFilesCommandAction struct { + Command string `json:"command"` + Path *string `json:"path,omitempty"` +} + +type ReadCommandAction struct { + Command string `json:"command"` + Name string `json:"name"` + Path LegacyAppPathString `json:"path"` +} + +type SearchCommandAction struct { + Command string `json:"command"` + Path *string `json:"path,omitempty"` + Query *string `json:"query,omitempty"` +} + +type UnknownCommandAction struct { + Command string `json:"command"` +} + +// CommandExecutionApprovalDecision is a mixed union: on the wire it is either a bare string +// (Unit) or a single-key object (one payload field set). Unknown variants +// decode without error and are retained in Raw(). +type CommandExecutionApprovalDecision struct { + // Unit holds the bare-string variant value, if that form was used. + Unit string + AcceptWithExecpolicyAmendment *CommandExecutionApprovalDecisionAcceptWithExecpolicyAmendment + ApplyNetworkPolicyAmendment *CommandExecutionApprovalDecisionApplyNetworkPolicyAmendment + raw rawMessage +} + +const ( + CommandExecutionApprovalDecisionUnitAccept = "accept" + CommandExecutionApprovalDecisionUnitAcceptForSession = "acceptForSession" + CommandExecutionApprovalDecisionUnitCancel = "cancel" + CommandExecutionApprovalDecisionUnitDecline = "decline" +) + +func (u *CommandExecutionApprovalDecision) UnmarshalJSON(data []byte) error { + *u = CommandExecutionApprovalDecision{} + u.raw = append(rawMessage(nil), data...) + if isJSONString(data) { + return jsonUnmarshal(data, &u.Unit) + } + var obj map[string]rawMessage + if err := jsonUnmarshal(data, &obj); err != nil { + return err + } + if len(obj) != 1 { + return nil + } + for key, payload := range obj { + switch key { + case "acceptWithExecpolicyAmendment": + u.AcceptWithExecpolicyAmendment = new(CommandExecutionApprovalDecisionAcceptWithExecpolicyAmendment) + return jsonUnmarshal(payload, u.AcceptWithExecpolicyAmendment) + case "applyNetworkPolicyAmendment": + u.ApplyNetworkPolicyAmendment = new(CommandExecutionApprovalDecisionApplyNetworkPolicyAmendment) + return jsonUnmarshal(payload, u.ApplyNetworkPolicyAmendment) + } + } + return nil +} + +func (u CommandExecutionApprovalDecision) MarshalJSON() ([]byte, error) { + if u.Unit != "" { + return jsonMarshal(u.Unit) + } + switch { + case u.AcceptWithExecpolicyAmendment != nil: + return marshalKeyed("acceptWithExecpolicyAmendment", u.AcceptWithExecpolicyAmendment) + case u.ApplyNetworkPolicyAmendment != nil: + return marshalKeyed("applyNetworkPolicyAmendment", u.ApplyNetworkPolicyAmendment) + } + if len(u.raw) > 0 { + return u.raw, nil + } + return nil, errNoVariant("CommandExecutionApprovalDecision") +} + +// Raw returns the original JSON for this union value, if it was decoded. +func (u CommandExecutionApprovalDecision) Raw() []byte { return u.raw } + +// DynamicToolCallOutputContentItem is an internally-tagged union (tag property "type"). +// Unknown variants decode without error: only Tag and Raw() are populated. +type DynamicToolCallOutputContentItem struct { + // Tag is the value of the "type" property observed on decode. + Tag string + InputAudio *InputAudioDynamicToolCallOutputContentItem + InputImage *InputImageDynamicToolCallOutputContentItem + InputText *InputTextDynamicToolCallOutputContentItem + raw rawMessage +} + +const ( + DynamicToolCallOutputContentItemTagInputAudio = "inputAudio" + DynamicToolCallOutputContentItemTagInputImage = "inputImage" + DynamicToolCallOutputContentItemTagInputText = "inputText" +) + +func (u *DynamicToolCallOutputContentItem) UnmarshalJSON(data []byte) error { + *u = DynamicToolCallOutputContentItem{} + u.raw = append(rawMessage(nil), data...) + var probe struct { + Tag string `json:"type"` + } + if err := jsonUnmarshal(data, &probe); err != nil { + return err + } + u.Tag = probe.Tag + switch probe.Tag { + case "inputAudio": + u.InputAudio = new(InputAudioDynamicToolCallOutputContentItem) + return jsonUnmarshal(data, u.InputAudio) + case "inputImage": + u.InputImage = new(InputImageDynamicToolCallOutputContentItem) + return jsonUnmarshal(data, u.InputImage) + case "inputText": + u.InputText = new(InputTextDynamicToolCallOutputContentItem) + return jsonUnmarshal(data, u.InputText) + } + return nil +} + +func (u DynamicToolCallOutputContentItem) MarshalJSON() ([]byte, error) { + switch { + case u.InputAudio != nil: + return marshalTagged("type", "inputAudio", u.InputAudio) + case u.InputImage != nil: + return marshalTagged("type", "inputImage", u.InputImage) + case u.InputText != nil: + return marshalTagged("type", "inputText", u.InputText) + } + if len(u.raw) > 0 { + return u.raw, nil + } + return nil, errNoVariant("DynamicToolCallOutputContentItem") +} + +// Raw returns the original JSON for this union value, if it was decoded. +func (u DynamicToolCallOutputContentItem) Raw() []byte { return u.raw } + +type InputAudioDynamicToolCallOutputContentItem struct { + AudioURL string `json:"audioUrl"` +} + +type InputImageDynamicToolCallOutputContentItem struct { + ImageURL string `json:"imageUrl"` +} + +type InputTextDynamicToolCallOutputContentItem struct { + Text string `json:"text"` +} + +// FileSystemPath is an internally-tagged union (tag property "type"). +// Unknown variants decode without error: only Tag and Raw() are populated. +type FileSystemPath struct { + // Tag is the value of the "type" property observed on decode. + Tag string + GlobPattern *GlobPatternFileSystemPath + Path *PathFileSystemPath + Special *SpecialFileSystemPath + raw rawMessage +} + +const ( + FileSystemPathTagGlobPattern = "glob_pattern" + FileSystemPathTagPath = "path" + FileSystemPathTagSpecial = "special" +) + +func (u *FileSystemPath) UnmarshalJSON(data []byte) error { + *u = FileSystemPath{} + u.raw = append(rawMessage(nil), data...) + var probe struct { + Tag string `json:"type"` + } + if err := jsonUnmarshal(data, &probe); err != nil { + return err + } + u.Tag = probe.Tag + switch probe.Tag { + case "glob_pattern": + u.GlobPattern = new(GlobPatternFileSystemPath) + return jsonUnmarshal(data, u.GlobPattern) + case "path": + u.Path = new(PathFileSystemPath) + return jsonUnmarshal(data, u.Path) + case "special": + u.Special = new(SpecialFileSystemPath) + return jsonUnmarshal(data, u.Special) + } + return nil +} + +func (u FileSystemPath) MarshalJSON() ([]byte, error) { + switch { + case u.GlobPattern != nil: + return marshalTagged("type", "glob_pattern", u.GlobPattern) + case u.Path != nil: + return marshalTagged("type", "path", u.Path) + case u.Special != nil: + return marshalTagged("type", "special", u.Special) + } + if len(u.raw) > 0 { + return u.raw, nil + } + return nil, errNoVariant("FileSystemPath") +} + +// Raw returns the original JSON for this union value, if it was decoded. +func (u FileSystemPath) Raw() []byte { return u.raw } + +type GlobPatternFileSystemPath struct { + Pattern string `json:"pattern"` +} + +type PathFileSystemPath struct { + Path LegacyAppPathString `json:"path"` +} + +type SpecialFileSystemPath struct { + Value FileSystemSpecialPath `json:"value"` +} + +// FileSystemSpecialPath is an internally-tagged union (tag property "kind"). +// Unknown variants decode without error: only Tag and Raw() are populated. +type FileSystemSpecialPath struct { + // Tag is the value of the "kind" property observed on decode. + Tag string + Minimal *MinimalFileSystemSpecialPath + ProjectRoots *KindFileSystemSpecialPath + Root *RootFileSystemSpecialPath + SlashTmp *SlashTmpFileSystemSpecialPath + Tmpdir *TmpdirFileSystemSpecialPath + Unknown *UnknownFileSystemSpecialPath + raw rawMessage +} + +const ( + FileSystemSpecialPathTagMinimal = "minimal" + FileSystemSpecialPathTagProjectRoots = "project_roots" + FileSystemSpecialPathTagRoot = "root" + FileSystemSpecialPathTagSlashTmp = "slash_tmp" + FileSystemSpecialPathTagTmpdir = "tmpdir" + FileSystemSpecialPathTagUnknown = "unknown" +) + +func (u *FileSystemSpecialPath) UnmarshalJSON(data []byte) error { + *u = FileSystemSpecialPath{} + u.raw = append(rawMessage(nil), data...) + var probe struct { + Tag string `json:"kind"` + } + if err := jsonUnmarshal(data, &probe); err != nil { + return err + } + u.Tag = probe.Tag + switch probe.Tag { + case "minimal": + u.Minimal = new(MinimalFileSystemSpecialPath) + return jsonUnmarshal(data, u.Minimal) + case "project_roots": + u.ProjectRoots = new(KindFileSystemSpecialPath) + return jsonUnmarshal(data, u.ProjectRoots) + case "root": + u.Root = new(RootFileSystemSpecialPath) + return jsonUnmarshal(data, u.Root) + case "slash_tmp": + u.SlashTmp = new(SlashTmpFileSystemSpecialPath) + return jsonUnmarshal(data, u.SlashTmp) + case "tmpdir": + u.Tmpdir = new(TmpdirFileSystemSpecialPath) + return jsonUnmarshal(data, u.Tmpdir) + case "unknown": + u.Unknown = new(UnknownFileSystemSpecialPath) + return jsonUnmarshal(data, u.Unknown) + } + return nil +} + +func (u FileSystemSpecialPath) MarshalJSON() ([]byte, error) { + switch { + case u.Minimal != nil: + return marshalTagged("kind", "minimal", u.Minimal) + case u.ProjectRoots != nil: + return marshalTagged("kind", "project_roots", u.ProjectRoots) + case u.Root != nil: + return marshalTagged("kind", "root", u.Root) + case u.SlashTmp != nil: + return marshalTagged("kind", "slash_tmp", u.SlashTmp) + case u.Tmpdir != nil: + return marshalTagged("kind", "tmpdir", u.Tmpdir) + case u.Unknown != nil: + return marshalTagged("kind", "unknown", u.Unknown) + } + if len(u.raw) > 0 { + return u.raw, nil + } + return nil, errNoVariant("FileSystemSpecialPath") +} + +// Raw returns the original JSON for this union value, if it was decoded. +func (u FileSystemSpecialPath) Raw() []byte { return u.raw } + +type MinimalFileSystemSpecialPath struct { +} + +type KindFileSystemSpecialPath struct { + Subpath *LegacyAppPathString `json:"subpath,omitempty"` +} + +type RootFileSystemSpecialPath struct { +} + +type SlashTmpFileSystemSpecialPath struct { +} + +type TmpdirFileSystemSpecialPath struct { +} + +type UnknownFileSystemSpecialPath struct { + Path string `json:"path"` + Subpath *LegacyAppPathString `json:"subpath,omitempty"` +} + +// Responses API compatible content items that can be returned by a tool call. This is a subset of ContentItem with the types we support as function call outputs. +// FunctionCallOutputContentItem is an internally-tagged union (tag property "type"). +// Unknown variants decode without error: only Tag and Raw() are populated. +type FunctionCallOutputContentItem struct { + // Tag is the value of the "type" property observed on decode. + Tag string + EncryptedContent *EncryptedContentFunctionCallOutputContentItem + InputAudio *InputAudioFunctionCallOutputContentItem + InputImage *InputImageFunctionCallOutputContentItem + InputText *InputTextFunctionCallOutputContentItem + raw rawMessage +} + +const ( + FunctionCallOutputContentItemTagEncryptedContent = "encrypted_content" + FunctionCallOutputContentItemTagInputAudio = "input_audio" + FunctionCallOutputContentItemTagInputImage = "input_image" + FunctionCallOutputContentItemTagInputText = "input_text" +) + +func (u *FunctionCallOutputContentItem) UnmarshalJSON(data []byte) error { + *u = FunctionCallOutputContentItem{} + u.raw = append(rawMessage(nil), data...) + var probe struct { + Tag string `json:"type"` + } + if err := jsonUnmarshal(data, &probe); err != nil { + return err + } + u.Tag = probe.Tag + switch probe.Tag { + case "encrypted_content": + u.EncryptedContent = new(EncryptedContentFunctionCallOutputContentItem) + return jsonUnmarshal(data, u.EncryptedContent) + case "input_audio": + u.InputAudio = new(InputAudioFunctionCallOutputContentItem) + return jsonUnmarshal(data, u.InputAudio) + case "input_image": + u.InputImage = new(InputImageFunctionCallOutputContentItem) + return jsonUnmarshal(data, u.InputImage) + case "input_text": + u.InputText = new(InputTextFunctionCallOutputContentItem) + return jsonUnmarshal(data, u.InputText) + } + return nil +} + +func (u FunctionCallOutputContentItem) MarshalJSON() ([]byte, error) { + switch { + case u.EncryptedContent != nil: + return marshalTagged("type", "encrypted_content", u.EncryptedContent) + case u.InputAudio != nil: + return marshalTagged("type", "input_audio", u.InputAudio) + case u.InputImage != nil: + return marshalTagged("type", "input_image", u.InputImage) + case u.InputText != nil: + return marshalTagged("type", "input_text", u.InputText) + } + if len(u.raw) > 0 { + return u.raw, nil + } + return nil, errNoVariant("FunctionCallOutputContentItem") +} + +// Raw returns the original JSON for this union value, if it was decoded. +func (u FunctionCallOutputContentItem) Raw() []byte { return u.raw } + +type EncryptedContentFunctionCallOutputContentItem struct { + EncryptedContent string `json:"encrypted_content"` +} + +type InputAudioFunctionCallOutputContentItem struct { + AudioURL string `json:"audio_url"` +} + +type InputImageFunctionCallOutputContentItem struct { + Detail *ImageDetail `json:"detail,omitempty"` + ImageURL string `json:"image_url"` +} + +type InputTextFunctionCallOutputContentItem struct { + Text string `json:"text"` +} + +// ImageGenerationFailure is an internally-tagged union (tag property "type"). +// Unknown variants decode without error: only Tag and Raw() are populated. +type ImageGenerationFailure struct { + // Tag is the value of the "type" property observed on decode. + Tag string + UsageLimitExceeded *UsageLimitExceededImageGenerationFailure + raw rawMessage +} + +const ( + ImageGenerationFailureTagUsageLimitExceeded = "usageLimitExceeded" +) + +func (u *ImageGenerationFailure) UnmarshalJSON(data []byte) error { + *u = ImageGenerationFailure{} + u.raw = append(rawMessage(nil), data...) + var probe struct { + Tag string `json:"type"` + } + if err := jsonUnmarshal(data, &probe); err != nil { + return err + } + u.Tag = probe.Tag + switch probe.Tag { + case "usageLimitExceeded": + u.UsageLimitExceeded = new(UsageLimitExceededImageGenerationFailure) + return jsonUnmarshal(data, u.UsageLimitExceeded) + } + return nil +} + +func (u ImageGenerationFailure) MarshalJSON() ([]byte, error) { + switch { + case u.UsageLimitExceeded != nil: + return marshalTagged("type", "usageLimitExceeded", u.UsageLimitExceeded) + } + if len(u.raw) > 0 { + return u.raw, nil + } + return nil, errNoVariant("ImageGenerationFailure") +} + +// Raw returns the original JSON for this union value, if it was decoded. +func (u ImageGenerationFailure) Raw() []byte { return u.raw } + +type UsageLimitExceededImageGenerationFailure struct { + LimitID string `json:"limitId"` + ResetsAt *int64 `json:"resetsAt,omitempty"` +} + +// LoginAccountParams is an internally-tagged union (tag property "type"). +// Unknown variants decode without error: only Tag and Raw() are populated. +type LoginAccountParams struct { + // Tag is the value of the "type" property observed on decode. + Tag string + AmazonBedrock *AmazonBedrockLoginAccountParams + AmazonBedrockAccessKeys *AmazonBedrockAccessKeysLoginAccountParams + APIKey *APIKeyLoginAccountParams + Chatgpt *ChatgptLoginAccountParams + ChatgptAuthTokens *ChatgptAuthTokensLoginAccountParams + ChatgptDeviceCode *ChatgptDeviceCodeLoginAccountParams + raw rawMessage +} + +const ( + LoginAccountParamsTagAmazonBedrock = "amazonBedrock" + LoginAccountParamsTagAmazonBedrockAccessKeys = "amazonBedrockAccessKeys" + LoginAccountParamsTagAPIKey = "apiKey" + LoginAccountParamsTagChatgpt = "chatgpt" + LoginAccountParamsTagChatgptAuthTokens = "chatgptAuthTokens" + LoginAccountParamsTagChatgptDeviceCode = "chatgptDeviceCode" +) + +func (u *LoginAccountParams) UnmarshalJSON(data []byte) error { + *u = LoginAccountParams{} + u.raw = append(rawMessage(nil), data...) + var probe struct { + Tag string `json:"type"` + } + if err := jsonUnmarshal(data, &probe); err != nil { + return err + } + u.Tag = probe.Tag + switch probe.Tag { + case "amazonBedrock": + u.AmazonBedrock = new(AmazonBedrockLoginAccountParams) + return jsonUnmarshal(data, u.AmazonBedrock) + case "amazonBedrockAccessKeys": + u.AmazonBedrockAccessKeys = new(AmazonBedrockAccessKeysLoginAccountParams) + return jsonUnmarshal(data, u.AmazonBedrockAccessKeys) + case "apiKey": + u.APIKey = new(APIKeyLoginAccountParams) + return jsonUnmarshal(data, u.APIKey) + case "chatgpt": + u.Chatgpt = new(ChatgptLoginAccountParams) + return jsonUnmarshal(data, u.Chatgpt) + case "chatgptAuthTokens": + u.ChatgptAuthTokens = new(ChatgptAuthTokensLoginAccountParams) + return jsonUnmarshal(data, u.ChatgptAuthTokens) + case "chatgptDeviceCode": + u.ChatgptDeviceCode = new(ChatgptDeviceCodeLoginAccountParams) + return jsonUnmarshal(data, u.ChatgptDeviceCode) + } + return nil +} + +func (u LoginAccountParams) MarshalJSON() ([]byte, error) { + switch { + case u.AmazonBedrock != nil: + return marshalTagged("type", "amazonBedrock", u.AmazonBedrock) + case u.AmazonBedrockAccessKeys != nil: + return marshalTagged("type", "amazonBedrockAccessKeys", u.AmazonBedrockAccessKeys) + case u.APIKey != nil: + return marshalTagged("type", "apiKey", u.APIKey) + case u.Chatgpt != nil: + return marshalTagged("type", "chatgpt", u.Chatgpt) + case u.ChatgptAuthTokens != nil: + return marshalTagged("type", "chatgptAuthTokens", u.ChatgptAuthTokens) + case u.ChatgptDeviceCode != nil: + return marshalTagged("type", "chatgptDeviceCode", u.ChatgptDeviceCode) + } + if len(u.raw) > 0 { + return u.raw, nil + } + return nil, errNoVariant("LoginAccountParams") +} + +// Raw returns the original JSON for this union value, if it was decoded. +func (u LoginAccountParams) Raw() []byte { return u.raw } + +// [UNSTABLE] Managed Amazon Bedrock login is experimental. +type AmazonBedrockLoginAccountParams struct { + APIKey string `json:"apiKey"` + Region string `json:"region"` +} + +// [UNSTABLE] Managed Amazon Bedrock AWS access key login is experimental. +type AmazonBedrockAccessKeysLoginAccountParams struct { + AccessKeyID string `json:"accessKeyId"` + Region string `json:"region"` + SecretAccessKey string `json:"secretAccessKey"` + SessionToken *string `json:"sessionToken,omitempty"` +} + +type APIKeyLoginAccountParams struct { + APIKey string `json:"apiKey"` +} + +type ChatgptLoginAccountParams struct { + AppBrand *LoginAppBrand `json:"appBrand,omitempty"` + CodexStreamlinedLogin *bool `json:"codexStreamlinedLogin,omitempty"` + UseHostedLoginSuccessPage *bool `json:"useHostedLoginSuccessPage,omitempty"` +} + +// [UNSTABLE] FOR OPENAI INTERNAL USE ONLY - DO NOT USE. The access token must contain the same scopes that Codex-managed ChatGPT auth tokens have. +type ChatgptAuthTokensLoginAccountParams struct { + // Access token (JWT) supplied by the client. This token is used for backend API requests and email extraction. + AccessToken string `json:"accessToken"` + // Workspace/account identifier supplied by the client. + ChatgptAccountID string `json:"chatgptAccountId"` + // Optional plan type supplied by the client. + ChatgptPlanType *string `json:"chatgptPlanType,omitempty"` +} + +type ChatgptDeviceCodeLoginAccountParams struct { +} + +// LoginAccountResponse is an internally-tagged union (tag property "type"). +// Unknown variants decode without error: only Tag and Raw() are populated. +type LoginAccountResponse struct { + // Tag is the value of the "type" property observed on decode. + Tag string + AmazonBedrock *AmazonBedrockLoginAccountResponse + APIKey *APIKeyLoginAccountResponse + Chatgpt *ChatgptLoginAccountResponse + ChatgptAuthTokens *ChatgptAuthTokensLoginAccountResponse + ChatgptDeviceCode *ChatgptDeviceCodeLoginAccountResponse + raw rawMessage +} + +const ( + LoginAccountResponseTagAmazonBedrock = "amazonBedrock" + LoginAccountResponseTagAPIKey = "apiKey" + LoginAccountResponseTagChatgpt = "chatgpt" + LoginAccountResponseTagChatgptAuthTokens = "chatgptAuthTokens" + LoginAccountResponseTagChatgptDeviceCode = "chatgptDeviceCode" +) + +func (u *LoginAccountResponse) UnmarshalJSON(data []byte) error { + *u = LoginAccountResponse{} + u.raw = append(rawMessage(nil), data...) + var probe struct { + Tag string `json:"type"` + } + if err := jsonUnmarshal(data, &probe); err != nil { + return err + } + u.Tag = probe.Tag + switch probe.Tag { + case "amazonBedrock": + u.AmazonBedrock = new(AmazonBedrockLoginAccountResponse) + return jsonUnmarshal(data, u.AmazonBedrock) + case "apiKey": + u.APIKey = new(APIKeyLoginAccountResponse) + return jsonUnmarshal(data, u.APIKey) + case "chatgpt": + u.Chatgpt = new(ChatgptLoginAccountResponse) + return jsonUnmarshal(data, u.Chatgpt) + case "chatgptAuthTokens": + u.ChatgptAuthTokens = new(ChatgptAuthTokensLoginAccountResponse) + return jsonUnmarshal(data, u.ChatgptAuthTokens) + case "chatgptDeviceCode": + u.ChatgptDeviceCode = new(ChatgptDeviceCodeLoginAccountResponse) + return jsonUnmarshal(data, u.ChatgptDeviceCode) + } + return nil +} + +func (u LoginAccountResponse) MarshalJSON() ([]byte, error) { + switch { + case u.AmazonBedrock != nil: + return marshalTagged("type", "amazonBedrock", u.AmazonBedrock) + case u.APIKey != nil: + return marshalTagged("type", "apiKey", u.APIKey) + case u.Chatgpt != nil: + return marshalTagged("type", "chatgpt", u.Chatgpt) + case u.ChatgptAuthTokens != nil: + return marshalTagged("type", "chatgptAuthTokens", u.ChatgptAuthTokens) + case u.ChatgptDeviceCode != nil: + return marshalTagged("type", "chatgptDeviceCode", u.ChatgptDeviceCode) + } + if len(u.raw) > 0 { + return u.raw, nil + } + return nil, errNoVariant("LoginAccountResponse") +} + +// Raw returns the original JSON for this union value, if it was decoded. +func (u LoginAccountResponse) Raw() []byte { return u.raw } + +type AmazonBedrockLoginAccountResponse struct { +} + +type APIKeyLoginAccountResponse struct { +} + +type ChatgptLoginAccountResponse struct { + // URL the client should open in a browser to initiate the OAuth flow. + AuthURL string `json:"authUrl"` + LoginID string `json:"loginId"` +} + +type ChatgptAuthTokensLoginAccountResponse struct { +} + +type ChatgptDeviceCodeLoginAccountResponse struct { + LoginID string `json:"loginId"` + // One-time code the user must enter after signing in. + UserCode string `json:"userCode"` + // URL the client should open in a browser to complete device code authorization. + VerificationURL string `json:"verificationUrl"` +} + +// McpServerElicitationRequestParams is an internally-tagged union (tag property "mode"). +// Unknown variants decode without error: only Tag and Raw() are populated. +type McpServerElicitationRequestParams struct { + // Tag is the value of the "mode" property observed on decode. + Tag string + Form *FormMcpServerElicitationRequestParams + OpenaiForm *OpenaiFormMcpServerElicitationRequestParams + URL *URLMcpServerElicitationRequestParams + raw rawMessage +} + +const ( + McpServerElicitationRequestParamsTagForm = "form" + McpServerElicitationRequestParamsTagOpenaiForm = "openai/form" + McpServerElicitationRequestParamsTagURL = "url" +) + +func (u *McpServerElicitationRequestParams) UnmarshalJSON(data []byte) error { + *u = McpServerElicitationRequestParams{} + u.raw = append(rawMessage(nil), data...) + var probe struct { + Tag string `json:"mode"` + } + if err := jsonUnmarshal(data, &probe); err != nil { + return err + } + u.Tag = probe.Tag + switch probe.Tag { + case "form": + u.Form = new(FormMcpServerElicitationRequestParams) + return jsonUnmarshal(data, u.Form) + case "openai/form": + u.OpenaiForm = new(OpenaiFormMcpServerElicitationRequestParams) + return jsonUnmarshal(data, u.OpenaiForm) + case "url": + u.URL = new(URLMcpServerElicitationRequestParams) + return jsonUnmarshal(data, u.URL) + } + return nil +} + +func (u McpServerElicitationRequestParams) MarshalJSON() ([]byte, error) { + switch { + case u.Form != nil: + return marshalTagged("mode", "form", u.Form) + case u.OpenaiForm != nil: + return marshalTagged("mode", "openai/form", u.OpenaiForm) + case u.URL != nil: + return marshalTagged("mode", "url", u.URL) + } + if len(u.raw) > 0 { + return u.raw, nil + } + return nil, errNoVariant("McpServerElicitationRequestParams") +} + +// Raw returns the original JSON for this union value, if it was decoded. +func (u McpServerElicitationRequestParams) Raw() []byte { return u.raw } + +type FormMcpServerElicitationRequestParams struct { + Meta any `json:"_meta,omitempty"` + Message string `json:"message"` + RequestedSchema McpElicitationSchema `json:"requestedSchema"` +} + +type OpenaiFormMcpServerElicitationRequestParams struct { + Meta any `json:"_meta,omitempty"` + Message string `json:"message"` + RequestedSchema any `json:"requestedSchema"` +} + +type URLMcpServerElicitationRequestParams struct { + Meta any `json:"_meta,omitempty"` + ElicitationID string `json:"elicitationId"` + Message string `json:"message"` + URL string `json:"url"` +} + +// PatchChangeKind is an internally-tagged union (tag property "type"). +// Unknown variants decode without error: only Tag and Raw() are populated. +type PatchChangeKind struct { + // Tag is the value of the "type" property observed on decode. + Tag string + Add *AddPatchChangeKind + Delete *DeletePatchChangeKind + Update *UpdatePatchChangeKind + raw rawMessage +} + +const ( + PatchChangeKindTagAdd = "add" + PatchChangeKindTagDelete = "delete" + PatchChangeKindTagUpdate = "update" +) + +func (u *PatchChangeKind) UnmarshalJSON(data []byte) error { + *u = PatchChangeKind{} + u.raw = append(rawMessage(nil), data...) + var probe struct { + Tag string `json:"type"` + } + if err := jsonUnmarshal(data, &probe); err != nil { + return err + } + u.Tag = probe.Tag + switch probe.Tag { + case "add": + u.Add = new(AddPatchChangeKind) + return jsonUnmarshal(data, u.Add) + case "delete": + u.Delete = new(DeletePatchChangeKind) + return jsonUnmarshal(data, u.Delete) + case "update": + u.Update = new(UpdatePatchChangeKind) + return jsonUnmarshal(data, u.Update) + } + return nil +} + +func (u PatchChangeKind) MarshalJSON() ([]byte, error) { + switch { + case u.Add != nil: + return marshalTagged("type", "add", u.Add) + case u.Delete != nil: + return marshalTagged("type", "delete", u.Delete) + case u.Update != nil: + return marshalTagged("type", "update", u.Update) + } + if len(u.raw) > 0 { + return u.raw, nil + } + return nil, errNoVariant("PatchChangeKind") +} + +// Raw returns the original JSON for this union value, if it was decoded. +func (u PatchChangeKind) Raw() []byte { return u.raw } + +type AddPatchChangeKind struct { +} + +type DeletePatchChangeKind struct { +} + +type UpdatePatchChangeKind struct { + MovePath *string `json:"move_path,omitempty"` +} + +// SandboxPolicy is an internally-tagged union (tag property "type"). +// Unknown variants decode without error: only Tag and Raw() are populated. +type SandboxPolicy struct { + // Tag is the value of the "type" property observed on decode. + Tag string + DangerFullAccess *DangerFullAccessSandboxPolicy + ExternalSandbox *ExternalSandboxSandboxPolicy + ReadOnly *ReadOnlySandboxPolicy + WorkspaceWrite *WorkspaceWriteSandboxPolicy + raw rawMessage +} + +const ( + SandboxPolicyTagDangerFullAccess = "dangerFullAccess" + SandboxPolicyTagExternalSandbox = "externalSandbox" + SandboxPolicyTagReadOnly = "readOnly" + SandboxPolicyTagWorkspaceWrite = "workspaceWrite" +) + +func (u *SandboxPolicy) UnmarshalJSON(data []byte) error { + *u = SandboxPolicy{} + u.raw = append(rawMessage(nil), data...) + var probe struct { + Tag string `json:"type"` + } + if err := jsonUnmarshal(data, &probe); err != nil { + return err + } + u.Tag = probe.Tag + switch probe.Tag { + case "dangerFullAccess": + u.DangerFullAccess = new(DangerFullAccessSandboxPolicy) + return jsonUnmarshal(data, u.DangerFullAccess) + case "externalSandbox": + u.ExternalSandbox = new(ExternalSandboxSandboxPolicy) + return jsonUnmarshal(data, u.ExternalSandbox) + case "readOnly": + u.ReadOnly = new(ReadOnlySandboxPolicy) + return jsonUnmarshal(data, u.ReadOnly) + case "workspaceWrite": + u.WorkspaceWrite = new(WorkspaceWriteSandboxPolicy) + return jsonUnmarshal(data, u.WorkspaceWrite) + } + return nil +} + +func (u SandboxPolicy) MarshalJSON() ([]byte, error) { + switch { + case u.DangerFullAccess != nil: + return marshalTagged("type", "dangerFullAccess", u.DangerFullAccess) + case u.ExternalSandbox != nil: + return marshalTagged("type", "externalSandbox", u.ExternalSandbox) + case u.ReadOnly != nil: + return marshalTagged("type", "readOnly", u.ReadOnly) + case u.WorkspaceWrite != nil: + return marshalTagged("type", "workspaceWrite", u.WorkspaceWrite) + } + if len(u.raw) > 0 { + return u.raw, nil + } + return nil, errNoVariant("SandboxPolicy") +} + +// Raw returns the original JSON for this union value, if it was decoded. +func (u SandboxPolicy) Raw() []byte { return u.raw } + +type DangerFullAccessSandboxPolicy struct { +} + +type ExternalSandboxSandboxPolicy struct { + NetworkAccess *NetworkAccess `json:"networkAccess,omitempty"` +} + +type ReadOnlySandboxPolicy struct { + NetworkAccess *bool `json:"networkAccess,omitempty"` +} + +type WorkspaceWriteSandboxPolicy struct { + ExcludeSlashTmp *bool `json:"excludeSlashTmp,omitempty"` + ExcludeTmpdirEnvVar *bool `json:"excludeTmpdirEnvVar,omitempty"` + NetworkAccess *bool `json:"networkAccess,omitempty"` + WritableRoots []AbsolutePathBuf `json:"writableRoots,omitempty"` +} + +// SessionSource is a mixed union: on the wire it is either a bare string +// (Unit) or a single-key object (one payload field set). Unknown variants +// decode without error and are retained in Raw(). +type SessionSource struct { + // Unit holds the bare-string variant value, if that form was used. + Unit string + Custom *string + SubAgent *SubAgentSource + raw rawMessage +} + +const ( + SessionSourceUnitAppServer = "appServer" + SessionSourceUnitCli = "cli" + SessionSourceUnitExec = "exec" + SessionSourceUnitUnknown = "unknown" + SessionSourceUnitVscode = "vscode" +) + +func (u *SessionSource) UnmarshalJSON(data []byte) error { + *u = SessionSource{} + u.raw = append(rawMessage(nil), data...) + if isJSONString(data) { + return jsonUnmarshal(data, &u.Unit) + } + var obj map[string]rawMessage + if err := jsonUnmarshal(data, &obj); err != nil { + return err + } + if len(obj) != 1 { + return nil + } + for key, payload := range obj { + switch key { + case "custom": + u.Custom = new(string) + return jsonUnmarshal(payload, u.Custom) + case "subAgent": + u.SubAgent = new(SubAgentSource) + return jsonUnmarshal(payload, u.SubAgent) + } + } + return nil +} + +func (u SessionSource) MarshalJSON() ([]byte, error) { + if u.Unit != "" { + return jsonMarshal(u.Unit) + } + switch { + case u.Custom != nil: + return marshalKeyed("custom", u.Custom) + case u.SubAgent != nil: + return marshalKeyed("subAgent", u.SubAgent) + } + if len(u.raw) > 0 { + return u.raw, nil + } + return nil, errNoVariant("SessionSource") +} + +// Raw returns the original JSON for this union value, if it was decoded. +func (u SessionSource) Raw() []byte { return u.raw } + +// SubAgentSource is a mixed union: on the wire it is either a bare string +// (Unit) or a single-key object (one payload field set). Unknown variants +// decode without error and are retained in Raw(). +type SubAgentSource struct { + // Unit holds the bare-string variant value, if that form was used. + Unit string + Other *string + ThreadSpawn *SubAgentSourceThreadSpawn + raw rawMessage +} + +const ( + SubAgentSourceUnitCompact = "compact" + SubAgentSourceUnitMemoryConsolidation = "memory_consolidation" + SubAgentSourceUnitReview = "review" +) + +func (u *SubAgentSource) UnmarshalJSON(data []byte) error { + *u = SubAgentSource{} + u.raw = append(rawMessage(nil), data...) + if isJSONString(data) { + return jsonUnmarshal(data, &u.Unit) + } + var obj map[string]rawMessage + if err := jsonUnmarshal(data, &obj); err != nil { + return err + } + if len(obj) != 1 { + return nil + } + for key, payload := range obj { + switch key { + case "other": + u.Other = new(string) + return jsonUnmarshal(payload, u.Other) + case "thread_spawn": + u.ThreadSpawn = new(SubAgentSourceThreadSpawn) + return jsonUnmarshal(payload, u.ThreadSpawn) + } + } + return nil +} + +func (u SubAgentSource) MarshalJSON() ([]byte, error) { + if u.Unit != "" { + return jsonMarshal(u.Unit) + } + switch { + case u.Other != nil: + return marshalKeyed("other", u.Other) + case u.ThreadSpawn != nil: + return marshalKeyed("thread_spawn", u.ThreadSpawn) + } + if len(u.raw) > 0 { + return u.raw, nil + } + return nil, errNoVariant("SubAgentSource") +} + +// Raw returns the original JSON for this union value, if it was decoded. +func (u SubAgentSource) Raw() []byte { return u.raw } + +// ThreadItem is an internally-tagged union (tag property "type"). +// Unknown variants decode without error: only Tag and Raw() are populated. +type ThreadItem struct { + // Tag is the value of the "type" property observed on decode. + Tag string + AgentMessage *AgentMessageThreadItem + CollabAgentToolCall *CollabAgentToolCallThreadItem + CommandExecution *CommandExecutionThreadItem + ContextCompaction *ContextCompactionThreadItem + DynamicToolCall *DynamicToolCallThreadItem + EnteredReviewMode *EnteredReviewModeThreadItem + ExitedReviewMode *ExitedReviewModeThreadItem + FileChange *FileChangeThreadItem + FunctionCallOutput *FunctionCallOutputThreadItem + HookPrompt *HookPromptThreadItem + ImageGeneration *ImageGenerationThreadItem + ImageView *ImageViewThreadItem + MCPToolCall *McpToolCallThreadItem + Plan *PlanThreadItem + Reasoning *ReasoningThreadItem + Sleep *SleepThreadItem + SubAgentActivity *SubAgentActivityThreadItem + UserMessage *UserMessageThreadItem + WebSearch *WebSearchThreadItem + raw rawMessage +} + +const ( + ThreadItemTagAgentMessage = "agentMessage" + ThreadItemTagCollabAgentToolCall = "collabAgentToolCall" + ThreadItemTagCommandExecution = "commandExecution" + ThreadItemTagContextCompaction = "contextCompaction" + ThreadItemTagDynamicToolCall = "dynamicToolCall" + ThreadItemTagEnteredReviewMode = "enteredReviewMode" + ThreadItemTagExitedReviewMode = "exitedReviewMode" + ThreadItemTagFileChange = "fileChange" + ThreadItemTagFunctionCallOutput = "functionCallOutput" + ThreadItemTagHookPrompt = "hookPrompt" + ThreadItemTagImageGeneration = "imageGeneration" + ThreadItemTagImageView = "imageView" + ThreadItemTagMCPToolCall = "mcpToolCall" + ThreadItemTagPlan = "plan" + ThreadItemTagReasoning = "reasoning" + ThreadItemTagSleep = "sleep" + ThreadItemTagSubAgentActivity = "subAgentActivity" + ThreadItemTagUserMessage = "userMessage" + ThreadItemTagWebSearch = "webSearch" +) + +func (u *ThreadItem) UnmarshalJSON(data []byte) error { + *u = ThreadItem{} + u.raw = append(rawMessage(nil), data...) + var probe struct { + Tag string `json:"type"` + } + if err := jsonUnmarshal(data, &probe); err != nil { + return err + } + u.Tag = probe.Tag + switch probe.Tag { + case "agentMessage": + u.AgentMessage = new(AgentMessageThreadItem) + return jsonUnmarshal(data, u.AgentMessage) + case "collabAgentToolCall": + u.CollabAgentToolCall = new(CollabAgentToolCallThreadItem) + return jsonUnmarshal(data, u.CollabAgentToolCall) + case "commandExecution": + u.CommandExecution = new(CommandExecutionThreadItem) + return jsonUnmarshal(data, u.CommandExecution) + case "contextCompaction": + u.ContextCompaction = new(ContextCompactionThreadItem) + return jsonUnmarshal(data, u.ContextCompaction) + case "dynamicToolCall": + u.DynamicToolCall = new(DynamicToolCallThreadItem) + return jsonUnmarshal(data, u.DynamicToolCall) + case "enteredReviewMode": + u.EnteredReviewMode = new(EnteredReviewModeThreadItem) + return jsonUnmarshal(data, u.EnteredReviewMode) + case "exitedReviewMode": + u.ExitedReviewMode = new(ExitedReviewModeThreadItem) + return jsonUnmarshal(data, u.ExitedReviewMode) + case "fileChange": + u.FileChange = new(FileChangeThreadItem) + return jsonUnmarshal(data, u.FileChange) + case "functionCallOutput": + u.FunctionCallOutput = new(FunctionCallOutputThreadItem) + return jsonUnmarshal(data, u.FunctionCallOutput) + case "hookPrompt": + u.HookPrompt = new(HookPromptThreadItem) + return jsonUnmarshal(data, u.HookPrompt) + case "imageGeneration": + u.ImageGeneration = new(ImageGenerationThreadItem) + return jsonUnmarshal(data, u.ImageGeneration) + case "imageView": + u.ImageView = new(ImageViewThreadItem) + return jsonUnmarshal(data, u.ImageView) + case "mcpToolCall": + u.MCPToolCall = new(McpToolCallThreadItem) + return jsonUnmarshal(data, u.MCPToolCall) + case "plan": + u.Plan = new(PlanThreadItem) + return jsonUnmarshal(data, u.Plan) + case "reasoning": + u.Reasoning = new(ReasoningThreadItem) + return jsonUnmarshal(data, u.Reasoning) + case "sleep": + u.Sleep = new(SleepThreadItem) + return jsonUnmarshal(data, u.Sleep) + case "subAgentActivity": + u.SubAgentActivity = new(SubAgentActivityThreadItem) + return jsonUnmarshal(data, u.SubAgentActivity) + case "userMessage": + u.UserMessage = new(UserMessageThreadItem) + return jsonUnmarshal(data, u.UserMessage) + case "webSearch": + u.WebSearch = new(WebSearchThreadItem) + return jsonUnmarshal(data, u.WebSearch) + } + return nil +} + +func (u ThreadItem) MarshalJSON() ([]byte, error) { + switch { + case u.AgentMessage != nil: + return marshalTagged("type", "agentMessage", u.AgentMessage) + case u.CollabAgentToolCall != nil: + return marshalTagged("type", "collabAgentToolCall", u.CollabAgentToolCall) + case u.CommandExecution != nil: + return marshalTagged("type", "commandExecution", u.CommandExecution) + case u.ContextCompaction != nil: + return marshalTagged("type", "contextCompaction", u.ContextCompaction) + case u.DynamicToolCall != nil: + return marshalTagged("type", "dynamicToolCall", u.DynamicToolCall) + case u.EnteredReviewMode != nil: + return marshalTagged("type", "enteredReviewMode", u.EnteredReviewMode) + case u.ExitedReviewMode != nil: + return marshalTagged("type", "exitedReviewMode", u.ExitedReviewMode) + case u.FileChange != nil: + return marshalTagged("type", "fileChange", u.FileChange) + case u.FunctionCallOutput != nil: + return marshalTagged("type", "functionCallOutput", u.FunctionCallOutput) + case u.HookPrompt != nil: + return marshalTagged("type", "hookPrompt", u.HookPrompt) + case u.ImageGeneration != nil: + return marshalTagged("type", "imageGeneration", u.ImageGeneration) + case u.ImageView != nil: + return marshalTagged("type", "imageView", u.ImageView) + case u.MCPToolCall != nil: + return marshalTagged("type", "mcpToolCall", u.MCPToolCall) + case u.Plan != nil: + return marshalTagged("type", "plan", u.Plan) + case u.Reasoning != nil: + return marshalTagged("type", "reasoning", u.Reasoning) + case u.Sleep != nil: + return marshalTagged("type", "sleep", u.Sleep) + case u.SubAgentActivity != nil: + return marshalTagged("type", "subAgentActivity", u.SubAgentActivity) + case u.UserMessage != nil: + return marshalTagged("type", "userMessage", u.UserMessage) + case u.WebSearch != nil: + return marshalTagged("type", "webSearch", u.WebSearch) + } + if len(u.raw) > 0 { + return u.raw, nil + } + return nil, errNoVariant("ThreadItem") +} + +// Raw returns the original JSON for this union value, if it was decoded. +func (u ThreadItem) Raw() []byte { return u.raw } + +type AgentMessageThreadItem struct { + Delivery *AgentMessageDelivery `json:"delivery,omitempty"` + ID string `json:"id"` + MemoryCitation *MemoryCitation `json:"memoryCitation,omitempty"` + Phase *MessagePhase `json:"phase,omitempty"` + Text string `json:"text"` +} + +type CollabAgentToolCallThreadItem struct { + // Last known status of the target agents, when available. + AgentsStates map[string]CollabAgentState `json:"agentsStates"` + // Unique identifier for this collab tool call. + ID string `json:"id"` + // Model requested for the spawned agent, when applicable. + Model *string `json:"model,omitempty"` + // Prompt text sent as part of the collab tool call, when available. + Prompt *string `json:"prompt,omitempty"` + // Reasoning effort requested for the spawned agent, when applicable. + ReasoningEffort *ReasoningEffort `json:"reasoningEffort,omitempty"` + // Thread ID of the receiving agent, when applicable. In case of spawn operation, this corresponds to the newly spawned agent. + ReceiverThreadIds []string `json:"receiverThreadIds"` + // Thread ID of the agent issuing the collab request. + SenderThreadID string `json:"senderThreadId"` + // Current status of the collab tool call. + Status CollabAgentToolCallStatus `json:"status"` + // Name of the collab tool that was invoked. + Tool CollabAgentTool `json:"tool"` +} + +func (v CollabAgentToolCallThreadItem) MarshalJSON() ([]byte, error) { + type plain CollabAgentToolCallThreadItem + p := plain(v) + if p.AgentsStates == nil { + p.AgentsStates = map[string]CollabAgentState{} + } + if p.ReceiverThreadIds == nil { + p.ReceiverThreadIds = []string{} + } + return jsonMarshal(p) +} + +type CommandExecutionThreadItem struct { + // The command's output, aggregated from stdout and stderr. + AggregatedOutput *string `json:"aggregatedOutput,omitempty"` + // The command to be executed. + Command string `json:"command"` + // A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together. + CommandActions []CommandAction `json:"commandActions"` + // The command's working directory. + Cwd LegacyAppPathString `json:"cwd"` + // The duration of the command execution in milliseconds. + DurationMs *int64 `json:"durationMs,omitempty"` + // The command's exit code. + ExitCode *int64 `json:"exitCode,omitempty"` + ID string `json:"id"` + // Trusted first-party plugin id when this command resolves to one plugin script. + PluginID *string `json:"pluginId,omitempty"` + // Identifier for the underlying PTY process (when available). + ProcessID *string `json:"processId,omitempty"` + // Safe plugin-relative path when this command resolves to one plugin script. + ScriptPath *string `json:"scriptPath,omitempty"` + Source *CommandExecutionSource `json:"source,omitempty"` + Status CommandExecutionStatus `json:"status"` +} + +func (v CommandExecutionThreadItem) MarshalJSON() ([]byte, error) { + type plain CommandExecutionThreadItem + p := plain(v) + if p.CommandActions == nil { + p.CommandActions = []CommandAction{} + } + return jsonMarshal(p) +} + +type ContextCompactionThreadItem struct { + ID string `json:"id"` +} + +type DynamicToolCallThreadItem struct { + Arguments any `json:"arguments"` + ContentItems []DynamicToolCallOutputContentItem `json:"contentItems,omitempty"` + // The duration of the dynamic tool call in milliseconds. + DurationMs *int64 `json:"durationMs,omitempty"` + ID string `json:"id"` + Namespace *string `json:"namespace,omitempty"` + Status DynamicToolCallStatus `json:"status"` + Success *bool `json:"success,omitempty"` + Tool string `json:"tool"` +} + +type EnteredReviewModeThreadItem struct { + ID string `json:"id"` + Review string `json:"review"` +} + +type ExitedReviewModeThreadItem struct { + ID string `json:"id"` + Review string `json:"review"` +} + +type FileChangeThreadItem struct { + Changes []FileUpdateChange `json:"changes"` + ID string `json:"id"` + Status PatchApplyStatus `json:"status"` +} + +func (v FileChangeThreadItem) MarshalJSON() ([]byte, error) { + type plain FileChangeThreadItem + p := plain(v) + if p.Changes == nil { + p.Changes = []FileUpdateChange{} + } + return jsonMarshal(p) +} + +type FunctionCallOutputThreadItem struct { + ID string `json:"id"` + Name string `json:"name"` + Namespace *string `json:"namespace,omitempty"` + Output rawMessage `json:"output"` +} + +type HookPromptThreadItem struct { + Fragments []HookPromptFragment `json:"fragments"` + ID string `json:"id"` +} + +func (v HookPromptThreadItem) MarshalJSON() ([]byte, error) { + type plain HookPromptThreadItem + p := plain(v) + if p.Fragments == nil { + p.Fragments = []HookPromptFragment{} + } + return jsonMarshal(p) +} + +type ImageGenerationThreadItem struct { + Failure *ImageGenerationFailure `json:"failure,omitempty"` + ID string `json:"id"` + Result string `json:"result"` + RevisedPrompt *string `json:"revisedPrompt,omitempty"` + SavedPath *AbsolutePathBuf `json:"savedPath,omitempty"` + Status string `json:"status"` + TransparentBackground *bool `json:"transparentBackground,omitempty"` +} + +type ImageViewThreadItem struct { + ID string `json:"id"` + Path LegacyAppPathString `json:"path"` +} + +type McpToolCallThreadItem struct { + AppContext *McpToolCallAppContext `json:"appContext,omitempty"` + Arguments any `json:"arguments"` + // The duration of the MCP tool call in milliseconds. + DurationMs *int64 `json:"durationMs,omitempty"` + Error *McpToolCallError `json:"error,omitempty"` + ID string `json:"id"` + // Deprecated: use `appContext.resourceUri` instead. + MCPAppResourceURI *string `json:"mcpAppResourceUri,omitempty"` + PluginID *string `json:"pluginId,omitempty"` + ReadOnlyHint *bool `json:"readOnlyHint,omitempty"` + Result *McpToolCallResult `json:"result,omitempty"` + Server string `json:"server"` + Status McpToolCallStatus `json:"status"` + Tool string `json:"tool"` +} + +// EXPERIMENTAL - proposed plan item content. The completed plan item is authoritative and may not match the concatenation of `PlanDelta` text. +type PlanThreadItem struct { + ID string `json:"id"` + Text string `json:"text"` +} + +type ReasoningThreadItem struct { + Content []string `json:"content,omitempty"` + ID string `json:"id"` + Summary []string `json:"summary,omitempty"` +} + +// Display item emitted by the interruptible `clock.sleep` tool. +type SleepThreadItem struct { + DurationMs uint64 `json:"durationMs"` + ID string `json:"id"` +} + +type SubAgentActivityThreadItem struct { + AgentPath string `json:"agentPath"` + AgentThreadID string `json:"agentThreadId"` + ID string `json:"id"` + Kind SubAgentActivityKind `json:"kind"` +} + +type UserMessageThreadItem struct { + ClientID *string `json:"clientId,omitempty"` + Content []UserInput `json:"content"` + ID string `json:"id"` +} + +func (v UserMessageThreadItem) MarshalJSON() ([]byte, error) { + type plain UserMessageThreadItem + p := plain(v) + if p.Content == nil { + p.Content = []UserInput{} + } + return jsonMarshal(p) +} + +type WebSearchThreadItem struct { + Action *WebSearchAction `json:"action,omitempty"` + ID string `json:"id"` + Query string `json:"query"` + // Structured search results returned out-of-band by standalone web search. + Results []any `json:"results,omitempty"` +} + +// ThreadStatus is an internally-tagged union (tag property "type"). +// Unknown variants decode without error: only Tag and Raw() are populated. +type ThreadStatus struct { + // Tag is the value of the "type" property observed on decode. + Tag string + Active *ActiveThreadStatus + Idle *IdleThreadStatus + NotLoaded *NotLoadedThreadStatus + SystemError *SystemErrorThreadStatus + raw rawMessage +} + +const ( + ThreadStatusTagActive = "active" + ThreadStatusTagIdle = "idle" + ThreadStatusTagNotLoaded = "notLoaded" + ThreadStatusTagSystemError = "systemError" +) + +func (u *ThreadStatus) UnmarshalJSON(data []byte) error { + *u = ThreadStatus{} + u.raw = append(rawMessage(nil), data...) + var probe struct { + Tag string `json:"type"` + } + if err := jsonUnmarshal(data, &probe); err != nil { + return err + } + u.Tag = probe.Tag + switch probe.Tag { + case "active": + u.Active = new(ActiveThreadStatus) + return jsonUnmarshal(data, u.Active) + case "idle": + u.Idle = new(IdleThreadStatus) + return jsonUnmarshal(data, u.Idle) + case "notLoaded": + u.NotLoaded = new(NotLoadedThreadStatus) + return jsonUnmarshal(data, u.NotLoaded) + case "systemError": + u.SystemError = new(SystemErrorThreadStatus) + return jsonUnmarshal(data, u.SystemError) + } + return nil +} + +func (u ThreadStatus) MarshalJSON() ([]byte, error) { + switch { + case u.Active != nil: + return marshalTagged("type", "active", u.Active) + case u.Idle != nil: + return marshalTagged("type", "idle", u.Idle) + case u.NotLoaded != nil: + return marshalTagged("type", "notLoaded", u.NotLoaded) + case u.SystemError != nil: + return marshalTagged("type", "systemError", u.SystemError) + } + if len(u.raw) > 0 { + return u.raw, nil + } + return nil, errNoVariant("ThreadStatus") +} + +// Raw returns the original JSON for this union value, if it was decoded. +func (u ThreadStatus) Raw() []byte { return u.raw } + +type ActiveThreadStatus struct { + ActiveFlags []ThreadActiveFlag `json:"activeFlags"` +} + +func (v ActiveThreadStatus) MarshalJSON() ([]byte, error) { + type plain ActiveThreadStatus + p := plain(v) + if p.ActiveFlags == nil { + p.ActiveFlags = []ThreadActiveFlag{} + } + return jsonMarshal(p) +} + +type IdleThreadStatus struct { +} + +type NotLoadedThreadStatus struct { +} + +type SystemErrorThreadStatus struct { +} + +// UserInput is an internally-tagged union (tag property "type"). +// Unknown variants decode without error: only Tag and Raw() are populated. +type UserInput struct { + // Tag is the value of the "type" property observed on decode. + Tag string + Audio *AudioUserInput + Image *ImageUserInput + LocalAudio *LocalAudioUserInput + LocalImage *LocalImageUserInput + Mention *MentionUserInput + Skill *SkillUserInput + Text *TextUserInput + raw rawMessage +} + +const ( + UserInputTagAudio = "audio" + UserInputTagImage = "image" + UserInputTagLocalAudio = "localAudio" + UserInputTagLocalImage = "localImage" + UserInputTagMention = "mention" + UserInputTagSkill = "skill" + UserInputTagText = "text" +) + +func (u *UserInput) UnmarshalJSON(data []byte) error { + *u = UserInput{} + u.raw = append(rawMessage(nil), data...) + var probe struct { + Tag string `json:"type"` + } + if err := jsonUnmarshal(data, &probe); err != nil { + return err + } + u.Tag = probe.Tag + switch probe.Tag { + case "audio": + u.Audio = new(AudioUserInput) + return jsonUnmarshal(data, u.Audio) + case "image": + u.Image = new(ImageUserInput) + return jsonUnmarshal(data, u.Image) + case "localAudio": + u.LocalAudio = new(LocalAudioUserInput) + return jsonUnmarshal(data, u.LocalAudio) + case "localImage": + u.LocalImage = new(LocalImageUserInput) + return jsonUnmarshal(data, u.LocalImage) + case "mention": + u.Mention = new(MentionUserInput) + return jsonUnmarshal(data, u.Mention) + case "skill": + u.Skill = new(SkillUserInput) + return jsonUnmarshal(data, u.Skill) + case "text": + u.Text = new(TextUserInput) + return jsonUnmarshal(data, u.Text) + } + return nil +} + +func (u UserInput) MarshalJSON() ([]byte, error) { + switch { + case u.Audio != nil: + return marshalTagged("type", "audio", u.Audio) + case u.Image != nil: + return marshalTagged("type", "image", u.Image) + case u.LocalAudio != nil: + return marshalTagged("type", "localAudio", u.LocalAudio) + case u.LocalImage != nil: + return marshalTagged("type", "localImage", u.LocalImage) + case u.Mention != nil: + return marshalTagged("type", "mention", u.Mention) + case u.Skill != nil: + return marshalTagged("type", "skill", u.Skill) + case u.Text != nil: + return marshalTagged("type", "text", u.Text) + } + if len(u.raw) > 0 { + return u.raw, nil + } + return nil, errNoVariant("UserInput") +} + +// Raw returns the original JSON for this union value, if it was decoded. +func (u UserInput) Raw() []byte { return u.raw } + +type AudioUserInput struct { + URL string `json:"url"` +} + +type ImageUserInput struct { + Detail *ImageDetail `json:"detail,omitempty"` + URL string `json:"url"` +} + +type LocalAudioUserInput struct { + Path string `json:"path"` +} + +type LocalImageUserInput struct { + Detail *ImageDetail `json:"detail,omitempty"` + Path string `json:"path"` +} + +type MentionUserInput struct { + Name string `json:"name"` + Path string `json:"path"` +} + +type SkillUserInput struct { + Name string `json:"name"` + Path string `json:"path"` +} + +type TextUserInput struct { + Text string `json:"text"` + // UI-defined spans within `text` used to render or persist special elements. + TextElements []TextElement `json:"text_elements,omitempty"` +} + +// WebSearchAction is an internally-tagged union (tag property "type"). +// Unknown variants decode without error: only Tag and Raw() are populated. +type WebSearchAction struct { + // Tag is the value of the "type" property observed on decode. + Tag string + FindInPage *FindInPageWebSearchAction + OpenPage *OpenPageWebSearchAction + Other *OtherWebSearchAction + Search *SearchWebSearchAction + raw rawMessage +} + +const ( + WebSearchActionTagFindInPage = "findInPage" + WebSearchActionTagOpenPage = "openPage" + WebSearchActionTagOther = "other" + WebSearchActionTagSearch = "search" +) + +func (u *WebSearchAction) UnmarshalJSON(data []byte) error { + *u = WebSearchAction{} + u.raw = append(rawMessage(nil), data...) + var probe struct { + Tag string `json:"type"` + } + if err := jsonUnmarshal(data, &probe); err != nil { + return err + } + u.Tag = probe.Tag + switch probe.Tag { + case "findInPage": + u.FindInPage = new(FindInPageWebSearchAction) + return jsonUnmarshal(data, u.FindInPage) + case "openPage": + u.OpenPage = new(OpenPageWebSearchAction) + return jsonUnmarshal(data, u.OpenPage) + case "other": + u.Other = new(OtherWebSearchAction) + return jsonUnmarshal(data, u.Other) + case "search": + u.Search = new(SearchWebSearchAction) + return jsonUnmarshal(data, u.Search) + } + return nil +} + +func (u WebSearchAction) MarshalJSON() ([]byte, error) { + switch { + case u.FindInPage != nil: + return marshalTagged("type", "findInPage", u.FindInPage) + case u.OpenPage != nil: + return marshalTagged("type", "openPage", u.OpenPage) + case u.Other != nil: + return marshalTagged("type", "other", u.Other) + case u.Search != nil: + return marshalTagged("type", "search", u.Search) + } + if len(u.raw) > 0 { + return u.raw, nil + } + return nil, errNoVariant("WebSearchAction") +} + +// Raw returns the original JSON for this union value, if it was decoded. +func (u WebSearchAction) Raw() []byte { return u.raw } + +type FindInPageWebSearchAction struct { + Pattern *string `json:"pattern,omitempty"` + URL *string `json:"url,omitempty"` +} + +type OpenPageWebSearchAction struct { + URL *string `json:"url,omitempty"` +} + +type OtherWebSearchAction struct { +} + +type SearchWebSearchAction struct { + Queries []string `json:"queries,omitempty"` + Query *string `json:"query,omitempty"` +} diff --git a/internal/agent/runtime/codex/protocolgen/emit.go b/internal/agent/runtime/codex/protocolgen/emit.go new file mode 100644 index 0000000000..ca907c8715 --- /dev/null +++ b/internal/agent/runtime/codex/protocolgen/emit.go @@ -0,0 +1,302 @@ +package protocolgen + +import ( + "fmt" + "sort" + "strings" +) + +// generator holds emission state: the classified closure plus nested inline +// types synthesized while resolving field expressions. +type generator struct { + corpus *corpus + defs map[string]*classified + + // synthesized inline object/enum types, keyed by their Go name. + extra map[string]*classified + // emission order for synthesized types (creation order, then sorted). + extraNames []string +} + +func newGenerator(c *corpus, defs map[string]*classified) *generator { + return &generator{corpus: c, defs: defs, extra: map[string]*classified{}} +} + +// defGoName maps a schema definition name to its Go type name, honoring the +// hand-written overrides. +func defGoName(name string) string { + if goName, ok := handWritten[name]; ok { + return goName + } + return name +} + +// registerExtra synthesizes a named struct for an inline object schema. +func (g *generator) registerExtra(name string, s *Schema) error { + if existing, ok := g.extra[name]; ok { + if existing.schema == s { + return nil + } + return fmt.Errorf("synthesized type %q defined twice with different schemas", name) + } + if _, ok := g.defs[name]; ok { + return fmt.Errorf("synthesized type %q collides with a schema definition", name) + } + cls, err := classify(name, s) + if err != nil { + return err + } + g.extra[name] = cls + g.extraNames = append(g.extraNames, name) + return nil +} + +// fieldExpr resolves a property schema to a Go type expression for a struct +// field, applying the pointer/omitempty policy: +// - slices and maps stay bare (nil is the absent value) +// - optional or nullable scalars/structs become pointers +func (g *generator) fieldExpr(s *Schema, owner, field string, required bool) (expr string, omitempty bool, err error) { + base, nullable, err := g.baseExpr(s, owner, field) + if err != nil { + return "", false, err + } + soft := strings.HasPrefix(base, "[]") || strings.HasPrefix(base, "map[") || base == "any" + if soft { + return base, !required, nil + } + if !required { + return "*" + base, true, nil + } + if nullable { + // Required but nullable: the key must stay present, so no omitempty — + // a nil pointer marshals as the explicit null the schema allows. + return "*" + base, false, nil + } + return base, false, nil +} + +// baseExpr resolves the non-null Go type for a schema node, synthesizing +// nested named types for inline objects. +func (g *generator) baseExpr(s *Schema, owner, field string) (expr string, nullable bool, err error) { + if s == nil || s.Any { + return "any", false, nil + } + if s.Ref != "" { + return defGoName(refName(s.Ref)), false, nil + } + if name, ok := s.singleAllOfRef(); ok { + return defGoName(name), false, nil + } + if len(s.AnyOf) == 2 { + var value *Schema + nulls := 0 + for _, v := range s.AnyOf { + if types, _ := v.nonNullTypes(); len(types) == 0 && len(v.Type) == 1 { + nulls++ + continue + } + value = v + } + if nulls == 1 && value != nil { + inner, _, err := g.baseExpr(value, owner, field) + return inner, true, err + } + } + if len(s.OneOf) > 0 || len(s.AnyOf) > 0 || len(s.AllOf) > 0 { + // Inline unions synthesize a named type so the union machinery applies. + name := owner + exportedName(field) + if err := g.registerExtra(name, s); err != nil { + return "", false, err + } + return name, false, nil + } + + types, isNullable := s.nonNullTypes() + if len(types) == 0 { + return "any", false, nil + } + if len(types) > 1 { + return "", false, fmt.Errorf("%s.%s: unsupported multi-type %v", owner, field, s.Type) + } + switch types[0] { + case "string": + return "string", isNullable, nil + case "boolean": + return "bool", isNullable, nil + case "integer": + if strings.HasPrefix(s.Format, "uint") { + return "uint64", isNullable, nil + } + return "int64", isNullable, nil + case "number": + return "float64", isNullable, nil + case "array": + elem, _, err := g.baseExpr(s.Items, owner, field+"Item") + if err != nil { + return "", false, err + } + return "[]" + elem, isNullable, nil + case "object": + if len(s.Properties) > 0 { + name := owner + exportedName(field) + if err := g.registerExtra(name, s); err != nil { + return "", false, err + } + return name, isNullable, nil + } + if s.AddlProps != nil && s.AddlProps.Schema != nil { + elem, _, err := g.baseExpr(s.AddlProps.Schema, owner, field+"Value") + if err != nil { + return "", false, err + } + return "map[string]" + elem, isNullable, nil + } + return "map[string]any", isNullable, nil + } + return "", false, fmt.Errorf("%s.%s: unsupported type %q", owner, field, types[0]) +} + +// writeDoc renders a description as a Go doc comment (first paragraph only). +func writeDoc(b *strings.Builder, description, indent string) { + if description == "" { + return + } + paragraph, _, _ := strings.Cut(description, "\n\n") + for _, line := range strings.Split(paragraph, "\n") { + b.WriteString(indent + "// " + strings.TrimRight(line, " ") + "\n") + } +} + +// emitStruct renders a struct definition for an object schema. +func (g *generator) emitStruct(b *strings.Builder, name string, s *Schema, skipProps map[string]bool) error { + required := map[string]bool{} + for _, r := range s.Required { + required[r] = true + } + writeDoc(b, s.Description, "") + fmt.Fprintf(b, "type %s struct {\n", name) + + props := make([]string, 0, len(s.Properties)) + for propName := range s.Properties { + if skipProps[propName] { + continue + } + props = append(props, propName) + } + sort.Strings(props) + + goNames := map[string]string{} + for _, propName := range props { + goName := exportedName(propName) + if prev, ok := goNames[goName]; ok { + return fmt.Errorf("%s: fields %q and %q map to the same Go name %s", name, prev, propName, goName) + } + goNames[goName] = propName + } + + type collectionField struct { + GoName string + Expr string + } + var requiredCollections []collectionField + for _, propName := range props { + prop := s.Properties[propName] + expr, omitempty, err := g.fieldExpr(prop, name, propName, required[propName]) + if err != nil { + return err + } + if required[propName] && (strings.HasPrefix(expr, "[]") || strings.HasPrefix(expr, "map[")) { + requiredCollections = append(requiredCollections, collectionField{GoName: exportedName(propName), Expr: expr}) + } + if prop != nil { + writeDoc(b, prop.Description, "\t") + } + tag := propName + if omitempty { + tag += ",omitempty" + } + fmt.Fprintf(b, "\t%s %s `json:\"%s\"`\n", exportedName(propName), expr, tag) + } + b.WriteString("}\n\n") + + // serde rejects null for required sequences/maps, and Go marshals nil + // slices/maps as null — normalize them to empty collections on the way out. + if len(requiredCollections) > 0 { + fmt.Fprintf(b, "func (v %s) MarshalJSON() ([]byte, error) {\n", name) + fmt.Fprintf(b, "\ttype plain %s\n", name) + b.WriteString("\tp := plain(v)\n") + for _, f := range requiredCollections { + fmt.Fprintf(b, "\tif p.%s == nil {\n\t\tp.%s = %s{}\n\t}\n", f.GoName, f.GoName, f.Expr) + } + b.WriteString("\treturn jsonMarshal(p)\n}\n\n") + } + return nil +} + +// emitEnum renders a string enum type with value constants. +func (*generator) emitEnum(b *strings.Builder, cls *classified) error { + values := cls.unitValues + if values == nil { + var err error + values, err = cls.schema.enumStrings() + if err != nil { + return fmt.Errorf("%s: %w", cls.name, err) + } + } + sorted := append([]string(nil), values...) + sort.Strings(sorted) + + writeDoc(b, cls.schema.Description, "") + fmt.Fprintf(b, "type %s string\n\n", cls.name) + b.WriteString("const (\n") + for _, v := range sorted { + fmt.Fprintf(b, "\t%s%s %s = %q\n", cls.name, exportedName(v), cls.name, v) + } + b.WriteString(")\n\n") + return nil +} + +// emitAlias renders primitive/array/map aliases. +func (g *generator) emitAlias(b *strings.Builder, cls *classified) error { + writeDoc(b, cls.schema.Description, "") + switch cls.kind { + case kindAlias: + types, _ := cls.schema.nonNullTypes() + goType := map[string]string{"string": "string", "boolean": "bool", "number": "float64"}[types[0]] + if types[0] == "integer" { + goType = "int64" + if strings.HasPrefix(cls.schema.Format, "uint") { + goType = "uint64" + } + } + if goType == "" { + return fmt.Errorf("%s: unsupported alias type %v", cls.name, types) + } + fmt.Fprintf(b, "type %s = %s\n\n", cls.name, goType) + case kindArrayAlias: + elem, _, err := g.baseExpr(cls.schema.Items, cls.name, "Item") + if err != nil { + return err + } + fmt.Fprintf(b, "type %s = []%s\n\n", cls.name, elem) + case kindMapAlias: + elem := "any" + if cls.schema.AddlProps != nil && cls.schema.AddlProps.Schema != nil { + var err error + elem, _, err = g.baseExpr(cls.schema.AddlProps.Schema, cls.name, "Value") + if err != nil { + return err + } + } + fmt.Fprintf(b, "type %s = map[string]%s\n\n", cls.name, elem) + case kindOpaqueUnion: + candidates := make([]string, 0, len(cls.schema.AnyOf)) + for _, v := range cls.schema.AnyOf { + candidates = append(candidates, refName(v.Ref)) + } + fmt.Fprintf(b, "// %s is an untagged union with no discriminator; decode the raw\n", cls.name) + fmt.Fprintf(b, "// JSON into one of: %s.\n", strings.Join(candidates, ", ")) + fmt.Fprintf(b, "type %s = rawMessage\n\n", cls.name) + } + return nil +} diff --git a/internal/agent/runtime/codex/protocolgen/emit_methods.go b/internal/agent/runtime/codex/protocolgen/emit_methods.go new file mode 100644 index 0000000000..efe72037d3 --- /dev/null +++ b/internal/agent/runtime/codex/protocolgen/emit_methods.go @@ -0,0 +1,77 @@ +package protocolgen + +import ( + "fmt" + "strings" +) + +// emitMethods renders method constants and the typed dispatch tables that tie +// wire method names to generated params/response types. +func (g *generator) emitMethods(b *strings.Builder) error { + version, err := snapshotVersion() + if err != nil { + return err + } + b.WriteString("// PinnedCodexVersion is the codex CLI version the vendored schema snapshot\n") + b.WriteString("// (and therefore these generated types) corresponds to.\n") + fmt.Fprintf(b, "const PinnedCodexVersion = %q\n\n", version) + + b.WriteString("// Client request methods (Memoh → app-server).\nconst (\n") + for _, m := range clientMethods { + fmt.Fprintf(b, "\tMethod%s = %q\n", exportedName(m.Method), m.Method) + } + b.WriteString(")\n\n") + + b.WriteString("// Server request methods (app-server → Memoh, expect a response).\nconst (\n") + for _, m := range serverRequestMethods { + fmt.Fprintf(b, "\tMethod%s = %q\n", exportedName(m.Method), m.Method) + } + b.WriteString(")\n\n") + + b.WriteString("// Server notification methods decoded into typed params.\nconst (\n") + for _, m := range serverNotifications { + fmt.Fprintf(b, "\tMethod%s = %q\n", exportedName(m), m) + } + b.WriteString(")\n\n") + + b.WriteString("// NewResponseForMethod returns a pointer to the zero response value for a\n") + b.WriteString("// client request method, ready for unmarshaling. ok is false for methods\n") + b.WriteString("// whose responses are not generated (initialize) or unknown methods.\n") + b.WriteString("func NewResponseForMethod(method string) (resp any, ok bool) {\n\tswitch method {\n") + for _, m := range clientMethods { + if m.Response == "" { + continue + } + fmt.Fprintf(b, "\tcase %q:\n\t\treturn new(%s), true\n", m.Method, m.Response) + } + b.WriteString("\t}\n\treturn nil, false\n}\n\n") + + b.WriteString("// DecodeServerRequestParams decodes the params of an app-server → Memoh\n") + b.WriteString("// request into its generated type. ok is false for unknown methods; the\n") + b.WriteString("// caller keeps the raw envelope in that case.\n") + b.WriteString("func DecodeServerRequestParams(method string, params []byte) (decoded any, ok bool, err error) {\n\tswitch method {\n") + for _, m := range serverRequestMethods { + variant := g.corpus.serverRequest[m.Method] + paramsRef := refName(variant.Properties["params"].Ref) + fmt.Fprintf(b, "\tcase %q:\n", m.Method) + fmt.Fprintf(b, "\t\tv := new(%s)\n\t\terr = jsonUnmarshal(params, v)\n\t\treturn v, true, err\n", paramsRef) + } + b.WriteString("\t}\n\treturn nil, false, nil\n}\n\n") + + b.WriteString("// DecodeServerNotificationParams decodes the params of an app-server\n") + b.WriteString("// notification into its generated type. ok is false for methods outside the\n") + b.WriteString("// typed subset; those still surface to the caller as raw envelopes.\n") + b.WriteString("func DecodeServerNotificationParams(method string, params []byte) (decoded any, ok bool, err error) {\n\tswitch method {\n") + for _, m := range serverNotifications { + variant := g.corpus.serverNotification[m] + fmt.Fprintf(b, "\tcase %q:\n", m) + params, hasParams := variant.Properties["params"] + if !hasParams || params.Ref == "" { + b.WriteString("\t\treturn nil, true, nil\n") + continue + } + fmt.Fprintf(b, "\t\tv := new(%s)\n\t\terr = jsonUnmarshal(params, v)\n\t\treturn v, true, err\n", refName(params.Ref)) + } + b.WriteString("\t}\n\treturn nil, false, nil\n}\n") + return nil +} diff --git a/internal/agent/runtime/codex/protocolgen/emit_unions.go b/internal/agent/runtime/codex/protocolgen/emit_unions.go new file mode 100644 index 0000000000..58e608f58b --- /dev/null +++ b/internal/agent/runtime/codex/protocolgen/emit_unions.go @@ -0,0 +1,148 @@ +package protocolgen + +import ( + "fmt" + "sort" + "strings" +) + +// emitTaggedUnion renders a serde internally-tagged union: one wrapper struct +// with a pointer per known variant, the observed tag value, and the raw bytes +// for unknown-variant tolerance. Variant payloads are emitted as their own +// named structs (from the variant titles) with the tag property stripped. +func (g *generator) emitTaggedUnion(b *strings.Builder, cls *classified) error { + name := cls.name + writeDoc(b, cls.schema.Description, "") + fmt.Fprintf(b, "// %s is an internally-tagged union (tag property %q).\n", name, cls.tagProperty) + fmt.Fprintf(b, "// Unknown variants decode without error: only Tag and Raw() are populated.\n") + fmt.Fprintf(b, "type %s struct {\n", name) + fmt.Fprintf(b, "\t// Tag is the value of the %q property observed on decode.\n", cls.tagProperty) + b.WriteString("\tTag string\n") + for _, v := range cls.variants { + fmt.Fprintf(b, "\t%s *%s\n", exportedName(v.TagValue), v.GoName) + } + b.WriteString("\traw rawMessage\n") + b.WriteString("}\n\n") + + // Tag value constants. + b.WriteString("const (\n") + for _, v := range cls.variants { + fmt.Fprintf(b, "\t%sTag%s = %q\n", name, exportedName(v.TagValue), v.TagValue) + } + b.WriteString(")\n\n") + + fmt.Fprintf(b, "func (u *%s) UnmarshalJSON(data []byte) error {\n", name) + // Reset first: encoding/json reuses slice elements and callers reuse + // variables, so stale variant pointers from a previous decode must not + // survive. The raw copy is freshly allocated because Raw() escapes. + fmt.Fprintf(b, "\t*u = %s{}\n", name) + b.WriteString("\tu.raw = append(rawMessage(nil), data...)\n") + fmt.Fprintf(b, "\tvar probe struct {\n\t\tTag string `json:%q`\n\t}\n", cls.tagProperty) + b.WriteString("\tif err := jsonUnmarshal(data, &probe); err != nil {\n\t\treturn err\n\t}\n") + b.WriteString("\tu.Tag = probe.Tag\n") + b.WriteString("\tswitch probe.Tag {\n") + for _, v := range cls.variants { + fmt.Fprintf(b, "\tcase %q:\n", v.TagValue) + fmt.Fprintf(b, "\t\tu.%s = new(%s)\n", exportedName(v.TagValue), v.GoName) + fmt.Fprintf(b, "\t\treturn jsonUnmarshal(data, u.%s)\n", exportedName(v.TagValue)) + } + b.WriteString("\t}\n\treturn nil\n}\n\n") + + fmt.Fprintf(b, "func (u %s) MarshalJSON() ([]byte, error) {\n", name) + b.WriteString("\tswitch {\n") + for _, v := range cls.variants { + fmt.Fprintf(b, "\tcase u.%s != nil:\n", exportedName(v.TagValue)) + fmt.Fprintf(b, "\t\treturn marshalTagged(%q, %q, u.%s)\n", cls.tagProperty, v.TagValue, exportedName(v.TagValue)) + } + b.WriteString("\t}\n") + b.WriteString("\tif len(u.raw) > 0 {\n\t\treturn u.raw, nil\n\t}\n") + fmt.Fprintf(b, "\treturn nil, errNoVariant(%q)\n", name) + b.WriteString("}\n\n") + + fmt.Fprintf(b, "// Raw returns the original JSON for this union value, if it was decoded.\n") + fmt.Fprintf(b, "func (u %s) Raw() []byte { return u.raw }\n\n", name) + + // Variant payload structs, tag property stripped. + for _, v := range cls.variants { + if err := g.emitStruct(b, v.GoName, v.Schema, map[string]bool{cls.tagProperty: true}); err != nil { + return err + } + } + return nil +} + +// emitMixedUnion renders a serde externally-tagged union whose variants are +// bare strings (unit variants) and/or single-key objects (payload variants). +func (g *generator) emitMixedUnion(b *strings.Builder, cls *classified) error { + name := cls.name + + type payload struct { + Key string + GoName string + Expr string + } + payloads := make([]payload, 0, len(cls.objectVariants)) + for _, v := range cls.objectVariants { + expr, _, err := g.baseExpr(v.Payload, name, v.Key) + if err != nil { + return err + } + payloads = append(payloads, payload{Key: v.Key, GoName: exportedName(v.Key), Expr: expr}) + } + sort.Slice(payloads, func(i, j int) bool { return payloads[i].Key < payloads[j].Key }) + units := append([]string(nil), cls.unitValues...) + sort.Strings(units) + + writeDoc(b, cls.schema.Description, "") + fmt.Fprintf(b, "// %s is a mixed union: on the wire it is either a bare string\n", name) + fmt.Fprintf(b, "// (Unit) or a single-key object (one payload field set). Unknown variants\n") + fmt.Fprintf(b, "// decode without error and are retained in Raw().\n") + fmt.Fprintf(b, "type %s struct {\n", name) + b.WriteString("\t// Unit holds the bare-string variant value, if that form was used.\n") + b.WriteString("\tUnit string\n") + for _, p := range payloads { + fmt.Fprintf(b, "\t%s *%s\n", p.GoName, p.Expr) + } + b.WriteString("\traw rawMessage\n") + b.WriteString("}\n\n") + + if len(units) > 0 { + b.WriteString("const (\n") + for _, v := range units { + fmt.Fprintf(b, "\t%sUnit%s = %q\n", name, exportedName(v), v) + } + b.WriteString(")\n\n") + } + + fmt.Fprintf(b, "func (u *%s) UnmarshalJSON(data []byte) error {\n", name) + fmt.Fprintf(b, "\t*u = %s{}\n", name) + b.WriteString("\tu.raw = append(rawMessage(nil), data...)\n") + b.WriteString("\tif isJSONString(data) {\n\t\treturn jsonUnmarshal(data, &u.Unit)\n\t}\n") + b.WriteString("\tvar obj map[string]rawMessage\n") + b.WriteString("\tif err := jsonUnmarshal(data, &obj); err != nil {\n\t\treturn err\n\t}\n") + b.WriteString("\tif len(obj) != 1 {\n\t\treturn nil\n\t}\n") + b.WriteString("\tfor key, payload := range obj {\n") + b.WriteString("\t\tswitch key {\n") + for _, p := range payloads { + fmt.Fprintf(b, "\t\tcase %q:\n", p.Key) + fmt.Fprintf(b, "\t\t\tu.%s = new(%s)\n", p.GoName, p.Expr) + fmt.Fprintf(b, "\t\t\treturn jsonUnmarshal(payload, u.%s)\n", p.GoName) + } + b.WriteString("\t\t}\n\t}\n\treturn nil\n}\n\n") + + fmt.Fprintf(b, "func (u %s) MarshalJSON() ([]byte, error) {\n", name) + b.WriteString("\tif u.Unit != \"\" {\n\t\treturn jsonMarshal(u.Unit)\n\t}\n") + b.WriteString("\tswitch {\n") + for _, p := range payloads { + fmt.Fprintf(b, "\tcase u.%s != nil:\n", p.GoName) + fmt.Fprintf(b, "\t\treturn marshalKeyed(%q, u.%s)\n", p.Key, p.GoName) + } + b.WriteString("\t}\n") + b.WriteString("\tif len(u.raw) > 0 {\n\t\treturn u.raw, nil\n\t}\n") + fmt.Fprintf(b, "\treturn nil, errNoVariant(%q)\n", name) + b.WriteString("}\n\n") + + fmt.Fprintf(b, "// Raw returns the original JSON for this union value, if it was decoded.\n") + fmt.Fprintf(b, "func (u %s) Raw() []byte { return u.raw }\n\n", name) + return nil +} diff --git a/internal/agent/runtime/codex/protocolgen/generate.go b/internal/agent/runtime/codex/protocolgen/generate.go new file mode 100644 index 0000000000..48708985fe --- /dev/null +++ b/internal/agent/runtime/codex/protocolgen/generate.go @@ -0,0 +1,113 @@ +package protocolgen + +import ( + "encoding/json" + "errors" + "fmt" + "go/format" + "sort" + "strings" +) + +// Generate produces the generated protocol source files, keyed by file name. +// Output is deterministic for a given schema snapshot and subset config. +func Generate() (map[string][]byte, error) { + c, err := loadCorpus() + if err != nil { + return nil, err + } + defs, err := closure(c) + if err != nil { + return nil, err + } + g := newGenerator(c, defs) + + var types, unions, methods strings.Builder + + names := make([]string, 0, len(defs)) + for name := range defs { + names = append(names, name) + } + sort.Strings(names) + + emitOne := func(cls *classified) error { + switch cls.kind { + case kindMethodUnion, kindHandWritten: + return nil + case kindStruct: + return g.emitStruct(&types, cls.name, cls.schema, nil) + case kindEnum: + return g.emitEnum(&types, cls) + case kindAlias, kindArrayAlias, kindMapAlias, kindOpaqueUnion: + return g.emitAlias(&types, cls) + case kindTaggedUnion: + return g.emitTaggedUnion(&unions, cls) + case kindMixedUnion: + return g.emitMixedUnion(&unions, cls) + } + return fmt.Errorf("definition %q: unknown kind", cls.name) + } + + for _, name := range names { + if err := emitOne(defs[name]); err != nil { + return nil, err + } + } + // Emitting definitions can synthesize types for inline schemas; emitting + // those can synthesize more, so drain the queue. + for i := 0; i < len(g.extraNames); i++ { + if err := emitOne(g.extra[g.extraNames[i]]); err != nil { + return nil, err + } + } + + if err := g.emitMethods(&methods); err != nil { + return nil, err + } + + version, err := snapshotVersion() + if err != nil { + return nil, err + } + header := fmt.Sprintf(`// Code generated by gen-codex-protocol from the codex app-server v2 JSON +// Schema snapshot (codex-cli %s). DO NOT EDIT. +// +// Regenerate with `+"`mise run codex-protocol-generate`"+`; refresh the snapshot +// itself with `+"`mise run codex-schema-sync`"+`. + +package protocol + +`, version) + + files := map[string][]byte{} + for name, body := range map[string]*strings.Builder{ + "types.gen.go": &types, + "unions.gen.go": &unions, + "methods.gen.go": &methods, + } { + formatted, err := format.Source([]byte(header + body.String())) + if err != nil { + return nil, fmt.Errorf("gofmt %s: %w", name, err) + } + files[name] = formatted + } + return files, nil +} + +// snapshotVersion reads the pinned codex version from schema/VERSION.json. +func snapshotVersion() (string, error) { + raw, err := schemaFS.ReadFile("schema/VERSION.json") + if err != nil { + return "", err + } + var v struct { + CodexVersion string `json:"codexVersion"` + } + if err := json.Unmarshal(raw, &v); err != nil { + return "", fmt.Errorf("schema/VERSION.json: %w", err) + } + if v.CodexVersion == "" { + return "", errors.New("schema/VERSION.json: missing codexVersion") + } + return v.CodexVersion, nil +} diff --git a/internal/agent/runtime/codex/protocolgen/naming.go b/internal/agent/runtime/codex/protocolgen/naming.go new file mode 100644 index 0000000000..7514c2fb6b --- /dev/null +++ b/internal/agent/runtime/codex/protocolgen/naming.go @@ -0,0 +1,86 @@ +package protocolgen + +import ( + "strings" + "unicode" +) + +// initialisms are words rendered in full caps in exported Go identifiers. +var initialisms = map[string]string{ + "id": "ID", + "url": "URL", + "uri": "URI", + "http": "HTTP", + "https": "HTTPS", + "api": "API", + "json": "JSON", + "uuid": "UUID", + "mcp": "MCP", + "pid": "PID", + "sdp": "SDP", +} + +// exportedName converts a JSON identifier (camelCase, kebab-case, snake_case, +// or slash-separated) into an exported Go identifier. +func exportedName(name string) string { + var b strings.Builder + for _, word := range splitWords(name) { + lower := strings.ToLower(word) + if repl, ok := initialisms[lower]; ok { + b.WriteString(repl) + continue + } + r := []rune(word) + b.WriteString(string(unicode.ToUpper(r[0])) + string(r[1:])) + } + return b.String() +} + +// isGoIdentifier reports whether s is usable as an exported Go type name. +func isGoIdentifier(s string) bool { + if s == "" { + return false + } + for i, r := range s { + if unicode.IsLetter(r) || r == '_' || (i > 0 && unicode.IsDigit(r)) { + continue + } + return false + } + return unicode.IsUpper([]rune(s)[0]) +} + +// splitWords breaks an identifier into words on case transitions and on the +// separators `-`, `_`, `/`, `.`, and spaces. +func splitWords(s string) []string { + var words []string + var current []rune + flush := func() { + if len(current) > 0 { + words = append(words, string(current)) + current = nil + } + } + runes := []rune(s) + for i, r := range runes { + switch { + case r == '-' || r == '_' || r == '/' || r == '.' || r == ' ' || r == '$': + flush() + case unicode.IsUpper(r): + // Start a new word on a lower→upper transition, or at the end of + // an acronym run (upper followed by lower). + if len(current) > 0 { + prev := current[len(current)-1] + nextLower := i+1 < len(runes) && unicode.IsLower(runes[i+1]) + if unicode.IsLower(prev) || unicode.IsDigit(prev) || (unicode.IsUpper(prev) && nextLower) { + flush() + } + } + current = append(current, r) + default: + current = append(current, r) + } + } + flush() + return words +} diff --git a/internal/agent/runtime/codex/protocolgen/resolve.go b/internal/agent/runtime/codex/protocolgen/resolve.go new file mode 100644 index 0000000000..2096624f7a --- /dev/null +++ b/internal/agent/runtime/codex/protocolgen/resolve.go @@ -0,0 +1,320 @@ +package protocolgen + +import ( + "fmt" + "sort" +) + +// defKind classifies a named definition into one of the shapes the emitter +// knows how to render. +type defKind int + +const ( + kindStruct defKind = iota + kindEnum + kindAlias // named alias of a primitive + kindArrayAlias // named alias of an array + kindMapAlias // named alias of a map + kindTaggedUnion // serde internally-tagged union (tag property + payload fields) + kindMixedUnion // serde externally-tagged union: bare-string units and/or single-key object variants + kindOpaqueUnion // untagged anyOf union with no discriminator; kept as raw JSON + kindMethodUnion // top-level method envelope unions; never emitted + kindHandWritten // mapped to a hand-written type in the protocol package +) + +// handWritten maps schema definitions to hand-written types in the protocol +// package, for shapes the generator should not attempt (RequestId is a +// string|number union that must round-trip byte-for-byte). +var handWritten = map[string]string{ + "FunctionCallOutputBody": "rawMessage", + "RequestId": "RequestID", +} + +type classified struct { + name string + kind defKind + schema *Schema + + // kindTaggedUnion + tagProperty string + variants []taggedVariant + + // kindMixedUnion + unitValues []string + objectVariants []mixedVariant +} + +type taggedVariant struct { + TagValue string + GoName string // from variant title + Schema *Schema +} + +type mixedVariant struct { + Key string + Payload *Schema +} + +var methodUnionNames = map[string]bool{ + "ClientRequest": true, + "ServerNotification": true, + "JSONRPCMessage": true, + "JSONRPCRequest": true, + "JSONRPCResponse": true, + "JSONRPCNotification": true, + "JSONRPCError": true, +} + +func classify(name string, s *Schema) (*classified, error) { + c := &classified{name: name, schema: s} + if methodUnionNames[name] { + c.kind = kindMethodUnion + return c, nil + } + if _, ok := handWritten[name]; ok { + c.kind = kindHandWritten + return c, nil + } + + switch { + case len(s.OneOf) > 0: + return classifyOneOf(c, s) + case len(s.AnyOf) > 0: + // Untagged unions carry no discriminator, so decoding into a typed + // wrapper would require structural trial; keep them raw and let the + // consumer decode into the candidate types on demand. + for _, v := range s.AnyOf { + if v.Ref == "" { + return nil, fmt.Errorf("definition %q: anyOf variant without $ref is not supported", name) + } + } + c.kind = kindOpaqueUnion + return c, nil + case len(s.AllOf) > 0: + return nil, fmt.Errorf("definition %q: top-level allOf is not supported", name) + } + + types, _ := s.nonNullTypes() + if len(types) != 1 { + return nil, fmt.Errorf("definition %q: unsupported type list %v", name, s.Type) + } + switch types[0] { + case "object": + if len(s.Properties) > 0 || s.AddlProps == nil { + c.kind = kindStruct + return c, nil + } + c.kind = kindMapAlias + return c, nil + case "string": + if len(s.Enum) > 0 { + c.kind = kindEnum + return c, nil + } + c.kind = kindAlias + return c, nil + case "integer", "number", "boolean": + c.kind = kindAlias + return c, nil + case "array": + c.kind = kindArrayAlias + return c, nil + } + return nil, fmt.Errorf("definition %q: unsupported type %q", name, types[0]) +} + +func classifyOneOf(c *classified, s *Schema) (*classified, error) { + var unitValues []string + var objectVariants []mixedVariant + var taggedVariants []*Schema + + for _, v := range s.OneOf { + types, _ := v.nonNullTypes() + switch { + case len(types) == 1 && types[0] == "string" && len(v.Enum) > 0: + values, err := v.enumStrings() + if err != nil { + return nil, fmt.Errorf("definition %q: %w", c.name, err) + } + unitValues = append(unitValues, values...) + case len(types) == 1 && types[0] == "object" && len(v.Properties) == 1 && len(v.Required) == 1 && v.Properties[v.Required[0]] != nil && v.Properties[v.Required[0]].Enum == nil: + key := v.Required[0] + objectVariants = append(objectVariants, mixedVariant{Key: key, Payload: v.Properties[key]}) + case len(types) == 1 && types[0] == "object": + taggedVariants = append(taggedVariants, v) + default: + return nil, fmt.Errorf("definition %q: oneOf variant with unsupported shape", c.name) + } + } + + switch { + case len(taggedVariants) == len(s.OneOf): + return classifyTagged(c, taggedVariants) + case len(taggedVariants) == 0 && len(objectVariants) == 0: + // Enum split across variants for per-value descriptions. + c.kind = kindEnum + c.unitValues = unitValues + return c, nil + case len(taggedVariants) == 0: + c.kind = kindMixedUnion + c.unitValues = unitValues + c.objectVariants = objectVariants + return c, nil + } + return nil, fmt.Errorf("definition %q: oneOf mixes tagged and other variant shapes", c.name) +} + +func classifyTagged(c *classified, variants []*Schema) (*classified, error) { + // Find the tag: a property present in every variant whose schema is a + // single-valued string enum. Prefer "type" when it qualifies. + tag := "" + candidates := map[string]int{} + for _, v := range variants { + for propName, prop := range v.Properties { + if prop != nil && len(prop.Enum) == 1 { + if types, _ := prop.nonNullTypes(); len(types) == 1 && types[0] == "string" { + candidates[propName]++ + } + } + } + } + if candidates["type"] == len(variants) { + tag = "type" + } else { + for propName, count := range candidates { + if count == len(variants) { + if tag != "" { + return nil, fmt.Errorf("definition %q: ambiguous union tag (%q vs %q)", c.name, tag, propName) + } + tag = propName + } + } + } + if tag == "" { + return nil, fmt.Errorf("definition %q: no common tag property across oneOf variants", c.name) + } + + c.kind = kindTaggedUnion + c.tagProperty = tag + for _, v := range variants { + values, err := v.Properties[tag].enumStrings() + if err != nil { + return nil, fmt.Errorf("definition %q: %w", c.name, err) + } + goName := v.Title + if !isGoIdentifier(goName) { + // Titles are the upstream naming source of truth, but a few are + // missing or carry Rust module paths (`Foov2::Bar`); synthesize + // the same "" shape well-formed siblings use. + goName = exportedName(values[0]) + c.name + } + c.variants = append(c.variants, taggedVariant{TagValue: values[0], GoName: goName, Schema: v}) + } + sort.Slice(c.variants, func(i, j int) bool { return c.variants[i].TagValue < c.variants[j].TagValue }) + return c, nil +} + +// closure walks $refs from the subset roots and returns every reachable +// definition, classified. +func closure(c *corpus) (map[string]*classified, error) { + seen := map[string]*classified{} + var visitSchema func(s *Schema) error + + visitDef := func(name string) error { + if _, ok := seen[name]; ok { + return nil + } + def, ok := c.defs[name] + if !ok { + return fmt.Errorf("unresolved $ref to %q", name) + } + cls, err := classify(name, def) + if err != nil { + return err + } + seen[name] = cls + return visitSchema(def) + } + + visitSchema = func(s *Schema) error { + if s == nil { + return nil + } + if s.Ref != "" { + return visitDef(refName(s.Ref)) + } + for _, group := range [][]*Schema{s.OneOf, s.AnyOf, s.AllOf} { + for _, sub := range group { + if err := visitSchema(sub); err != nil { + return err + } + } + } + names := make([]string, 0, len(s.Properties)) + for propName := range s.Properties { + names = append(names, propName) + } + sort.Strings(names) + for _, propName := range names { + if err := visitSchema(s.Properties[propName]); err != nil { + return err + } + } + if err := visitSchema(s.Items); err != nil { + return err + } + if s.AddlProps != nil { + if err := visitSchema(s.AddlProps.Schema); err != nil { + return err + } + } + return nil + } + + root := func(name string) error { return visitDef(name) } + + for _, m := range clientMethods { + variant, ok := c.clientRequest[m.Method] + if !ok { + return nil, fmt.Errorf("client method %q not present in ClientRequest union", m.Method) + } + if params, ok := variant.Properties["params"]; ok && params.Ref != "" { + if err := root(refName(params.Ref)); err != nil { + return nil, err + } + } + if m.Response != "" { + if err := root(m.Response); err != nil { + return nil, err + } + } + } + for _, m := range serverRequestMethods { + variant, ok := c.serverRequest[m.Method] + if !ok { + return nil, fmt.Errorf("server request %q not present in ServerRequest union", m.Method) + } + params, ok := variant.Properties["params"] + if !ok || params.Ref == "" { + return nil, fmt.Errorf("server request %q has no params $ref", m.Method) + } + if err := root(refName(params.Ref)); err != nil { + return nil, err + } + if err := root(m.Response); err != nil { + return nil, err + } + } + for _, method := range serverNotifications { + variant, ok := c.serverNotification[method] + if !ok { + return nil, fmt.Errorf("notification %q not present in ServerNotification union", method) + } + if params, ok := variant.Properties["params"]; ok && params.Ref != "" { + if err := root(refName(params.Ref)); err != nil { + return nil, err + } + } + } + return seen, nil +} diff --git a/internal/agent/runtime/codex/protocolgen/schema.go b/internal/agent/runtime/codex/protocolgen/schema.go new file mode 100644 index 0000000000..e55acdd4fb --- /dev/null +++ b/internal/agent/runtime/codex/protocolgen/schema.go @@ -0,0 +1,282 @@ +// Package protocolgen generates the Go types for the codex app-server v2 +// protocol from the vendored JSON Schema snapshot under schema/. +// +// The snapshot is produced by the pinned codex CLI (`codex app-server +// generate-json-schema`, non-experimental). Generic JSON-Schema code +// generators cannot represent the serde union encodings this protocol uses, +// so this package implements a small purpose-built generator that handles +// exactly the schema shapes present in the snapshot and fails loudly on +// anything else. +package protocolgen + +import ( + "bytes" + "embed" + "encoding/json" + "errors" + "fmt" + "sort" + "strings" +) + +//go:embed schema/*.json +var schemaFS embed.FS + +// Schema models the subset of JSON Schema draft-07 that the codex snapshot +// actually uses. Loading fails if a document contains anything outside it. +// The boolean schema form (`true` = anything) is represented by Any. +type Schema struct { + // Any marks the boolean schema `true`, which accepts any value. + Any bool `json:"-"` + + Ref string `json:"$ref"` + SchemaTag string `json:"$schema"` + Type typeList `json:"type"` + Properties map[string]*Schema `json:"properties"` + Required []string `json:"required"` + Items *Schema `json:"items"` + AddlProps *addlProps `json:"additionalProperties"` + Enum []json.RawMessage `json:"enum"` + OneOf []*Schema `json:"oneOf"` + AnyOf []*Schema `json:"anyOf"` + AllOf []*Schema `json:"allOf"` + Format string `json:"format"` + Title string `json:"title"` + Description string `json:"description"` + Definitions map[string]*Schema `json:"definitions"` + Default json.RawMessage `json:"default"` + Minimum json.RawMessage `json:"minimum"` + MinLength json.RawMessage `json:"minLength"` +} + +func (s *Schema) UnmarshalJSON(b []byte) error { + trimmed := bytes.TrimSpace(b) + if bytes.Equal(trimmed, []byte("true")) { + *s = Schema{Any: true} + return nil + } + if bytes.Equal(trimmed, []byte("false")) { + return errors.New("boolean schema `false` is not supported") + } + type plain Schema + var p plain + dec := json.NewDecoder(bytes.NewReader(b)) + dec.DisallowUnknownFields() + if err := dec.Decode(&p); err != nil { + return err + } + *s = Schema(p) + return nil +} + +// typeList accepts both `"type": "string"` and `"type": ["string", "null"]`. +type typeList []string + +func (t *typeList) UnmarshalJSON(b []byte) error { + if len(b) > 0 && b[0] == '"' { + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + *t = typeList{s} + return nil + } + var list []string + if err := json.Unmarshal(b, &list); err != nil { + return err + } + *t = list + return nil +} + +// addlProps accepts boolean or schema forms of additionalProperties. +type addlProps struct { + Bool *bool + Schema *Schema +} + +func (a *addlProps) UnmarshalJSON(b []byte) error { + trimmed := bytes.TrimSpace(b) + if bytes.Equal(trimmed, []byte("true")) || bytes.Equal(trimmed, []byte("false")) { + v := trimmed[0] == 't' + a.Bool = &v + return nil + } + a.Schema = new(Schema) + return json.Unmarshal(b, a.Schema) +} + +// nonNullTypes returns the schema's type list with "null" removed and reports +// whether "null" was present. +func (s *Schema) nonNullTypes() (types []string, nullable bool) { + for _, t := range s.Type { + if t == "null" { + nullable = true + continue + } + types = append(types, t) + } + return types, nullable +} + +// singleAllOfRef unwraps the `allOf: [{$ref}]` + description pattern, the only +// allOf form present in the snapshot. +func (s *Schema) singleAllOfRef() (string, bool) { + if len(s.AllOf) == 1 && s.AllOf[0].Ref != "" { + return refName(s.AllOf[0].Ref), true + } + return "", false +} + +func refName(ref string) string { + const prefix = "#/definitions/" + if !strings.HasPrefix(ref, prefix) { + panic(fmt.Sprintf("protocolgen: unsupported $ref %q", ref)) + } + return strings.TrimPrefix(ref, prefix) +} + +// enumStrings decodes an enum whose values must all be strings. +func (s *Schema) enumStrings() ([]string, error) { + out := make([]string, 0, len(s.Enum)) + for _, raw := range s.Enum { + var v string + if err := json.Unmarshal(raw, &v); err != nil { + return nil, fmt.Errorf("non-string enum value %s", raw) + } + out = append(out, v) + } + return out, nil +} + +// corpus is the merged definition table plus the top-level union documents. +type corpus struct { + defs map[string]*Schema + // clientRequest, serverRequest, serverNotification map method name to the + // oneOf variant schema describing that method's envelope. + clientRequest map[string]*Schema + serverRequest map[string]*Schema + serverNotification map[string]*Schema +} + +// standalone response documents vendored as separate files: file name (minus +// .json) is the type name. +var standaloneDocs = []string{ + "CommandExecutionRequestApprovalResponse", + "FileChangeRequestApprovalResponse", + "PermissionsRequestApprovalResponse", + "ToolRequestUserInputResponse", + "McpServerElicitationRequestResponse", + "ChatgptAuthTokensRefreshResponse", +} + +func loadCorpus() (*corpus, error) { + c := &corpus{defs: map[string]*Schema{}} + + bundle, err := loadDoc("schema/codex_app_server_protocol.v2.schemas.json") + if err != nil { + return nil, err + } + if err := c.mergeDefs(bundle.Definitions, "v2 bundle"); err != nil { + return nil, err + } + + serverReq, err := loadDoc("schema/ServerRequest.json") + if err != nil { + return nil, err + } + if err := c.mergeDefs(serverReq.Definitions, "ServerRequest.json"); err != nil { + return nil, err + } + + for _, name := range standaloneDocs { + doc, err := loadDoc("schema/" + name + ".json") + if err != nil { + return nil, err + } + if err := c.mergeDefs(doc.Definitions, name+".json"); err != nil { + return nil, err + } + // The document root itself is the named type. + root := *doc + root.Definitions = nil + if err := c.mergeDefs(map[string]*Schema{name: &root}, name+".json root"); err != nil { + return nil, err + } + } + + c.clientRequest, err = methodVariants(c.defs["ClientRequest"], "ClientRequest") + if err != nil { + return nil, err + } + c.serverNotification, err = methodVariants(c.defs["ServerNotification"], "ServerNotification") + if err != nil { + return nil, err + } + serverReqRoot := *serverReq + serverReqRoot.Definitions = nil + c.serverRequest, err = methodVariants(&serverReqRoot, "ServerRequest") + if err != nil { + return nil, err + } + return c, nil +} + +func loadDoc(path string) (*Schema, error) { + raw, err := schemaFS.ReadFile(path) + if err != nil { + return nil, err + } + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + var s Schema + if err := dec.Decode(&s); err != nil { + return nil, fmt.Errorf("%s: %w", path, err) + } + return &s, nil +} + +// mergeDefs adds definitions, requiring byte-identical shapes on collision so +// the bundle and standalone documents cannot silently disagree. +func (c *corpus) mergeDefs(defs map[string]*Schema, source string) error { + names := make([]string, 0, len(defs)) + for name := range defs { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + def := defs[name] + existing, ok := c.defs[name] + if !ok { + c.defs[name] = def + continue + } + a, _ := json.Marshal(existing) + b, _ := json.Marshal(def) + if !bytes.Equal(a, b) { + return fmt.Errorf("definition %q from %s conflicts with an earlier copy", name, source) + } + } + return nil +} + +// methodVariants indexes a method-envelope union (oneOf of objects carrying a +// const `method` property) by method name. +func methodVariants(s *Schema, unionName string) (map[string]*Schema, error) { + if s == nil || len(s.OneOf) == 0 { + return nil, fmt.Errorf("%s: missing or not a oneOf union", unionName) + } + out := make(map[string]*Schema, len(s.OneOf)) + for _, variant := range s.OneOf { + methodSchema, ok := variant.Properties["method"] + if !ok || len(methodSchema.Enum) != 1 { + return nil, fmt.Errorf("%s: variant without singleton method enum", unionName) + } + methods, err := methodSchema.enumStrings() + if err != nil { + return nil, fmt.Errorf("%s: %w", unionName, err) + } + out[methods[0]] = variant + } + return out, nil +} diff --git a/internal/agent/runtime/codex/protocolgen/schema/ChatgptAuthTokensRefreshResponse.json b/internal/agent/runtime/codex/protocolgen/schema/ChatgptAuthTokensRefreshResponse.json new file mode 100644 index 0000000000..6d88e784c5 --- /dev/null +++ b/internal/agent/runtime/codex/protocolgen/schema/ChatgptAuthTokensRefreshResponse.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "accessToken": { + "type": "string" + }, + "chatgptAccountId": { + "type": "string" + }, + "chatgptPlanType": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "accessToken", + "chatgptAccountId" + ], + "title": "ChatgptAuthTokensRefreshResponse", + "type": "object" +} \ No newline at end of file diff --git a/internal/agent/runtime/codex/protocolgen/schema/ClientNotification.json b/internal/agent/runtime/codex/protocolgen/schema/ClientNotification.json new file mode 100644 index 0000000000..dde0b31fbd --- /dev/null +++ b/internal/agent/runtime/codex/protocolgen/schema/ClientNotification.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "oneOf": [ + { + "properties": { + "method": { + "enum": [ + "initialized" + ], + "title": "InitializedNotificationMethod", + "type": "string" + } + }, + "required": [ + "method" + ], + "title": "InitializedNotification", + "type": "object" + } + ], + "title": "ClientNotification" +} \ No newline at end of file diff --git a/internal/agent/runtime/codex/protocolgen/schema/CommandExecutionRequestApprovalResponse.json b/internal/agent/runtime/codex/protocolgen/schema/CommandExecutionRequestApprovalResponse.json new file mode 100644 index 0000000000..0b7986fba9 --- /dev/null +++ b/internal/agent/runtime/codex/protocolgen/schema/CommandExecutionRequestApprovalResponse.json @@ -0,0 +1,116 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "CommandExecutionApprovalDecision": { + "oneOf": [ + { + "description": "User approved the command.", + "enum": [ + "accept" + ], + "type": "string" + }, + { + "description": "User approved the command and future prompts in the same session-scoped approval cache should run without prompting.", + "enum": [ + "acceptForSession" + ], + "type": "string" + }, + { + "additionalProperties": false, + "description": "User approved the command, and wants to apply the proposed execpolicy amendment so future matching commands can run without prompting.", + "properties": { + "acceptWithExecpolicyAmendment": { + "properties": { + "execpolicy_amendment": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "execpolicy_amendment" + ], + "type": "object" + } + }, + "required": [ + "acceptWithExecpolicyAmendment" + ], + "title": "AcceptWithExecpolicyAmendmentCommandExecutionApprovalDecision", + "type": "object" + }, + { + "additionalProperties": false, + "description": "User chose a persistent network policy rule (allow/deny) for this host.", + "properties": { + "applyNetworkPolicyAmendment": { + "properties": { + "network_policy_amendment": { + "$ref": "#/definitions/NetworkPolicyAmendment" + } + }, + "required": [ + "network_policy_amendment" + ], + "type": "object" + } + }, + "required": [ + "applyNetworkPolicyAmendment" + ], + "title": "ApplyNetworkPolicyAmendmentCommandExecutionApprovalDecision", + "type": "object" + }, + { + "description": "User denied the command. The agent will continue the turn.", + "enum": [ + "decline" + ], + "type": "string" + }, + { + "description": "User denied the command. The turn will also be immediately interrupted.", + "enum": [ + "cancel" + ], + "type": "string" + } + ] + }, + "NetworkPolicyAmendment": { + "properties": { + "action": { + "$ref": "#/definitions/NetworkPolicyRuleAction" + }, + "host": { + "type": "string" + } + }, + "required": [ + "action", + "host" + ], + "type": "object" + }, + "NetworkPolicyRuleAction": { + "enum": [ + "allow", + "deny" + ], + "type": "string" + } + }, + "properties": { + "decision": { + "$ref": "#/definitions/CommandExecutionApprovalDecision" + } + }, + "required": [ + "decision" + ], + "title": "CommandExecutionRequestApprovalResponse", + "type": "object" +} \ No newline at end of file diff --git a/internal/agent/runtime/codex/protocolgen/schema/FileChangeRequestApprovalResponse.json b/internal/agent/runtime/codex/protocolgen/schema/FileChangeRequestApprovalResponse.json new file mode 100644 index 0000000000..f20035e3d7 --- /dev/null +++ b/internal/agent/runtime/codex/protocolgen/schema/FileChangeRequestApprovalResponse.json @@ -0,0 +1,47 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "FileChangeApprovalDecision": { + "oneOf": [ + { + "description": "User approved the file changes.", + "enum": [ + "accept" + ], + "type": "string" + }, + { + "description": "User approved the file changes and future changes to the same files should run without prompting.", + "enum": [ + "acceptForSession" + ], + "type": "string" + }, + { + "description": "User denied the file changes. The agent will continue the turn.", + "enum": [ + "decline" + ], + "type": "string" + }, + { + "description": "User denied the file changes. The turn will also be immediately interrupted.", + "enum": [ + "cancel" + ], + "type": "string" + } + ] + } + }, + "properties": { + "decision": { + "$ref": "#/definitions/FileChangeApprovalDecision" + } + }, + "required": [ + "decision" + ], + "title": "FileChangeRequestApprovalResponse", + "type": "object" +} \ No newline at end of file diff --git a/internal/agent/runtime/codex/protocolgen/schema/McpServerElicitationRequestResponse.json b/internal/agent/runtime/codex/protocolgen/schema/McpServerElicitationRequestResponse.json new file mode 100644 index 0000000000..13390a06cf --- /dev/null +++ b/internal/agent/runtime/codex/protocolgen/schema/McpServerElicitationRequestResponse.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "McpServerElicitationAction": { + "enum": [ + "accept", + "decline", + "cancel" + ], + "type": "string" + } + }, + "properties": { + "_meta": { + "description": "Optional client metadata for form-mode action handling." + }, + "action": { + "$ref": "#/definitions/McpServerElicitationAction" + }, + "content": { + "description": "Structured user input for accepted elicitations, mirroring RMCP `CreateElicitationResult`.\n\nThis is nullable because decline/cancel responses have no content." + } + }, + "required": [ + "action" + ], + "title": "McpServerElicitationRequestResponse", + "type": "object" +} \ No newline at end of file diff --git a/internal/agent/runtime/codex/protocolgen/schema/PermissionsRequestApprovalResponse.json b/internal/agent/runtime/codex/protocolgen/schema/PermissionsRequestApprovalResponse.json new file mode 100644 index 0000000000..a21e00a19a --- /dev/null +++ b/internal/agent/runtime/codex/protocolgen/schema/PermissionsRequestApprovalResponse.json @@ -0,0 +1,322 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "AdditionalFileSystemPermissions": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/FileSystemSandboxEntry" + }, + "type": [ + "array", + "null" + ] + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, + "read": { + "description": "This will be removed in favor of `entries`.", + "items": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": [ + "array", + "null" + ] + }, + "write": { + "description": "This will be removed in favor of `entries`.", + "items": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": [ + "array", + "null" + ] + } + }, + "type": "object" + }, + "AdditionalNetworkPermissions": { + "properties": { + "enabled": { + "type": [ + "boolean", + "null" + ] + } + }, + "type": "object" + }, + "FileSystemAccessMode": { + "enum": [ + "read", + "write", + "deny" + ], + "type": "string" + }, + "FileSystemPath": { + "oneOf": [ + { + "properties": { + "path": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": { + "enum": [ + "path" + ], + "title": "PathFileSystemPathType", + "type": "string" + } + }, + "required": [ + "path", + "type" + ], + "title": "PathFileSystemPath", + "type": "object" + }, + { + "properties": { + "pattern": { + "type": "string" + }, + "type": { + "enum": [ + "glob_pattern" + ], + "title": "GlobPatternFileSystemPathType", + "type": "string" + } + }, + "required": [ + "pattern", + "type" + ], + "title": "GlobPatternFileSystemPath", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "special" + ], + "title": "SpecialFileSystemPathType", + "type": "string" + }, + "value": { + "$ref": "#/definitions/FileSystemSpecialPath" + } + }, + "required": [ + "type", + "value" + ], + "title": "SpecialFileSystemPath", + "type": "object" + } + ] + }, + "FileSystemSandboxEntry": { + "properties": { + "access": { + "$ref": "#/definitions/FileSystemAccessMode" + }, + "path": { + "$ref": "#/definitions/FileSystemPath" + } + }, + "required": [ + "access", + "path" + ], + "type": "object" + }, + "FileSystemSpecialPath": { + "oneOf": [ + { + "properties": { + "kind": { + "enum": [ + "root" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "RootFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "minimal" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "MinimalFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "project_roots" + ], + "type": "string" + }, + "subpath": { + "anyOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "kind" + ], + "title": "KindFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "tmpdir" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "TmpdirFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "slash_tmp" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "SlashTmpFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "unknown" + ], + "type": "string" + }, + "path": { + "type": "string" + }, + "subpath": { + "anyOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "kind", + "path" + ], + "type": "object" + } + ] + }, + "GrantedPermissionProfile": { + "properties": { + "fileSystem": { + "anyOf": [ + { + "$ref": "#/definitions/AdditionalFileSystemPermissions" + }, + { + "type": "null" + } + ] + }, + "network": { + "anyOf": [ + { + "$ref": "#/definitions/AdditionalNetworkPermissions" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "LegacyAppPathString": { + "type": "string" + }, + "PermissionGrantScope": { + "enum": [ + "turn", + "session" + ], + "type": "string" + } + }, + "properties": { + "permissions": { + "$ref": "#/definitions/GrantedPermissionProfile" + }, + "scope": { + "allOf": [ + { + "$ref": "#/definitions/PermissionGrantScope" + } + ], + "default": "turn" + }, + "strictAutoReview": { + "description": "Review every subsequent command in this turn before normal sandboxed execution.", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "permissions" + ], + "title": "PermissionsRequestApprovalResponse", + "type": "object" +} \ No newline at end of file diff --git a/internal/agent/runtime/codex/protocolgen/schema/ServerRequest.json b/internal/agent/runtime/codex/protocolgen/schema/ServerRequest.json new file mode 100644 index 0000000000..e701e96481 --- /dev/null +++ b/internal/agent/runtime/codex/protocolgen/schema/ServerRequest.json @@ -0,0 +1,2079 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "AbsolutePathBuf": { + "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", + "type": "string" + }, + "AdditionalFileSystemPermissions": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/FileSystemSandboxEntry" + }, + "type": [ + "array", + "null" + ] + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, + "read": { + "description": "This will be removed in favor of `entries`.", + "items": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": [ + "array", + "null" + ] + }, + "write": { + "description": "This will be removed in favor of `entries`.", + "items": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": [ + "array", + "null" + ] + } + }, + "type": "object" + }, + "AdditionalNetworkPermissions": { + "properties": { + "enabled": { + "type": [ + "boolean", + "null" + ] + } + }, + "type": "object" + }, + "AdditionalPermissionProfile": { + "properties": { + "fileSystem": { + "anyOf": [ + { + "$ref": "#/definitions/AdditionalFileSystemPermissions" + }, + { + "type": "null" + } + ] + }, + "network": { + "anyOf": [ + { + "$ref": "#/definitions/AdditionalNetworkPermissions" + }, + { + "type": "null" + } + ], + "description": "Partial overlay used for per-command permission requests." + } + }, + "type": "object" + }, + "ApplyPatchApprovalParams": { + "properties": { + "callId": { + "description": "Use to correlate this with [codex_protocol::protocol::PatchApplyBeginEvent] and [codex_protocol::protocol::PatchApplyEndEvent].", + "type": "string" + }, + "conversationId": { + "$ref": "#/definitions/ThreadId" + }, + "fileChanges": { + "additionalProperties": { + "$ref": "#/definitions/FileChange" + }, + "type": "object" + }, + "grantRoot": { + "description": "When set, the agent is asking the user to allow writes under this root for the remainder of the session (unclear if this is honored today).", + "type": [ + "string", + "null" + ] + }, + "reason": { + "description": "Optional explanatory reason (e.g. request for extra write access).", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "callId", + "conversationId", + "fileChanges" + ], + "type": "object" + }, + "AttestationGenerateParams": { + "type": "object" + }, + "ChatgptAuthTokensRefreshParams": { + "properties": { + "previousAccountId": { + "description": "Workspace/account identifier that Codex was previously using.\n\nClients that manage multiple accounts/workspaces can use this as a hint to refresh the token for the correct workspace.\n\nThis may be `null` when the prior auth state did not include a workspace identifier (`chatgpt_account_id`).", + "type": [ + "string", + "null" + ] + }, + "reason": { + "$ref": "#/definitions/ChatgptAuthTokensRefreshReason" + } + }, + "required": [ + "reason" + ], + "type": "object" + }, + "ChatgptAuthTokensRefreshReason": { + "oneOf": [ + { + "description": "Codex attempted a backend request and received `401 Unauthorized`.", + "enum": [ + "unauthorized" + ], + "type": "string" + } + ] + }, + "CommandAction": { + "oneOf": [ + { + "properties": { + "command": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": { + "enum": [ + "read" + ], + "title": "ReadCommandActionType", + "type": "string" + } + }, + "required": [ + "command", + "name", + "path", + "type" + ], + "title": "ReadCommandAction", + "type": "object" + }, + { + "properties": { + "command": { + "type": "string" + }, + "path": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "listFiles" + ], + "title": "ListFilesCommandActionType", + "type": "string" + } + }, + "required": [ + "command", + "type" + ], + "title": "ListFilesCommandAction", + "type": "object" + }, + { + "properties": { + "command": { + "type": "string" + }, + "path": { + "type": [ + "string", + "null" + ] + }, + "query": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "search" + ], + "title": "SearchCommandActionType", + "type": "string" + } + }, + "required": [ + "command", + "type" + ], + "title": "SearchCommandAction", + "type": "object" + }, + { + "properties": { + "command": { + "type": "string" + }, + "type": { + "enum": [ + "unknown" + ], + "title": "UnknownCommandActionType", + "type": "string" + } + }, + "required": [ + "command", + "type" + ], + "title": "UnknownCommandAction", + "type": "object" + } + ] + }, + "CommandExecutionApprovalDecision": { + "oneOf": [ + { + "description": "User approved the command.", + "enum": [ + "accept" + ], + "type": "string" + }, + { + "description": "User approved the command and future prompts in the same session-scoped approval cache should run without prompting.", + "enum": [ + "acceptForSession" + ], + "type": "string" + }, + { + "additionalProperties": false, + "description": "User approved the command, and wants to apply the proposed execpolicy amendment so future matching commands can run without prompting.", + "properties": { + "acceptWithExecpolicyAmendment": { + "properties": { + "execpolicy_amendment": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "execpolicy_amendment" + ], + "type": "object" + } + }, + "required": [ + "acceptWithExecpolicyAmendment" + ], + "title": "AcceptWithExecpolicyAmendmentCommandExecutionApprovalDecision", + "type": "object" + }, + { + "additionalProperties": false, + "description": "User chose a persistent network policy rule (allow/deny) for this host.", + "properties": { + "applyNetworkPolicyAmendment": { + "properties": { + "network_policy_amendment": { + "$ref": "#/definitions/NetworkPolicyAmendment" + } + }, + "required": [ + "network_policy_amendment" + ], + "type": "object" + } + }, + "required": [ + "applyNetworkPolicyAmendment" + ], + "title": "ApplyNetworkPolicyAmendmentCommandExecutionApprovalDecision", + "type": "object" + }, + { + "description": "User denied the command. The agent will continue the turn.", + "enum": [ + "decline" + ], + "type": "string" + }, + { + "description": "User denied the command. The turn will also be immediately interrupted.", + "enum": [ + "cancel" + ], + "type": "string" + } + ] + }, + "CommandExecutionApprovalKind": { + "description": "Distinguishes a command approval from input sent to an existing terminal.", + "enum": [ + "command", + "writeStdin" + ], + "type": "string" + }, + "CommandExecutionRequestApprovalParams": { + "properties": { + "approvalId": { + "description": "Unique identifier for this specific approval callback.\n\nFor regular shell/unified_exec approvals, this is null.\n\nFor zsh-exec-bridge subcommand approvals, multiple callbacks can belong to one parent `itemId`, so `approvalId` is a distinct opaque callback id (a UUID) used to disambiguate routing. Stdin approvals also use a distinct callback id; inspect `kind` to distinguish them.", + "type": [ + "string", + "null" + ] + }, + "command": { + "description": "The command to be executed.", + "type": [ + "string", + "null" + ] + }, + "commandActions": { + "description": "Best-effort parsed command actions for friendly display.", + "items": { + "$ref": "#/definitions/CommandAction" + }, + "type": [ + "array", + "null" + ] + }, + "cwd": { + "anyOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + }, + { + "type": "null" + } + ], + "description": "The command's working directory." + }, + "environmentId": { + "default": null, + "description": "Environment in which the command will run.", + "type": [ + "string", + "null" + ] + }, + "itemId": { + "type": "string" + }, + "kind": { + "allOf": [ + { + "$ref": "#/definitions/CommandExecutionApprovalKind" + } + ], + "default": "command", + "description": "Kind of action under review. Defaults to `command` for older servers." + }, + "networkApprovalContext": { + "anyOf": [ + { + "$ref": "#/definitions/NetworkApprovalContext" + }, + { + "type": "null" + } + ], + "description": "Optional context for a managed-network approval prompt." + }, + "proposedExecpolicyAmendment": { + "description": "Optional proposed execpolicy amendment to allow similar commands without prompting.", + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "proposedNetworkPolicyAmendments": { + "description": "Optional proposed network policy amendments (allow/deny host) for future requests.", + "items": { + "$ref": "#/definitions/NetworkPolicyAmendment" + }, + "type": [ + "array", + "null" + ] + }, + "reason": { + "description": "Optional explanatory reason (e.g. request for network access).", + "type": [ + "string", + "null" + ] + }, + "startedAtMs": { + "description": "Unix timestamp (in milliseconds) when this approval request started.", + "format": "int64", + "type": "integer" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "itemId", + "startedAtMs", + "threadId", + "turnId" + ], + "type": "object" + }, + "DynamicToolCallParams": { + "properties": { + "arguments": true, + "callId": { + "type": "string" + }, + "namespace": { + "type": [ + "string", + "null" + ] + }, + "threadId": { + "type": "string" + }, + "tool": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "arguments", + "callId", + "threadId", + "tool", + "turnId" + ], + "type": "object" + }, + "ExecCommandApprovalParams": { + "properties": { + "approvalId": { + "description": "Identifier for this specific approval callback.", + "type": [ + "string", + "null" + ] + }, + "callId": { + "description": "Use to correlate this with [codex_protocol::protocol::ExecCommandBeginEvent] and [codex_protocol::protocol::ExecCommandEndEvent].", + "type": "string" + }, + "command": { + "items": { + "type": "string" + }, + "type": "array" + }, + "conversationId": { + "$ref": "#/definitions/ThreadId" + }, + "cwd": { + "type": "string" + }, + "parsedCmd": { + "items": { + "$ref": "#/definitions/ParsedCommand" + }, + "type": "array" + }, + "reason": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "callId", + "command", + "conversationId", + "cwd", + "parsedCmd" + ], + "type": "object" + }, + "FileChange": { + "oneOf": [ + { + "properties": { + "content": { + "type": "string" + }, + "type": { + "enum": [ + "add" + ], + "title": "AddFileChangeType", + "type": "string" + } + }, + "required": [ + "content", + "type" + ], + "title": "AddFileChange", + "type": "object" + }, + { + "properties": { + "content": { + "type": "string" + }, + "type": { + "enum": [ + "delete" + ], + "title": "DeleteFileChangeType", + "type": "string" + } + }, + "required": [ + "content", + "type" + ], + "title": "DeleteFileChange", + "type": "object" + }, + { + "properties": { + "move_path": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "update" + ], + "title": "UpdateFileChangeType", + "type": "string" + }, + "unified_diff": { + "type": "string" + } + }, + "required": [ + "type", + "unified_diff" + ], + "title": "UpdateFileChange", + "type": "object" + } + ] + }, + "FileChangeRequestApprovalParams": { + "properties": { + "grantRoot": { + "description": "[UNSTABLE] When set, the agent is asking the user to allow writes under this root for the remainder of the session (unclear if this is honored today).", + "type": [ + "string", + "null" + ] + }, + "itemId": { + "type": "string" + }, + "reason": { + "description": "Optional explanatory reason (e.g. request for extra write access).", + "type": [ + "string", + "null" + ] + }, + "startedAtMs": { + "description": "Unix timestamp (in milliseconds) when this approval request started.", + "format": "int64", + "type": "integer" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "itemId", + "startedAtMs", + "threadId", + "turnId" + ], + "type": "object" + }, + "FileSystemAccessMode": { + "enum": [ + "read", + "write", + "deny" + ], + "type": "string" + }, + "FileSystemPath": { + "oneOf": [ + { + "properties": { + "path": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": { + "enum": [ + "path" + ], + "title": "PathFileSystemPathType", + "type": "string" + } + }, + "required": [ + "path", + "type" + ], + "title": "PathFileSystemPath", + "type": "object" + }, + { + "properties": { + "pattern": { + "type": "string" + }, + "type": { + "enum": [ + "glob_pattern" + ], + "title": "GlobPatternFileSystemPathType", + "type": "string" + } + }, + "required": [ + "pattern", + "type" + ], + "title": "GlobPatternFileSystemPath", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "special" + ], + "title": "SpecialFileSystemPathType", + "type": "string" + }, + "value": { + "$ref": "#/definitions/FileSystemSpecialPath" + } + }, + "required": [ + "type", + "value" + ], + "title": "SpecialFileSystemPath", + "type": "object" + } + ] + }, + "FileSystemSandboxEntry": { + "properties": { + "access": { + "$ref": "#/definitions/FileSystemAccessMode" + }, + "path": { + "$ref": "#/definitions/FileSystemPath" + } + }, + "required": [ + "access", + "path" + ], + "type": "object" + }, + "FileSystemSpecialPath": { + "oneOf": [ + { + "properties": { + "kind": { + "enum": [ + "root" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "RootFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "minimal" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "MinimalFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "project_roots" + ], + "type": "string" + }, + "subpath": { + "anyOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "kind" + ], + "title": "KindFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "tmpdir" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "TmpdirFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "slash_tmp" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "SlashTmpFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "unknown" + ], + "type": "string" + }, + "path": { + "type": "string" + }, + "subpath": { + "anyOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "kind", + "path" + ], + "type": "object" + } + ] + }, + "LegacyAppPathString": { + "type": "string" + }, + "McpElicitationArrayType": { + "enum": [ + "array" + ], + "type": "string" + }, + "McpElicitationBooleanSchema": { + "additionalProperties": false, + "properties": { + "default": { + "type": [ + "boolean", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "type": { + "$ref": "#/definitions/McpElicitationBooleanType" + } + }, + "required": [ + "type" + ], + "type": "object" + }, + "McpElicitationBooleanType": { + "enum": [ + "boolean" + ], + "type": "string" + }, + "McpElicitationConstOption": { + "additionalProperties": false, + "properties": { + "const": { + "type": "string" + }, + "title": { + "type": "string" + } + }, + "required": [ + "const", + "title" + ], + "type": "object" + }, + "McpElicitationEnumSchema": { + "anyOf": [ + { + "$ref": "#/definitions/McpElicitationSingleSelectEnumSchema" + }, + { + "$ref": "#/definitions/McpElicitationMultiSelectEnumSchema" + }, + { + "$ref": "#/definitions/McpElicitationLegacyTitledEnumSchema" + } + ] + }, + "McpElicitationLegacyTitledEnumSchema": { + "additionalProperties": false, + "properties": { + "default": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "enum": { + "items": { + "type": "string" + }, + "type": "array" + }, + "enumNames": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "type": { + "$ref": "#/definitions/McpElicitationStringType" + } + }, + "required": [ + "enum", + "type" + ], + "type": "object" + }, + "McpElicitationMultiSelectEnumSchema": { + "anyOf": [ + { + "$ref": "#/definitions/McpElicitationUntitledMultiSelectEnumSchema" + }, + { + "$ref": "#/definitions/McpElicitationTitledMultiSelectEnumSchema" + } + ] + }, + "McpElicitationNumberSchema": { + "additionalProperties": false, + "properties": { + "default": { + "format": "double", + "type": [ + "number", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "maximum": { + "format": "double", + "type": [ + "number", + "null" + ] + }, + "minimum": { + "format": "double", + "type": [ + "number", + "null" + ] + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "type": { + "$ref": "#/definitions/McpElicitationNumberType" + } + }, + "required": [ + "type" + ], + "type": "object" + }, + "McpElicitationNumberType": { + "enum": [ + "number", + "integer" + ], + "type": "string" + }, + "McpElicitationObjectType": { + "enum": [ + "object" + ], + "type": "string" + }, + "McpElicitationPrimitiveSchema": { + "anyOf": [ + { + "$ref": "#/definitions/McpElicitationEnumSchema" + }, + { + "$ref": "#/definitions/McpElicitationStringSchema" + }, + { + "$ref": "#/definitions/McpElicitationNumberSchema" + }, + { + "$ref": "#/definitions/McpElicitationBooleanSchema" + } + ] + }, + "McpElicitationSchema": { + "additionalProperties": false, + "description": "Typed form schema for MCP `elicitation/create` requests.\n\nThis matches the `requestedSchema` shape from the MCP 2025-11-25 `ElicitRequestFormParams` schema.", + "properties": { + "$schema": { + "type": [ + "string", + "null" + ] + }, + "properties": { + "additionalProperties": { + "$ref": "#/definitions/McpElicitationPrimitiveSchema" + }, + "type": "object" + }, + "required": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "type": { + "$ref": "#/definitions/McpElicitationObjectType" + } + }, + "required": [ + "properties", + "type" + ], + "type": "object" + }, + "McpElicitationSingleSelectEnumSchema": { + "anyOf": [ + { + "$ref": "#/definitions/McpElicitationUntitledSingleSelectEnumSchema" + }, + { + "$ref": "#/definitions/McpElicitationTitledSingleSelectEnumSchema" + } + ] + }, + "McpElicitationStringFormat": { + "enum": [ + "email", + "uri", + "date", + "date-time" + ], + "type": "string" + }, + "McpElicitationStringSchema": { + "additionalProperties": false, + "properties": { + "default": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "format": { + "anyOf": [ + { + "$ref": "#/definitions/McpElicitationStringFormat" + }, + { + "type": "null" + } + ] + }, + "maxLength": { + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "minLength": { + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "type": { + "$ref": "#/definitions/McpElicitationStringType" + } + }, + "required": [ + "type" + ], + "type": "object" + }, + "McpElicitationStringType": { + "enum": [ + "string" + ], + "type": "string" + }, + "McpElicitationTitledEnumItems": { + "additionalProperties": false, + "properties": { + "anyOf": { + "items": { + "$ref": "#/definitions/McpElicitationConstOption" + }, + "type": "array" + } + }, + "required": [ + "anyOf" + ], + "type": "object" + }, + "McpElicitationTitledMultiSelectEnumSchema": { + "additionalProperties": false, + "properties": { + "default": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "items": { + "$ref": "#/definitions/McpElicitationTitledEnumItems" + }, + "maxItems": { + "format": "uint64", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "minItems": { + "format": "uint64", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "type": { + "$ref": "#/definitions/McpElicitationArrayType" + } + }, + "required": [ + "items", + "type" + ], + "type": "object" + }, + "McpElicitationTitledSingleSelectEnumSchema": { + "additionalProperties": false, + "properties": { + "default": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "oneOf": { + "items": { + "$ref": "#/definitions/McpElicitationConstOption" + }, + "type": "array" + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "type": { + "$ref": "#/definitions/McpElicitationStringType" + } + }, + "required": [ + "oneOf", + "type" + ], + "type": "object" + }, + "McpElicitationUntitledEnumItems": { + "additionalProperties": false, + "properties": { + "enum": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": { + "$ref": "#/definitions/McpElicitationStringType" + } + }, + "required": [ + "enum", + "type" + ], + "type": "object" + }, + "McpElicitationUntitledMultiSelectEnumSchema": { + "additionalProperties": false, + "properties": { + "default": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "items": { + "$ref": "#/definitions/McpElicitationUntitledEnumItems" + }, + "maxItems": { + "format": "uint64", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "minItems": { + "format": "uint64", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "type": { + "$ref": "#/definitions/McpElicitationArrayType" + } + }, + "required": [ + "items", + "type" + ], + "type": "object" + }, + "McpElicitationUntitledSingleSelectEnumSchema": { + "additionalProperties": false, + "properties": { + "default": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "enum": { + "items": { + "type": "string" + }, + "type": "array" + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "type": { + "$ref": "#/definitions/McpElicitationStringType" + } + }, + "required": [ + "enum", + "type" + ], + "type": "object" + }, + "McpServerElicitationRequestParams": { + "oneOf": [ + { + "properties": { + "_meta": true, + "message": { + "type": "string" + }, + "mode": { + "enum": [ + "form" + ], + "type": "string" + }, + "requestedSchema": { + "$ref": "#/definitions/McpElicitationSchema" + } + }, + "required": [ + "message", + "mode", + "requestedSchema" + ], + "type": "object" + }, + { + "properties": { + "_meta": true, + "message": { + "type": "string" + }, + "mode": { + "enum": [ + "openai/form" + ], + "type": "string" + }, + "requestedSchema": true + }, + "required": [ + "message", + "mode", + "requestedSchema" + ], + "type": "object" + }, + { + "properties": { + "_meta": true, + "elicitationId": { + "type": "string" + }, + "message": { + "type": "string" + }, + "mode": { + "enum": [ + "url" + ], + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "elicitationId", + "message", + "mode", + "url" + ], + "type": "object" + } + ], + "properties": { + "serverName": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "description": "Active Codex turn when this elicitation was observed, if app-server could correlate one.\n\nThis is nullable because MCP models elicitation as a standalone server-to-client request identified by the MCP server request id. It may be triggered during a turn, but turn context is app-server correlation rather than part of the protocol identity of the elicitation itself.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "serverName", + "threadId" + ], + "type": "object" + }, + "NetworkApprovalContext": { + "properties": { + "host": { + "type": "string" + }, + "protocol": { + "$ref": "#/definitions/NetworkApprovalProtocol" + } + }, + "required": [ + "host", + "protocol" + ], + "type": "object" + }, + "NetworkApprovalProtocol": { + "enum": [ + "http", + "https", + "socks5Tcp", + "socks5Udp" + ], + "type": "string" + }, + "NetworkPolicyAmendment": { + "properties": { + "action": { + "$ref": "#/definitions/NetworkPolicyRuleAction" + }, + "host": { + "type": "string" + } + }, + "required": [ + "action", + "host" + ], + "type": "object" + }, + "NetworkPolicyRuleAction": { + "enum": [ + "allow", + "deny" + ], + "type": "string" + }, + "ParsedCommand": { + "oneOf": [ + { + "properties": { + "cmd": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "description": "(Best effort) Path to the file being read by the command. When possible, this is an absolute path, though when relative, it should be resolved against the `cwd`` that will be used to run the command to derive the absolute path.", + "type": "string" + }, + "type": { + "enum": [ + "read" + ], + "title": "ReadParsedCommandType", + "type": "string" + } + }, + "required": [ + "cmd", + "name", + "path", + "type" + ], + "title": "ReadParsedCommand", + "type": "object" + }, + { + "properties": { + "cmd": { + "type": "string" + }, + "path": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "list_files" + ], + "title": "ListFilesParsedCommandType", + "type": "string" + } + }, + "required": [ + "cmd", + "type" + ], + "title": "ListFilesParsedCommand", + "type": "object" + }, + { + "properties": { + "cmd": { + "type": "string" + }, + "path": { + "type": [ + "string", + "null" + ] + }, + "query": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "search" + ], + "title": "SearchParsedCommandType", + "type": "string" + } + }, + "required": [ + "cmd", + "type" + ], + "title": "SearchParsedCommand", + "type": "object" + }, + { + "properties": { + "cmd": { + "type": "string" + }, + "type": { + "enum": [ + "unknown" + ], + "title": "UnknownParsedCommandType", + "type": "string" + } + }, + "required": [ + "cmd", + "type" + ], + "title": "UnknownParsedCommand", + "type": "object" + } + ] + }, + "PermissionsRequestApprovalParams": { + "properties": { + "cwd": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "environmentId": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "itemId": { + "type": "string" + }, + "permissions": { + "$ref": "#/definitions/RequestPermissionProfile" + }, + "reason": { + "type": [ + "string", + "null" + ] + }, + "startedAtMs": { + "description": "Unix timestamp (in milliseconds) when this approval request started.", + "format": "int64", + "type": "integer" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "cwd", + "itemId", + "permissions", + "startedAtMs", + "threadId", + "turnId" + ], + "type": "object" + }, + "RequestId": { + "anyOf": [ + { + "type": "string" + }, + { + "format": "int64", + "type": "integer" + } + ] + }, + "RequestPermissionProfile": { + "additionalProperties": false, + "properties": { + "fileSystem": { + "anyOf": [ + { + "$ref": "#/definitions/AdditionalFileSystemPermissions" + }, + { + "type": "null" + } + ] + }, + "network": { + "anyOf": [ + { + "$ref": "#/definitions/AdditionalNetworkPermissions" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "ThreadId": { + "type": "string" + }, + "ToolRequestUserInputOption": { + "description": "EXPERIMENTAL. Defines a single selectable option for request_user_input.", + "properties": { + "description": { + "type": "string" + }, + "label": { + "type": "string" + } + }, + "required": [ + "description", + "label" + ], + "type": "object" + }, + "ToolRequestUserInputParams": { + "description": "EXPERIMENTAL. Params sent with a request_user_input event.", + "properties": { + "autoResolutionMs": { + "default": null, + "description": "@deprecated Use `isBlocking` to decide whether the request should block.", + "format": "uint64", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "isBlocking": { + "type": "boolean" + }, + "itemId": { + "type": "string" + }, + "questions": { + "items": { + "$ref": "#/definitions/ToolRequestUserInputQuestion" + }, + "type": "array" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "isBlocking", + "itemId", + "questions", + "threadId", + "turnId" + ], + "type": "object" + }, + "ToolRequestUserInputQuestion": { + "description": "EXPERIMENTAL. Represents one request_user_input question and its required options.", + "properties": { + "header": { + "type": "string" + }, + "id": { + "type": "string" + }, + "isOther": { + "default": false, + "type": "boolean" + }, + "isSecret": { + "default": false, + "type": "boolean" + }, + "options": { + "items": { + "$ref": "#/definitions/ToolRequestUserInputOption" + }, + "type": [ + "array", + "null" + ] + }, + "question": { + "type": "string" + } + }, + "required": [ + "header", + "id", + "question" + ], + "type": "object" + } + }, + "description": "Request initiated from the server and sent to the client.", + "oneOf": [ + { + "description": "NEW APIs Sent when approval is requested for a specific command execution. This request is used for Turns started via turn/start.", + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "item/commandExecution/requestApproval" + ], + "title": "Item/commandExecution/requestApprovalRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/CommandExecutionRequestApprovalParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Item/commandExecution/requestApprovalRequest", + "type": "object" + }, + { + "description": "Sent when approval is requested for a specific file change. This request is used for Turns started via turn/start.", + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "item/fileChange/requestApproval" + ], + "title": "Item/fileChange/requestApprovalRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FileChangeRequestApprovalParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Item/fileChange/requestApprovalRequest", + "type": "object" + }, + { + "description": "EXPERIMENTAL - Request input from the user for a tool call.", + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "item/tool/requestUserInput" + ], + "title": "Item/tool/requestUserInputRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ToolRequestUserInputParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Item/tool/requestUserInputRequest", + "type": "object" + }, + { + "description": "Request input for an MCP server elicitation.", + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "mcpServer/elicitation/request" + ], + "title": "McpServer/elicitation/requestRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/McpServerElicitationRequestParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "McpServer/elicitation/requestRequest", + "type": "object" + }, + { + "description": "Request approval for additional permissions from the user.", + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "item/permissions/requestApproval" + ], + "title": "Item/permissions/requestApprovalRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/PermissionsRequestApprovalParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Item/permissions/requestApprovalRequest", + "type": "object" + }, + { + "description": "Execute a dynamic tool call on the client.", + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "item/tool/call" + ], + "title": "Item/tool/callRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/DynamicToolCallParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Item/tool/callRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "account/chatgptAuthTokens/refresh" + ], + "title": "Account/chatgptAuthTokens/refreshRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ChatgptAuthTokensRefreshParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Account/chatgptAuthTokens/refreshRequest", + "type": "object" + }, + { + "description": "Generate a fresh upstream attestation result on demand.", + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "attestation/generate" + ], + "title": "Attestation/generateRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/AttestationGenerateParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Attestation/generateRequest", + "type": "object" + }, + { + "description": "DEPRECATED APIs below Request to approve a patch. This request is used for Turns started via the legacy APIs (i.e. SendUserTurn, SendUserMessage).", + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "applyPatchApproval" + ], + "title": "ApplyPatchApprovalRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ApplyPatchApprovalParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "ApplyPatchApprovalRequest", + "type": "object" + }, + { + "description": "Request to exec a command. This request is used for Turns started via the legacy APIs (i.e. SendUserTurn, SendUserMessage).", + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "execCommandApproval" + ], + "title": "ExecCommandApprovalRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ExecCommandApprovalParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "ExecCommandApprovalRequest", + "type": "object" + } + ], + "title": "ServerRequest" +} \ No newline at end of file diff --git a/internal/agent/runtime/codex/protocolgen/schema/ToolRequestUserInputResponse.json b/internal/agent/runtime/codex/protocolgen/schema/ToolRequestUserInputResponse.json new file mode 100644 index 0000000000..3fd6fbc335 --- /dev/null +++ b/internal/agent/runtime/codex/protocolgen/schema/ToolRequestUserInputResponse.json @@ -0,0 +1,34 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "ToolRequestUserInputAnswer": { + "description": "EXPERIMENTAL. Captures a user's answer to a request_user_input question.", + "properties": { + "answers": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "answers" + ], + "type": "object" + } + }, + "description": "EXPERIMENTAL. Response payload mapping question ids to answers.", + "properties": { + "answers": { + "additionalProperties": { + "$ref": "#/definitions/ToolRequestUserInputAnswer" + }, + "type": "object" + } + }, + "required": [ + "answers" + ], + "title": "ToolRequestUserInputResponse", + "type": "object" +} \ No newline at end of file diff --git a/internal/agent/runtime/codex/protocolgen/schema/VERSION.json b/internal/agent/runtime/codex/protocolgen/schema/VERSION.json new file mode 100644 index 0000000000..3b76d6b971 --- /dev/null +++ b/internal/agent/runtime/codex/protocolgen/schema/VERSION.json @@ -0,0 +1,16 @@ +{ + "codexVersion": "0.151.0", + "command": "codex app-server generate-json-schema --out ", + "files": [ + "codex_app_server_protocol.v2.schemas.json", + "ServerRequest.json", + "ClientNotification.json", + "CommandExecutionRequestApprovalResponse.json", + "FileChangeRequestApprovalResponse.json", + "PermissionsRequestApprovalResponse.json", + "ToolRequestUserInputResponse.json", + "McpServerElicitationRequestResponse.json", + "ChatgptAuthTokensRefreshResponse.json" + ], + "notes": "v2-only snapshot. Regenerate with the pinned codex CLI via `mise run codex-schema-sync`; any diff means the pinned binary and this snapshot disagree and the generated Go under internal/agent/runtime/codex/protocol must be re-reviewed." +} diff --git a/internal/agent/runtime/codex/protocolgen/schema/codex_app_server_protocol.v2.schemas.json b/internal/agent/runtime/codex/protocolgen/schema/codex_app_server_protocol.v2.schemas.json new file mode 100644 index 0000000000..e0830128d8 --- /dev/null +++ b/internal/agent/runtime/codex/protocolgen/schema/codex_app_server_protocol.v2.schemas.json @@ -0,0 +1,22847 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "AbsolutePathBuf": { + "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", + "type": "string" + }, + "Account": { + "oneOf": [ + { + "properties": { + "type": { + "enum": [ + "apiKey" + ], + "title": "ApiKeyAccountType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "ApiKeyAccount", + "type": "object" + }, + { + "properties": { + "email": { + "type": [ + "string", + "null" + ] + }, + "planType": { + "$ref": "#/definitions/PlanType" + }, + "type": { + "enum": [ + "chatgpt" + ], + "title": "ChatgptAccountType", + "type": "string" + } + }, + "required": [ + "email", + "planType", + "type" + ], + "title": "ChatgptAccount", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "amazonBedrock" + ], + "title": "AmazonBedrockAccountType", + "type": "string" + }, + "usesCodexManagedCredentials": { + "default": false, + "type": "boolean" + } + }, + "required": [ + "type" + ], + "title": "AmazonBedrockAccount", + "type": "object" + } + ] + }, + "AccountLoginCompletedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "error": { + "type": [ + "string", + "null" + ] + }, + "loginId": { + "type": [ + "string", + "null" + ] + }, + "onboardingEntrypoint": { + "anyOf": [ + { + "$ref": "#/definitions/DesktopOnboardingEntrypoint" + }, + { + "type": "null" + } + ] + }, + "success": { + "type": "boolean" + } + }, + "required": [ + "success" + ], + "title": "AccountLoginCompletedNotification", + "type": "object" + }, + "AccountRateLimitsUpdatedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Sparse rolling rate-limit update.\n\nClients should merge available values into the most recent `account/rateLimits/read` response or refetch that snapshot. Nullable account metadata may be unavailable in a rolling update and does not clear a previously observed value.", + "properties": { + "rateLimits": { + "$ref": "#/definitions/RateLimitSnapshot" + } + }, + "required": [ + "rateLimits" + ], + "title": "AccountRateLimitsUpdatedNotification", + "type": "object" + }, + "AccountTokenUsageDailyBucket": { + "properties": { + "startDate": { + "type": "string" + }, + "tokens": { + "format": "int64", + "type": "integer" + } + }, + "required": [ + "startDate", + "tokens" + ], + "type": "object" + }, + "AccountTokenUsageSummary": { + "properties": { + "currentStreakDays": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "lifetimeTokens": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "longestRunningTurnSec": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "longestStreakDays": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "peakDailyTokens": { + "format": "int64", + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" + }, + "AccountUpdatedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "authMode": { + "anyOf": [ + { + "$ref": "#/definitions/AuthMode" + }, + { + "type": "null" + } + ] + }, + "planType": { + "anyOf": [ + { + "$ref": "#/definitions/PlanType" + }, + { + "type": "null" + } + ] + } + }, + "title": "AccountUpdatedNotification", + "type": "object" + }, + "ActivePermissionProfile": { + "properties": { + "extends": { + "default": null, + "description": "Parent profile identifier from the selected permissions profile's `extends` setting, when present.", + "type": [ + "string", + "null" + ] + }, + "id": { + "description": "Identifier from `default_permissions` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.]` profile.", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "AddCreditsNudgeCreditType": { + "enum": [ + "credits", + "usage_limit" + ], + "type": "string" + }, + "AddCreditsNudgeEmailStatus": { + "enum": [ + "sent", + "cooldown_active" + ], + "type": "string" + }, + "AdditionalContextEntry": { + "properties": { + "kind": { + "$ref": "#/definitions/AdditionalContextKind" + }, + "value": { + "type": "string" + } + }, + "required": [ + "kind", + "value" + ], + "type": "object" + }, + "AdditionalContextKind": { + "enum": [ + "untrusted", + "application" + ], + "type": "string" + }, + "AdditionalFileSystemPermissions": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/FileSystemSandboxEntry" + }, + "type": [ + "array", + "null" + ] + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, + "read": { + "description": "This will be removed in favor of `entries`.", + "items": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": [ + "array", + "null" + ] + }, + "write": { + "description": "This will be removed in favor of `entries`.", + "items": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": [ + "array", + "null" + ] + } + }, + "type": "object" + }, + "AdditionalNetworkPermissions": { + "properties": { + "enabled": { + "type": [ + "boolean", + "null" + ] + } + }, + "type": "object" + }, + "AgentMessageDelivery": { + "enum": [ + "async" + ], + "type": "string" + }, + "AgentMessageDeltaNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "delta": { + "type": "string" + }, + "itemId": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "delta", + "itemId", + "threadId", + "turnId" + ], + "title": "AgentMessageDeltaNotification", + "type": "object" + }, + "AgentMessageInputContent": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "input_text" + ], + "title": "InputTextAgentMessageInputContentType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextAgentMessageInputContent", + "type": "object" + }, + { + "properties": { + "encrypted_content": { + "type": "string" + }, + "type": { + "enum": [ + "encrypted_content" + ], + "title": "EncryptedContentAgentMessageInputContentType", + "type": "string" + } + }, + "required": [ + "encrypted_content", + "type" + ], + "title": "EncryptedContentAgentMessageInputContent", + "type": "object" + } + ] + }, + "AgentPath": { + "type": "string" + }, + "AllowDenyRequirement": { + "enum": [ + "allow", + "deny" + ], + "type": "string" + }, + "AnalyticsConfig": { + "additionalProperties": true, + "properties": { + "enabled": { + "type": [ + "boolean", + "null" + ] + } + }, + "type": "object" + }, + "AppBranding": { + "description": "EXPERIMENTAL - app metadata returned by app-list APIs.", + "properties": { + "category": { + "type": [ + "string", + "null" + ] + }, + "developer": { + "type": [ + "string", + "null" + ] + }, + "isDiscoverableApp": { + "type": "boolean" + }, + "privacyPolicy": { + "type": [ + "string", + "null" + ] + }, + "termsOfService": { + "type": [ + "string", + "null" + ] + }, + "website": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "isDiscoverableApp" + ], + "type": "object" + }, + "AppConfig": { + "properties": { + "approvals_reviewer": { + "anyOf": [ + { + "$ref": "#/definitions/ApprovalsReviewer" + }, + { + "type": "null" + } + ] + }, + "default_tools_approval_mode": { + "anyOf": [ + { + "$ref": "#/definitions/AppToolApproval" + }, + { + "type": "null" + } + ] + }, + "default_tools_enabled": { + "type": [ + "boolean", + "null" + ] + }, + "destructive_enabled": { + "type": [ + "boolean", + "null" + ] + }, + "enabled": { + "default": true, + "type": "boolean" + }, + "open_world_enabled": { + "type": [ + "boolean", + "null" + ] + }, + "tools": { + "anyOf": [ + { + "$ref": "#/definitions/AppToolsConfig" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "AppInfo": { + "description": "EXPERIMENTAL - app metadata returned by app-list APIs.", + "properties": { + "appMetadata": { + "anyOf": [ + { + "$ref": "#/definitions/AppMetadata" + }, + { + "type": "null" + } + ] + }, + "branding": { + "anyOf": [ + { + "$ref": "#/definitions/AppBranding" + }, + { + "type": "null" + } + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "distributionChannel": { + "type": [ + "string", + "null" + ] + }, + "iconAssets": { + "additionalProperties": { + "type": "string" + }, + "type": [ + "object", + "null" + ] + }, + "iconDarkAssets": { + "additionalProperties": { + "type": "string" + }, + "type": [ + "object", + "null" + ] + }, + "id": { + "type": "string" + }, + "installUrl": { + "type": [ + "string", + "null" + ] + }, + "isAccessible": { + "default": false, + "type": "boolean" + }, + "isEnabled": { + "default": true, + "description": "Whether this app is enabled in config.toml. Example: ```toml [apps.bad_app] enabled = false ```", + "type": "boolean" + }, + "labels": { + "additionalProperties": { + "type": "string" + }, + "type": [ + "object", + "null" + ] + }, + "logoUrl": { + "type": [ + "string", + "null" + ] + }, + "logoUrlDark": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": "string" + }, + "pluginDisplayNames": { + "default": [], + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "id", + "name" + ], + "type": "object" + }, + "AppListUpdatedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "EXPERIMENTAL - notification emitted when the app list changes.", + "properties": { + "data": { + "items": { + "$ref": "#/definitions/AppInfo" + }, + "type": "array" + } + }, + "required": [ + "data" + ], + "title": "AppListUpdatedNotification", + "type": "object" + }, + "AppMetadata": { + "properties": { + "categories": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "developer": { + "type": [ + "string", + "null" + ] + }, + "firstPartyRequiresInstall": { + "type": [ + "boolean", + "null" + ] + }, + "review": { + "anyOf": [ + { + "$ref": "#/definitions/AppReview" + }, + { + "type": "null" + } + ] + }, + "screenshots": { + "items": { + "$ref": "#/definitions/AppScreenshot" + }, + "type": [ + "array", + "null" + ] + }, + "seoDescription": { + "type": [ + "string", + "null" + ] + }, + "showInComposerWhenUnlinked": { + "type": [ + "boolean", + "null" + ] + }, + "subCategories": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "version": { + "type": [ + "string", + "null" + ] + }, + "versionId": { + "type": [ + "string", + "null" + ] + }, + "versionNotes": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "AppReview": { + "properties": { + "status": { + "type": "string" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "AppScreenshot": { + "properties": { + "fileId": { + "type": [ + "string", + "null" + ] + }, + "url": { + "type": [ + "string", + "null" + ] + }, + "userPrompt": { + "type": "string" + } + }, + "required": [ + "userPrompt" + ], + "type": "object" + }, + "AppSummary": { + "description": "EXPERIMENTAL - app metadata summary for plugin responses.", + "properties": { + "category": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "id": { + "type": "string" + }, + "installUrl": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ], + "type": "object" + }, + "AppTemplateSummary": { + "properties": { + "canonicalConnectorId": { + "type": [ + "string", + "null" + ] + }, + "category": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "logoUrl": { + "type": [ + "string", + "null" + ] + }, + "logoUrlDark": { + "type": [ + "string", + "null" + ] + }, + "materializedAppIds": { + "items": { + "type": "string" + }, + "type": "array" + }, + "name": { + "type": "string" + }, + "reason": { + "anyOf": [ + { + "$ref": "#/definitions/AppTemplateUnavailableReason" + }, + { + "type": "null" + } + ] + }, + "templateId": { + "type": "string" + } + }, + "required": [ + "materializedAppIds", + "name", + "templateId" + ], + "type": "object" + }, + "AppTemplateUnavailableReason": { + "enum": [ + "NOT_CONFIGURED_FOR_WORKSPACE", + "NO_ACTIVE_WORKSPACE" + ], + "type": "string" + }, + "AppToolApproval": { + "enum": [ + "auto", + "prompt", + "writes", + "approve" + ], + "type": "string" + }, + "AppToolConfig": { + "properties": { + "approval_mode": { + "anyOf": [ + { + "$ref": "#/definitions/AppToolApproval" + }, + { + "type": "null" + } + ] + }, + "enabled": { + "type": [ + "boolean", + "null" + ] + } + }, + "type": "object" + }, + "AppToolSummary": { + "description": "EXPERIMENTAL - metadata returned by app/read.", + "properties": { + "description": { + "type": "string" + }, + "disabledReason": { + "type": [ + "string", + "null" + ] + }, + "isEnabled": { + "default": true, + "type": "boolean" + }, + "isReadOnly": { + "default": false, + "type": "boolean" + }, + "name": { + "type": "string" + }, + "title": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "description", + "name" + ], + "type": "object" + }, + "AppToolsConfig": { + "type": "object" + }, + "ApprovalsReviewer": { + "description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.", + "enum": [ + "user", + "auto_review", + "guardian_subagent" + ], + "type": "string" + }, + "AppsConfig": { + "properties": { + "_default": { + "anyOf": [ + { + "$ref": "#/definitions/AppsDefaultConfig" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "type": "object" + }, + "AppsDefaultConfig": { + "properties": { + "approvals_reviewer": { + "anyOf": [ + { + "$ref": "#/definitions/ApprovalsReviewer" + }, + { + "type": "null" + } + ] + }, + "default_tools_approval_mode": { + "anyOf": [ + { + "$ref": "#/definitions/AppToolApproval" + }, + { + "type": "null" + } + ] + }, + "destructive_enabled": { + "default": true, + "type": "boolean" + }, + "enabled": { + "default": true, + "type": "boolean" + }, + "open_world_enabled": { + "default": true, + "type": "boolean" + } + }, + "type": "object" + }, + "AppsInstalledParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Read the committed installed connector runtime snapshot.", + "properties": { + "forceRefresh": { + "description": "When true and Apps are permitted, refresh and publish the hosted connector runtime tool snapshot first.", + "type": "boolean" + }, + "threadId": { + "description": "Optional loaded thread id used to evaluate effective app configuration.", + "type": [ + "string", + "null" + ] + } + }, + "title": "AppsInstalledParams", + "type": "object" + }, + "AppsInstalledResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "The installed connectors in one committed runtime snapshot.", + "properties": { + "apps": { + "items": { + "$ref": "#/definitions/InstalledApp" + }, + "type": "array" + } + }, + "required": [ + "apps" + ], + "title": "AppsInstalledResponse", + "type": "object" + }, + "AppsListParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "EXPERIMENTAL - list available apps/connectors.", + "properties": { + "cursor": { + "description": "Opaque pagination cursor returned by a previous call.", + "type": [ + "string", + "null" + ] + }, + "forceRefetch": { + "description": "When true, bypass app caches and fetch the latest data from sources.", + "type": "boolean" + }, + "limit": { + "description": "Optional page size; defaults to a reasonable server-side value.", + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "threadId": { + "description": "Optional thread id used to evaluate app feature gating from that thread's config.", + "type": [ + "string", + "null" + ] + } + }, + "title": "AppsListParams", + "type": "object" + }, + "AppsListResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "EXPERIMENTAL - app list response.", + "properties": { + "data": { + "items": { + "$ref": "#/definitions/AppInfo" + }, + "type": "array" + }, + "nextCursor": { + "description": "Opaque cursor to pass to the next call to continue after the last item. If None, there are no more items to return.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "data" + ], + "title": "AppsListResponse", + "type": "object" + }, + "AppsReadParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "EXPERIMENTAL - read metadata for specific apps/connectors.", + "properties": { + "appIds": { + "description": "App ids to read. The server accepts at most 100 ids and deduplicates repeated ids while preserving their first-request order.", + "items": { + "type": "string" + }, + "type": "array" + }, + "includeTools": { + "description": "When true, include display-only public tool summaries in the returned metadata.", + "type": "boolean" + }, + "threadId": { + "description": "Optional loaded thread id used to evaluate effective app configuration.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "appIds" + ], + "title": "AppsReadParams", + "type": "object" + }, + "AppsReadResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "EXPERIMENTAL - app/read response.", + "properties": { + "apps": { + "items": { + "$ref": "#/definitions/ConnectorMetadata" + }, + "type": "array" + }, + "missingAppIds": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "apps", + "missingAppIds" + ], + "title": "AppsReadResponse", + "type": "object" + }, + "AskForApproval": { + "oneOf": [ + { + "enum": [ + "untrusted", + "on-request", + "never" + ], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "granular": { + "properties": { + "mcp_elicitations": { + "type": "boolean" + }, + "request_permissions": { + "default": false, + "type": "boolean" + }, + "rules": { + "type": "boolean" + }, + "sandbox_approval": { + "type": "boolean" + }, + "skill_approval": { + "default": false, + "type": "boolean" + } + }, + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "type": "object" + } + }, + "required": [ + "granular" + ], + "title": "GranularAskForApproval", + "type": "object" + } + ] + }, + "AuthMode": { + "description": "Authentication mode for OpenAI-backed providers.", + "oneOf": [ + { + "description": "OpenAI API key provided by the caller and stored by Codex.", + "enum": [ + "apikey" + ], + "type": "string" + }, + { + "description": "ChatGPT OAuth managed by Codex (tokens persisted and refreshed by Codex).", + "enum": [ + "chatgpt" + ], + "type": "string" + }, + { + "description": "[UNSTABLE] FOR OPENAI INTERNAL USE ONLY - DO NOT USE.\n\nChatGPT auth tokens are supplied by an external host app and are only stored in memory. Token refresh must be handled by the external host app.", + "enum": [ + "chatgptAuthTokens" + ], + "type": "string" + }, + { + "description": "Backend auth supplied as request headers.", + "enum": [ + "headers" + ], + "type": "string" + }, + { + "description": "Programmatic Codex auth backed by a registered Agent Identity.", + "enum": [ + "agentIdentity" + ], + "type": "string" + }, + { + "description": "Programmatic Codex auth backed by a personal access token.", + "enum": [ + "personalAccessToken" + ], + "type": "string" + }, + { + "description": "Amazon Bedrock bearer token managed by Codex.", + "enum": [ + "bedrockApiKey" + ], + "type": "string" + }, + { + "description": "Amazon Bedrock AWS access keys managed by Codex.", + "enum": [ + "bedrockAccessKeys" + ], + "type": "string" + } + ] + }, + "AutoCompactTokenLimitScope": { + "description": "Selects which part of the active context is charged against `model_auto_compact_token_limit`.", + "oneOf": [ + { + "description": "Count the full active context against the limit.", + "enum": [ + "total" + ], + "type": "string" + }, + { + "description": "Count sampled output and later growth after the carried window prefix.", + "enum": [ + "body_after_prefix" + ], + "type": "string" + } + ] + }, + "AutoReviewDecisionSource": { + "description": "[UNSTABLE] Source that produced a terminal approval auto-review decision.", + "enum": [ + "agent" + ], + "type": "string" + }, + "AutoReviewRequirements": { + "properties": { + "ignoreRules": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "requiredOnModels": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + } + }, + "type": "object" + }, + "BrowserUseAccessApprovalLifetime": { + "enum": [ + "turn", + "thread" + ], + "type": "string" + }, + "BrowserUseConfig": { + "properties": { + "allow_history_access": { + "type": [ + "boolean", + "null" + ] + }, + "default_origin_policy": { + "anyOf": [ + { + "$ref": "#/definitions/BrowserUseOriginPolicyConfig" + }, + { + "type": "null" + } + ] + }, + "origins": { + "additionalProperties": { + "$ref": "#/definitions/BrowserUseOriginPolicyConfig" + }, + "type": [ + "object", + "null" + ] + } + }, + "type": "object" + }, + "BrowserUseOriginPolicy": { + "properties": { + "access": { + "anyOf": [ + { + "$ref": "#/definitions/AllowDenyRequirement" + }, + { + "type": "null" + } + ] + }, + "accessApprovalLifetime": { + "anyOf": [ + { + "$ref": "#/definitions/BrowserUseAccessApprovalLifetime" + }, + { + "type": "null" + } + ] + }, + "autoReview": { + "anyOf": [ + { + "$ref": "#/definitions/AllowDenyRequirement" + }, + { + "type": "null" + } + ] + }, + "downloads": { + "anyOf": [ + { + "$ref": "#/definitions/AllowDenyRequirement" + }, + { + "type": "null" + } + ] + }, + "fullCdpAccess": { + "anyOf": [ + { + "$ref": "#/definitions/AllowDenyRequirement" + }, + { + "type": "null" + } + ] + }, + "persistentApproval": { + "type": [ + "boolean", + "null" + ] + }, + "uploads": { + "anyOf": [ + { + "$ref": "#/definitions/AllowDenyRequirement" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "BrowserUseOriginPolicyConfig": { + "properties": { + "access": { + "anyOf": [ + { + "$ref": "#/definitions/AllowDenyRequirement" + }, + { + "type": "null" + } + ] + }, + "downloads": { + "anyOf": [ + { + "$ref": "#/definitions/AllowDenyRequirement" + }, + { + "type": "null" + } + ] + }, + "full_cdp_access": { + "anyOf": [ + { + "$ref": "#/definitions/AllowDenyRequirement" + }, + { + "type": "null" + } + ] + }, + "uploads": { + "anyOf": [ + { + "$ref": "#/definitions/AllowDenyRequirement" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "BrowserUseRequirements": { + "properties": { + "allowGlobalPersistentApproval": { + "type": [ + "boolean", + "null" + ] + }, + "allowHistoryAccess": { + "type": [ + "boolean", + "null" + ] + }, + "defaultOriginPolicy": { + "anyOf": [ + { + "$ref": "#/definitions/BrowserUseOriginPolicy" + }, + { + "type": "null" + } + ] + }, + "disableAutoReview": { + "type": [ + "boolean", + "null" + ] + }, + "origins": { + "additionalProperties": { + "$ref": "#/definitions/BrowserUseOriginPolicy" + }, + "type": [ + "object", + "null" + ] + } + }, + "type": "object" + }, + "ByteRange": { + "properties": { + "end": { + "format": "uint", + "minimum": 0.0, + "type": "integer" + }, + "start": { + "format": "uint", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "end", + "start" + ], + "type": "object" + }, + "CancelLoginAccountParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "loginId": { + "type": "string" + } + }, + "required": [ + "loginId" + ], + "title": "CancelLoginAccountParams", + "type": "object" + }, + "CancelLoginAccountResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "status": { + "$ref": "#/definitions/CancelLoginAccountStatus" + } + }, + "required": [ + "status" + ], + "title": "CancelLoginAccountResponse", + "type": "object" + }, + "CancelLoginAccountStatus": { + "enum": [ + "canceled", + "notFound" + ], + "type": "string" + }, + "CapabilityRootLocation": { + "description": "Location used to resolve a selected capability root.", + "oneOf": [ + { + "description": "A path owned by an execution environment.", + "properties": { + "environmentId": { + "type": "string" + }, + "path": { + "description": "Absolute path for the root in the selected environment.", + "type": "string" + }, + "type": { + "enum": [ + "environment" + ], + "title": "EnvironmentCapabilityRootLocationType", + "type": "string" + } + }, + "required": [ + "environmentId", + "path", + "type" + ], + "title": "EnvironmentCapabilityRootLocation", + "type": "object" + } + ] + }, + "CliAuthCredentialsStoreMode": { + "enum": [ + "file", + "keyring", + "auto", + "ephemeral" + ], + "type": "string" + }, + "ClientInfo": { + "properties": { + "name": { + "type": "string" + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "version": { + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "type": "object" + }, + "ClientRequest": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Request from the client to the server.", + "oneOf": [ + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "initialize" + ], + "title": "InitializeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/InitializeParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "InitializeRequest", + "type": "object" + }, + { + "description": "NEW APIs", + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/start" + ], + "title": "Thread/startRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadStartParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/startRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/resume" + ], + "title": "Thread/resumeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadResumeParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/resumeRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/fork" + ], + "title": "Thread/forkRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadForkParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/forkRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/archive" + ], + "title": "Thread/archiveRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadArchiveParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/archiveRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/delete" + ], + "title": "Thread/deleteRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadDeleteParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/deleteRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/unsubscribe" + ], + "title": "Thread/unsubscribeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadUnsubscribeParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/unsubscribeRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/name/set" + ], + "title": "Thread/name/setRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadSetNameParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/name/setRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/goal/set" + ], + "title": "Thread/goal/setRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadGoalSetParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/goal/setRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/goal/get" + ], + "title": "Thread/goal/getRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadGoalGetParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/goal/getRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/goal/clear" + ], + "title": "Thread/goal/clearRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadGoalClearParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/goal/clearRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/metadata/update" + ], + "title": "Thread/metadata/updateRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadMetadataUpdateParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/metadata/updateRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/section/move" + ], + "title": "Thread/section/moveRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadSectionMoveParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/section/moveRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/unarchive" + ], + "title": "Thread/unarchiveRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadUnarchiveParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/unarchiveRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/compact/start" + ], + "title": "Thread/compact/startRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadCompactStartParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/compact/startRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/shellCommand" + ], + "title": "Thread/shellCommandRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadShellCommandParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/shellCommandRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/approveGuardianDeniedAction" + ], + "title": "Thread/approveGuardianDeniedActionRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadApproveGuardianDeniedActionParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/approveGuardianDeniedActionRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/rollback" + ], + "title": "Thread/rollbackRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadRollbackParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/rollbackRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/revert" + ], + "title": "Thread/revertRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadRevertParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/revertRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/list" + ], + "title": "Thread/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/listRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "threadSection/list" + ], + "title": "ThreadSection/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadSectionListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "ThreadSection/listRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "threadSection/create" + ], + "title": "ThreadSection/createRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadSectionCreateParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "ThreadSection/createRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "threadSection/update" + ], + "title": "ThreadSection/updateRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadSectionUpdateParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "ThreadSection/updateRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "threadSection/delete" + ], + "title": "ThreadSection/deleteRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadSectionDeleteParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "ThreadSection/deleteRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/loaded/list" + ], + "title": "Thread/loaded/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadLoadedListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/loaded/listRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/read" + ], + "title": "Thread/readRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadReadParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/readRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/turns/list" + ], + "title": "Thread/turns/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadTurnsListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/turns/listRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/items/list" + ], + "title": "Thread/items/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadItemsListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/items/listRequest", + "type": "object" + }, + { + "description": "Append raw Responses API items to the thread history without starting a user turn.", + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/inject_items" + ], + "title": "Thread/injectItemsRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadInjectItemsParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/injectItemsRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "skills/list" + ], + "title": "Skills/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/SkillsListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Skills/listRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "skills/extraRoots/set" + ], + "title": "Skills/extraRoots/setRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/SkillsExtraRootsSetParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Skills/extraRoots/setRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "hooks/list" + ], + "title": "Hooks/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/HooksListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Hooks/listRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "marketplace/add" + ], + "title": "Marketplace/addRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/MarketplaceAddParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Marketplace/addRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "marketplace/remove" + ], + "title": "Marketplace/removeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/MarketplaceRemoveParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Marketplace/removeRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "marketplace/upgrade" + ], + "title": "Marketplace/upgradeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/MarketplaceUpgradeParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Marketplace/upgradeRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "plugin/list" + ], + "title": "Plugin/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/PluginListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Plugin/listRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "plugin/installed" + ], + "title": "Plugin/installedRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/PluginInstalledParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Plugin/installedRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "plugin/read" + ], + "title": "Plugin/readRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/PluginReadParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Plugin/readRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "plugin/skill/read" + ], + "title": "Plugin/skill/readRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/PluginSkillReadParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Plugin/skill/readRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "plugin/share/save" + ], + "title": "Plugin/share/saveRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/PluginShareSaveParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Plugin/share/saveRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "plugin/share/updateTargets" + ], + "title": "Plugin/share/updateTargetsRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/PluginShareUpdateTargetsParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Plugin/share/updateTargetsRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "plugin/share/list" + ], + "title": "Plugin/share/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/PluginShareListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Plugin/share/listRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "plugin/share/checkout" + ], + "title": "Plugin/share/checkoutRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/PluginShareCheckoutParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Plugin/share/checkoutRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "plugin/share/delete" + ], + "title": "Plugin/share/deleteRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/PluginShareDeleteParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Plugin/share/deleteRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "app/read" + ], + "title": "App/readRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/AppsReadParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "App/readRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "app/list" + ], + "title": "App/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/AppsListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "App/listRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "app/installed" + ], + "title": "App/installedRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/AppsInstalledParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "App/installedRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/readFile" + ], + "title": "Fs/readFileRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsReadFileParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/readFileRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/writeFile" + ], + "title": "Fs/writeFileRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsWriteFileParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/writeFileRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/createDirectory" + ], + "title": "Fs/createDirectoryRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsCreateDirectoryParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/createDirectoryRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/getMetadata" + ], + "title": "Fs/getMetadataRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsGetMetadataParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/getMetadataRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/readDirectory" + ], + "title": "Fs/readDirectoryRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsReadDirectoryParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/readDirectoryRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/remove" + ], + "title": "Fs/removeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsRemoveParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/removeRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/copy" + ], + "title": "Fs/copyRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsCopyParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/copyRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/watch" + ], + "title": "Fs/watchRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsWatchParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/watchRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fs/unwatch" + ], + "title": "Fs/unwatchRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsUnwatchParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Fs/unwatchRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "skills/config/write" + ], + "title": "Skills/config/writeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/SkillsConfigWriteParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Skills/config/writeRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "plugin/install" + ], + "title": "Plugin/installRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/PluginInstallParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Plugin/installRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "plugin/uninstall" + ], + "title": "Plugin/uninstallRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/PluginUninstallParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Plugin/uninstallRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "turn/start" + ], + "title": "Turn/startRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/TurnStartParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Turn/startRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "turn/steer" + ], + "title": "Turn/steerRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/TurnSteerParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Turn/steerRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "turn/interrupt" + ], + "title": "Turn/interruptRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/TurnInterruptParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Turn/interruptRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "review/start" + ], + "title": "Review/startRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ReviewStartParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Review/startRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "model/list" + ], + "title": "Model/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ModelListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Model/listRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "modelProvider/capabilities/read" + ], + "title": "ModelProvider/capabilities/readRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ModelProviderCapabilitiesReadParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "ModelProvider/capabilities/readRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "experimentalFeature/list" + ], + "title": "ExperimentalFeature/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ExperimentalFeatureListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "ExperimentalFeature/listRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "permissionProfile/list" + ], + "title": "PermissionProfile/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/PermissionProfileListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "PermissionProfile/listRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "experimentalFeature/enablement/set" + ], + "title": "ExperimentalFeature/enablement/setRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ExperimentalFeatureEnablementSetParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "ExperimentalFeature/enablement/setRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "mcpServer/oauth/login" + ], + "title": "McpServer/oauth/loginRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/McpServerOauthLoginParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "McpServer/oauth/loginRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "config/mcpServer/reload" + ], + "title": "Config/mcpServer/reloadRequestMethod", + "type": "string" + }, + "params": { + "type": "null" + } + }, + "required": [ + "id", + "method" + ], + "title": "Config/mcpServer/reloadRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "mcpServerStatus/list" + ], + "title": "McpServerStatus/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ListMcpServerStatusParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "McpServerStatus/listRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "mcpServer/resource/read" + ], + "title": "McpServer/resource/readRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/McpResourceReadParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "McpServer/resource/readRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "mcpServer/tool/call" + ], + "title": "McpServer/tool/callRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/McpServerToolCallParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "McpServer/tool/callRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "windowsSandbox/setupStart" + ], + "title": "WindowsSandbox/setupStartRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/WindowsSandboxSetupStartParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "WindowsSandbox/setupStartRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "windowsSandbox/readiness" + ], + "title": "WindowsSandbox/readinessRequestMethod", + "type": "string" + }, + "params": { + "type": "null" + } + }, + "required": [ + "id", + "method" + ], + "title": "WindowsSandbox/readinessRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "account/login/start" + ], + "title": "Account/login/startRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/LoginAccountParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Account/login/startRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "account/login/cancel" + ], + "title": "Account/login/cancelRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/CancelLoginAccountParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Account/login/cancelRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "account/logout" + ], + "title": "Account/logoutRequestMethod", + "type": "string" + }, + "params": { + "type": "null" + } + }, + "required": [ + "id", + "method" + ], + "title": "Account/logoutRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "account/rateLimits/read" + ], + "title": "Account/rateLimits/readRequestMethod", + "type": "string" + }, + "params": { + "type": "null" + } + }, + "required": [ + "id", + "method" + ], + "title": "Account/rateLimits/readRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "account/rateLimitResetCredit/consume" + ], + "title": "Account/rateLimitResetCredit/consumeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ConsumeAccountRateLimitResetCreditParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Account/rateLimitResetCredit/consumeRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "account/usage/read" + ], + "title": "Account/usage/readRequestMethod", + "type": "string" + }, + "params": { + "anyOf": [ + { + "$ref": "#/definitions/GetAccountTokenUsageParams" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "method" + ], + "title": "Account/usage/readRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "account/workspaceMessages/read" + ], + "title": "Account/workspaceMessages/readRequestMethod", + "type": "string" + }, + "params": { + "type": "null" + } + }, + "required": [ + "id", + "method" + ], + "title": "Account/workspaceMessages/readRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "account/sendAddCreditsNudgeEmail" + ], + "title": "Account/sendAddCreditsNudgeEmailRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/SendAddCreditsNudgeEmailParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Account/sendAddCreditsNudgeEmailRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "feedback/upload" + ], + "title": "Feedback/uploadRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FeedbackUploadParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Feedback/uploadRequest", + "type": "object" + }, + { + "description": "Execute a standalone command (argv vector) under the server's sandbox.", + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "command/exec" + ], + "title": "Command/execRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/CommandExecParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Command/execRequest", + "type": "object" + }, + { + "description": "Write stdin bytes to a running `command/exec` session or close stdin.", + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "command/exec/write" + ], + "title": "Command/exec/writeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/CommandExecWriteParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Command/exec/writeRequest", + "type": "object" + }, + { + "description": "Terminate a running `command/exec` session by client-supplied `processId`.", + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "command/exec/terminate" + ], + "title": "Command/exec/terminateRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/CommandExecTerminateParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Command/exec/terminateRequest", + "type": "object" + }, + { + "description": "Resize a running PTY-backed `command/exec` session by client-supplied `processId`.", + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "command/exec/resize" + ], + "title": "Command/exec/resizeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/CommandExecResizeParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Command/exec/resizeRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "config/read" + ], + "title": "Config/readRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ConfigReadParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Config/readRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "externalAgentConfig/detect" + ], + "title": "ExternalAgentConfig/detectRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ExternalAgentConfigDetectParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "ExternalAgentConfig/detectRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "externalAgentConfig/import" + ], + "title": "ExternalAgentConfig/importRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ExternalAgentConfigImportParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "ExternalAgentConfig/importRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "externalAgentConfig/import/recordHistory" + ], + "title": "ExternalAgentConfig/import/recordHistoryRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ExternalAgentConfigImportHistoryRecordParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "ExternalAgentConfig/import/recordHistoryRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "externalAgentConfig/import/readHistories" + ], + "title": "ExternalAgentConfig/import/readHistoriesRequestMethod", + "type": "string" + }, + "params": { + "type": "null" + } + }, + "required": [ + "id", + "method" + ], + "title": "ExternalAgentConfig/import/readHistoriesRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "config/value/write" + ], + "title": "Config/value/writeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ConfigValueWriteParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Config/value/writeRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "config/batchWrite" + ], + "title": "Config/batchWriteRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ConfigBatchWriteParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Config/batchWriteRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "configRequirements/read" + ], + "title": "ConfigRequirements/readRequestMethod", + "type": "string" + }, + "params": { + "type": "null" + } + }, + "required": [ + "id", + "method" + ], + "title": "ConfigRequirements/readRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "account/read" + ], + "title": "Account/readRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/GetAccountParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Account/readRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "fuzzyFileSearch" + ], + "title": "FuzzyFileSearchRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FuzzyFileSearchParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "FuzzyFileSearchRequest", + "type": "object" + } + ], + "title": "ClientRequest" + }, + "CodexErrorInfo": { + "description": "This translation layer make sure that we expose codex error code in camel case.\n\nWhen an upstream HTTP status is available (for example, from the Responses API or a provider), it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant.", + "oneOf": [ + { + "enum": [ + "contextWindowExceeded", + "sessionBudgetExceeded", + "usageLimitExceeded", + "rateLimitExceeded", + "serverOverloaded", + "cyberPolicy", + "misalignmentPolicyViolation", + "internalServerError", + "unauthorized", + "badRequest", + "threadRollbackFailed", + "sandboxError", + "other" + ], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "httpConnectionFailed": { + "properties": { + "httpStatusCode": { + "format": "uint16", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" + } + }, + "required": [ + "httpConnectionFailed" + ], + "title": "HttpConnectionFailedCodexErrorInfo", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Failed to connect to the response SSE stream.", + "properties": { + "responseStreamConnectionFailed": { + "properties": { + "httpStatusCode": { + "format": "uint16", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" + } + }, + "required": [ + "responseStreamConnectionFailed" + ], + "title": "ResponseStreamConnectionFailedCodexErrorInfo", + "type": "object" + }, + { + "additionalProperties": false, + "description": "The response SSE stream disconnected in the middle of a turn before completion.", + "properties": { + "responseStreamDisconnected": { + "properties": { + "httpStatusCode": { + "format": "uint16", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" + } + }, + "required": [ + "responseStreamDisconnected" + ], + "title": "ResponseStreamDisconnectedCodexErrorInfo", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Reached the retry limit for responses.", + "properties": { + "responseTooManyFailedAttempts": { + "properties": { + "httpStatusCode": { + "format": "uint16", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" + } + }, + "required": [ + "responseTooManyFailedAttempts" + ], + "title": "ResponseTooManyFailedAttemptsCodexErrorInfo", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Returned when `turn/start` or `turn/steer` is submitted while the current active turn cannot accept same-turn steering, for example `/review` or manual `/compact`.", + "properties": { + "activeTurnNotSteerable": { + "properties": { + "turnKind": { + "$ref": "#/definitions/NonSteerableTurnKind" + } + }, + "required": [ + "turnKind" + ], + "type": "object" + } + }, + "required": [ + "activeTurnNotSteerable" + ], + "title": "ActiveTurnNotSteerableCodexErrorInfo", + "type": "object" + } + ] + }, + "CodexResponseHandoffMode": { + "enum": [ + "thinking", + "commentary", + "bemTags" + ], + "type": "string" + }, + "CollabAgentState": { + "properties": { + "message": { + "type": [ + "string", + "null" + ] + }, + "status": { + "$ref": "#/definitions/CollabAgentStatus" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "CollabAgentStatus": { + "enum": [ + "pendingInit", + "running", + "interrupted", + "completed", + "errored", + "shutdown", + "notFound" + ], + "type": "string" + }, + "CollabAgentTool": { + "enum": [ + "spawnAgent", + "sendInput", + "resumeAgent", + "wait", + "closeAgent", + "sendMessage", + "followupTask", + "interruptAgent", + "listAgents" + ], + "type": "string" + }, + "CollabAgentToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed", + "interrupted" + ], + "type": "string" + }, + "CollaborationMode": { + "description": "Collaboration mode for a Codex session.", + "properties": { + "mode": { + "$ref": "#/definitions/ModeKind" + }, + "settings": { + "$ref": "#/definitions/Settings" + } + }, + "required": [ + "mode", + "settings" + ], + "type": "object" + }, + "CollaborationModeMask": { + "description": "EXPERIMENTAL - collaboration mode preset metadata for clients.", + "properties": { + "mode": { + "anyOf": [ + { + "$ref": "#/definitions/ModeKind" + }, + { + "type": "null" + } + ] + }, + "model": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": "string" + }, + "reasoning_effort": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ] + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "CommandAction": { + "oneOf": [ + { + "properties": { + "command": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": { + "enum": [ + "read" + ], + "title": "ReadCommandActionType", + "type": "string" + } + }, + "required": [ + "command", + "name", + "path", + "type" + ], + "title": "ReadCommandAction", + "type": "object" + }, + { + "properties": { + "command": { + "type": "string" + }, + "path": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "listFiles" + ], + "title": "ListFilesCommandActionType", + "type": "string" + } + }, + "required": [ + "command", + "type" + ], + "title": "ListFilesCommandAction", + "type": "object" + }, + { + "properties": { + "command": { + "type": "string" + }, + "path": { + "type": [ + "string", + "null" + ] + }, + "query": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "search" + ], + "title": "SearchCommandActionType", + "type": "string" + } + }, + "required": [ + "command", + "type" + ], + "title": "SearchCommandAction", + "type": "object" + }, + { + "properties": { + "command": { + "type": "string" + }, + "type": { + "enum": [ + "unknown" + ], + "title": "UnknownCommandActionType", + "type": "string" + } + }, + "required": [ + "command", + "type" + ], + "title": "UnknownCommandAction", + "type": "object" + } + ] + }, + "CommandExecOutputDeltaNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Base64-encoded output chunk emitted for a streaming `command/exec` request.\n\nThese notifications are connection-scoped. If the originating connection closes, the server terminates the process.", + "properties": { + "capReached": { + "description": "`true` on the final streamed chunk for a stream when `outputBytesCap` truncated later output on that stream.", + "type": "boolean" + }, + "deltaBase64": { + "description": "Base64-encoded output bytes.", + "type": "string" + }, + "processId": { + "description": "Client-supplied, connection-scoped `processId` from the original `command/exec` request.", + "type": "string" + }, + "stream": { + "allOf": [ + { + "$ref": "#/definitions/CommandExecOutputStream" + } + ], + "description": "Output stream for this chunk." + } + }, + "required": [ + "capReached", + "deltaBase64", + "processId", + "stream" + ], + "title": "CommandExecOutputDeltaNotification", + "type": "object" + }, + "CommandExecOutputStream": { + "description": "Stream label for `command/exec/outputDelta` notifications.", + "oneOf": [ + { + "description": "stdout stream. PTY mode multiplexes terminal output here.", + "enum": [ + "stdout" + ], + "type": "string" + }, + { + "description": "stderr stream.", + "enum": [ + "stderr" + ], + "type": "string" + } + ] + }, + "CommandExecParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Run a standalone command (argv vector) in the server sandbox without creating a thread or turn.\n\nThe final `command/exec` response is deferred until the process exits and is sent only after all `command/exec/outputDelta` notifications for that connection have been emitted.", + "properties": { + "command": { + "description": "Command argv vector. Empty arrays are rejected.", + "items": { + "type": "string" + }, + "type": "array" + }, + "cwd": { + "description": "Optional working directory. Defaults to the server cwd.", + "type": [ + "string", + "null" + ] + }, + "disableOutputCap": { + "description": "Disable stdout/stderr capture truncation for this request.\n\nCannot be combined with `outputBytesCap`.", + "type": "boolean" + }, + "disableTimeout": { + "description": "Disable the timeout entirely for this request.\n\nCannot be combined with `timeoutMs`.", + "type": "boolean" + }, + "env": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "description": "Optional environment overrides merged into the server-computed environment.\n\nMatching names override inherited values. Set a key to `null` to unset an inherited variable.", + "type": [ + "object", + "null" + ] + }, + "outputBytesCap": { + "description": "Optional per-stream stdout/stderr capture cap in bytes.\n\nWhen omitted, the server default applies. Cannot be combined with `disableOutputCap`.", + "format": "uint", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "processId": { + "description": "Optional client-supplied, connection-scoped process id.\n\nRequired for `tty`, `streamStdin`, `streamStdoutStderr`, and follow-up `command/exec/write`, `command/exec/resize`, and `command/exec/terminate` calls. When omitted, buffered execution gets an internal id that is not exposed to the client.", + "type": [ + "string", + "null" + ] + }, + "sandboxPolicy": { + "anyOf": [ + { + "$ref": "#/definitions/SandboxPolicy" + }, + { + "type": "null" + } + ], + "description": "Optional sandbox policy for this command.\n\nUses the same shape as thread/turn execution sandbox configuration and defaults to the user's configured policy when omitted. Cannot be combined with `permissionProfile`." + }, + "size": { + "anyOf": [ + { + "$ref": "#/definitions/CommandExecTerminalSize" + }, + { + "type": "null" + } + ], + "description": "Optional initial PTY size in character cells. Only valid when `tty` is true." + }, + "streamStdin": { + "description": "Allow follow-up `command/exec/write` requests to write stdin bytes.\n\nRequires a client-supplied `processId`.", + "type": "boolean" + }, + "streamStdoutStderr": { + "description": "Stream stdout/stderr via `command/exec/outputDelta` notifications.\n\nStreamed bytes are not duplicated into the final response and require a client-supplied `processId`.", + "type": "boolean" + }, + "timeoutMs": { + "description": "Optional timeout in milliseconds.\n\nWhen omitted, the server default applies. Cannot be combined with `disableTimeout`.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "tty": { + "description": "Enable PTY mode.\n\nThis implies `streamStdin` and `streamStdoutStderr`.", + "type": "boolean" + } + }, + "required": [ + "command" + ], + "title": "CommandExecParams", + "type": "object" + }, + "CommandExecResizeParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Resize a running PTY-backed `command/exec` session.", + "properties": { + "processId": { + "description": "Client-supplied, connection-scoped `processId` from the original `command/exec` request.", + "type": "string" + }, + "size": { + "allOf": [ + { + "$ref": "#/definitions/CommandExecTerminalSize" + } + ], + "description": "New PTY size in character cells." + } + }, + "required": [ + "processId", + "size" + ], + "title": "CommandExecResizeParams", + "type": "object" + }, + "CommandExecResizeResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Empty success response for `command/exec/resize`.", + "title": "CommandExecResizeResponse", + "type": "object" + }, + "CommandExecResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Final buffered result for `command/exec`.", + "properties": { + "exitCode": { + "description": "Process exit code.", + "format": "int32", + "type": "integer" + }, + "stderr": { + "description": "Buffered stderr capture.\n\nEmpty when stderr was streamed via `command/exec/outputDelta`.", + "type": "string" + }, + "stdout": { + "description": "Buffered stdout capture.\n\nEmpty when stdout was streamed via `command/exec/outputDelta`.", + "type": "string" + } + }, + "required": [ + "exitCode", + "stderr", + "stdout" + ], + "title": "CommandExecResponse", + "type": "object" + }, + "CommandExecTerminalSize": { + "description": "PTY size in character cells for `command/exec` PTY sessions.", + "properties": { + "cols": { + "description": "Terminal width in character cells.", + "format": "uint16", + "minimum": 0.0, + "type": "integer" + }, + "rows": { + "description": "Terminal height in character cells.", + "format": "uint16", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "cols", + "rows" + ], + "type": "object" + }, + "CommandExecTerminateParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Terminate a running `command/exec` session.", + "properties": { + "processId": { + "description": "Client-supplied, connection-scoped `processId` from the original `command/exec` request.", + "type": "string" + } + }, + "required": [ + "processId" + ], + "title": "CommandExecTerminateParams", + "type": "object" + }, + "CommandExecTerminateResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Empty success response for `command/exec/terminate`.", + "title": "CommandExecTerminateResponse", + "type": "object" + }, + "CommandExecWriteParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Write stdin bytes to a running `command/exec` session, close stdin, or both.", + "properties": { + "closeStdin": { + "description": "Close stdin after writing `deltaBase64`, if present.", + "type": "boolean" + }, + "deltaBase64": { + "description": "Optional base64-encoded stdin bytes to write.", + "type": [ + "string", + "null" + ] + }, + "processId": { + "description": "Client-supplied, connection-scoped `processId` from the original `command/exec` request.", + "type": "string" + } + }, + "required": [ + "processId" + ], + "title": "CommandExecWriteParams", + "type": "object" + }, + "CommandExecWriteResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Empty success response for `command/exec/write`.", + "title": "CommandExecWriteResponse", + "type": "object" + }, + "CommandExecutionOutputDeltaNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "delta": { + "type": "string" + }, + "itemId": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "delta", + "itemId", + "threadId", + "turnId" + ], + "title": "CommandExecutionOutputDeltaNotification", + "type": "object" + }, + "CommandExecutionSource": { + "enum": [ + "agent", + "userShell", + "unifiedExecStartup", + "unifiedExecInteraction" + ], + "type": "string" + }, + "CommandExecutionStatus": { + "enum": [ + "inProgress", + "completed", + "failed", + "declined" + ], + "type": "string" + }, + "CommandMigration": { + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "ComputerUseConfig": { + "properties": { + "default_app_access": { + "anyOf": [ + { + "$ref": "#/definitions/AllowDenyRequirement" + }, + { + "type": "null" + } + ] + }, + "macos": { + "anyOf": [ + { + "$ref": "#/definitions/ComputerUseMacosConfig" + }, + { + "type": "null" + } + ] + }, + "windows": { + "anyOf": [ + { + "$ref": "#/definitions/ComputerUseWindowsConfig" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "ComputerUseMacosConfig": { + "properties": { + "bundle_ids": { + "additionalProperties": { + "$ref": "#/definitions/AllowDenyRequirement" + }, + "type": [ + "object", + "null" + ] + } + }, + "type": "object" + }, + "ComputerUseMacosRequirements": { + "properties": { + "bundleIds": { + "additionalProperties": { + "$ref": "#/definitions/AllowDenyRequirement" + }, + "type": [ + "object", + "null" + ] + } + }, + "type": "object" + }, + "ComputerUseRequirements": { + "properties": { + "allowLockedComputerUse": { + "type": [ + "boolean", + "null" + ] + }, + "allowPersistentApproval": { + "type": [ + "boolean", + "null" + ] + }, + "defaultAppAccess": { + "anyOf": [ + { + "$ref": "#/definitions/AllowDenyRequirement" + }, + { + "type": "null" + } + ] + }, + "macos": { + "anyOf": [ + { + "$ref": "#/definitions/ComputerUseMacosRequirements" + }, + { + "type": "null" + } + ] + }, + "windows": { + "anyOf": [ + { + "$ref": "#/definitions/ComputerUseWindowsRequirements" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "ComputerUseWindowsConfig": { + "properties": { + "aumids": { + "additionalProperties": { + "$ref": "#/definitions/AllowDenyRequirement" + }, + "type": [ + "object", + "null" + ] + }, + "exes": { + "items": { + "$ref": "#/definitions/ComputerUseWindowsExeConfig" + }, + "type": [ + "array", + "null" + ] + } + }, + "type": "object" + }, + "ComputerUseWindowsExeConfig": { + "properties": { + "access": { + "$ref": "#/definitions/AllowDenyRequirement" + }, + "binary_name": { + "type": [ + "string", + "null" + ] + }, + "product_name": { + "type": "string" + }, + "publisher_name": { + "type": "string" + } + }, + "required": [ + "access", + "product_name", + "publisher_name" + ], + "type": "object" + }, + "ComputerUseWindowsExeRequirement": { + "properties": { + "access": { + "$ref": "#/definitions/AllowDenyRequirement" + }, + "binaryName": { + "type": [ + "string", + "null" + ] + }, + "productName": { + "type": "string" + }, + "publisherName": { + "type": "string" + } + }, + "required": [ + "access", + "productName", + "publisherName" + ], + "type": "object" + }, + "ComputerUseWindowsRequirements": { + "properties": { + "aumids": { + "additionalProperties": { + "$ref": "#/definitions/AllowDenyRequirement" + }, + "type": [ + "object", + "null" + ] + }, + "exes": { + "items": { + "$ref": "#/definitions/ComputerUseWindowsExeRequirement" + }, + "type": [ + "array", + "null" + ] + } + }, + "type": "object" + }, + "Config": { + "additionalProperties": true, + "properties": { + "analytics": { + "anyOf": [ + { + "$ref": "#/definitions/AnalyticsConfig" + }, + { + "type": "null" + } + ] + }, + "approval_policy": { + "anyOf": [ + { + "$ref": "#/definitions/AskForApproval" + }, + { + "type": "null" + } + ] + }, + "approvals_reviewer": { + "anyOf": [ + { + "$ref": "#/definitions/ApprovalsReviewer" + }, + { + "type": "null" + } + ], + "description": "[UNSTABLE] Optional default for where approval requests are routed for review." + }, + "browser_use": { + "anyOf": [ + { + "$ref": "#/definitions/BrowserUseConfig" + }, + { + "type": "null" + } + ] + }, + "compact_prompt": { + "type": [ + "string", + "null" + ] + }, + "computer_use": { + "anyOf": [ + { + "$ref": "#/definitions/ComputerUseConfig" + }, + { + "type": "null" + } + ] + }, + "desktop": { + "additionalProperties": true, + "type": [ + "object", + "null" + ] + }, + "developer_instructions": { + "type": [ + "string", + "null" + ] + }, + "forced_chatgpt_workspace_id": { + "anyOf": [ + { + "$ref": "#/definitions/ForcedChatgptWorkspaceIds" + }, + { + "type": "null" + } + ] + }, + "forced_login_method": { + "anyOf": [ + { + "$ref": "#/definitions/ForcedLoginMethod" + }, + { + "type": "null" + } + ] + }, + "instructions": { + "type": [ + "string", + "null" + ] + }, + "model": { + "type": [ + "string", + "null" + ] + }, + "model_auto_compact_token_limit": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "model_auto_compact_token_limit_scope": { + "anyOf": [ + { + "$ref": "#/definitions/AutoCompactTokenLimitScope" + }, + { + "type": "null" + } + ] + }, + "model_context_window": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "model_provider": { + "type": [ + "string", + "null" + ] + }, + "model_reasoning_effort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ] + }, + "model_reasoning_summary": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningSummary" + }, + { + "type": "null" + } + ] + }, + "model_verbosity": { + "anyOf": [ + { + "$ref": "#/definitions/Verbosity" + }, + { + "type": "null" + } + ] + }, + "review_model": { + "type": [ + "string", + "null" + ] + }, + "sandbox_mode": { + "anyOf": [ + { + "$ref": "#/definitions/SandboxMode" + }, + { + "type": "null" + } + ] + }, + "sandbox_workspace_write": { + "anyOf": [ + { + "$ref": "#/definitions/SandboxWorkspaceWrite" + }, + { + "type": "null" + } + ] + }, + "service_tier": { + "type": [ + "string", + "null" + ] + }, + "tools": { + "anyOf": [ + { + "$ref": "#/definitions/ToolsV2" + }, + { + "type": "null" + } + ] + }, + "web_search": { + "anyOf": [ + { + "$ref": "#/definitions/WebSearchMode" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "ConfigBatchWriteParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "edits": { + "items": { + "$ref": "#/definitions/ConfigEdit" + }, + "type": "array" + }, + "expectedVersion": { + "type": [ + "string", + "null" + ] + }, + "filePath": { + "description": "Path to the config file to write; defaults to the user's `config.toml` when omitted.", + "type": [ + "string", + "null" + ] + }, + "reloadUserConfig": { + "description": "When true, hot-reload updated runtime settings into loaded threads after writing. Session-static model, reasoning-effort, Plan-mode reasoning-effort, service-tier, and personality defaults are not reloaded.", + "type": "boolean" + } + }, + "required": [ + "edits" + ], + "title": "ConfigBatchWriteParams", + "type": "object" + }, + "ConfigEdit": { + "properties": { + "keyPath": { + "type": "string" + }, + "mergeStrategy": { + "$ref": "#/definitions/MergeStrategy" + }, + "value": true + }, + "required": [ + "keyPath", + "mergeStrategy", + "value" + ], + "type": "object" + }, + "ConfigLayer": { + "properties": { + "config": true, + "disabledReason": { + "type": [ + "string", + "null" + ] + }, + "name": { + "$ref": "#/definitions/ConfigLayerSource" + }, + "version": { + "type": "string" + } + }, + "required": [ + "config", + "name", + "version" + ], + "type": "object" + }, + "ConfigLayerMetadata": { + "properties": { + "name": { + "$ref": "#/definitions/ConfigLayerSource" + }, + "version": { + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "type": "object" + }, + "ConfigLayerSource": { + "oneOf": [ + { + "description": "Default configuration supplied with the installed Codex package.", + "properties": { + "file": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Path to the packaged default configuration file." + }, + "type": { + "enum": [ + "packagedDefaults" + ], + "title": "PackagedDefaultsConfigLayerSourceType", + "type": "string" + } + }, + "required": [ + "file", + "type" + ], + "title": "PackagedDefaultsConfigLayerSource", + "type": "object" + }, + { + "description": "Managed preferences layer delivered by MDM (macOS only).", + "properties": { + "domain": { + "type": "string" + }, + "key": { + "type": "string" + }, + "type": { + "enum": [ + "mdm" + ], + "title": "MdmConfigLayerSourceType", + "type": "string" + } + }, + "required": [ + "domain", + "key", + "type" + ], + "title": "MdmConfigLayerSource", + "type": "object" + }, + { + "description": "Managed config layer from a file (usually `managed_config.toml`).", + "properties": { + "file": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "This is the path to the system config.toml file, though it is not guaranteed to exist." + }, + "type": { + "enum": [ + "system" + ], + "title": "SystemConfigLayerSourceType", + "type": "string" + } + }, + "required": [ + "file", + "type" + ], + "title": "SystemConfigLayerSource", + "type": "object" + }, + { + "description": "Enterprise-managed config layer delivered by the cloud config bundle.", + "properties": { + "id": { + "description": "Stable identifier for the delivered layer.", + "type": "string" + }, + "name": { + "description": "Admin-facing name for the delivered layer. This is surfaced in diagnostics so users know which cloud layer needs administrator attention.", + "type": "string" + }, + "type": { + "enum": [ + "enterpriseManaged" + ], + "title": "EnterpriseManagedConfigLayerSourceType", + "type": "string" + } + }, + "required": [ + "id", + "name", + "type" + ], + "title": "EnterpriseManagedConfigLayerSource", + "type": "object" + }, + { + "description": "User config layer from $CODEX_HOME/config.toml. This layer is special in that it is expected to be: - writable by the user - generally outside the workspace directory", + "properties": { + "file": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "This is the path to the user's config.toml file, though it is not guaranteed to exist." + }, + "profile": { + "description": "Name of the selected profile-v2 config layered on top of the base user config, when this layer represents one.", + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "user" + ], + "title": "UserConfigLayerSourceType", + "type": "string" + } + }, + "required": [ + "file", + "type" + ], + "title": "UserConfigLayerSource", + "type": "object" + }, + { + "description": "Path to a .codex/ folder within a project. There could be multiple of these between `cwd` and the project/repo root.", + "properties": { + "dotCodexFolder": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": { + "enum": [ + "project" + ], + "title": "ProjectConfigLayerSourceType", + "type": "string" + } + }, + "required": [ + "dotCodexFolder", + "type" + ], + "title": "ProjectConfigLayerSource", + "type": "object" + }, + { + "description": "Session-layer overrides supplied via `-c`/`--config`.", + "properties": { + "type": { + "enum": [ + "sessionFlags" + ], + "title": "SessionFlagsConfigLayerSourceType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "SessionFlagsConfigLayerSource", + "type": "object" + }, + { + "description": "`managed_config.toml` was designed to be a config that was loaded as the last layer on top of everything else. This scheme did not quite work out as intended, but we keep this variant as a \"best effort\" while we phase out `managed_config.toml` in favor of `requirements.toml`.", + "properties": { + "file": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": { + "enum": [ + "legacyManagedConfigTomlFromFile" + ], + "title": "LegacyManagedConfigTomlFromFileConfigLayerSourceType", + "type": "string" + } + }, + "required": [ + "file", + "type" + ], + "title": "LegacyManagedConfigTomlFromFileConfigLayerSource", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "legacyManagedConfigTomlFromMdm" + ], + "title": "LegacyManagedConfigTomlFromMdmConfigLayerSourceType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "LegacyManagedConfigTomlFromMdmConfigLayerSource", + "type": "object" + } + ] + }, + "ConfigReadParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "cwd": { + "description": "Optional working directory to resolve project config layers. If specified, return the effective config as seen from that directory (i.e., including any project layers between `cwd` and the project/repo root).", + "type": [ + "string", + "null" + ] + }, + "includeLayers": { + "type": "boolean" + } + }, + "title": "ConfigReadParams", + "type": "object" + }, + "ConfigReadResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "config": { + "$ref": "#/definitions/Config" + }, + "layers": { + "items": { + "$ref": "#/definitions/ConfigLayer" + }, + "type": [ + "array", + "null" + ] + }, + "origins": { + "additionalProperties": { + "$ref": "#/definitions/ConfigLayerMetadata" + }, + "type": "object" + } + }, + "required": [ + "config", + "origins" + ], + "title": "ConfigReadResponse", + "type": "object" + }, + "ConfigRequirements": { + "properties": { + "additionalDeveloperInstructions": { + "type": [ + "string", + "null" + ] + }, + "allowAppshots": { + "type": [ + "boolean", + "null" + ] + }, + "allowBrowserAndComputerUse": { + "type": [ + "boolean", + "null" + ] + }, + "allowLoginShell": { + "type": [ + "boolean", + "null" + ] + }, + "allowManagedHooksOnly": { + "type": [ + "boolean", + "null" + ] + }, + "allowRemoteControl": { + "type": [ + "boolean", + "null" + ] + }, + "allowedApprovalPolicies": { + "items": { + "$ref": "#/definitions/AskForApproval" + }, + "type": [ + "array", + "null" + ] + }, + "allowedPermissionProfiles": { + "additionalProperties": { + "type": "boolean" + }, + "type": [ + "object", + "null" + ] + }, + "allowedSandboxModes": { + "items": { + "$ref": "#/definitions/SandboxMode" + }, + "type": [ + "array", + "null" + ] + }, + "allowedWebSearchModes": { + "items": { + "$ref": "#/definitions/WebSearchMode" + }, + "type": [ + "array", + "null" + ] + }, + "allowedWindowsSandboxImplementations": { + "items": { + "$ref": "#/definitions/WindowsSandboxSetupMode" + }, + "type": [ + "array", + "null" + ] + }, + "autoReview": { + "anyOf": [ + { + "$ref": "#/definitions/AutoReviewRequirements" + }, + { + "type": "null" + } + ] + }, + "browserUse": { + "anyOf": [ + { + "$ref": "#/definitions/BrowserUseRequirements" + }, + { + "type": "null" + } + ] + }, + "chatgptBaseUrl": { + "type": [ + "string", + "null" + ] + }, + "checkForUpdateOnStartup": { + "type": [ + "boolean", + "null" + ] + }, + "cliAuthCredentialsStore": { + "anyOf": [ + { + "$ref": "#/definitions/CliAuthCredentialsStoreMode" + }, + { + "type": "null" + } + ] + }, + "computerUse": { + "anyOf": [ + { + "$ref": "#/definitions/ComputerUseRequirements" + }, + { + "type": "null" + } + ] + }, + "defaultPermissions": { + "type": [ + "string", + "null" + ] + }, + "enforceResidency": { + "anyOf": [ + { + "$ref": "#/definitions/ResidencyRequirement" + }, + { + "type": "null" + } + ] + }, + "featureRequirements": { + "additionalProperties": { + "type": "boolean" + }, + "type": [ + "object", + "null" + ] + }, + "feedback": { + "anyOf": [ + { + "$ref": "#/definitions/FeedbackRequirements" + }, + { + "type": "null" + } + ] + }, + "inAppBrowser": { + "anyOf": [ + { + "$ref": "#/definitions/InAppBrowserRequirements" + }, + { + "type": "null" + } + ] + }, + "logDir": { + "type": [ + "string", + "null" + ] + }, + "modelCatalogJson": { + "type": [ + "string", + "null" + ] + }, + "models": { + "anyOf": [ + { + "$ref": "#/definitions/ModelsRequirements" + }, + { + "type": "null" + } + ] + }, + "sqliteHome": { + "type": [ + "string", + "null" + ] + }, + "windowsSandboxPrivateDesktop": { + "type": [ + "boolean", + "null" + ] + } + }, + "type": "object" + }, + "ConfigRequirementsReadResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "requirements": { + "anyOf": [ + { + "$ref": "#/definitions/ConfigRequirements" + }, + { + "type": "null" + } + ], + "description": "Null if no requirements are configured (e.g. no requirements.toml/MDM entries)." + } + }, + "title": "ConfigRequirementsReadResponse", + "type": "object" + }, + "ConfigValueWriteParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "expectedVersion": { + "type": [ + "string", + "null" + ] + }, + "filePath": { + "description": "Path to the config file to write; defaults to the user's `config.toml` when omitted.", + "type": [ + "string", + "null" + ] + }, + "keyPath": { + "type": "string" + }, + "mergeStrategy": { + "$ref": "#/definitions/MergeStrategy" + }, + "value": true + }, + "required": [ + "keyPath", + "mergeStrategy", + "value" + ], + "title": "ConfigValueWriteParams", + "type": "object" + }, + "ConfigWarningNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "details": { + "description": "Optional extra guidance or error details.", + "type": [ + "string", + "null" + ] + }, + "path": { + "description": "Optional path to the config file that triggered the warning.", + "type": [ + "string", + "null" + ] + }, + "range": { + "anyOf": [ + { + "$ref": "#/definitions/TextRange" + }, + { + "type": "null" + } + ], + "description": "Optional range for the error location inside the config file." + }, + "summary": { + "description": "Concise summary of the warning.", + "type": "string" + } + }, + "required": [ + "summary" + ], + "title": "ConfigWarningNotification", + "type": "object" + }, + "ConfigWriteResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "filePath": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Canonical path to the config file that was written." + }, + "overriddenMetadata": { + "anyOf": [ + { + "$ref": "#/definitions/OverriddenMetadata" + }, + { + "type": "null" + } + ] + }, + "status": { + "$ref": "#/definitions/WriteStatus" + }, + "version": { + "type": "string" + } + }, + "required": [ + "filePath", + "status", + "version" + ], + "title": "ConfigWriteResponse", + "type": "object" + }, + "ConfiguredHookHandler": { + "oneOf": [ + { + "properties": { + "additionalContextLimit": { + "description": "Approximate token threshold for spilling this hook's `additionalContext` to disk. `null` uses 2,500 tokens; `0` disables spilling for this hook. The threshold is evaluated against the original context; a spilled preview also includes recovery metadata.", + "format": "uint", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "async": { + "type": "boolean" + }, + "command": { + "type": "string" + }, + "commandWindows": { + "type": [ + "string", + "null" + ] + }, + "statusMessage": { + "type": [ + "string", + "null" + ] + }, + "timeoutSec": { + "format": "uint64", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "command" + ], + "title": "CommandConfiguredHookHandlerType", + "type": "string" + } + }, + "required": [ + "async", + "command", + "type" + ], + "title": "CommandConfiguredHookHandler", + "type": "object" + }, + { + "properties": { + "input": { + "additionalProperties": true, + "type": "object" + }, + "server": { + "type": "string" + }, + "statusMessage": { + "type": [ + "string", + "null" + ] + }, + "timeoutSec": { + "format": "uint64", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "mcp_tool" + ], + "title": "McpToolConfiguredHookHandlerType", + "type": "string" + } + }, + "required": [ + "input", + "server", + "tool", + "type" + ], + "title": "McpToolConfiguredHookHandler", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "prompt" + ], + "title": "PromptConfiguredHookHandlerType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "PromptConfiguredHookHandler", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "agent" + ], + "title": "AgentConfiguredHookHandlerType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "AgentConfiguredHookHandler", + "type": "object" + } + ] + }, + "ConfiguredHookMatcherGroup": { + "properties": { + "hooks": { + "items": { + "$ref": "#/definitions/ConfiguredHookHandler" + }, + "type": "array" + }, + "matcher": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "hooks" + ], + "type": "object" + }, + "ConnectorMetadata": { + "description": "EXPERIMENTAL - metadata returned by app/read.", + "properties": { + "description": { + "type": [ + "string", + "null" + ] + }, + "distributionChannel": { + "type": [ + "string", + "null" + ] + }, + "iconUrl": { + "type": [ + "string", + "null" + ] + }, + "iconUrlDark": { + "type": [ + "string", + "null" + ] + }, + "id": { + "type": "string" + }, + "installUrl": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": "string" + }, + "pluginDisplayNames": { + "default": [], + "items": { + "type": "string" + }, + "type": "array" + }, + "toolSummaries": { + "items": { + "$ref": "#/definitions/AppToolSummary" + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "id", + "name" + ], + "type": "object" + }, + "ConsumeAccountRateLimitResetCreditOutcome": { + "oneOf": [ + { + "description": "A reset credit was consumed and the eligible rate-limit windows were reset.", + "enum": [ + "reset" + ], + "type": "string" + }, + { + "description": "No current rate-limit window is eligible for a reset.", + "enum": [ + "nothingToReset" + ], + "type": "string" + }, + { + "description": "The account has no earned reset credits available.", + "enum": [ + "noCredit" + ], + "type": "string" + }, + { + "description": "The same idempotency key already completed a reset successfully.", + "enum": [ + "alreadyRedeemed" + ], + "type": "string" + } + ] + }, + "ConsumeAccountRateLimitResetCreditParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "creditId": { + "description": "Opaque reset-credit identifier to redeem. When omitted, the backend selects the next available credit.", + "type": [ + "string", + "null" + ] + }, + "idempotencyKey": { + "description": "Identifies one logical reset attempt. A UUID is recommended; reuse the same value when retrying that attempt.", + "type": "string" + } + }, + "required": [ + "idempotencyKey" + ], + "title": "ConsumeAccountRateLimitResetCreditParams", + "type": "object" + }, + "ConsumeAccountRateLimitResetCreditResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "outcome": { + "$ref": "#/definitions/ConsumeAccountRateLimitResetCreditOutcome" + } + }, + "required": [ + "outcome" + ], + "title": "ConsumeAccountRateLimitResetCreditResponse", + "type": "object" + }, + "ContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "input_text" + ], + "title": "InputTextContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextContentItem", + "type": "object" + }, + { + "properties": { + "detail": { + "anyOf": [ + { + "$ref": "#/definitions/ImageDetail" + }, + { + "type": "null" + } + ] + }, + "image_url": { + "type": "string" + }, + "type": { + "enum": [ + "input_image" + ], + "title": "InputImageContentItemType", + "type": "string" + } + }, + "required": [ + "image_url", + "type" + ], + "title": "InputImageContentItem", + "type": "object" + }, + { + "properties": { + "audio_url": { + "type": "string" + }, + "type": { + "enum": [ + "input_audio" + ], + "title": "InputAudioContentItemType", + "type": "string" + } + }, + "required": [ + "audio_url", + "type" + ], + "title": "InputAudioContentItem", + "type": "object" + }, + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "output_text" + ], + "title": "OutputTextContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "OutputTextContentItem", + "type": "object" + } + ] + }, + "ContextCompactedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Deprecated: Use `ContextCompaction` item type instead.", + "properties": { + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "threadId", + "turnId" + ], + "title": "ContextCompactedNotification", + "type": "object" + }, + "ConversationTextRole": { + "enum": [ + "user", + "developer", + "assistant" + ], + "type": "string" + }, + "CreditsSnapshot": { + "properties": { + "balance": { + "type": [ + "string", + "null" + ] + }, + "hasCredits": { + "type": "boolean" + }, + "unlimited": { + "type": "boolean" + } + }, + "required": [ + "hasCredits", + "unlimited" + ], + "type": "object" + }, + "CyberAccessProgram": { + "description": "Requested cyber treatment for a ChatGPT-authenticated Codex turn. Authorization and model-tier restrictions remain server-owned.", + "enum": [ + "standard", + "daybreakBlue", + "daybreakRed" + ], + "type": "string" + }, + "DeprecationNoticeNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "details": { + "description": "Optional extra guidance, such as migration steps or rationale.", + "type": [ + "string", + "null" + ] + }, + "summary": { + "description": "Concise summary of what is deprecated.", + "type": "string" + } + }, + "required": [ + "summary" + ], + "title": "DeprecationNoticeNotification", + "type": "object" + }, + "DesktopOnboardingEntrypoint": { + "enum": [ + "life_sciences" + ], + "type": "string" + }, + "DynamicToolCallOutputContentItem": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "inputText" + ], + "title": "InputTextDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "imageUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputImage" + ], + "title": "InputImageDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "imageUrl", + "type" + ], + "title": "InputImageDynamicToolCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "audioUrl": { + "type": "string" + }, + "type": { + "enum": [ + "inputAudio" + ], + "title": "InputAudioDynamicToolCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "audioUrl", + "type" + ], + "title": "InputAudioDynamicToolCallOutputContentItem", + "type": "object" + } + ] + }, + "DynamicToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, + "DynamicToolNamespaceTool": { + "oneOf": [ + { + "properties": { + "deferLoading": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "inputSchema": true, + "name": { + "type": "string" + }, + "type": { + "enum": [ + "function" + ], + "title": "FunctionDynamicToolNamespaceToolType", + "type": "string" + } + }, + "required": [ + "description", + "inputSchema", + "name", + "type" + ], + "title": "FunctionDynamicToolNamespaceTool", + "type": "object" + } + ] + }, + "DynamicToolSpec": { + "oneOf": [ + { + "properties": { + "deferLoading": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "inputSchema": true, + "name": { + "type": "string" + }, + "type": { + "enum": [ + "function" + ], + "title": "FunctionDynamicToolSpecType", + "type": "string" + } + }, + "required": [ + "description", + "inputSchema", + "name", + "type" + ], + "title": "FunctionDynamicToolSpec", + "type": "object" + }, + { + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "tools": { + "items": { + "$ref": "#/definitions/DynamicToolNamespaceTool" + }, + "type": "array" + }, + "type": { + "enum": [ + "namespace" + ], + "title": "NamespaceDynamicToolSpecType", + "type": "string" + } + }, + "required": [ + "description", + "name", + "tools", + "type" + ], + "title": "NamespaceDynamicToolSpec", + "type": "object" + } + ] + }, + "EnvironmentConnectionNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "environmentId": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "environmentId", + "threadId" + ], + "title": "EnvironmentConnectionNotification", + "type": "object" + }, + "ErrorNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "error": { + "$ref": "#/definitions/TurnError" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + }, + "willRetry": { + "type": "boolean" + } + }, + "required": [ + "error", + "threadId", + "turnId", + "willRetry" + ], + "title": "ErrorNotification", + "type": "object" + }, + "ExperimentalFeature": { + "properties": { + "announcement": { + "description": "Announcement copy shown to users when the feature is introduced. Null when this feature is not in beta.", + "type": [ + "string", + "null" + ] + }, + "defaultEnabled": { + "description": "Whether this feature is enabled by default.", + "type": "boolean" + }, + "description": { + "description": "Short summary describing what the feature does. Null when this feature is not in beta.", + "type": [ + "string", + "null" + ] + }, + "displayName": { + "description": "User-facing display name shown in the experimental features UI. Null when this feature is not in beta.", + "type": [ + "string", + "null" + ] + }, + "enabled": { + "description": "Whether this feature is currently enabled in the loaded config.", + "type": "boolean" + }, + "name": { + "description": "Stable key used in config.toml and CLI flag toggles.", + "type": "string" + }, + "stage": { + "allOf": [ + { + "$ref": "#/definitions/ExperimentalFeatureStage" + } + ], + "description": "Lifecycle stage of this feature flag." + } + }, + "required": [ + "defaultEnabled", + "enabled", + "name", + "stage" + ], + "type": "object" + }, + "ExperimentalFeatureEnablementSetParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "enablement": { + "additionalProperties": { + "type": "boolean" + }, + "description": "Process-wide runtime feature enablement keyed by canonical feature name.\n\nOnly named features are updated. Omitted features are left unchanged. Send an empty map for a no-op.", + "type": "object" + } + }, + "required": [ + "enablement" + ], + "title": "ExperimentalFeatureEnablementSetParams", + "type": "object" + }, + "ExperimentalFeatureEnablementSetResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "enablement": { + "additionalProperties": { + "type": "boolean" + }, + "description": "Feature enablement entries updated by this request.", + "type": "object" + } + }, + "required": [ + "enablement" + ], + "title": "ExperimentalFeatureEnablementSetResponse", + "type": "object" + }, + "ExperimentalFeatureListParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "cursor": { + "description": "Opaque pagination cursor returned by a previous call.", + "type": [ + "string", + "null" + ] + }, + "limit": { + "description": "Optional page size; defaults to a reasonable server-side value.", + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "threadId": { + "description": "Optional loaded thread id. Pass this when showing feature state for an existing thread so enablement is computed from that thread's refreshed config, including project-local config for the thread's cwd.", + "type": [ + "string", + "null" + ] + } + }, + "title": "ExperimentalFeatureListParams", + "type": "object" + }, + "ExperimentalFeatureListResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "data": { + "items": { + "$ref": "#/definitions/ExperimentalFeature" + }, + "type": "array" + }, + "nextCursor": { + "description": "Opaque cursor to pass to the next call to continue after the last item. If None, there are no more items to return.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "data" + ], + "title": "ExperimentalFeatureListResponse", + "type": "object" + }, + "ExperimentalFeatureStage": { + "oneOf": [ + { + "description": "Feature is available for user testing and feedback.", + "enum": [ + "beta" + ], + "type": "string" + }, + { + "description": "Feature is still being built and not ready for broad use.", + "enum": [ + "underDevelopment" + ], + "type": "string" + }, + { + "description": "Feature is production-ready.", + "enum": [ + "stable" + ], + "type": "string" + }, + { + "description": "Feature is deprecated and should be avoided.", + "enum": [ + "deprecated" + ], + "type": "string" + }, + { + "description": "Feature flag is retained only for backwards compatibility.", + "enum": [ + "removed" + ], + "type": "string" + } + ] + }, + "ExternalAgentConfigDetectParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "cwds": { + "description": "Zero or more working directories to include for repo-scoped detection.", + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "includeHome": { + "description": "If true, include detection under the user's home directory.", + "type": "boolean" + }, + "maxSessionAgeDays": { + "description": "Maximum age in days for detected sessions. Missing values use the default limit.", + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "maxSessions": { + "description": "Maximum number of sessions to detect. Missing values use the default limit.", + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "migrationSource": { + "description": "Optional migration-source selector. Missing or unrecognized values use the default source.", + "type": [ + "string", + "null" + ] + }, + "source": { + "description": "Deprecated field retained for compatibility. This field is ignored; use `migrationSource` to select the migration source.", + "type": [ + "string", + "null" + ] + } + }, + "title": "ExternalAgentConfigDetectParams", + "type": "object" + }, + "ExternalAgentConfigDetectResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "connectors": { + "default": [], + "items": { + "$ref": "#/definitions/ExternalAgentDetectedConnectorCandidate" + }, + "type": "array" + }, + "items": { + "items": { + "$ref": "#/definitions/ExternalAgentConfigMigrationItem" + }, + "type": "array" + } + }, + "required": [ + "items" + ], + "title": "ExternalAgentConfigDetectResponse", + "type": "object" + }, + "ExternalAgentConfigImportCompletedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "importId": { + "type": "string" + }, + "itemTypeResults": { + "items": { + "$ref": "#/definitions/ExternalAgentConfigImportTypeResult" + }, + "type": "array" + } + }, + "required": [ + "importId", + "itemTypeResults" + ], + "title": "ExternalAgentConfigImportCompletedNotification", + "type": "object" + }, + "ExternalAgentConfigImportHistoriesReadResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "connectors": { + "items": { + "$ref": "#/definitions/ExternalAgentImportedConnectorCandidate" + }, + "type": "array" + }, + "data": { + "items": { + "$ref": "#/definitions/ExternalAgentConfigImportHistory" + }, + "type": "array" + } + }, + "required": [ + "connectors", + "data" + ], + "title": "ExternalAgentConfigImportHistoriesReadResponse", + "type": "object" + }, + "ExternalAgentConfigImportHistory": { + "properties": { + "completedAtMs": { + "format": "int64", + "type": "integer" + }, + "failures": { + "items": { + "$ref": "#/definitions/ExternalAgentConfigImportItemTypeFailure" + }, + "type": "array" + }, + "importId": { + "type": "string" + }, + "providerId": { + "type": [ + "string", + "null" + ] + }, + "successes": { + "items": { + "$ref": "#/definitions/ExternalAgentConfigImportItemTypeSuccess" + }, + "type": "array" + } + }, + "required": [ + "completedAtMs", + "failures", + "importId", + "successes" + ], + "type": "object" + }, + "ExternalAgentConfigImportHistoryRecordParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "itemTypeResults": { + "description": "Completed results grouped by imported item type.", + "items": { + "$ref": "#/definitions/ExternalAgentConfigImportHistoryRecordTypeResultParams" + }, + "type": "array" + }, + "providerId": { + "description": "Opaque provider identifier for the externally completed import.", + "type": "string" + } + }, + "required": [ + "itemTypeResults", + "providerId" + ], + "title": "ExternalAgentConfigImportHistoryRecordParams", + "type": "object" + }, + "ExternalAgentConfigImportHistoryRecordResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "importId": { + "type": "string" + } + }, + "required": [ + "importId" + ], + "title": "ExternalAgentConfigImportHistoryRecordResponse", + "type": "object" + }, + "ExternalAgentConfigImportHistoryRecordSuccessParams": { + "properties": { + "cwd": { + "type": [ + "string", + "null" + ] + }, + "itemType": { + "$ref": "#/definitions/ExternalAgentConfigMigrationItemType" + }, + "source": { + "type": [ + "string", + "null" + ] + }, + "target": { + "type": [ + "string", + "null" + ] + }, + "title": { + "default": null, + "description": "Original title for an imported session, when available.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "itemType" + ], + "type": "object" + }, + "ExternalAgentConfigImportHistoryRecordTypeResultParams": { + "properties": { + "failures": { + "items": { + "$ref": "#/definitions/ExternalAgentConfigImportItemTypeFailure" + }, + "type": "array" + }, + "itemType": { + "$ref": "#/definitions/ExternalAgentConfigMigrationItemType" + }, + "successes": { + "items": { + "$ref": "#/definitions/ExternalAgentConfigImportHistoryRecordSuccessParams" + }, + "type": "array" + } + }, + "required": [ + "failures", + "itemType", + "successes" + ], + "type": "object" + }, + "ExternalAgentConfigImportItemTypeFailure": { + "properties": { + "cwd": { + "type": [ + "string", + "null" + ] + }, + "errorType": { + "type": [ + "string", + "null" + ] + }, + "failureStage": { + "type": "string" + }, + "itemType": { + "$ref": "#/definitions/ExternalAgentConfigMigrationItemType" + }, + "message": { + "type": "string" + }, + "source": { + "type": [ + "string", + "null" + ] + }, + "subErrorType": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "failureStage", + "itemType", + "message" + ], + "type": "object" + }, + "ExternalAgentConfigImportItemTypeSuccess": { + "properties": { + "cwd": { + "type": [ + "string", + "null" + ] + }, + "itemType": { + "$ref": "#/definitions/ExternalAgentConfigMigrationItemType" + }, + "source": { + "type": [ + "string", + "null" + ] + }, + "target": { + "type": [ + "string", + "null" + ] + }, + "title": { + "default": null, + "description": "Original title for an imported session; null for other item types.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "itemType" + ], + "type": "object" + }, + "ExternalAgentConfigImportParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "migrationItems": { + "items": { + "$ref": "#/definitions/ExternalAgentConfigMigrationItem" + }, + "type": "array" + }, + "migrationSource": { + "description": "Migration-source selector used to produce the migration items. Pass the same value to detection and import; missing or unrecognized values use the default source.", + "type": [ + "string", + "null" + ] + }, + "providerId": { + "description": "Opaque provider identifier supplied by the caller for analytics attribution and import history display. This does not select the migration source.", + "type": [ + "string", + "null" + ] + }, + "source": { + "description": "Optional identifier for the product that initiated the import.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "migrationItems" + ], + "title": "ExternalAgentConfigImportParams", + "type": "object" + }, + "ExternalAgentConfigImportProgressNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "importId": { + "type": "string" + }, + "itemTypeResults": { + "items": { + "$ref": "#/definitions/ExternalAgentConfigImportTypeResult" + }, + "type": "array" + } + }, + "required": [ + "importId", + "itemTypeResults" + ], + "title": "ExternalAgentConfigImportProgressNotification", + "type": "object" + }, + "ExternalAgentConfigImportResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "importId": { + "type": "string" + } + }, + "required": [ + "importId" + ], + "title": "ExternalAgentConfigImportResponse", + "type": "object" + }, + "ExternalAgentConfigImportTypeResult": { + "properties": { + "failures": { + "items": { + "$ref": "#/definitions/ExternalAgentConfigImportItemTypeFailure" + }, + "type": "array" + }, + "itemType": { + "$ref": "#/definitions/ExternalAgentConfigMigrationItemType" + }, + "successes": { + "items": { + "$ref": "#/definitions/ExternalAgentConfigImportItemTypeSuccess" + }, + "type": "array" + } + }, + "required": [ + "failures", + "itemType", + "successes" + ], + "type": "object" + }, + "ExternalAgentConfigMigrationItem": { + "properties": { + "cwd": { + "description": "Null or empty means home-scoped migration; non-empty means repo-scoped migration.", + "type": [ + "string", + "null" + ] + }, + "description": { + "type": "string" + }, + "details": { + "anyOf": [ + { + "$ref": "#/definitions/MigrationDetails" + }, + { + "type": "null" + } + ] + }, + "itemType": { + "$ref": "#/definitions/ExternalAgentConfigMigrationItemType" + } + }, + "required": [ + "description", + "itemType" + ], + "type": "object" + }, + "ExternalAgentConfigMigrationItemType": { + "enum": [ + "AGENTS_MD", + "CONFIG", + "SKILLS", + "PLUGINS", + "MCP_SERVER_CONFIG", + "SUBAGENTS", + "HOOKS", + "COMMANDS", + "MEMORY", + "SESSIONS" + ], + "type": "string" + }, + "ExternalAgentDetectedConnectorCandidate": { + "properties": { + "name": { + "type": "string" + }, + "sessionCount": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "source": { + "$ref": "#/definitions/ExternalAgentDetectedConnectorSource" + } + }, + "required": [ + "name", + "sessionCount", + "source" + ], + "type": "object" + }, + "ExternalAgentDetectedConnectorSource": { + "enum": [ + "remoteMcpServersConfig", + "sessionToolUse" + ], + "type": "string" + }, + "ExternalAgentImportedConnectorCandidate": { + "properties": { + "name": { + "type": "string" + }, + "sessionCount": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "source": { + "$ref": "#/definitions/ExternalAgentImportedConnectorSource" + } + }, + "required": [ + "name", + "sessionCount", + "source" + ], + "type": "object" + }, + "ExternalAgentImportedConnectorSource": { + "enum": [ + "remoteMcpServersConfig" + ], + "type": "string" + }, + "FeedbackRequirements": { + "properties": { + "enabled": { + "type": [ + "boolean", + "null" + ] + } + }, + "type": "object" + }, + "FeedbackUploadParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "classification": { + "type": "string" + }, + "extraLogFiles": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "includeLogs": { + "type": "boolean" + }, + "reason": { + "type": [ + "string", + "null" + ] + }, + "tags": { + "additionalProperties": { + "type": "string" + }, + "type": [ + "object", + "null" + ] + }, + "threadId": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "classification" + ], + "title": "FeedbackUploadParams", + "type": "object" + }, + "FeedbackUploadResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "FeedbackUploadResponse", + "type": "object" + }, + "FileChangeOutputDeltaNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Deprecated legacy notification for `apply_patch` textual output.\n\nThe server no longer emits this notification.", + "properties": { + "delta": { + "type": "string" + }, + "itemId": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "delta", + "itemId", + "threadId", + "turnId" + ], + "title": "FileChangeOutputDeltaNotification", + "type": "object" + }, + "FileChangePatchUpdatedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "changes": { + "items": { + "$ref": "#/definitions/FileUpdateChange" + }, + "type": "array" + }, + "itemId": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "changes", + "itemId", + "threadId", + "turnId" + ], + "title": "FileChangePatchUpdatedNotification", + "type": "object" + }, + "FileSystemAccessMode": { + "enum": [ + "read", + "write", + "deny" + ], + "type": "string" + }, + "FileSystemPath": { + "oneOf": [ + { + "properties": { + "path": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": { + "enum": [ + "path" + ], + "title": "PathFileSystemPathType", + "type": "string" + } + }, + "required": [ + "path", + "type" + ], + "title": "PathFileSystemPath", + "type": "object" + }, + { + "properties": { + "pattern": { + "type": "string" + }, + "type": { + "enum": [ + "glob_pattern" + ], + "title": "GlobPatternFileSystemPathType", + "type": "string" + } + }, + "required": [ + "pattern", + "type" + ], + "title": "GlobPatternFileSystemPath", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "special" + ], + "title": "SpecialFileSystemPathType", + "type": "string" + }, + "value": { + "$ref": "#/definitions/FileSystemSpecialPath" + } + }, + "required": [ + "type", + "value" + ], + "title": "SpecialFileSystemPath", + "type": "object" + } + ] + }, + "FileSystemSandboxEntry": { + "properties": { + "access": { + "$ref": "#/definitions/FileSystemAccessMode" + }, + "path": { + "$ref": "#/definitions/FileSystemPath" + } + }, + "required": [ + "access", + "path" + ], + "type": "object" + }, + "FileSystemSpecialPath": { + "oneOf": [ + { + "properties": { + "kind": { + "enum": [ + "root" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "RootFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "minimal" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "MinimalFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "project_roots" + ], + "type": "string" + }, + "subpath": { + "anyOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "kind" + ], + "title": "KindFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "tmpdir" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "TmpdirFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "slash_tmp" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "title": "SlashTmpFileSystemSpecialPath", + "type": "object" + }, + { + "properties": { + "kind": { + "enum": [ + "unknown" + ], + "type": "string" + }, + "path": { + "type": "string" + }, + "subpath": { + "anyOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "kind", + "path" + ], + "type": "object" + } + ] + }, + "FileUpdateChange": { + "properties": { + "diff": { + "type": "string" + }, + "kind": { + "$ref": "#/definitions/PatchChangeKind" + }, + "path": { + "type": "string" + } + }, + "required": [ + "diff", + "kind", + "path" + ], + "type": "object" + }, + "ForcedChatgptWorkspaceIds": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ], + "description": "Backward-compatible API shape for ChatGPT workspace login restrictions." + }, + "ForcedLoginMethod": { + "enum": [ + "chatgpt", + "api" + ], + "type": "string" + }, + "FsChangedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Filesystem watch notification emitted for `fs/watch` subscribers.", + "properties": { + "changedPaths": { + "description": "File or directory paths associated with this event.", + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "watchId": { + "description": "Watch identifier previously provided to `fs/watch`.", + "type": "string" + } + }, + "required": [ + "changedPaths", + "watchId" + ], + "title": "FsChangedNotification", + "type": "object" + }, + "FsCopyParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Copy a file or directory tree on the host filesystem.", + "properties": { + "destinationPath": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute destination path." + }, + "recursive": { + "description": "Required for directory copies; ignored for file copies.", + "type": "boolean" + }, + "sourcePath": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute source path." + } + }, + "required": [ + "destinationPath", + "sourcePath" + ], + "title": "FsCopyParams", + "type": "object" + }, + "FsCopyResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/copy`.", + "title": "FsCopyResponse", + "type": "object" + }, + "FsCreateDirectoryParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Create a directory on the host filesystem.", + "properties": { + "path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute directory path to create." + }, + "recursive": { + "description": "Whether parent directories should also be created. Defaults to `true`.", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "path" + ], + "title": "FsCreateDirectoryParams", + "type": "object" + }, + "FsCreateDirectoryResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/createDirectory`.", + "title": "FsCreateDirectoryResponse", + "type": "object" + }, + "FsGetMetadataParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Request metadata for an absolute path.", + "properties": { + "path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute path to inspect." + } + }, + "required": [ + "path" + ], + "title": "FsGetMetadataParams", + "type": "object" + }, + "FsGetMetadataResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Metadata returned by `fs/getMetadata`.", + "properties": { + "createdAtMs": { + "description": "File creation time in Unix milliseconds when available, otherwise `0`.", + "format": "int64", + "type": "integer" + }, + "isDirectory": { + "description": "Whether the path resolves to a directory.", + "type": "boolean" + }, + "isFile": { + "description": "Whether the path resolves to a regular file.", + "type": "boolean" + }, + "isSymlink": { + "description": "Whether the path itself is a symbolic link.", + "type": "boolean" + }, + "modifiedAtMs": { + "description": "File modification time in Unix milliseconds when available, otherwise `0`.", + "format": "int64", + "type": "integer" + } + }, + "required": [ + "createdAtMs", + "isDirectory", + "isFile", + "isSymlink", + "modifiedAtMs" + ], + "title": "FsGetMetadataResponse", + "type": "object" + }, + "FsReadDirectoryEntry": { + "description": "A directory entry returned by `fs/readDirectory`.", + "properties": { + "fileName": { + "description": "Direct child entry name only, not an absolute or relative path.", + "type": "string" + }, + "isDirectory": { + "description": "Whether this entry resolves to a directory.", + "type": "boolean" + }, + "isFile": { + "description": "Whether this entry resolves to a regular file.", + "type": "boolean" + } + }, + "required": [ + "fileName", + "isDirectory", + "isFile" + ], + "type": "object" + }, + "FsReadDirectoryParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "List direct child names for a directory.", + "properties": { + "path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute directory path to read." + } + }, + "required": [ + "path" + ], + "title": "FsReadDirectoryParams", + "type": "object" + }, + "FsReadDirectoryResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Directory entries returned by `fs/readDirectory`.", + "properties": { + "entries": { + "description": "Direct child entries in the requested directory.", + "items": { + "$ref": "#/definitions/FsReadDirectoryEntry" + }, + "type": "array" + } + }, + "required": [ + "entries" + ], + "title": "FsReadDirectoryResponse", + "type": "object" + }, + "FsReadFileParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Read a file from the host filesystem.", + "properties": { + "path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute path to read." + } + }, + "required": [ + "path" + ], + "title": "FsReadFileParams", + "type": "object" + }, + "FsReadFileResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Base64-encoded file contents returned by `fs/readFile`.", + "properties": { + "dataBase64": { + "description": "File contents encoded as base64.", + "type": "string" + } + }, + "required": [ + "dataBase64" + ], + "title": "FsReadFileResponse", + "type": "object" + }, + "FsRemoveParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Remove a file or directory tree from the host filesystem.", + "properties": { + "force": { + "description": "Whether missing paths should be ignored. Defaults to `true`.", + "type": [ + "boolean", + "null" + ] + }, + "path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute path to remove." + }, + "recursive": { + "description": "Whether directory removal should recurse. Defaults to `true`.", + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "path" + ], + "title": "FsRemoveParams", + "type": "object" + }, + "FsRemoveResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/remove`.", + "title": "FsRemoveResponse", + "type": "object" + }, + "FsUnwatchParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Stop filesystem watch notifications for a prior `fs/watch`.", + "properties": { + "watchId": { + "description": "Watch identifier previously provided to `fs/watch`.", + "type": "string" + } + }, + "required": [ + "watchId" + ], + "title": "FsUnwatchParams", + "type": "object" + }, + "FsUnwatchResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/unwatch`.", + "title": "FsUnwatchResponse", + "type": "object" + }, + "FsWatchParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Start filesystem watch notifications for an absolute path.", + "properties": { + "path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute file or directory path to watch." + }, + "watchId": { + "description": "Connection-scoped watch identifier used for `fs/unwatch` and `fs/changed`.", + "type": "string" + } + }, + "required": [ + "path", + "watchId" + ], + "title": "FsWatchParams", + "type": "object" + }, + "FsWatchResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/watch`.", + "properties": { + "path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Canonicalized path associated with the watch." + } + }, + "required": [ + "path" + ], + "title": "FsWatchResponse", + "type": "object" + }, + "FsWriteFileParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Write a file on the host filesystem.", + "properties": { + "dataBase64": { + "description": "File contents encoded as base64.", + "type": "string" + }, + "path": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Absolute path to write." + } + }, + "required": [ + "dataBase64", + "path" + ], + "title": "FsWriteFileParams", + "type": "object" + }, + "FsWriteFileResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful response for `fs/writeFile`.", + "title": "FsWriteFileResponse", + "type": "object" + }, + "FunctionCallOutputBody": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "$ref": "#/definitions/FunctionCallOutputContentItem" + }, + "type": "array" + } + ] + }, + "FunctionCallOutputContentItem": { + "description": "Responses API compatible content items that can be returned by a tool call. This is a subset of ContentItem with the types we support as function call outputs.", + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "input_text" + ], + "title": "InputTextFunctionCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "InputTextFunctionCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "detail": { + "anyOf": [ + { + "$ref": "#/definitions/ImageDetail" + }, + { + "type": "null" + } + ] + }, + "image_url": { + "type": "string" + }, + "type": { + "enum": [ + "input_image" + ], + "title": "InputImageFunctionCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "image_url", + "type" + ], + "title": "InputImageFunctionCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "audio_url": { + "type": "string" + }, + "type": { + "enum": [ + "input_audio" + ], + "title": "InputAudioFunctionCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "audio_url", + "type" + ], + "title": "InputAudioFunctionCallOutputContentItem", + "type": "object" + }, + { + "properties": { + "encrypted_content": { + "type": "string" + }, + "type": { + "enum": [ + "encrypted_content" + ], + "title": "EncryptedContentFunctionCallOutputContentItemType", + "type": "string" + } + }, + "required": [ + "encrypted_content", + "type" + ], + "title": "EncryptedContentFunctionCallOutputContentItem", + "type": "object" + } + ] + }, + "FuzzyFileSearchMatchType": { + "enum": [ + "file", + "directory" + ], + "type": "string" + }, + "FuzzyFileSearchParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "cancellationToken": { + "type": [ + "string", + "null" + ] + }, + "query": { + "type": "string" + }, + "roots": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "query", + "roots" + ], + "title": "FuzzyFileSearchParams", + "type": "object" + }, + "FuzzyFileSearchResult": { + "description": "Superset of [`codex_file_search::FileMatch`]", + "properties": { + "file_name": { + "type": "string" + }, + "indices": { + "items": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "type": [ + "array", + "null" + ] + }, + "match_type": { + "$ref": "#/definitions/FuzzyFileSearchMatchType" + }, + "path": { + "type": "string" + }, + "root": { + "type": "string" + }, + "score": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "file_name", + "match_type", + "path", + "root", + "score" + ], + "type": "object" + }, + "FuzzyFileSearchSessionCompletedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "sessionId": { + "type": "string" + } + }, + "required": [ + "sessionId" + ], + "title": "FuzzyFileSearchSessionCompletedNotification", + "type": "object" + }, + "FuzzyFileSearchSessionUpdatedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "files": { + "items": { + "$ref": "#/definitions/FuzzyFileSearchResult" + }, + "type": "array" + }, + "query": { + "type": "string" + }, + "sessionId": { + "type": "string" + } + }, + "required": [ + "files", + "query", + "sessionId" + ], + "title": "FuzzyFileSearchSessionUpdatedNotification", + "type": "object" + }, + "GetAccountParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "refreshToken": { + "description": "When `true`, requests a proactive token refresh before returning.\n\nIn managed auth mode this triggers the normal refresh-token flow. In external auth mode this flag is ignored. Clients should refresh tokens themselves and call `account/login/start` with `chatgptAuthTokens`.", + "type": "boolean" + } + }, + "title": "GetAccountParams", + "type": "object" + }, + "GetAccountRateLimitsResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "rateLimitResetCredits": { + "anyOf": [ + { + "$ref": "#/definitions/RateLimitResetCreditsSummary" + }, + { + "type": "null" + } + ] + }, + "rateLimits": { + "allOf": [ + { + "$ref": "#/definitions/RateLimitSnapshot" + } + ], + "description": "Backward-compatible single-bucket view; mirrors the historical payload." + }, + "rateLimitsByLimitId": { + "additionalProperties": { + "$ref": "#/definitions/RateLimitSnapshot" + }, + "description": "Multi-bucket view keyed by metered `limit_id` (for example, `codex`).", + "type": [ + "object", + "null" + ] + } + }, + "required": [ + "rateLimits" + ], + "title": "GetAccountRateLimitsResponse", + "type": "object" + }, + "GetAccountResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "account": { + "anyOf": [ + { + "$ref": "#/definitions/Account" + }, + { + "type": "null" + } + ] + }, + "requiresOpenaiAuth": { + "type": "boolean" + } + }, + "required": [ + "requiresOpenaiAuth" + ], + "title": "GetAccountResponse", + "type": "object" + }, + "GetAccountTokenUsageParams": { + "properties": { + "threadId": { + "description": "When present, read estimated usage for this thread instead of account-wide token activity.", + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "GetAccountTokenUsageResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "dailyUsageBuckets": { + "items": { + "$ref": "#/definitions/AccountTokenUsageDailyBucket" + }, + "type": [ + "array", + "null" + ] + }, + "summary": { + "$ref": "#/definitions/AccountTokenUsageSummary" + }, + "threadUsage": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadUsage" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Estimated usage when a thread was requested and its billing route is available." + } + }, + "required": [ + "summary" + ], + "title": "GetAccountTokenUsageResponse", + "type": "object" + }, + "GetWorkspaceMessagesResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "featureEnabled": { + "description": "Whether the workspace-message backend route is available for this client.", + "type": "boolean" + }, + "messages": { + "description": "Active workspace messages returned by the backend.", + "items": { + "$ref": "#/definitions/WorkspaceMessage" + }, + "type": "array" + } + }, + "required": [ + "featureEnabled", + "messages" + ], + "title": "GetWorkspaceMessagesResponse", + "type": "object" + }, + "GitInfo": { + "properties": { + "branch": { + "type": [ + "string", + "null" + ] + }, + "originUrl": { + "type": [ + "string", + "null" + ] + }, + "sha": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "GuardianApprovalReview": { + "description": "[UNSTABLE] Temporary approval auto-review payload used by `item/autoApprovalReview/*` notifications. This shape is expected to change soon.", + "properties": { + "rationale": { + "type": [ + "string", + "null" + ] + }, + "riskLevel": { + "anyOf": [ + { + "$ref": "#/definitions/GuardianRiskLevel" + }, + { + "type": "null" + } + ] + }, + "status": { + "$ref": "#/definitions/GuardianApprovalReviewStatus" + }, + "userAuthorization": { + "anyOf": [ + { + "$ref": "#/definitions/GuardianUserAuthorization" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "GuardianApprovalReviewAction": { + "oneOf": [ + { + "properties": { + "command": { + "type": "string" + }, + "cwd": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "source": { + "$ref": "#/definitions/GuardianCommandSource" + }, + "type": { + "enum": [ + "command" + ], + "title": "CommandGuardianApprovalReviewActionType", + "type": "string" + } + }, + "required": [ + "command", + "cwd", + "source", + "type" + ], + "title": "CommandGuardianApprovalReviewAction", + "type": "object" + }, + { + "properties": { + "argv": { + "items": { + "type": "string" + }, + "type": "array" + }, + "cwd": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "program": { + "type": "string" + }, + "source": { + "$ref": "#/definitions/GuardianCommandSource" + }, + "type": { + "enum": [ + "execve" + ], + "title": "ExecveGuardianApprovalReviewActionType", + "type": "string" + } + }, + "required": [ + "argv", + "cwd", + "program", + "source", + "type" + ], + "title": "ExecveGuardianApprovalReviewAction", + "type": "object" + }, + { + "description": "A child approval for input to an existing command execution item.", + "properties": { + "approvalId": { + "type": "string" + }, + "cwd": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "processId": { + "type": "string" + }, + "stdin": { + "type": "string" + }, + "type": { + "enum": [ + "writeStdin" + ], + "title": "WriteStdinGuardianApprovalReviewActionType", + "type": "string" + } + }, + "required": [ + "approvalId", + "cwd", + "processId", + "stdin", + "type" + ], + "title": "WriteStdinGuardianApprovalReviewAction", + "type": "object" + }, + { + "properties": { + "cwd": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "files": { + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "type": { + "enum": [ + "applyPatch" + ], + "title": "ApplyPatchGuardianApprovalReviewActionType", + "type": "string" + } + }, + "required": [ + "cwd", + "files", + "type" + ], + "title": "ApplyPatchGuardianApprovalReviewAction", + "type": "object" + }, + { + "properties": { + "host": { + "type": "string" + }, + "port": { + "format": "uint16", + "minimum": 0.0, + "type": "integer" + }, + "protocol": { + "$ref": "#/definitions/NetworkApprovalProtocol" + }, + "target": { + "type": "string" + }, + "type": { + "enum": [ + "networkAccess" + ], + "title": "NetworkAccessGuardianApprovalReviewActionType", + "type": "string" + } + }, + "required": [ + "host", + "port", + "protocol", + "target", + "type" + ], + "title": "NetworkAccessGuardianApprovalReviewAction", + "type": "object" + }, + { + "properties": { + "connectorId": { + "type": [ + "string", + "null" + ] + }, + "connectorName": { + "type": [ + "string", + "null" + ] + }, + "server": { + "type": "string" + }, + "toolName": { + "type": "string" + }, + "toolTitle": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "mcpToolCall" + ], + "title": "McpToolCallGuardianApprovalReviewActionType", + "type": "string" + } + }, + "required": [ + "server", + "toolName", + "type" + ], + "title": "McpToolCallGuardianApprovalReviewAction", + "type": "object" + }, + { + "properties": { + "permissions": { + "$ref": "#/definitions/RequestPermissionProfile" + }, + "reason": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "requestPermissions" + ], + "title": "RequestPermissionsGuardianApprovalReviewActionType", + "type": "string" + } + }, + "required": [ + "permissions", + "type" + ], + "title": "RequestPermissionsGuardianApprovalReviewAction", + "type": "object" + } + ] + }, + "GuardianApprovalReviewStatus": { + "description": "[UNSTABLE] Lifecycle state for an approval auto-review.", + "enum": [ + "inProgress", + "approved", + "denied", + "timedOut", + "aborted" + ], + "type": "string" + }, + "GuardianCommandSource": { + "enum": [ + "shell", + "unifiedExec" + ], + "type": "string" + }, + "GuardianRiskLevel": { + "description": "[UNSTABLE] Risk level assigned by approval auto-review.", + "enum": [ + "low", + "medium", + "high", + "critical" + ], + "type": "string" + }, + "GuardianUserAuthorization": { + "description": "[UNSTABLE] Authorization level assigned by approval auto-review.", + "enum": [ + "unknown", + "low", + "medium", + "high" + ], + "type": "string" + }, + "GuardianWarningNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "message": { + "description": "Concise guardian warning message for the user.", + "type": "string" + }, + "threadId": { + "description": "Thread target for the guardian warning.", + "type": "string" + } + }, + "required": [ + "message", + "threadId" + ], + "title": "GuardianWarningNotification", + "type": "object" + }, + "HookCompletedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "run": { + "$ref": "#/definitions/HookRunSummary" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "run", + "threadId" + ], + "title": "HookCompletedNotification", + "type": "object" + }, + "HookErrorInfo": { + "properties": { + "message": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": [ + "message", + "path" + ], + "type": "object" + }, + "HookEventName": { + "enum": [ + "preToolUse", + "permissionRequest", + "postToolUse", + "preCompact", + "postCompact", + "sessionStart", + "sessionEnd", + "userPromptSubmit", + "subagentStart", + "subagentStop", + "stop", + "interrupt" + ], + "type": "string" + }, + "HookExecutionMode": { + "enum": [ + "sync", + "async" + ], + "type": "string" + }, + "HookHandlerType": { + "enum": [ + "command", + "mcpTool", + "prompt", + "agent" + ], + "type": "string" + }, + "HookMetadata": { + "oneOf": [ + { + "properties": { + "async": { + "default": false, + "type": "boolean" + }, + "command": { + "type": "string" + }, + "handlerType": { + "enum": [ + "command" + ], + "type": "string" + } + }, + "required": [ + "command", + "handlerType" + ], + "type": "object" + }, + { + "properties": { + "handlerType": { + "enum": [ + "mcpTool" + ], + "type": "string" + }, + "server": { + "type": "string" + }, + "tool": { + "type": "string" + } + }, + "required": [ + "handlerType", + "server", + "tool" + ], + "type": "object" + }, + { + "properties": { + "handlerType": { + "enum": [ + "prompt" + ], + "type": "string" + } + }, + "required": [ + "handlerType" + ], + "title": "PromptHookMetadata", + "type": "object" + }, + { + "properties": { + "handlerType": { + "enum": [ + "agent" + ], + "type": "string" + } + }, + "required": [ + "handlerType" + ], + "title": "AgentHookMetadata", + "type": "object" + } + ], + "properties": { + "additionalContextLimit": { + "description": "Configured `additionalContext` spill threshold. `null` uses 2,500 tokens; `0` disables spilling.", + "format": "uint", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "currentHash": { + "type": "string" + }, + "displayOrder": { + "format": "int64", + "type": "integer" + }, + "enabled": { + "type": "boolean" + }, + "eventName": { + "$ref": "#/definitions/HookEventName" + }, + "isManaged": { + "type": "boolean" + }, + "key": { + "type": "string" + }, + "matcher": { + "type": [ + "string", + "null" + ] + }, + "pluginId": { + "type": [ + "string", + "null" + ] + }, + "source": { + "$ref": "#/definitions/HookSource" + }, + "sourcePath": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "statusMessage": { + "type": [ + "string", + "null" + ] + }, + "timeoutSec": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + }, + "trustStatus": { + "$ref": "#/definitions/HookTrustStatus" + } + }, + "required": [ + "currentHash", + "displayOrder", + "enabled", + "eventName", + "isManaged", + "key", + "source", + "sourcePath", + "timeoutSec", + "trustStatus" + ], + "type": "object" + }, + "HookMigration": { + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "HookOutputEntry": { + "properties": { + "kind": { + "$ref": "#/definitions/HookOutputEntryKind" + }, + "text": { + "type": "string" + } + }, + "required": [ + "kind", + "text" + ], + "type": "object" + }, + "HookOutputEntryKind": { + "enum": [ + "warning", + "stop", + "feedback", + "context", + "error" + ], + "type": "string" + }, + "HookPromptFragment": { + "properties": { + "hookRunId": { + "type": "string" + }, + "text": { + "type": "string" + } + }, + "required": [ + "hookRunId", + "text" + ], + "type": "object" + }, + "HookRunStatus": { + "enum": [ + "running", + "completed", + "failed", + "blocked", + "stopped" + ], + "type": "string" + }, + "HookRunSummary": { + "properties": { + "completedAt": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "displayOrder": { + "format": "int64", + "type": "integer" + }, + "durationMs": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "entries": { + "items": { + "$ref": "#/definitions/HookOutputEntry" + }, + "type": "array" + }, + "eventName": { + "$ref": "#/definitions/HookEventName" + }, + "executionMode": { + "$ref": "#/definitions/HookExecutionMode" + }, + "handlerType": { + "$ref": "#/definitions/HookHandlerType" + }, + "id": { + "type": "string" + }, + "scope": { + "$ref": "#/definitions/HookScope" + }, + "source": { + "allOf": [ + { + "$ref": "#/definitions/HookSource" + } + ], + "default": "unknown" + }, + "sourcePath": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "startedAt": { + "format": "int64", + "type": "integer" + }, + "status": { + "$ref": "#/definitions/HookRunStatus" + }, + "statusMessage": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "displayOrder", + "entries", + "eventName", + "executionMode", + "handlerType", + "id", + "scope", + "sourcePath", + "startedAt", + "status" + ], + "type": "object" + }, + "HookScope": { + "enum": [ + "thread", + "turn" + ], + "type": "string" + }, + "HookSource": { + "enum": [ + "system", + "user", + "project", + "mdm", + "sessionFlags", + "plugin", + "cloudRequirements", + "cloudManagedConfig", + "legacyManagedConfigFile", + "legacyManagedConfigMdm", + "unknown" + ], + "type": "string" + }, + "HookStartedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "run": { + "$ref": "#/definitions/HookRunSummary" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "run", + "threadId" + ], + "title": "HookStartedNotification", + "type": "object" + }, + "HookTrustStatus": { + "enum": [ + "managed", + "untrusted", + "trusted", + "modified" + ], + "type": "string" + }, + "HooksListEntry": { + "properties": { + "cwd": { + "type": "string" + }, + "errors": { + "items": { + "$ref": "#/definitions/HookErrorInfo" + }, + "type": "array" + }, + "hooks": { + "items": { + "$ref": "#/definitions/HookMetadata" + }, + "type": "array" + }, + "warnings": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "cwd", + "errors", + "hooks", + "warnings" + ], + "type": "object" + }, + "HooksListParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "cwds": { + "description": "When empty, defaults to the current session working directory.", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "title": "HooksListParams", + "type": "object" + }, + "HooksListResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "data": { + "items": { + "$ref": "#/definitions/HooksListEntry" + }, + "type": "array" + } + }, + "required": [ + "data" + ], + "title": "HooksListResponse", + "type": "object" + }, + "ImageDetail": { + "enum": [ + "auto", + "low", + "high", + "original" + ], + "type": "string" + }, + "ImageGenerationFailure": { + "oneOf": [ + { + "properties": { + "limitId": { + "type": "string" + }, + "resetsAt": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "usageLimitExceeded" + ], + "title": "UsageLimitExceededImageGenerationFailureType", + "type": "string" + } + }, + "required": [ + "limitId", + "type" + ], + "title": "UsageLimitExceededImageGenerationFailure", + "type": "object" + } + ] + }, + "InAppBrowserRequirements": { + "properties": { + "allowExternalBrowserSettingsImport": { + "type": [ + "boolean", + "null" + ] + } + }, + "type": "object" + }, + "InitializeCapabilities": { + "description": "Client-declared capabilities negotiated during initialize.", + "properties": { + "experimentalApi": { + "default": false, + "description": "Opt into receiving experimental API methods and fields.", + "type": "boolean" + }, + "extensions": { + "additionalProperties": true, + "description": "MCP extension settings declared by the app-server client.", + "type": [ + "object", + "null" + ] + }, + "mcpServerOpenaiFormElicitation": { + "description": "Legacy opt-in for the `openai/form` MCP extension.\n\nNew clients should declare `openai/form` in [`Self::extensions`].", + "type": "boolean" + }, + "optOutNotificationMethods": { + "description": "Exact notification method names that should be suppressed for this connection (for example `thread/started`).", + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "requestAttestation": { + "default": false, + "description": "Opt into `attestation/generate` requests for upstream `x-oai-attestation`.", + "type": "boolean" + } + }, + "type": "object" + }, + "InitializeParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "capabilities": { + "anyOf": [ + { + "$ref": "#/definitions/InitializeCapabilities" + }, + { + "type": "null" + } + ] + }, + "clientInfo": { + "$ref": "#/definitions/ClientInfo" + } + }, + "required": [ + "clientInfo" + ], + "title": "InitializeParams", + "type": "object" + }, + "InputModality": { + "description": "Canonical user-input modality tags advertised by a model.", + "oneOf": [ + { + "description": "Plain text turns and tool payloads.", + "enum": [ + "text" + ], + "type": "string" + }, + { + "description": "Image attachments included in user turns.", + "enum": [ + "image" + ], + "type": "string" + }, + { + "description": "Audio attachments included in user turns.", + "enum": [ + "audio" + ], + "type": "string" + } + ] + }, + "InstalledApp": { + "description": "Installed connector runtime state.", + "properties": { + "callable": { + "description": "Whether the connector is enabled and has a non-synthetic, model-visible tool allowed by effective MCP and app/tool policy in the committed runtime snapshot.", + "type": "boolean" + }, + "enabled": { + "description": "Effective enabled state after applying global, workspace, local, and managed configuration at read time.", + "type": "boolean" + }, + "id": { + "type": "string" + }, + "runtimeName": { + "description": "Best-effort name carried by the runtime tool catalog. Canonical app metadata remains owned by `app/read`.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "callable", + "enabled", + "id" + ], + "type": "object" + }, + "InternalChatMessageMetadataPassthrough": { + "description": "Internal Responses API passthrough metadata copied into underlying chat messages.\n\nResponses API strongly types this payload. Do not modify it without first getting API approval and making the corresponding Responses API change.", + "properties": { + "turn_id": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "ItemCompletedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "completedAtMs": { + "description": "Unix timestamp (in milliseconds) when this item lifecycle completed.", + "format": "int64", + "type": "integer" + }, + "item": { + "$ref": "#/definitions/ThreadItem" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "completedAtMs", + "item", + "threadId", + "turnId" + ], + "title": "ItemCompletedNotification", + "type": "object" + }, + "ItemGuardianApprovalReviewCompletedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "[UNSTABLE] Temporary notification payload for approval auto-review. This shape is expected to change soon.", + "properties": { + "action": { + "$ref": "#/definitions/GuardianApprovalReviewAction" + }, + "completedAtMs": { + "description": "Unix timestamp (in milliseconds) when this review completed.", + "format": "int64", + "type": "integer" + }, + "decisionSource": { + "$ref": "#/definitions/AutoReviewDecisionSource" + }, + "review": { + "$ref": "#/definitions/GuardianApprovalReview" + }, + "reviewId": { + "description": "Stable identifier for this review.", + "type": "string" + }, + "startedAtMs": { + "description": "Unix timestamp (in milliseconds) when this review started.", + "format": "int64", + "type": "integer" + }, + "targetItemId": { + "description": "Identifier for the reviewed item or tool call when one exists.\n\nIn most cases, one review maps to one target item. The exceptions are - execve reviews, where a single command may contain multiple execve calls to review (only possible when using the shell_zsh_fork feature) - stdin reviews, which refer to the existing parent command item and have a separate approval ID in the action payload - network policy reviews, where there is no target item\n\nA network call is triggered by a CommandExecution item, so having a target_item_id set to the CommandExecution item would be misleading because the review is about the network call, not the command execution. Therefore, target_item_id is set to None for network policy reviews.", + "type": [ + "string", + "null" + ] + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "action", + "completedAtMs", + "decisionSource", + "review", + "reviewId", + "startedAtMs", + "threadId", + "turnId" + ], + "title": "ItemGuardianApprovalReviewCompletedNotification", + "type": "object" + }, + "ItemGuardianApprovalReviewStartedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "[UNSTABLE] Temporary notification payload for approval auto-review. This shape is expected to change soon.", + "properties": { + "action": { + "$ref": "#/definitions/GuardianApprovalReviewAction" + }, + "review": { + "$ref": "#/definitions/GuardianApprovalReview" + }, + "reviewId": { + "description": "Stable identifier for this review.", + "type": "string" + }, + "startedAtMs": { + "description": "Unix timestamp (in milliseconds) when this review started.", + "format": "int64", + "type": "integer" + }, + "targetItemId": { + "description": "Identifier for the reviewed item or tool call when one exists.\n\nIn most cases, one review maps to one target item. The exceptions are - execve reviews, where a single command may contain multiple execve calls to review (only possible when using the shell_zsh_fork feature) - stdin reviews, which refer to the existing parent command item and have a separate approval ID in the action payload - network policy reviews, where there is no target item\n\nA network call is triggered by a CommandExecution item, so having a target_item_id set to the CommandExecution item would be misleading because the review is about the network call, not the command execution. Therefore, target_item_id is set to None for network policy reviews.", + "type": [ + "string", + "null" + ] + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "action", + "review", + "reviewId", + "startedAtMs", + "threadId", + "turnId" + ], + "title": "ItemGuardianApprovalReviewStartedNotification", + "type": "object" + }, + "ItemStartedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "item": { + "$ref": "#/definitions/ThreadItem" + }, + "startedAtMs": { + "description": "Unix timestamp (in milliseconds) when this item lifecycle started.", + "format": "int64", + "type": "integer" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "item", + "startedAtMs", + "threadId", + "turnId" + ], + "title": "ItemStartedNotification", + "type": "object" + }, + "LegacyAppPathString": { + "type": "string" + }, + "ListMcpServerStatusParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "cursor": { + "description": "Opaque pagination cursor returned by a previous call.", + "type": [ + "string", + "null" + ] + }, + "detail": { + "anyOf": [ + { + "$ref": "#/definitions/McpServerStatusDetail" + }, + { + "type": "null" + } + ], + "description": "Controls how much MCP inventory data to fetch for each server. Defaults to `Full` when omitted." + }, + "limit": { + "description": "Optional page size; defaults to a server-defined value.", + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "threadId": { + "type": [ + "string", + "null" + ] + } + }, + "title": "ListMcpServerStatusParams", + "type": "object" + }, + "ListMcpServerStatusResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "data": { + "items": { + "$ref": "#/definitions/McpServerStatus" + }, + "type": "array" + }, + "nextCursor": { + "description": "Opaque cursor to pass to the next call to continue after the last item. If None, there are no more items to return.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "data" + ], + "title": "ListMcpServerStatusResponse", + "type": "object" + }, + "LocalShellAction": { + "oneOf": [ + { + "properties": { + "command": { + "items": { + "type": "string" + }, + "type": "array" + }, + "env": { + "additionalProperties": { + "type": "string" + }, + "type": [ + "object", + "null" + ] + }, + "timeout_ms": { + "format": "uint64", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "exec" + ], + "title": "ExecLocalShellActionType", + "type": "string" + }, + "user": { + "type": [ + "string", + "null" + ] + }, + "working_directory": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "command", + "type" + ], + "title": "ExecLocalShellAction", + "type": "object" + } + ] + }, + "LocalShellStatus": { + "enum": [ + "completed", + "in_progress", + "incomplete" + ], + "type": "string" + }, + "LoginAccountParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "oneOf": [ + { + "properties": { + "apiKey": { + "type": "string" + }, + "type": { + "enum": [ + "apiKey" + ], + "title": "ApiKeyv2::LoginAccountParamsType", + "type": "string" + } + }, + "required": [ + "apiKey", + "type" + ], + "title": "ApiKeyv2::LoginAccountParams", + "type": "object" + }, + { + "properties": { + "appBrand": { + "anyOf": [ + { + "$ref": "#/definitions/LoginAppBrand" + }, + { + "type": "null" + } + ], + "default": null + }, + "codexStreamlinedLogin": { + "type": "boolean" + }, + "type": { + "enum": [ + "chatgpt" + ], + "title": "Chatgptv2::LoginAccountParamsType", + "type": "string" + }, + "useHostedLoginSuccessPage": { + "type": "boolean" + } + }, + "required": [ + "type" + ], + "title": "Chatgptv2::LoginAccountParams", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "chatgptDeviceCode" + ], + "title": "ChatgptDeviceCodev2::LoginAccountParamsType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "ChatgptDeviceCodev2::LoginAccountParams", + "type": "object" + }, + { + "description": "[UNSTABLE] FOR OPENAI INTERNAL USE ONLY - DO NOT USE. The access token must contain the same scopes that Codex-managed ChatGPT auth tokens have.", + "properties": { + "accessToken": { + "description": "Access token (JWT) supplied by the client. This token is used for backend API requests and email extraction.", + "type": "string" + }, + "chatgptAccountId": { + "description": "Workspace/account identifier supplied by the client.", + "type": "string" + }, + "chatgptPlanType": { + "description": "Optional plan type supplied by the client.\n\nWhen `null`, Codex attempts to derive the plan type from access-token claims. If unavailable, the plan defaults to `unknown`.", + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "chatgptAuthTokens" + ], + "title": "ChatgptAuthTokensv2::LoginAccountParamsType", + "type": "string" + } + }, + "required": [ + "accessToken", + "chatgptAccountId", + "type" + ], + "title": "ChatgptAuthTokensv2::LoginAccountParams", + "type": "object" + }, + { + "description": "[UNSTABLE] Managed Amazon Bedrock login is experimental.", + "properties": { + "apiKey": { + "type": "string" + }, + "region": { + "type": "string" + }, + "type": { + "enum": [ + "amazonBedrock" + ], + "title": "AmazonBedrockv2::LoginAccountParamsType", + "type": "string" + } + }, + "required": [ + "apiKey", + "region", + "type" + ], + "title": "AmazonBedrockv2::LoginAccountParams", + "type": "object" + }, + { + "description": "[UNSTABLE] Managed Amazon Bedrock AWS access key login is experimental.", + "properties": { + "accessKeyId": { + "type": "string" + }, + "region": { + "type": "string" + }, + "secretAccessKey": { + "type": "string" + }, + "sessionToken": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "amazonBedrockAccessKeys" + ], + "title": "AmazonBedrockAccessKeysv2::LoginAccountParamsType", + "type": "string" + } + }, + "required": [ + "accessKeyId", + "region", + "secretAccessKey", + "type" + ], + "title": "AmazonBedrockAccessKeysv2::LoginAccountParams", + "type": "object" + } + ], + "title": "LoginAccountParams" + }, + "LoginAccountResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "oneOf": [ + { + "properties": { + "type": { + "enum": [ + "apiKey" + ], + "title": "ApiKeyv2::LoginAccountResponseType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "ApiKeyv2::LoginAccountResponse", + "type": "object" + }, + { + "properties": { + "authUrl": { + "description": "URL the client should open in a browser to initiate the OAuth flow.", + "type": "string" + }, + "loginId": { + "type": "string" + }, + "type": { + "enum": [ + "chatgpt" + ], + "title": "Chatgptv2::LoginAccountResponseType", + "type": "string" + } + }, + "required": [ + "authUrl", + "loginId", + "type" + ], + "title": "Chatgptv2::LoginAccountResponse", + "type": "object" + }, + { + "properties": { + "loginId": { + "type": "string" + }, + "type": { + "enum": [ + "chatgptDeviceCode" + ], + "title": "ChatgptDeviceCodev2::LoginAccountResponseType", + "type": "string" + }, + "userCode": { + "description": "One-time code the user must enter after signing in.", + "type": "string" + }, + "verificationUrl": { + "description": "URL the client should open in a browser to complete device code authorization.", + "type": "string" + } + }, + "required": [ + "loginId", + "type", + "userCode", + "verificationUrl" + ], + "title": "ChatgptDeviceCodev2::LoginAccountResponse", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "chatgptAuthTokens" + ], + "title": "ChatgptAuthTokensv2::LoginAccountResponseType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "ChatgptAuthTokensv2::LoginAccountResponse", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "amazonBedrock" + ], + "title": "AmazonBedrockv2::LoginAccountResponseType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "AmazonBedrockv2::LoginAccountResponse", + "type": "object" + } + ], + "title": "LoginAccountResponse" + }, + "LoginAppBrand": { + "enum": [ + "codex", + "chatgpt" + ], + "type": "string" + }, + "LogoutAccountResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "LogoutAccountResponse", + "type": "object" + }, + "ManagedHooksRequirements": { + "properties": { + "Interrupt": { + "default": [], + "items": { + "$ref": "#/definitions/ConfiguredHookMatcherGroup" + }, + "type": "array" + }, + "PermissionRequest": { + "items": { + "$ref": "#/definitions/ConfiguredHookMatcherGroup" + }, + "type": "array" + }, + "PostCompact": { + "items": { + "$ref": "#/definitions/ConfiguredHookMatcherGroup" + }, + "type": "array" + }, + "PostToolUse": { + "items": { + "$ref": "#/definitions/ConfiguredHookMatcherGroup" + }, + "type": "array" + }, + "PreCompact": { + "items": { + "$ref": "#/definitions/ConfiguredHookMatcherGroup" + }, + "type": "array" + }, + "PreToolUse": { + "items": { + "$ref": "#/definitions/ConfiguredHookMatcherGroup" + }, + "type": "array" + }, + "SessionEnd": { + "default": [], + "items": { + "$ref": "#/definitions/ConfiguredHookMatcherGroup" + }, + "type": "array" + }, + "SessionStart": { + "items": { + "$ref": "#/definitions/ConfiguredHookMatcherGroup" + }, + "type": "array" + }, + "Stop": { + "items": { + "$ref": "#/definitions/ConfiguredHookMatcherGroup" + }, + "type": "array" + }, + "SubagentStart": { + "items": { + "$ref": "#/definitions/ConfiguredHookMatcherGroup" + }, + "type": "array" + }, + "SubagentStop": { + "items": { + "$ref": "#/definitions/ConfiguredHookMatcherGroup" + }, + "type": "array" + }, + "UserPromptSubmit": { + "items": { + "$ref": "#/definitions/ConfiguredHookMatcherGroup" + }, + "type": "array" + }, + "managedDir": { + "type": [ + "string", + "null" + ] + }, + "windowsManagedDir": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "PermissionRequest", + "PostCompact", + "PostToolUse", + "PreCompact", + "PreToolUse", + "SessionStart", + "Stop", + "SubagentStart", + "SubagentStop", + "UserPromptSubmit" + ], + "type": "object" + }, + "MarketplaceAddParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "refName": { + "type": [ + "string", + "null" + ] + }, + "source": { + "type": "string" + }, + "sparsePaths": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "source" + ], + "title": "MarketplaceAddParams", + "type": "object" + }, + "MarketplaceAddResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "alreadyAdded": { + "type": "boolean" + }, + "installedRoot": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "marketplaceName": { + "type": "string" + } + }, + "required": [ + "alreadyAdded", + "installedRoot", + "marketplaceName" + ], + "title": "MarketplaceAddResponse", + "type": "object" + }, + "MarketplaceInterface": { + "properties": { + "displayName": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "MarketplaceLoadErrorInfo": { + "properties": { + "marketplacePath": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "message": { + "type": "string" + } + }, + "required": [ + "marketplacePath", + "message" + ], + "type": "object" + }, + "MarketplaceRemoveParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "marketplaceName": { + "type": "string" + } + }, + "required": [ + "marketplaceName" + ], + "title": "MarketplaceRemoveParams", + "type": "object" + }, + "MarketplaceRemoveResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "installedRoot": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "marketplaceName": { + "type": "string" + } + }, + "required": [ + "marketplaceName" + ], + "title": "MarketplaceRemoveResponse", + "type": "object" + }, + "MarketplaceUpgradeErrorInfo": { + "properties": { + "marketplaceName": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "marketplaceName", + "message" + ], + "type": "object" + }, + "MarketplaceUpgradeParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "marketplaceName": { + "type": [ + "string", + "null" + ] + } + }, + "title": "MarketplaceUpgradeParams", + "type": "object" + }, + "MarketplaceUpgradeResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "errors": { + "items": { + "$ref": "#/definitions/MarketplaceUpgradeErrorInfo" + }, + "type": "array" + }, + "selectedMarketplaces": { + "items": { + "type": "string" + }, + "type": "array" + }, + "upgradedRoots": { + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + } + }, + "required": [ + "errors", + "selectedMarketplaces", + "upgradedRoots" + ], + "title": "MarketplaceUpgradeResponse", + "type": "object" + }, + "McpAuthStatus": { + "enum": [ + "unknown", + "unsupported", + "notLoggedIn", + "bearerToken", + "oAuth" + ], + "type": "string" + }, + "McpResourceReadParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "connectorId": { + "type": [ + "string", + "null" + ] + }, + "originCallId": { + "description": "Originating MCP tool call used to select the resource's app.", + "type": [ + "string", + "null" + ] + }, + "server": { + "type": "string" + }, + "threadId": { + "type": [ + "string", + "null" + ] + }, + "uri": { + "type": "string" + } + }, + "required": [ + "server", + "uri" + ], + "title": "McpResourceReadParams", + "type": "object" + }, + "McpResourceReadResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "contents": { + "items": { + "$ref": "#/definitions/ResourceContent" + }, + "type": "array" + }, + "originCallId": { + "description": "Originating call when the server applied app-specific resource scoping.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "contents" + ], + "title": "McpResourceReadResponse", + "type": "object" + }, + "McpServerConnectionStatus": { + "enum": [ + "notStarted", + "starting", + "connected", + "authenticationRequired", + "failed", + "cancelled", + "disabled" + ], + "type": "string" + }, + "McpServerEventNotification": { + "properties": { + "method": { + "type": "string" + }, + "params": true + }, + "required": [ + "method", + "params" + ], + "type": "object" + }, + "McpServerEventStreamNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "notification": { + "$ref": "#/definitions/McpServerEventNotification" + }, + "subscriptionId": { + "type": "string" + } + }, + "required": [ + "notification", + "subscriptionId" + ], + "title": "McpServerEventStreamNotification", + "type": "object" + }, + "McpServerInfo": { + "description": "Presentation metadata advertised by an initialized MCP server.", + "properties": { + "description": { + "type": [ + "string", + "null" + ] + }, + "icons": { + "items": true, + "type": [ + "array", + "null" + ] + }, + "name": { + "type": "string" + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "version": { + "type": "string" + }, + "websiteUrl": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "name", + "version" + ], + "type": "object" + }, + "McpServerMigration": { + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "McpServerOauthClientRegistration": { + "enum": [ + "auto", + "cimd", + "dcr" + ], + "type": "string" + }, + "McpServerOauthLoginCompletedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "error": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": "string" + }, + "success": { + "type": "boolean" + }, + "threadId": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "name", + "success" + ], + "title": "McpServerOauthLoginCompletedNotification", + "type": "object" + }, + "McpServerOauthLoginParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "clientRegistration": { + "anyOf": [ + { + "$ref": "#/definitions/McpServerOauthClientRegistration" + }, + { + "type": "null" + } + ], + "description": "Registration strategy for this login only; omission selects automatic discovery." + }, + "name": { + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "threadId": { + "type": [ + "string", + "null" + ] + }, + "timeoutSecs": { + "format": "int64", + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "name" + ], + "title": "McpServerOauthLoginParams", + "type": "object" + }, + "McpServerOauthLoginResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "authorizationUrl": { + "type": "string" + } + }, + "required": [ + "authorizationUrl" + ], + "title": "McpServerOauthLoginResponse", + "type": "object" + }, + "McpServerRefreshResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "McpServerRefreshResponse", + "type": "object" + }, + "McpServerStartupFailureReason": { + "enum": [ + "reauthenticationRequired" + ], + "type": "string" + }, + "McpServerStartupState": { + "enum": [ + "starting", + "ready", + "failed", + "cancelled" + ], + "type": "string" + }, + "McpServerStatus": { + "properties": { + "authStatus": { + "$ref": "#/definitions/McpAuthStatus" + }, + "name": { + "type": "string" + }, + "pluginId": { + "type": [ + "string", + "null" + ] + }, + "resourceTemplates": { + "items": { + "$ref": "#/definitions/ResourceTemplate" + }, + "type": "array" + }, + "resources": { + "items": { + "$ref": "#/definitions/Resource" + }, + "type": "array" + }, + "runtimeStatus": { + "anyOf": [ + { + "$ref": "#/definitions/McpServerConnectionStatus" + }, + { + "type": "null" + } + ], + "description": "Current thread-runtime connection state; null when unavailable or the configuration changed." + }, + "serverInfo": { + "anyOf": [ + { + "$ref": "#/definitions/McpServerInfo" + }, + { + "type": "null" + } + ] + }, + "tools": { + "additionalProperties": { + "$ref": "#/definitions/Tool" + }, + "type": "object" + } + }, + "required": [ + "authStatus", + "name", + "resourceTemplates", + "resources", + "tools" + ], + "type": "object" + }, + "McpServerStatusDetail": { + "enum": [ + "full", + "toolsAndAuthOnly" + ], + "type": "string" + }, + "McpServerStatusUpdatedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "error": { + "type": [ + "string", + "null" + ] + }, + "failureReason": { + "anyOf": [ + { + "$ref": "#/definitions/McpServerStartupFailureReason" + }, + { + "type": "null" + } + ] + }, + "name": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/McpServerStartupState" + }, + "threadId": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "name", + "status" + ], + "title": "McpServerStatusUpdatedNotification", + "type": "object" + }, + "McpServerToolCallParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "_meta": true, + "arguments": true, + "server": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "tool": { + "type": "string" + } + }, + "required": [ + "server", + "threadId", + "tool" + ], + "title": "McpServerToolCallParams", + "type": "object" + }, + "McpServerToolCallResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "_meta": true, + "content": { + "items": true, + "type": "array" + }, + "isError": { + "type": [ + "boolean", + "null" + ] + }, + "structuredContent": true + }, + "required": [ + "content" + ], + "title": "McpServerToolCallResponse", + "type": "object" + }, + "McpToolCallAppContext": { + "properties": { + "actionName": { + "type": [ + "string", + "null" + ] + }, + "appName": { + "type": [ + "string", + "null" + ] + }, + "connectorId": { + "type": "string" + }, + "linkId": { + "type": [ + "string", + "null" + ] + }, + "resourceUri": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "connectorId" + ], + "type": "object" + }, + "McpToolCallError": { + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, + "McpToolCallProgressNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "itemId": { + "type": "string" + }, + "message": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "itemId", + "message", + "threadId", + "turnId" + ], + "title": "McpToolCallProgressNotification", + "type": "object" + }, + "McpToolCallResult": { + "properties": { + "_meta": true, + "content": { + "items": true, + "type": "array" + }, + "structuredContent": true + }, + "required": [ + "content" + ], + "type": "object" + }, + "McpToolCallStatus": { + "enum": [ + "inProgress", + "completed", + "failed" + ], + "type": "string" + }, + "MemoryCitation": { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/MemoryCitationEntry" + }, + "type": "array" + }, + "threadIds": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "entries", + "threadIds" + ], + "type": "object" + }, + "MemoryCitationEntry": { + "properties": { + "lineEnd": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "lineStart": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "note": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": [ + "lineEnd", + "lineStart", + "note", + "path" + ], + "type": "object" + }, + "MergeStrategy": { + "enum": [ + "replace", + "upsert" + ], + "type": "string" + }, + "MessagePhase": { + "description": "Classifies an assistant message as interim commentary or final answer text.\n\nProviders do not emit this consistently, so callers must treat `None` as \"phase unknown\" and keep compatibility behavior for legacy models.", + "oneOf": [ + { + "description": "Mid-turn assistant text (for example preamble/progress narration).\n\nAdditional tool calls or assistant output may follow before turn completion.", + "enum": [ + "commentary" + ], + "type": "string" + }, + { + "description": "The assistant's terminal answer text for the current turn.", + "enum": [ + "final_answer" + ], + "type": "string" + } + ] + }, + "MigrationDetails": { + "properties": { + "commands": { + "default": [], + "items": { + "$ref": "#/definitions/CommandMigration" + }, + "type": "array" + }, + "hooks": { + "default": [], + "items": { + "$ref": "#/definitions/HookMigration" + }, + "type": "array" + }, + "mcpServers": { + "default": [], + "items": { + "$ref": "#/definitions/McpServerMigration" + }, + "type": "array" + }, + "memory": { + "items": { + "type": "string" + }, + "type": "array" + }, + "plugins": { + "default": [], + "items": { + "$ref": "#/definitions/PluginsMigration" + }, + "type": "array" + }, + "sessions": { + "default": [], + "items": { + "$ref": "#/definitions/SessionMigration" + }, + "type": "array" + }, + "skills": { + "default": [], + "items": { + "$ref": "#/definitions/SkillMigration" + }, + "type": "array" + }, + "subagents": { + "default": [], + "items": { + "$ref": "#/definitions/SubagentMigration" + }, + "type": "array" + } + }, + "type": "object" + }, + "MisalignmentErrorDetails": { + "properties": { + "detailedExplanation": { + "description": "A substantive localized explanation is required before offering continuation.", + "type": [ + "string", + "null" + ] + }, + "errorType": { + "description": "Open-ended classification; clients must accept categories added by Responses.", + "type": [ + "string", + "null" + ] + }, + "steer": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentSteer" + }, + { + "type": "null" + } + ], + "description": "Instruction to submit as the next turn's user input if continuation is confirmed." + } + }, + "type": "object" + }, + "MisalignmentSteer": { + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, + "ModeKind": { + "description": "Initial collaboration mode to use when the TUI starts.", + "enum": [ + "plan", + "default" + ], + "type": "string" + }, + "Model": { + "properties": { + "additionalSpeedTiers": { + "default": [], + "description": "Deprecated: use `serviceTiers` instead.", + "items": { + "type": "string" + }, + "type": "array" + }, + "availabilityNux": { + "anyOf": [ + { + "$ref": "#/definitions/ModelAvailabilityNux" + }, + { + "type": "null" + } + ] + }, + "defaultReasoningEffort": { + "$ref": "#/definitions/ReasoningEffort" + }, + "defaultServiceTier": { + "default": null, + "description": "Catalog default service tier id for this model, when one is configured.", + "type": [ + "string", + "null" + ] + }, + "description": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "hidden": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "inputModalities": { + "default": [ + "text", + "image" + ], + "items": { + "$ref": "#/definitions/InputModality" + }, + "type": "array" + }, + "isDefault": { + "type": "boolean" + }, + "model": { + "type": "string" + }, + "modelSpecialty": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "multiAgentVersion": { + "anyOf": [ + { + "$ref": "#/definitions/MultiAgentVersion" + }, + { + "type": "null" + } + ], + "description": "Multi-agent runtime declared by this model, when available." + }, + "serviceTiers": { + "default": [], + "items": { + "$ref": "#/definitions/ModelServiceTier" + }, + "type": "array" + }, + "supportedReasoningEfforts": { + "items": { + "$ref": "#/definitions/ReasoningEffortOption" + }, + "type": "array" + }, + "supportsPersonality": { + "default": false, + "type": "boolean" + }, + "upgrade": { + "type": [ + "string", + "null" + ] + }, + "upgradeInfo": { + "anyOf": [ + { + "$ref": "#/definitions/ModelUpgradeInfo" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "defaultReasoningEffort", + "description", + "displayName", + "hidden", + "id", + "isDefault", + "model", + "supportedReasoningEfforts" + ], + "type": "object" + }, + "ModelAvailabilityNux": { + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, + "ModelListParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "cursor": { + "description": "Opaque pagination cursor returned by a previous call.", + "type": [ + "string", + "null" + ] + }, + "includeHidden": { + "description": "When true, include models that are hidden from the default picker list.", + "type": [ + "boolean", + "null" + ] + }, + "limit": { + "description": "Optional page size; defaults to a reasonable server-side value.", + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "title": "ModelListParams", + "type": "object" + }, + "ModelListResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "data": { + "items": { + "$ref": "#/definitions/Model" + }, + "type": "array" + }, + "nextCursor": { + "description": "Opaque cursor to pass to the next call to continue after the last item. If None, there are no more items to return.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "data" + ], + "title": "ModelListResponse", + "type": "object" + }, + "ModelProviderCapabilitiesReadParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ModelProviderCapabilitiesReadParams", + "type": "object" + }, + "ModelProviderCapabilitiesReadResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "imageGeneration": { + "type": "boolean" + }, + "namespaceTools": { + "type": "boolean" + }, + "webSearch": { + "type": "boolean" + } + }, + "required": [ + "imageGeneration", + "namespaceTools", + "webSearch" + ], + "title": "ModelProviderCapabilitiesReadResponse", + "type": "object" + }, + "ModelRerouteReason": { + "enum": [ + "highRiskCyberActivity" + ], + "type": "string" + }, + "ModelReroutedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "fromModel": { + "type": "string" + }, + "reason": { + "$ref": "#/definitions/ModelRerouteReason" + }, + "threadId": { + "type": "string" + }, + "toModel": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "fromModel", + "reason", + "threadId", + "toModel", + "turnId" + ], + "title": "ModelReroutedNotification", + "type": "object" + }, + "ModelSafetyBufferingUpdatedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "fasterModel": { + "type": [ + "string", + "null" + ] + }, + "model": { + "type": "string" + }, + "reasons": { + "items": { + "type": "string" + }, + "type": "array" + }, + "showBufferingUi": { + "type": "boolean" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + }, + "useCases": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "model", + "reasons", + "showBufferingUi", + "threadId", + "turnId", + "useCases" + ], + "title": "ModelSafetyBufferingUpdatedNotification", + "type": "object" + }, + "ModelServiceTier": { + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "description", + "id", + "name" + ], + "type": "object" + }, + "ModelUpgradeInfo": { + "properties": { + "migrationMarkdown": { + "type": [ + "string", + "null" + ] + }, + "model": { + "type": "string" + }, + "modelLink": { + "type": [ + "string", + "null" + ] + }, + "retirementAt": { + "description": "Informational Unix timestamp for this upgrade's scheduled retirement, if known.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "upgradeCopy": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "model" + ], + "type": "object" + }, + "ModelVerification": { + "enum": [ + "trustedAccessForCyber" + ], + "type": "string" + }, + "ModelVerificationNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + }, + "verifications": { + "items": { + "$ref": "#/definitions/ModelVerification" + }, + "type": "array" + } + }, + "required": [ + "threadId", + "turnId", + "verifications" + ], + "title": "ModelVerificationNotification", + "type": "object" + }, + "ModelsRequirements": { + "properties": { + "newThread": { + "anyOf": [ + { + "$ref": "#/definitions/NewThreadModelDefaults" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "MultiAgentMode": { + "description": "Controls the effective multi-agent delegation instructions for a turn. `custom` means the configured mode hint defines the policy instead of a built-in policy.", + "oneOf": [ + { + "enum": [ + "explicitRequestOnly", + "proactive" + ], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "custom": { + "type": "string" + } + }, + "required": [ + "custom" + ], + "title": "CustomMultiAgentMode", + "type": "object" + } + ] + }, + "MultiAgentVersion": { + "description": "Multi-agent runtime supported by a model.", + "enum": [ + "disabled", + "v1", + "v2" + ], + "type": "string" + }, + "NetworkAccess": { + "enum": [ + "restricted", + "enabled" + ], + "type": "string" + }, + "NetworkApprovalProtocol": { + "enum": [ + "http", + "https", + "socks5Tcp", + "socks5Udp" + ], + "type": "string" + }, + "NetworkDomainPermission": { + "enum": [ + "allow", + "deny" + ], + "type": "string" + }, + "NetworkRequirements": { + "properties": { + "allowLocalBinding": { + "type": [ + "boolean", + "null" + ] + }, + "allowUnixSockets": { + "description": "Legacy compatibility view derived from `unix_sockets`.", + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "allowUpstreamProxy": { + "type": [ + "boolean", + "null" + ] + }, + "allowedDomains": { + "description": "Legacy compatibility view derived from `domains`.", + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "dangerouslyAllowAllUnixSockets": { + "type": [ + "boolean", + "null" + ] + }, + "dangerouslyAllowNonLoopbackProxy": { + "type": [ + "boolean", + "null" + ] + }, + "deniedDomains": { + "description": "Legacy compatibility view derived from `domains`.", + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "domains": { + "additionalProperties": { + "$ref": "#/definitions/NetworkDomainPermission" + }, + "description": "Canonical network permission map for `experimental_network`.", + "type": [ + "object", + "null" + ] + }, + "enabled": { + "type": [ + "boolean", + "null" + ] + }, + "httpPort": { + "format": "uint16", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "managedAllowedDomainsOnly": { + "description": "When true, only managed allowlist entries are respected while managed network enforcement is active.", + "type": [ + "boolean", + "null" + ] + }, + "socksPort": { + "format": "uint16", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "unixSockets": { + "additionalProperties": { + "$ref": "#/definitions/NetworkUnixSocketPermission" + }, + "description": "Canonical unix socket permission map for `experimental_network`.", + "type": [ + "object", + "null" + ] + } + }, + "type": "object" + }, + "NetworkUnixSocketPermission": { + "enum": [ + "allow", + "deny" + ], + "type": "string" + }, + "NewThreadModelDefaults": { + "properties": { + "model": { + "type": [ + "string", + "null" + ] + }, + "modelReasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ] + }, + "serviceTier": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "NonSteerableTurnKind": { + "enum": [ + "review", + "compact" + ], + "type": "string" + }, + "NullableGetAccountTokenUsageParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "anyOf": [ + { + "$ref": "#/definitions/GetAccountTokenUsageParams" + }, + { + "type": "null" + } + ], + "title": "Nullable_GetAccountTokenUsageParams" + }, + "OverriddenMetadata": { + "properties": { + "effectiveValue": true, + "message": { + "type": "string" + }, + "overridingLayer": { + "$ref": "#/definitions/ConfigLayerMetadata" + } + }, + "required": [ + "effectiveValue", + "message", + "overridingLayer" + ], + "type": "object" + }, + "PatchApplyStatus": { + "enum": [ + "inProgress", + "completed", + "failed", + "declined" + ], + "type": "string" + }, + "PatchChangeKind": { + "oneOf": [ + { + "properties": { + "type": { + "enum": [ + "add" + ], + "title": "AddPatchChangeKindType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "AddPatchChangeKind", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "delete" + ], + "title": "DeletePatchChangeKindType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "DeletePatchChangeKind", + "type": "object" + }, + { + "properties": { + "move_path": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "update" + ], + "title": "UpdatePatchChangeKindType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "UpdatePatchChangeKind", + "type": "object" + } + ] + }, + "PathUri": { + "type": "string" + }, + "PermissionProfileListParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "cursor": { + "description": "Opaque pagination cursor returned by a previous call.", + "type": [ + "string", + "null" + ] + }, + "cwd": { + "description": "Optional working directory to resolve project config layers.", + "type": [ + "string", + "null" + ] + }, + "limit": { + "description": "Optional page size; defaults to the full result set.", + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "title": "PermissionProfileListParams", + "type": "object" + }, + "PermissionProfileListResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "data": { + "items": { + "$ref": "#/definitions/PermissionProfileSummary" + }, + "type": "array" + }, + "nextCursor": { + "description": "Opaque cursor to pass to the next call to continue after the last item. If None, there are no more items to return.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "data" + ], + "title": "PermissionProfileListResponse", + "type": "object" + }, + "PermissionProfileSummary": { + "properties": { + "allowed": { + "description": "Whether the effective requirements allow selecting this profile.", + "type": "boolean" + }, + "description": { + "description": "Optional user-facing description for display in clients.", + "type": [ + "string", + "null" + ] + }, + "id": { + "description": "Available permission profile identifier.", + "type": "string" + } + }, + "required": [ + "allowed", + "id" + ], + "type": "object" + }, + "Personality": { + "enum": [ + "none", + "friendly", + "pragmatic" + ], + "type": "string" + }, + "PlanDeltaNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "EXPERIMENTAL - proposed plan streaming deltas for plan items. Clients should not assume concatenated deltas match the completed plan item content.", + "properties": { + "delta": { + "type": "string" + }, + "itemId": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "delta", + "itemId", + "threadId", + "turnId" + ], + "title": "PlanDeltaNotification", + "type": "object" + }, + "PlanType": { + "enum": [ + "free", + "go", + "plus", + "pro", + "prolite", + "team", + "self_serve_business_prolite", + "self_serve_business_usage_based", + "business", + "ent26", + "enterprise_cbp_automation", + "enterprise_cbp_usage_based", + "enterprise", + "edu", + "edu_plus", + "edu_pro", + "unknown" + ], + "type": "string" + }, + "PluginAuthPolicy": { + "enum": [ + "ON_INSTALL", + "ON_USE" + ], + "type": "string" + }, + "PluginAvailability": { + "oneOf": [ + { + "enum": [ + "DISABLED_BY_ADMIN" + ], + "type": "string" + }, + { + "description": "Plugin-service currently sends `\"ENABLED\"` for available remote plugins. Codex app-server exposes `\"AVAILABLE\"` in its API; the alias keeps decoding compatible with that upstream response.", + "enum": [ + "AVAILABLE" + ], + "type": "string" + } + ] + }, + "PluginDetail": { + "properties": { + "appTemplates": { + "items": { + "$ref": "#/definitions/AppTemplateSummary" + }, + "type": "array" + }, + "apps": { + "items": { + "$ref": "#/definitions/AppSummary" + }, + "type": "array" + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "hooks": { + "items": { + "$ref": "#/definitions/PluginHookSummary" + }, + "type": "array" + }, + "marketplaceName": { + "type": "string" + }, + "marketplacePath": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "mcpServers": { + "items": { + "type": "string" + }, + "type": "array" + }, + "scheduledTasks": { + "items": { + "$ref": "#/definitions/ScheduledTaskSummary" + }, + "type": [ + "array", + "null" + ] + }, + "shareUrl": { + "type": [ + "string", + "null" + ] + }, + "skills": { + "items": { + "$ref": "#/definitions/SkillSummary" + }, + "type": "array" + }, + "summary": { + "$ref": "#/definitions/PluginSummary" + } + }, + "required": [ + "appTemplates", + "apps", + "hooks", + "marketplaceName", + "mcpServers", + "skills", + "summary" + ], + "type": "object" + }, + "PluginDisabledReason": { + "enum": [ + "disabled_by_admin", + "plan_not_eligible", + "required_app_unavailable", + "unknown" + ], + "type": "string" + }, + "PluginHookSummary": { + "properties": { + "eventName": { + "$ref": "#/definitions/HookEventName" + }, + "key": { + "type": "string" + } + }, + "required": [ + "eventName", + "key" + ], + "type": "object" + }, + "PluginInstallParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "installAttemptId": { + "description": "Client-generated identifier used to correlate one installation attempt.", + "type": [ + "string", + "null" + ] + }, + "marketplacePath": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "pluginName": { + "type": "string" + }, + "remoteMarketplaceName": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "pluginName" + ], + "title": "PluginInstallParams", + "type": "object" + }, + "PluginInstallPolicy": { + "enum": [ + "NOT_AVAILABLE", + "AVAILABLE", + "INSTALLED_BY_DEFAULT" + ], + "type": "string" + }, + "PluginInstallPolicySource": { + "enum": [ + "WORKSPACE_SETTING", + "IMPLICIT_CANONICAL_APP" + ], + "type": "string" + }, + "PluginInstallResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "appsNeedingAuth": { + "items": { + "$ref": "#/definitions/AppSummary" + }, + "type": "array" + }, + "authPolicy": { + "$ref": "#/definitions/PluginAuthPolicy" + } + }, + "required": [ + "appsNeedingAuth", + "authPolicy" + ], + "title": "PluginInstallResponse", + "type": "object" + }, + "PluginInstalledParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "cwds": { + "description": "Optional working directories used to discover repo marketplaces.", + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": [ + "array", + "null" + ] + }, + "installSuggestionPluginNames": { + "description": "Additional uninstalled plugin names that should be returned when present locally. This is used by mention surfaces that intentionally expose install entrypoints.", + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + } + }, + "title": "PluginInstalledParams", + "type": "object" + }, + "PluginInstalledResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "marketplaceLoadErrors": { + "default": [], + "items": { + "$ref": "#/definitions/MarketplaceLoadErrorInfo" + }, + "type": "array" + }, + "marketplaces": { + "items": { + "$ref": "#/definitions/PluginMarketplaceEntry" + }, + "type": "array" + } + }, + "required": [ + "marketplaces" + ], + "title": "PluginInstalledResponse", + "type": "object" + }, + "PluginInterface": { + "properties": { + "brandColor": { + "type": [ + "string", + "null" + ] + }, + "capabilities": { + "items": { + "type": "string" + }, + "type": "array" + }, + "category": { + "type": [ + "string", + "null" + ] + }, + "composerIcon": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ], + "description": "Local composer icon path, resolved from the installed plugin package." + }, + "composerIconUrl": { + "description": "Remote composer icon URL from the plugin catalog.", + "type": [ + "string", + "null" + ] + }, + "defaultPrompt": { + "description": "Starter prompts for the plugin. Capped at 3 entries with a maximum of 128 characters per entry.", + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "developerName": { + "type": [ + "string", + "null" + ] + }, + "displayName": { + "type": [ + "string", + "null" + ] + }, + "logo": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ], + "description": "Local logo path, resolved from the installed plugin package." + }, + "logoDark": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ], + "description": "Local dark-mode logo path, resolved from the installed plugin package." + }, + "logoUrl": { + "description": "Remote logo URL from the plugin catalog.", + "type": [ + "string", + "null" + ] + }, + "logoUrlDark": { + "description": "Remote dark-mode logo URL from the plugin catalog.", + "type": [ + "string", + "null" + ] + }, + "longDescription": { + "type": [ + "string", + "null" + ] + }, + "privacyPolicyUrl": { + "type": [ + "string", + "null" + ] + }, + "screenshotUrls": { + "description": "Remote screenshot URLs from the plugin catalog.", + "items": { + "type": "string" + }, + "type": "array" + }, + "screenshots": { + "description": "Local screenshot paths, resolved from the installed plugin package.", + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "shortDescription": { + "type": [ + "string", + "null" + ] + }, + "termsOfServiceUrl": { + "type": [ + "string", + "null" + ] + }, + "websiteUrl": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "capabilities", + "screenshotUrls", + "screenshots" + ], + "type": "object" + }, + "PluginListMarketplaceKind": { + "enum": [ + "local", + "vertical", + "workspace-directory", + "shared-with-me", + "created-by-me-remote" + ], + "type": "string" + }, + "PluginListParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "cwds": { + "description": "Optional working directories used to discover repo marketplaces. When omitted, only home-scoped marketplaces and the official curated marketplace are considered.", + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": [ + "array", + "null" + ] + }, + "forceRefetch": { + "description": "Whether the client requests a fresh remote plugin catalog fetch.", + "type": "boolean" + }, + "marketplaceKinds": { + "description": "Optional marketplace kind filter. When omitted, only local marketplaces are queried, plus the default remote catalog when enabled by feature flag.", + "items": { + "$ref": "#/definitions/PluginListMarketplaceKind" + }, + "type": [ + "array", + "null" + ] + } + }, + "title": "PluginListParams", + "type": "object" + }, + "PluginListResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "featuredPluginIds": { + "default": [], + "items": { + "type": "string" + }, + "type": "array" + }, + "marketplaceLoadErrors": { + "default": [], + "items": { + "$ref": "#/definitions/MarketplaceLoadErrorInfo" + }, + "type": "array" + }, + "marketplaces": { + "items": { + "$ref": "#/definitions/PluginMarketplaceEntry" + }, + "type": "array" + } + }, + "required": [ + "marketplaces" + ], + "title": "PluginListResponse", + "type": "object" + }, + "PluginMarketplaceEntry": { + "properties": { + "interface": { + "anyOf": [ + { + "$ref": "#/definitions/MarketplaceInterface" + }, + { + "type": "null" + } + ] + }, + "name": { + "type": "string" + }, + "path": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ], + "description": "Local marketplace file path when the marketplace is backed by a local file. Remote-only catalog marketplaces do not have a local path." + }, + "plugins": { + "items": { + "$ref": "#/definitions/PluginSummary" + }, + "type": "array" + } + }, + "required": [ + "name", + "plugins" + ], + "type": "object" + }, + "PluginReadParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "marketplacePath": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "pluginName": { + "type": "string" + }, + "remoteMarketplaceName": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "pluginName" + ], + "title": "PluginReadParams", + "type": "object" + }, + "PluginReadResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "plugin": { + "$ref": "#/definitions/PluginDetail" + } + }, + "required": [ + "plugin" + ], + "title": "PluginReadResponse", + "type": "object" + }, + "PluginSearchResult": { + "properties": { + "marketplaceName": { + "type": "string" + }, + "marketplacePath": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "plugin": { + "$ref": "#/definitions/PluginSummary" + } + }, + "required": [ + "marketplaceName", + "plugin" + ], + "type": "object" + }, + "PluginSearchScope": { + "enum": [ + "global", + "workspace", + "personal" + ], + "type": "string" + }, + "PluginShareCheckoutParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "remotePluginId": { + "type": "string" + } + }, + "required": [ + "remotePluginId" + ], + "title": "PluginShareCheckoutParams", + "type": "object" + }, + "PluginShareCheckoutResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "marketplaceName": { + "type": "string" + }, + "marketplacePath": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "pluginId": { + "type": "string" + }, + "pluginName": { + "type": "string" + }, + "pluginPath": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "remotePluginId": { + "type": "string" + }, + "remoteVersion": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "marketplaceName", + "marketplacePath", + "pluginId", + "pluginName", + "pluginPath", + "remotePluginId" + ], + "title": "PluginShareCheckoutResponse", + "type": "object" + }, + "PluginShareContext": { + "properties": { + "canPublishToWorkspace": { + "default": null, + "type": [ + "boolean", + "null" + ] + }, + "creatorAccountUserId": { + "type": [ + "string", + "null" + ] + }, + "creatorName": { + "type": [ + "string", + "null" + ] + }, + "discoverability": { + "anyOf": [ + { + "$ref": "#/definitions/PluginShareDiscoverability" + }, + { + "type": "null" + } + ] + }, + "remotePluginId": { + "type": "string" + }, + "remoteVersion": { + "default": null, + "description": "Version of the remote shared plugin release when available.", + "type": [ + "string", + "null" + ] + }, + "sharePrincipals": { + "items": { + "$ref": "#/definitions/PluginSharePrincipal" + }, + "type": [ + "array", + "null" + ] + }, + "shareUrl": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "remotePluginId" + ], + "type": "object" + }, + "PluginShareDeleteParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "remotePluginId": { + "type": "string" + } + }, + "required": [ + "remotePluginId" + ], + "title": "PluginShareDeleteParams", + "type": "object" + }, + "PluginShareDeleteResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PluginShareDeleteResponse", + "type": "object" + }, + "PluginShareDiscoverability": { + "enum": [ + "LISTED", + "UNLISTED", + "PRIVATE" + ], + "type": "string" + }, + "PluginShareListItem": { + "properties": { + "localPluginPath": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "plugin": { + "$ref": "#/definitions/PluginSummary" + } + }, + "required": [ + "plugin" + ], + "type": "object" + }, + "PluginShareListParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PluginShareListParams", + "type": "object" + }, + "PluginShareListResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "data": { + "items": { + "$ref": "#/definitions/PluginShareListItem" + }, + "type": "array" + } + }, + "required": [ + "data" + ], + "title": "PluginShareListResponse", + "type": "object" + }, + "PluginSharePrincipal": { + "properties": { + "name": { + "type": "string" + }, + "principalId": { + "type": "string" + }, + "principalType": { + "$ref": "#/definitions/PluginSharePrincipalType" + }, + "role": { + "$ref": "#/definitions/PluginSharePrincipalRole" + } + }, + "required": [ + "name", + "principalId", + "principalType", + "role" + ], + "type": "object" + }, + "PluginSharePrincipalRole": { + "enum": [ + "reader", + "editor", + "owner" + ], + "type": "string" + }, + "PluginSharePrincipalType": { + "enum": [ + "user", + "group", + "workspace" + ], + "type": "string" + }, + "PluginShareSaveParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "discoverability": { + "anyOf": [ + { + "$ref": "#/definitions/PluginShareDiscoverability" + }, + { + "type": "null" + } + ] + }, + "pluginPath": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "remotePluginId": { + "type": [ + "string", + "null" + ] + }, + "shareTargets": { + "items": { + "$ref": "#/definitions/PluginShareTarget" + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "pluginPath" + ], + "title": "PluginShareSaveParams", + "type": "object" + }, + "PluginShareSaveResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "canPublishToWorkspace": { + "default": null, + "type": [ + "boolean", + "null" + ] + }, + "remotePluginId": { + "type": "string" + }, + "shareUrl": { + "type": "string" + } + }, + "required": [ + "remotePluginId", + "shareUrl" + ], + "title": "PluginShareSaveResponse", + "type": "object" + }, + "PluginShareTarget": { + "properties": { + "principalId": { + "type": "string" + }, + "principalType": { + "$ref": "#/definitions/PluginSharePrincipalType" + }, + "role": { + "$ref": "#/definitions/PluginShareTargetRole" + } + }, + "required": [ + "principalId", + "principalType", + "role" + ], + "type": "object" + }, + "PluginShareTargetRole": { + "enum": [ + "reader", + "editor" + ], + "type": "string" + }, + "PluginShareUpdateDiscoverability": { + "enum": [ + "UNLISTED", + "PRIVATE", + "LISTED" + ], + "type": "string" + }, + "PluginShareUpdateTargetsParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "discoverability": { + "$ref": "#/definitions/PluginShareUpdateDiscoverability" + }, + "remotePluginId": { + "type": "string" + }, + "shareTargets": { + "items": { + "$ref": "#/definitions/PluginShareTarget" + }, + "type": "array" + } + }, + "required": [ + "discoverability", + "remotePluginId", + "shareTargets" + ], + "title": "PluginShareUpdateTargetsParams", + "type": "object" + }, + "PluginShareUpdateTargetsResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "discoverability": { + "$ref": "#/definitions/PluginShareDiscoverability" + }, + "principals": { + "items": { + "$ref": "#/definitions/PluginSharePrincipal" + }, + "type": "array" + } + }, + "required": [ + "discoverability", + "principals" + ], + "title": "PluginShareUpdateTargetsResponse", + "type": "object" + }, + "PluginSkillReadParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "remoteMarketplaceName": { + "type": "string" + }, + "remotePluginId": { + "type": "string" + }, + "skillName": { + "type": "string" + } + }, + "required": [ + "remoteMarketplaceName", + "remotePluginId", + "skillName" + ], + "title": "PluginSkillReadParams", + "type": "object" + }, + "PluginSkillReadResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "contents": { + "type": [ + "string", + "null" + ] + } + }, + "title": "PluginSkillReadResponse", + "type": "object" + }, + "PluginSource": { + "oneOf": [ + { + "properties": { + "path": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": { + "enum": [ + "local" + ], + "title": "LocalPluginSourceType", + "type": "string" + } + }, + "required": [ + "path", + "type" + ], + "title": "LocalPluginSource", + "type": "object" + }, + { + "properties": { + "path": { + "type": [ + "string", + "null" + ] + }, + "refName": { + "type": [ + "string", + "null" + ] + }, + "sha": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "git" + ], + "title": "GitPluginSourceType", + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "type", + "url" + ], + "title": "GitPluginSource", + "type": "object" + }, + { + "properties": { + "package": { + "type": "string" + }, + "registry": { + "description": "Optional HTTPS registry URL. Authentication stays in the user's npm config.", + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "npm" + ], + "title": "NpmPluginSourceType", + "type": "string" + }, + "version": { + "description": "Optional npm version or version range.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "package", + "type" + ], + "title": "NpmPluginSource", + "type": "object" + }, + { + "description": "The plugin is available in the remote catalog. Download metadata is kept server-side and is not exposed through the app-server API.", + "properties": { + "type": { + "enum": [ + "remote" + ], + "title": "RemotePluginSourceType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "RemotePluginSource", + "type": "object" + } + ] + }, + "PluginSummary": { + "properties": { + "authPolicy": { + "$ref": "#/definitions/PluginAuthPolicy" + }, + "availability": { + "allOf": [ + { + "$ref": "#/definitions/PluginAvailability" + } + ], + "default": "AVAILABLE", + "description": "Availability state for installing and using the plugin." + }, + "disabledReason": { + "anyOf": [ + { + "$ref": "#/definitions/PluginDisabledReason" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why the remote plugin is unavailable, when provided by plugin-service." + }, + "eligiblePlanTypes": { + "default": null, + "description": "Raw plugin-service plan identifiers eligible to install the plugin.", + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "installPolicy": { + "$ref": "#/definitions/PluginInstallPolicy" + }, + "installPolicySource": { + "anyOf": [ + { + "$ref": "#/definitions/PluginInstallPolicySource" + }, + { + "type": "null" + } + ] + }, + "installed": { + "type": "boolean" + }, + "installedAt": { + "default": null, + "description": "Unix timestamp in seconds when the remote plugin was installed, when available.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "interface": { + "anyOf": [ + { + "$ref": "#/definitions/PluginInterface" + }, + { + "type": "null" + } + ] + }, + "keywords": { + "default": [], + "items": { + "type": "string" + }, + "type": "array" + }, + "localVersion": { + "default": null, + "description": "Version of the locally materialized plugin package when available.", + "type": [ + "string", + "null" + ] + }, + "mustShowInstallationInterstitial": { + "default": null, + "type": [ + "boolean", + "null" + ] + }, + "name": { + "type": "string" + }, + "remotePluginId": { + "description": "Backend remote plugin identifier when available.", + "type": [ + "string", + "null" + ] + }, + "shareContext": { + "anyOf": [ + { + "$ref": "#/definitions/PluginShareContext" + }, + { + "type": "null" + } + ], + "description": "Remote sharing context associated with this plugin when available." + }, + "source": { + "$ref": "#/definitions/PluginSource" + }, + "version": { + "default": null, + "description": "Version advertised by the remote marketplace backend when available.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "authPolicy", + "enabled", + "id", + "installPolicy", + "installed", + "name", + "source" + ], + "type": "object" + }, + "PluginUninstallParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "pluginId": { + "type": "string" + } + }, + "required": [ + "pluginId" + ], + "title": "PluginUninstallParams", + "type": "object" + }, + "PluginUninstallResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PluginUninstallResponse", + "type": "object" + }, + "PluginsMigration": { + "properties": { + "marketplaceName": { + "type": "string" + }, + "pluginNames": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "marketplaceName", + "pluginNames" + ], + "type": "object" + }, + "ProcessExitedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Final process exit notification for `process/spawn`.", + "properties": { + "exitCode": { + "description": "Process exit code.", + "format": "int32", + "type": "integer" + }, + "processHandle": { + "description": "Client-supplied, connection-scoped `processHandle` from `process/spawn`.", + "type": "string" + }, + "stderr": { + "description": "Buffered stderr capture.\n\nEmpty when stderr was streamed via `process/outputDelta`.", + "type": "string" + }, + "stderrCapReached": { + "description": "Whether stderr reached `outputBytesCap`.\n\nIn streaming mode, stderr is empty and cap state is also reported on the final stderr `process/outputDelta` notification.", + "type": "boolean" + }, + "stdout": { + "description": "Buffered stdout capture.\n\nEmpty when stdout was streamed via `process/outputDelta`.", + "type": "string" + }, + "stdoutCapReached": { + "description": "Whether stdout reached `outputBytesCap`.\n\nIn streaming mode, stdout is empty and cap state is also reported on the final stdout `process/outputDelta` notification.", + "type": "boolean" + } + }, + "required": [ + "exitCode", + "processHandle", + "stderr", + "stderrCapReached", + "stdout", + "stdoutCapReached" + ], + "title": "ProcessExitedNotification", + "type": "object" + }, + "ProcessOutputDeltaNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Base64-encoded output chunk emitted for a streaming `process/spawn` request.", + "properties": { + "capReached": { + "description": "True on the final streamed chunk for this stream when output was truncated by `outputBytesCap`.", + "type": "boolean" + }, + "deltaBase64": { + "description": "Base64-encoded output bytes.", + "type": "string" + }, + "processHandle": { + "description": "Client-supplied, connection-scoped `processHandle` from `process/spawn`.", + "type": "string" + }, + "stream": { + "allOf": [ + { + "$ref": "#/definitions/ProcessOutputStream" + } + ], + "description": "Output stream this chunk belongs to." + } + }, + "required": [ + "capReached", + "deltaBase64", + "processHandle", + "stream" + ], + "title": "ProcessOutputDeltaNotification", + "type": "object" + }, + "ProcessOutputStream": { + "description": "Stream label for `process/outputDelta` notifications.", + "oneOf": [ + { + "description": "stdout stream. PTY mode multiplexes terminal output here.", + "enum": [ + "stdout" + ], + "type": "string" + }, + { + "description": "stderr stream.", + "enum": [ + "stderr" + ], + "type": "string" + } + ] + }, + "ProcessTerminalSize": { + "description": "PTY size in character cells for `process/spawn` PTY sessions.", + "properties": { + "cols": { + "description": "Terminal width in character cells.", + "format": "uint16", + "minimum": 0.0, + "type": "integer" + }, + "rows": { + "description": "Terminal height in character cells.", + "format": "uint16", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "cols", + "rows" + ], + "type": "object" + }, + "Project": { + "properties": { + "createdAt": { + "format": "int64", + "type": "integer" + }, + "id": { + "type": "string" + }, + "metadata": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "name": { + "type": "string" + }, + "position": { + "format": "int64", + "type": "integer" + }, + "roots": { + "items": { + "$ref": "#/definitions/ProjectRoot" + }, + "type": "array" + }, + "updatedAt": { + "format": "int64", + "type": "integer" + } + }, + "required": [ + "createdAt", + "id", + "metadata", + "name", + "position", + "roots", + "updatedAt" + ], + "type": "object" + }, + "ProjectChangeType": { + "enum": [ + "created", + "updated", + "deleted" + ], + "type": "string" + }, + "ProjectChangedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "changeType": { + "$ref": "#/definitions/ProjectChangeType" + }, + "projectId": { + "type": "string" + } + }, + "required": [ + "changeType", + "projectId" + ], + "title": "ProjectChangedNotification", + "type": "object" + }, + "ProjectRoot": { + "properties": { + "path": { + "$ref": "#/definitions/AbsolutePathBuf" + } + }, + "required": [ + "path" + ], + "type": "object" + }, + "QueuedSubmission": { + "properties": { + "clientUserMessageId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "input": { + "items": { + "$ref": "#/definitions/UserInput" + }, + "type": "array" + } + }, + "required": [ + "clientUserMessageId", + "id", + "input" + ], + "type": "object" + }, + "RateLimitReachedType": { + "enum": [ + "rate_limit_reached", + "workspace_owner_credits_depleted", + "workspace_member_credits_depleted", + "workspace_owner_usage_limit_reached", + "workspace_member_usage_limit_reached" + ], + "type": "string" + }, + "RateLimitResetCredit": { + "properties": { + "description": { + "description": "Backend-provided display description for this credit, or `null` when unavailable.", + "type": [ + "string", + "null" + ] + }, + "expiresAt": { + "description": "Unix timestamp in seconds when the credit expires, or `null` if it does not expire.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "grantedAt": { + "description": "Unix timestamp in seconds when the credit was granted.", + "format": "int64", + "type": "integer" + }, + "id": { + "description": "Opaque backend identifier for this reset credit.", + "type": "string" + }, + "resetType": { + "$ref": "#/definitions/RateLimitResetType" + }, + "status": { + "$ref": "#/definitions/RateLimitResetCreditStatus" + }, + "title": { + "description": "Backend-provided display title for this credit, or `null` when unavailable.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "grantedAt", + "id", + "resetType", + "status" + ], + "type": "object" + }, + "RateLimitResetCreditStatus": { + "enum": [ + "available", + "redeeming", + "redeemed", + "unknown" + ], + "type": "string" + }, + "RateLimitResetCreditsSummary": { + "properties": { + "availableCount": { + "format": "int64", + "type": "integer" + }, + "credits": { + "description": "Detail rows for available reset credits, when the backend provides them.\n\n`null` means only `availableCount` is known, while an empty array means details were fetched and no available credits were returned. The backend may cap this list, so its length can be less than `availableCount`.", + "items": { + "$ref": "#/definitions/RateLimitResetCredit" + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "availableCount" + ], + "type": "object" + }, + "RateLimitResetType": { + "enum": [ + "codexRateLimits", + "unknown" + ], + "type": "string" + }, + "RateLimitSnapshot": { + "properties": { + "credits": { + "anyOf": [ + { + "$ref": "#/definitions/CreditsSnapshot" + }, + { + "type": "null" + } + ] + }, + "individualLimit": { + "anyOf": [ + { + "$ref": "#/definitions/SpendControlLimitSnapshot" + }, + { + "type": "null" + } + ] + }, + "limitId": { + "type": [ + "string", + "null" + ] + }, + "limitName": { + "type": [ + "string", + "null" + ] + }, + "planType": { + "anyOf": [ + { + "$ref": "#/definitions/PlanType" + }, + { + "type": "null" + } + ] + }, + "primary": { + "anyOf": [ + { + "$ref": "#/definitions/RateLimitWindow" + }, + { + "type": "null" + } + ] + }, + "rateLimitReachedType": { + "anyOf": [ + { + "$ref": "#/definitions/RateLimitReachedType" + }, + { + "type": "null" + } + ] + }, + "secondary": { + "anyOf": [ + { + "$ref": "#/definitions/RateLimitWindow" + }, + { + "type": "null" + } + ] + }, + "spendControlReached": { + "description": "Backend-reported spend-control state. `None` is unavailable, not a sparse-update recovery.", + "type": [ + "boolean", + "null" + ] + } + }, + "type": "object" + }, + "RateLimitWindow": { + "properties": { + "resetsAt": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "usedPercent": { + "format": "int32", + "type": "integer" + }, + "windowDurationMins": { + "format": "int64", + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "usedPercent" + ], + "type": "object" + }, + "RawResponseCompletedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Internal-only notification containing the exact usage from one upstream Responses API completion.", + "properties": { + "responseId": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + }, + "usage": { + "anyOf": [ + { + "$ref": "#/definitions/TokenUsageBreakdown" + }, + { + "type": "null" + } + ] + }, + "usageMetadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseUsageMetadata" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "responseId", + "threadId", + "turnId" + ], + "title": "RawResponseCompletedNotification", + "type": "object" + }, + "RawResponseItemCompletedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "item": { + "$ref": "#/definitions/ResponseItem" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "item", + "threadId", + "turnId" + ], + "title": "RawResponseItemCompletedNotification", + "type": "object" + }, + "RealtimeConversationVersion": { + "enum": [ + "v1", + "v2", + "v3" + ], + "type": "string" + }, + "RealtimeOutputModality": { + "enum": [ + "text", + "audio" + ], + "type": "string" + }, + "RealtimeVoice": { + "enum": [ + "alloy", + "arbor", + "ash", + "ballad", + "breeze", + "cedar", + "coral", + "cove", + "echo", + "ember", + "juniper", + "maple", + "marin", + "sage", + "shimmer", + "sol", + "spruce", + "vale", + "verse" + ], + "type": "string" + }, + "RealtimeVoicesList": { + "properties": { + "defaultV1": { + "$ref": "#/definitions/RealtimeVoice" + }, + "defaultV2": { + "$ref": "#/definitions/RealtimeVoice" + }, + "v1": { + "items": { + "$ref": "#/definitions/RealtimeVoice" + }, + "type": "array" + }, + "v2": { + "items": { + "$ref": "#/definitions/RealtimeVoice" + }, + "type": "array" + } + }, + "required": [ + "defaultV1", + "defaultV2", + "v1", + "v2" + ], + "type": "object" + }, + "ReasoningEffort": { + "description": "A non-empty reasoning effort value advertised by the model.", + "minLength": 1, + "type": "string" + }, + "ReasoningEffortOption": { + "properties": { + "description": { + "type": "string" + }, + "reasoningEffort": { + "$ref": "#/definitions/ReasoningEffort" + } + }, + "required": [ + "description", + "reasoningEffort" + ], + "type": "object" + }, + "ReasoningItemContent": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "reasoning_text" + ], + "title": "ReasoningTextReasoningItemContentType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "ReasoningTextReasoningItemContent", + "type": "object" + }, + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "text" + ], + "title": "TextReasoningItemContentType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "TextReasoningItemContent", + "type": "object" + } + ] + }, + "ReasoningItemReasoningSummary": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "type": { + "enum": [ + "summary_text" + ], + "title": "SummaryTextReasoningItemReasoningSummaryType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "SummaryTextReasoningItemReasoningSummary", + "type": "object" + } + ] + }, + "ReasoningSummary": { + "description": "A summary of the reasoning performed by the model. This can be useful for debugging and understanding the model's reasoning process. See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#reasoning-summaries", + "oneOf": [ + { + "enum": [ + "auto", + "concise", + "detailed" + ], + "type": "string" + }, + { + "description": "Option to disable reasoning summaries.", + "enum": [ + "none" + ], + "type": "string" + } + ] + }, + "ReasoningSummaryPartAddedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "itemId": { + "type": "string" + }, + "summaryIndex": { + "format": "int64", + "type": "integer" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "itemId", + "summaryIndex", + "threadId", + "turnId" + ], + "title": "ReasoningSummaryPartAddedNotification", + "type": "object" + }, + "ReasoningSummaryTextDeltaNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "delta": { + "type": "string" + }, + "itemId": { + "type": "string" + }, + "summaryIndex": { + "format": "int64", + "type": "integer" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "delta", + "itemId", + "summaryIndex", + "threadId", + "turnId" + ], + "title": "ReasoningSummaryTextDeltaNotification", + "type": "object" + }, + "ReasoningTextDeltaNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "contentIndex": { + "format": "int64", + "type": "integer" + }, + "delta": { + "type": "string" + }, + "itemId": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "contentIndex", + "delta", + "itemId", + "threadId", + "turnId" + ], + "title": "ReasoningTextDeltaNotification", + "type": "object" + }, + "RemoteControlConnectionStatus": { + "enum": [ + "disabled", + "connecting", + "connected", + "errored" + ], + "type": "string" + }, + "RemoteControlDisableParams": { + "properties": { + "ephemeral": { + "type": "boolean" + } + }, + "type": "object" + }, + "RemoteControlEnableParams": { + "properties": { + "ephemeral": { + "type": "boolean" + } + }, + "type": "object" + }, + "RemoteControlStatusChangedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Current remote-control connection status and remote identity exposed to clients.", + "properties": { + "environmentId": { + "type": [ + "string", + "null" + ] + }, + "installationId": { + "type": "string" + }, + "serverName": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/RemoteControlConnectionStatus" + } + }, + "required": [ + "installationId", + "serverName", + "status" + ], + "title": "RemoteControlStatusChangedNotification", + "type": "object" + }, + "RequestId": { + "anyOf": [ + { + "type": "string" + }, + { + "format": "int64", + "type": "integer" + } + ] + }, + "RequestPermissionProfile": { + "additionalProperties": false, + "properties": { + "fileSystem": { + "anyOf": [ + { + "$ref": "#/definitions/AdditionalFileSystemPermissions" + }, + { + "type": "null" + } + ] + }, + "network": { + "anyOf": [ + { + "$ref": "#/definitions/AdditionalNetworkPermissions" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "ResidencyRequirement": { + "enum": [ + "us" + ], + "type": "string" + }, + "Resource": { + "description": "A known resource that the server is capable of reading.", + "properties": { + "_meta": true, + "annotations": true, + "description": { + "type": [ + "string", + "null" + ] + }, + "icons": { + "items": true, + "type": [ + "array", + "null" + ] + }, + "mimeType": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": "string" + }, + "size": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "uri": { + "type": "string" + } + }, + "required": [ + "name", + "uri" + ], + "type": "object" + }, + "ResourceContent": { + "anyOf": [ + { + "properties": { + "_meta": true, + "mimeType": { + "type": [ + "string", + "null" + ] + }, + "text": { + "type": "string" + }, + "uri": { + "description": "The URI of this resource.", + "type": "string" + } + }, + "required": [ + "text", + "uri" + ], + "type": "object" + }, + { + "properties": { + "_meta": true, + "blob": { + "type": "string" + }, + "mimeType": { + "type": [ + "string", + "null" + ] + }, + "uri": { + "description": "The URI of this resource.", + "type": "string" + } + }, + "required": [ + "blob", + "uri" + ], + "type": "object" + } + ], + "description": "Contents returned when reading a resource from an MCP server." + }, + "ResourceTemplate": { + "description": "A template description for resources available on the server.", + "properties": { + "annotations": true, + "description": { + "type": [ + "string", + "null" + ] + }, + "mimeType": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": "string" + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "uriTemplate": { + "type": "string" + } + }, + "required": [ + "name", + "uriTemplate" + ], + "type": "object" + }, + "ResponseItem": { + "oneOf": [ + { + "properties": { + "content": { + "items": { + "$ref": "#/definitions/ContentItem" + }, + "type": "array" + }, + "id": { + "type": [ + "string", + "null" + ] + }, + "internal_chat_message_metadata_passthrough": { + "anyOf": [ + { + "$ref": "#/definitions/InternalChatMessageMetadataPassthrough" + }, + { + "type": "null" + } + ] + }, + "phase": { + "anyOf": [ + { + "$ref": "#/definitions/MessagePhase" + }, + { + "type": "null" + } + ] + }, + "role": { + "type": "string" + }, + "type": { + "enum": [ + "message" + ], + "title": "MessageResponseItemType", + "type": "string" + } + }, + "required": [ + "content", + "role", + "type" + ], + "title": "MessageResponseItem", + "type": "object" + }, + { + "properties": { + "author": { + "type": "string" + }, + "content": { + "items": { + "$ref": "#/definitions/AgentMessageInputContent" + }, + "type": "array" + }, + "id": { + "type": [ + "string", + "null" + ] + }, + "internal_chat_message_metadata_passthrough": { + "anyOf": [ + { + "$ref": "#/definitions/InternalChatMessageMetadataPassthrough" + }, + { + "type": "null" + } + ] + }, + "recipient": { + "type": "string" + }, + "type": { + "enum": [ + "agent_message" + ], + "title": "AgentMessageResponseItemType", + "type": "string" + } + }, + "required": [ + "author", + "content", + "recipient", + "type" + ], + "title": "AgentMessageResponseItem", + "type": "object" + }, + { + "properties": { + "content": { + "default": null, + "items": { + "$ref": "#/definitions/ReasoningItemContent" + }, + "type": [ + "array", + "null" + ] + }, + "encrypted_content": { + "type": [ + "string", + "null" + ] + }, + "id": { + "type": [ + "string", + "null" + ] + }, + "internal_chat_message_metadata_passthrough": { + "anyOf": [ + { + "$ref": "#/definitions/InternalChatMessageMetadataPassthrough" + }, + { + "type": "null" + } + ] + }, + "summary": { + "items": { + "$ref": "#/definitions/ReasoningItemReasoningSummary" + }, + "type": "array" + }, + "type": { + "enum": [ + "reasoning" + ], + "title": "ReasoningResponseItemType", + "type": "string" + } + }, + "required": [ + "summary", + "type" + ], + "title": "ReasoningResponseItem", + "type": "object" + }, + { + "properties": { + "action": { + "$ref": "#/definitions/LocalShellAction" + }, + "call_id": { + "description": "Set when using the Responses API.", + "type": [ + "string", + "null" + ] + }, + "id": { + "description": "Legacy id field retained for compatibility with older payloads.", + "type": [ + "string", + "null" + ] + }, + "internal_chat_message_metadata_passthrough": { + "anyOf": [ + { + "$ref": "#/definitions/InternalChatMessageMetadataPassthrough" + }, + { + "type": "null" + } + ] + }, + "status": { + "$ref": "#/definitions/LocalShellStatus" + }, + "type": { + "enum": [ + "local_shell_call" + ], + "title": "LocalShellCallResponseItemType", + "type": "string" + } + }, + "required": [ + "action", + "status", + "type" + ], + "title": "LocalShellCallResponseItem", + "type": "object" + }, + { + "properties": { + "arguments": { + "type": "string" + }, + "call_id": { + "type": "string" + }, + "encrypted_function_args": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "id": { + "type": [ + "string", + "null" + ] + }, + "internal_chat_message_metadata_passthrough": { + "anyOf": [ + { + "$ref": "#/definitions/InternalChatMessageMetadataPassthrough" + }, + { + "type": "null" + } + ] + }, + "name": { + "type": "string" + }, + "namespace": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "function_call" + ], + "title": "FunctionCallResponseItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "call_id", + "name", + "type" + ], + "title": "FunctionCallResponseItem", + "type": "object" + }, + { + "properties": { + "arguments": true, + "call_id": { + "type": [ + "string", + "null" + ] + }, + "execution": { + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ] + }, + "internal_chat_message_metadata_passthrough": { + "anyOf": [ + { + "$ref": "#/definitions/InternalChatMessageMetadataPassthrough" + }, + { + "type": "null" + } + ] + }, + "status": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "tool_search_call" + ], + "title": "ToolSearchCallResponseItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "execution", + "type" + ], + "title": "ToolSearchCallResponseItem", + "type": "object" + }, + { + "properties": { + "call_id": { + "type": [ + "string", + "null" + ] + }, + "id": { + "type": [ + "string", + "null" + ] + }, + "internal_chat_message_metadata_passthrough": { + "anyOf": [ + { + "$ref": "#/definitions/InternalChatMessageMetadataPassthrough" + }, + { + "type": "null" + } + ] + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "namespace": { + "type": [ + "string", + "null" + ] + }, + "output": { + "$ref": "#/definitions/FunctionCallOutputBody" + }, + "type": { + "enum": [ + "function_call_output" + ], + "title": "FunctionCallOutputResponseItemType", + "type": "string" + } + }, + "required": [ + "output", + "type" + ], + "title": "FunctionCallOutputResponseItem", + "type": "object" + }, + { + "properties": { + "call_id": { + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ] + }, + "input": { + "type": "string" + }, + "internal_chat_message_metadata_passthrough": { + "anyOf": [ + { + "$ref": "#/definitions/InternalChatMessageMetadataPassthrough" + }, + { + "type": "null" + } + ] + }, + "name": { + "type": "string" + }, + "namespace": { + "type": [ + "string", + "null" + ] + }, + "status": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "custom_tool_call" + ], + "title": "CustomToolCallResponseItemType", + "type": "string" + } + }, + "required": [ + "call_id", + "input", + "name", + "type" + ], + "title": "CustomToolCallResponseItem", + "type": "object" + }, + { + "properties": { + "call_id": { + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ] + }, + "internal_chat_message_metadata_passthrough": { + "anyOf": [ + { + "$ref": "#/definitions/InternalChatMessageMetadataPassthrough" + }, + { + "type": "null" + } + ] + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "output": { + "$ref": "#/definitions/FunctionCallOutputBody" + }, + "type": { + "enum": [ + "custom_tool_call_output" + ], + "title": "CustomToolCallOutputResponseItemType", + "type": "string" + } + }, + "required": [ + "call_id", + "output", + "type" + ], + "title": "CustomToolCallOutputResponseItem", + "type": "object" + }, + { + "properties": { + "call_id": { + "type": [ + "string", + "null" + ] + }, + "execution": { + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ] + }, + "internal_chat_message_metadata_passthrough": { + "anyOf": [ + { + "$ref": "#/definitions/InternalChatMessageMetadataPassthrough" + }, + { + "type": "null" + } + ] + }, + "status": { + "type": "string" + }, + "tools": { + "items": true, + "type": "array" + }, + "type": { + "enum": [ + "tool_search_output" + ], + "title": "ToolSearchOutputResponseItemType", + "type": "string" + } + }, + "required": [ + "execution", + "status", + "tools", + "type" + ], + "title": "ToolSearchOutputResponseItem", + "type": "object" + }, + { + "properties": { + "action": { + "anyOf": [ + { + "$ref": "#/definitions/ResponsesApiWebSearchAction" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": [ + "string", + "null" + ] + }, + "internal_chat_message_metadata_passthrough": { + "anyOf": [ + { + "$ref": "#/definitions/InternalChatMessageMetadataPassthrough" + }, + { + "type": "null" + } + ] + }, + "status": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "web_search_call" + ], + "title": "WebSearchCallResponseItemType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "WebSearchCallResponseItem", + "type": "object" + }, + { + "properties": { + "id": { + "type": [ + "string", + "null" + ] + }, + "internal_chat_message_metadata_passthrough": { + "anyOf": [ + { + "$ref": "#/definitions/InternalChatMessageMetadataPassthrough" + }, + { + "type": "null" + } + ] + }, + "result": { + "type": "string" + }, + "revised_prompt": { + "type": [ + "string", + "null" + ] + }, + "status": { + "type": "string" + }, + "type": { + "enum": [ + "image_generation_call" + ], + "title": "ImageGenerationCallResponseItemType", + "type": "string" + } + }, + "required": [ + "result", + "status", + "type" + ], + "title": "ImageGenerationCallResponseItem", + "type": "object" + }, + { + "properties": { + "encrypted_content": { + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ] + }, + "internal_chat_message_metadata_passthrough": { + "anyOf": [ + { + "$ref": "#/definitions/InternalChatMessageMetadataPassthrough" + }, + { + "type": "null" + } + ] + }, + "type": { + "enum": [ + "compaction" + ], + "title": "CompactionResponseItemType", + "type": "string" + } + }, + "required": [ + "encrypted_content", + "type" + ], + "title": "CompactionResponseItem", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "compaction_trigger" + ], + "title": "CompactionTriggerResponseItemType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "CompactionTriggerResponseItem", + "type": "object" + }, + { + "properties": { + "encrypted_content": { + "type": [ + "string", + "null" + ] + }, + "id": { + "type": [ + "string", + "null" + ] + }, + "internal_chat_message_metadata_passthrough": { + "anyOf": [ + { + "$ref": "#/definitions/InternalChatMessageMetadataPassthrough" + }, + { + "type": "null" + } + ] + }, + "type": { + "enum": [ + "context_compaction" + ], + "title": "ContextCompactionResponseItemType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "ContextCompactionResponseItem", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "other" + ], + "title": "OtherResponseItemType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "OtherResponseItem", + "type": "object" + } + ] + }, + "ResponseUsageMetadata": { + "description": "Usage metadata reported for one upstream response.", + "properties": { + "amount": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "ResponsesApiWebSearchAction": { + "oneOf": [ + { + "properties": { + "queries": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "query": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "search" + ], + "title": "SearchResponsesApiWebSearchActionType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "SearchResponsesApiWebSearchAction", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "open_page" + ], + "title": "OpenPageResponsesApiWebSearchActionType", + "type": "string" + }, + "url": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "type" + ], + "title": "OpenPageResponsesApiWebSearchAction", + "type": "object" + }, + { + "properties": { + "pattern": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "find_in_page" + ], + "title": "FindInPageResponsesApiWebSearchActionType", + "type": "string" + }, + "url": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "type" + ], + "title": "FindInPageResponsesApiWebSearchAction", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "other" + ], + "title": "OtherResponsesApiWebSearchActionType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "OtherResponsesApiWebSearchAction", + "type": "object" + } + ] + }, + "ReviewDelivery": { + "enum": [ + "inline", + "detached" + ], + "type": "string" + }, + "ReviewStartParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "delivery": { + "anyOf": [ + { + "$ref": "#/definitions/ReviewDelivery" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Where to run the review: inline (default) on the current thread or detached on a new thread (returned in `reviewThreadId`)." + }, + "target": { + "$ref": "#/definitions/ReviewTarget" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "target", + "threadId" + ], + "title": "ReviewStartParams", + "type": "object" + }, + "ReviewStartResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "reviewThreadId": { + "description": "Identifies the thread where the review runs.\n\nFor inline reviews, this is the original thread id. For detached reviews, this is the id of the new review thread.", + "type": "string" + }, + "turn": { + "$ref": "#/definitions/Turn" + } + }, + "required": [ + "reviewThreadId", + "turn" + ], + "title": "ReviewStartResponse", + "type": "object" + }, + "ReviewTarget": { + "oneOf": [ + { + "description": "Review the working tree: staged, unstaged, and untracked files.", + "properties": { + "type": { + "enum": [ + "uncommittedChanges" + ], + "title": "UncommittedChangesReviewTargetType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "UncommittedChangesReviewTarget", + "type": "object" + }, + { + "description": "Review changes between the current branch and the given base branch.", + "properties": { + "branch": { + "type": "string" + }, + "type": { + "enum": [ + "baseBranch" + ], + "title": "BaseBranchReviewTargetType", + "type": "string" + } + }, + "required": [ + "branch", + "type" + ], + "title": "BaseBranchReviewTarget", + "type": "object" + }, + { + "description": "Review the changes introduced by a specific commit.", + "properties": { + "sha": { + "type": "string" + }, + "title": { + "description": "Optional human-readable label (e.g., commit subject) for UIs.", + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "commit" + ], + "title": "CommitReviewTargetType", + "type": "string" + } + }, + "required": [ + "sha", + "type" + ], + "title": "CommitReviewTarget", + "type": "object" + }, + { + "description": "Arbitrary instructions, equivalent to the old free-form prompt.", + "properties": { + "instructions": { + "type": "string" + }, + "type": { + "enum": [ + "custom" + ], + "title": "CustomReviewTargetType", + "type": "string" + } + }, + "required": [ + "instructions", + "type" + ], + "title": "CustomReviewTarget", + "type": "object" + } + ] + }, + "SandboxMode": { + "enum": [ + "read-only", + "workspace-write", + "danger-full-access" + ], + "type": "string" + }, + "SandboxPolicy": { + "oneOf": [ + { + "properties": { + "type": { + "enum": [ + "dangerFullAccess" + ], + "title": "DangerFullAccessSandboxPolicyType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "DangerFullAccessSandboxPolicy", + "type": "object" + }, + { + "properties": { + "networkAccess": { + "default": false, + "type": "boolean" + }, + "type": { + "enum": [ + "readOnly" + ], + "title": "ReadOnlySandboxPolicyType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "ReadOnlySandboxPolicy", + "type": "object" + }, + { + "properties": { + "networkAccess": { + "allOf": [ + { + "$ref": "#/definitions/NetworkAccess" + } + ], + "default": "restricted" + }, + "type": { + "enum": [ + "externalSandbox" + ], + "title": "ExternalSandboxSandboxPolicyType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "ExternalSandboxSandboxPolicy", + "type": "object" + }, + { + "properties": { + "excludeSlashTmp": { + "default": false, + "type": "boolean" + }, + "excludeTmpdirEnvVar": { + "default": false, + "type": "boolean" + }, + "networkAccess": { + "default": false, + "type": "boolean" + }, + "type": { + "enum": [ + "workspaceWrite" + ], + "title": "WorkspaceWriteSandboxPolicyType", + "type": "string" + }, + "writableRoots": { + "default": [], + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + } + }, + "required": [ + "type" + ], + "title": "WorkspaceWriteSandboxPolicy", + "type": "object" + } + ] + }, + "SandboxWorkspaceWrite": { + "properties": { + "exclude_slash_tmp": { + "default": false, + "type": "boolean" + }, + "exclude_tmpdir_env_var": { + "default": false, + "type": "boolean" + }, + "network_access": { + "default": false, + "type": "boolean" + }, + "writable_roots": { + "default": [], + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "ScheduledTaskSchedule": { + "oneOf": [ + { + "properties": { + "days": { + "items": { + "$ref": "#/definitions/ScheduledTaskWeekday" + }, + "type": [ + "array", + "null" + ] + }, + "intervalHours": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "type": { + "enum": [ + "hourly" + ], + "title": "HourlyScheduledTaskScheduleType", + "type": "string" + } + }, + "required": [ + "intervalHours", + "type" + ], + "title": "HourlyScheduledTaskSchedule", + "type": "object" + }, + { + "properties": { + "time": { + "type": "string" + }, + "type": { + "enum": [ + "daily" + ], + "title": "DailyScheduledTaskScheduleType", + "type": "string" + } + }, + "required": [ + "time", + "type" + ], + "title": "DailyScheduledTaskSchedule", + "type": "object" + }, + { + "properties": { + "time": { + "type": "string" + }, + "type": { + "enum": [ + "weekdays" + ], + "title": "WeekdaysScheduledTaskScheduleType", + "type": "string" + } + }, + "required": [ + "time", + "type" + ], + "title": "WeekdaysScheduledTaskSchedule", + "type": "object" + }, + { + "properties": { + "days": { + "items": { + "$ref": "#/definitions/ScheduledTaskWeekday" + }, + "type": "array" + }, + "time": { + "type": "string" + }, + "type": { + "enum": [ + "weekly" + ], + "title": "WeeklyScheduledTaskScheduleType", + "type": "string" + } + }, + "required": [ + "days", + "time", + "type" + ], + "title": "WeeklyScheduledTaskSchedule", + "type": "object" + } + ] + }, + "ScheduledTaskSummary": { + "properties": { + "key": { + "type": "string" + }, + "name": { + "type": "string" + }, + "prompt": { + "type": "string" + }, + "schedule": { + "$ref": "#/definitions/ScheduledTaskSchedule" + } + }, + "required": [ + "key", + "name", + "prompt", + "schedule" + ], + "type": "object" + }, + "ScheduledTaskWeekday": { + "enum": [ + "MO", + "TU", + "WE", + "TH", + "FR", + "SA", + "SU" + ], + "type": "string" + }, + "SelectedCapabilityRoot": { + "description": "A user-selected root that can expose one or more runtime capabilities.", + "properties": { + "id": { + "description": "Stable identifier supplied by the capability selection platform.", + "type": "string" + }, + "location": { + "allOf": [ + { + "$ref": "#/definitions/CapabilityRootLocation" + } + ], + "description": "Where the selected root can be resolved." + } + }, + "required": [ + "id", + "location" + ], + "type": "object" + }, + "SendAddCreditsNudgeEmailParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "creditType": { + "$ref": "#/definitions/AddCreditsNudgeCreditType" + } + }, + "required": [ + "creditType" + ], + "title": "SendAddCreditsNudgeEmailParams", + "type": "object" + }, + "SendAddCreditsNudgeEmailResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "status": { + "$ref": "#/definitions/AddCreditsNudgeEmailStatus" + } + }, + "required": [ + "status" + ], + "title": "SendAddCreditsNudgeEmailResponse", + "type": "object" + }, + "ServerDiagnosticsGauge": { + "properties": { + "name": { + "type": "string" + }, + "value": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "name", + "value" + ], + "type": "object" + }, + "ServerDiagnosticsProcess": { + "properties": { + "id": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "physicalFootprintBytes": { + "format": "uint64", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "residentMemoryBytes": { + "format": "uint64", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "ServerNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Notification sent from the server to the client.", + "oneOf": [ + { + "description": "NEW NOTIFICATIONS", + "properties": { + "method": { + "enum": [ + "error" + ], + "title": "ErrorNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ErrorNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "ErrorNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/started" + ], + "title": "Thread/startedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadStartedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/startedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/status/changed" + ], + "title": "Thread/status/changedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadStatusChangedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/status/changedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/archived" + ], + "title": "Thread/archivedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadArchivedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/archivedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/deleted" + ], + "title": "Thread/deletedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadDeletedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/deletedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/unarchived" + ], + "title": "Thread/unarchivedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadUnarchivedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/unarchivedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/closed" + ], + "title": "Thread/closedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadClosedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/closedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/reverted" + ], + "title": "Thread/revertedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadRevertedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/revertedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "skills/changed" + ], + "title": "Skills/changedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/SkillsChangedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Skills/changedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/name/updated" + ], + "title": "Thread/name/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadNameUpdatedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/name/updatedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/goal/updated" + ], + "title": "Thread/goal/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadGoalUpdatedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/goal/updatedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/goal/cleared" + ], + "title": "Thread/goal/clearedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadGoalClearedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/goal/clearedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/queue/changed" + ], + "title": "Thread/queue/changedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadQueueChangedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/queue/changedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "project/changed" + ], + "title": "Project/changedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ProjectChangedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Project/changedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/project/updated" + ], + "title": "Thread/project/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadProjectUpdatedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/project/updatedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/environment/connected" + ], + "title": "Thread/environment/connectedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/EnvironmentConnectionNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/environment/connectedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/environment/disconnected" + ], + "title": "Thread/environment/disconnectedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/EnvironmentConnectionNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/environment/disconnectedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/settings/updated" + ], + "title": "Thread/settings/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadSettingsUpdatedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/settings/updatedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/tokenUsage/updated" + ], + "title": "Thread/tokenUsage/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadTokenUsageUpdatedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/tokenUsage/updatedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "turn/started" + ], + "title": "Turn/startedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/TurnStartedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Turn/startedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "hook/started" + ], + "title": "Hook/startedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/HookStartedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Hook/startedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "turn/completed" + ], + "title": "Turn/completedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/TurnCompletedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Turn/completedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "hook/completed" + ], + "title": "Hook/completedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/HookCompletedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Hook/completedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "turn/diff/updated" + ], + "title": "Turn/diff/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/TurnDiffUpdatedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Turn/diff/updatedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "turn/plan/updated" + ], + "title": "Turn/plan/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/TurnPlanUpdatedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Turn/plan/updatedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "item/started" + ], + "title": "Item/startedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ItemStartedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Item/startedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "item/autoApprovalReview/started" + ], + "title": "Item/autoApprovalReview/startedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ItemGuardianApprovalReviewStartedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Item/autoApprovalReview/startedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "item/autoApprovalReview/completed" + ], + "title": "Item/autoApprovalReview/completedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ItemGuardianApprovalReviewCompletedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Item/autoApprovalReview/completedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "autoApprovalReview/strictReviewRequired" + ], + "title": "AutoApprovalReview/strictReviewRequiredNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/StrictReviewRequiredNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "AutoApprovalReview/strictReviewRequiredNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "item/completed" + ], + "title": "Item/completedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ItemCompletedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Item/completedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "item/agentMessage/delta" + ], + "title": "Item/agentMessage/deltaNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/AgentMessageDeltaNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Item/agentMessage/deltaNotification", + "type": "object" + }, + { + "description": "EXPERIMENTAL - proposed plan streaming deltas for plan items.", + "properties": { + "method": { + "enum": [ + "item/plan/delta" + ], + "title": "Item/plan/deltaNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/PlanDeltaNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Item/plan/deltaNotification", + "type": "object" + }, + { + "description": "Stream base64-encoded stdout/stderr chunks for a running `command/exec` session.", + "properties": { + "method": { + "enum": [ + "command/exec/outputDelta" + ], + "title": "Command/exec/outputDeltaNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/CommandExecOutputDeltaNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Command/exec/outputDeltaNotification", + "type": "object" + }, + { + "description": "Stream base64-encoded stdout/stderr chunks for a running `process/spawn` session.", + "properties": { + "method": { + "enum": [ + "process/outputDelta" + ], + "title": "Process/outputDeltaNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ProcessOutputDeltaNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Process/outputDeltaNotification", + "type": "object" + }, + { + "description": "Final exit notification for a `process/spawn` session.", + "properties": { + "method": { + "enum": [ + "process/exited" + ], + "title": "Process/exitedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ProcessExitedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Process/exitedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "item/commandExecution/outputDelta" + ], + "title": "Item/commandExecution/outputDeltaNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/CommandExecutionOutputDeltaNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Item/commandExecution/outputDeltaNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "item/commandExecution/terminalInteraction" + ], + "title": "Item/commandExecution/terminalInteractionNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/TerminalInteractionNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Item/commandExecution/terminalInteractionNotification", + "type": "object" + }, + { + "description": "Deprecated legacy apply_patch output stream notification.", + "properties": { + "method": { + "enum": [ + "item/fileChange/outputDelta" + ], + "title": "Item/fileChange/outputDeltaNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FileChangeOutputDeltaNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Item/fileChange/outputDeltaNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "item/fileChange/patchUpdated" + ], + "title": "Item/fileChange/patchUpdatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FileChangePatchUpdatedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Item/fileChange/patchUpdatedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "serverRequest/resolved" + ], + "title": "ServerRequest/resolvedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ServerRequestResolvedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "ServerRequest/resolvedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "item/mcpToolCall/progress" + ], + "title": "Item/mcpToolCall/progressNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/McpToolCallProgressNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Item/mcpToolCall/progressNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "mcpServer/oauthLogin/completed" + ], + "title": "McpServer/oauthLogin/completedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/McpServerOauthLoginCompletedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "McpServer/oauthLogin/completedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "mcpServer/startupStatus/updated" + ], + "title": "McpServer/startupStatus/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/McpServerStatusUpdatedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "McpServer/startupStatus/updatedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "mcpServer/event/stream/notification" + ], + "title": "McpServer/event/stream/notificationNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/McpServerEventStreamNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "McpServer/event/stream/notificationNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "account/updated" + ], + "title": "Account/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/AccountUpdatedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Account/updatedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "account/rateLimits/updated" + ], + "title": "Account/rateLimits/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/AccountRateLimitsUpdatedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Account/rateLimits/updatedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "app/list/updated" + ], + "title": "App/list/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/AppListUpdatedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "App/list/updatedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "remoteControl/status/changed" + ], + "title": "RemoteControl/status/changedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/RemoteControlStatusChangedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "RemoteControl/status/changedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "externalAgentConfig/import/progress" + ], + "title": "ExternalAgentConfig/import/progressNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ExternalAgentConfigImportProgressNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "ExternalAgentConfig/import/progressNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "externalAgentConfig/import/completed" + ], + "title": "ExternalAgentConfig/import/completedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ExternalAgentConfigImportCompletedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "ExternalAgentConfig/import/completedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "fs/changed" + ], + "title": "Fs/changedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FsChangedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Fs/changedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "item/reasoning/summaryTextDelta" + ], + "title": "Item/reasoning/summaryTextDeltaNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ReasoningSummaryTextDeltaNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Item/reasoning/summaryTextDeltaNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "item/reasoning/summaryPartAdded" + ], + "title": "Item/reasoning/summaryPartAddedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ReasoningSummaryPartAddedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Item/reasoning/summaryPartAddedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "item/reasoning/textDelta" + ], + "title": "Item/reasoning/textDeltaNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ReasoningTextDeltaNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Item/reasoning/textDeltaNotification", + "type": "object" + }, + { + "description": "Deprecated: Use `ContextCompaction` item type instead.", + "properties": { + "method": { + "enum": [ + "thread/compacted" + ], + "title": "Thread/compactedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ContextCompactedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/compactedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "model/rerouted" + ], + "title": "Model/reroutedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ModelReroutedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Model/reroutedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "model/verification" + ], + "title": "Model/verificationNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ModelVerificationNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Model/verificationNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "turn/moderationMetadata" + ], + "title": "Turn/moderationMetadataNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/TurnModerationMetadataNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Turn/moderationMetadataNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "model/safetyBuffering/updated" + ], + "title": "Model/safetyBuffering/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ModelSafetyBufferingUpdatedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Model/safetyBuffering/updatedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "warning" + ], + "title": "WarningNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/WarningNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "WarningNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "guardianWarning" + ], + "title": "GuardianWarningNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/GuardianWarningNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "GuardianWarningNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "deprecationNotice" + ], + "title": "DeprecationNoticeNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/DeprecationNoticeNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "DeprecationNoticeNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "configWarning" + ], + "title": "ConfigWarningNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ConfigWarningNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "ConfigWarningNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "fuzzyFileSearch/sessionUpdated" + ], + "title": "FuzzyFileSearch/sessionUpdatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FuzzyFileSearchSessionUpdatedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "FuzzyFileSearch/sessionUpdatedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "fuzzyFileSearch/sessionCompleted" + ], + "title": "FuzzyFileSearch/sessionCompletedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/FuzzyFileSearchSessionCompletedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "FuzzyFileSearch/sessionCompletedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/realtime/started" + ], + "title": "Thread/realtime/startedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadRealtimeStartedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/realtime/startedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/realtime/itemAdded" + ], + "title": "Thread/realtime/itemAddedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadRealtimeItemAddedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/realtime/itemAddedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/realtime/item/started" + ], + "title": "Thread/realtime/item/startedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadRealtimeItemStartedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/realtime/item/startedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/realtime/item/transcript/delta" + ], + "title": "Thread/realtime/item/transcript/deltaNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadRealtimeItemTranscriptDeltaNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/realtime/item/transcript/deltaNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/realtime/item/completed" + ], + "title": "Thread/realtime/item/completedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadRealtimeItemCompletedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/realtime/item/completedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/realtime/transcript/delta" + ], + "title": "Thread/realtime/transcript/deltaNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadRealtimeTranscriptDeltaNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/realtime/transcript/deltaNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/realtime/transcript/done" + ], + "title": "Thread/realtime/transcript/doneNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadRealtimeTranscriptDoneNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/realtime/transcript/doneNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/realtime/outputAudio/delta" + ], + "title": "Thread/realtime/outputAudio/deltaNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadRealtimeOutputAudioDeltaNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/realtime/outputAudio/deltaNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/realtime/sdp" + ], + "title": "Thread/realtime/sdpNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadRealtimeSdpNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/realtime/sdpNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/realtime/error" + ], + "title": "Thread/realtime/errorNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadRealtimeErrorNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/realtime/errorNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/realtime/closed" + ], + "title": "Thread/realtime/closedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadRealtimeClosedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/realtime/closedNotification", + "type": "object" + }, + { + "description": "Notifies the user of world-writable directories on Windows, which cannot be protected by the sandbox.", + "properties": { + "method": { + "enum": [ + "windows/worldWritableWarning" + ], + "title": "Windows/worldWritableWarningNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/WindowsWorldWritableWarningNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Windows/worldWritableWarningNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "windowsSandbox/setupCompleted" + ], + "title": "WindowsSandbox/setupCompletedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/WindowsSandboxSetupCompletedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "WindowsSandbox/setupCompletedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "account/login/completed" + ], + "title": "Account/login/completedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/AccountLoginCompletedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Account/login/completedNotification", + "type": "object" + } + ], + "properties": { + "emittedAtMs": { + "description": "Unix timestamp (in milliseconds) when app-server emitted this notification.", + "format": "int64", + "type": "integer" + } + }, + "title": "ServerNotification" + }, + "ServerRequestResolvedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "requestId": { + "$ref": "#/definitions/RequestId" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "requestId", + "threadId" + ], + "title": "ServerRequestResolvedNotification", + "type": "object" + }, + "SessionMigration": { + "properties": { + "cwd": { + "type": "string" + }, + "path": { + "type": "string" + }, + "title": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "cwd", + "path" + ], + "type": "object" + }, + "SessionSource": { + "oneOf": [ + { + "enum": [ + "cli", + "vscode", + "exec", + "appServer", + "unknown" + ], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "custom": { + "type": "string" + } + }, + "required": [ + "custom" + ], + "title": "CustomSessionSource", + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "subAgent": { + "$ref": "#/definitions/SubAgentSource" + } + }, + "required": [ + "subAgent" + ], + "title": "SubAgentSessionSource", + "type": "object" + } + ] + }, + "Settings": { + "description": "Settings for a collaboration mode.", + "properties": { + "developer_instructions": { + "type": [ + "string", + "null" + ] + }, + "model": { + "type": "string" + }, + "reasoning_effort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "model" + ], + "type": "object" + }, + "SkillDependencies": { + "properties": { + "tools": { + "items": { + "$ref": "#/definitions/SkillToolDependency" + }, + "type": "array" + } + }, + "required": [ + "tools" + ], + "type": "object" + }, + "SkillErrorInfo": { + "properties": { + "message": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": [ + "message", + "path" + ], + "type": "object" + }, + "SkillInterface": { + "properties": { + "brandColor": { + "type": [ + "string", + "null" + ] + }, + "defaultPrompt": { + "type": [ + "string", + "null" + ] + }, + "displayName": { + "type": [ + "string", + "null" + ] + }, + "iconLarge": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "iconLargeUrl": { + "description": "Remote large icon URL from the plugin catalog.", + "type": [ + "string", + "null" + ] + }, + "iconSmall": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "iconSmallUrl": { + "description": "Remote small icon URL from the plugin catalog.", + "type": [ + "string", + "null" + ] + }, + "shortDescription": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "SkillMetadata": { + "properties": { + "dependencies": { + "anyOf": [ + { + "$ref": "#/definitions/SkillDependencies" + }, + { + "type": "null" + } + ] + }, + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "interface": { + "anyOf": [ + { + "$ref": "#/definitions/SkillInterface" + }, + { + "type": "null" + } + ] + }, + "name": { + "type": "string" + }, + "path": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "pluginId": { + "description": "Owning plugin ID, matching `PluginSummary.id`, when known.", + "type": [ + "string", + "null" + ] + }, + "scope": { + "$ref": "#/definitions/SkillScope" + }, + "shortDescription": { + "description": "Legacy short_description from SKILL.md. Prefer SKILL.json interface.short_description.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "description", + "enabled", + "name", + "path", + "scope" + ], + "type": "object" + }, + "SkillMigration": { + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "SkillScope": { + "enum": [ + "user", + "repo", + "system", + "admin" + ], + "type": "string" + }, + "SkillSummary": { + "properties": { + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "interface": { + "anyOf": [ + { + "$ref": "#/definitions/SkillInterface" + }, + { + "type": "null" + } + ] + }, + "name": { + "type": "string" + }, + "path": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "shortDescription": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "description", + "enabled", + "name" + ], + "type": "object" + }, + "SkillToolDependency": { + "properties": { + "command": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "transport": { + "type": [ + "string", + "null" + ] + }, + "type": { + "type": "string" + }, + "url": { + "type": [ + "string", + "null" + ] + }, + "value": { + "type": "string" + } + }, + "required": [ + "type", + "value" + ], + "type": "object" + }, + "SkillsChangedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Notification emitted when watched local skill files change.\n\nTreat this as an invalidation signal and re-run `skills/list` with the client's current parameters when refreshed skill metadata is needed.", + "title": "SkillsChangedNotification", + "type": "object" + }, + "SkillsConfigWriteParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "enabled": { + "type": "boolean" + }, + "name": { + "description": "Name-based selector.", + "type": [ + "string", + "null" + ] + }, + "path": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ], + "description": "Path-based selector." + } + }, + "required": [ + "enabled" + ], + "title": "SkillsConfigWriteParams", + "type": "object" + }, + "SkillsConfigWriteResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "effectiveEnabled": { + "type": "boolean" + } + }, + "required": [ + "effectiveEnabled" + ], + "title": "SkillsConfigWriteResponse", + "type": "object" + }, + "SkillsExtraRootsSetParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "extraRoots": { + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + } + }, + "required": [ + "extraRoots" + ], + "title": "SkillsExtraRootsSetParams", + "type": "object" + }, + "SkillsExtraRootsSetResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SkillsExtraRootsSetResponse", + "type": "object" + }, + "SkillsListEntry": { + "properties": { + "cwd": { + "type": "string" + }, + "errors": { + "items": { + "$ref": "#/definitions/SkillErrorInfo" + }, + "type": "array" + }, + "skills": { + "items": { + "$ref": "#/definitions/SkillMetadata" + }, + "type": "array" + } + }, + "required": [ + "cwd", + "errors", + "skills" + ], + "type": "object" + }, + "SkillsListParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "cwds": { + "description": "When empty, defaults to the current session working directory.", + "items": { + "type": "string" + }, + "type": "array" + }, + "forceReload": { + "description": "When true, bypass the skills cache and re-scan skills from disk.", + "type": "boolean" + } + }, + "title": "SkillsListParams", + "type": "object" + }, + "SkillsListResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "data": { + "items": { + "$ref": "#/definitions/SkillsListEntry" + }, + "type": "array" + } + }, + "required": [ + "data" + ], + "title": "SkillsListResponse", + "type": "object" + }, + "SortDirection": { + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "SpendControlLimitSnapshot": { + "properties": { + "limit": { + "type": "string" + }, + "remainingPercent": { + "format": "int32", + "type": "integer" + }, + "resetsAt": { + "format": "int64", + "type": "integer" + }, + "used": { + "type": "string" + } + }, + "required": [ + "limit", + "remainingPercent", + "resetsAt", + "used" + ], + "type": "object" + }, + "StrictReviewRequiredNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "startedAtMs": { + "description": "Unix timestamp (in milliseconds) when this review started.", + "format": "int64", + "type": "integer" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "startedAtMs", + "threadId", + "turnId" + ], + "title": "StrictReviewRequiredNotification", + "type": "object" + }, + "SubAgentActivityKind": { + "enum": [ + "started", + "interacted", + "interrupted", + "completed" + ], + "type": "string" + }, + "SubAgentSource": { + "oneOf": [ + { + "enum": [ + "review", + "compact", + "memory_consolidation" + ], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "thread_spawn": { + "properties": { + "agent_nickname": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "agent_path": { + "anyOf": [ + { + "$ref": "#/definitions/AgentPath" + }, + { + "type": "null" + } + ], + "default": null + }, + "agent_role": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "depth": { + "format": "int32", + "type": "integer" + }, + "parent_thread_id": { + "$ref": "#/definitions/ThreadId" + } + }, + "required": [ + "depth", + "parent_thread_id" + ], + "type": "object" + } + }, + "required": [ + "thread_spawn" + ], + "title": "ThreadSpawnSubAgentSource", + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "other": { + "type": "string" + } + }, + "required": [ + "other" + ], + "title": "OtherSubAgentSource", + "type": "object" + } + ] + }, + "SubagentMigration": { + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "TerminalInteractionNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "itemId": { + "type": "string" + }, + "processId": { + "type": "string" + }, + "stdin": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "itemId", + "processId", + "stdin", + "threadId", + "turnId" + ], + "title": "TerminalInteractionNotification", + "type": "object" + }, + "TextElement": { + "properties": { + "byteRange": { + "allOf": [ + { + "$ref": "#/definitions/ByteRange" + } + ], + "description": "Byte range in the parent `text` buffer that this element occupies." + }, + "placeholder": { + "description": "Optional human-readable placeholder for the element, displayed in the UI.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "byteRange" + ], + "type": "object" + }, + "TextPosition": { + "properties": { + "column": { + "description": "1-based column number (in Unicode scalar values).", + "format": "uint", + "minimum": 0.0, + "type": "integer" + }, + "line": { + "description": "1-based line number.", + "format": "uint", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "column", + "line" + ], + "type": "object" + }, + "TextRange": { + "properties": { + "end": { + "$ref": "#/definitions/TextPosition" + }, + "start": { + "$ref": "#/definitions/TextPosition" + } + }, + "required": [ + "end", + "start" + ], + "type": "object" + }, + "Thread": { + "properties": { + "agentNickname": { + "description": "Optional random unique nickname assigned to an AgentControl-spawned sub-agent.", + "type": [ + "string", + "null" + ] + }, + "agentRole": { + "description": "Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.", + "type": [ + "string", + "null" + ] + }, + "cliVersion": { + "description": "Version of the CLI that created the thread.", + "type": "string" + }, + "createdAt": { + "description": "Unix timestamp (in seconds) when the thread was created.", + "format": "int64", + "type": "integer" + }, + "cwd": { + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ], + "description": "Working directory captured for the thread." + }, + "ephemeral": { + "description": "Whether the thread is ephemeral and should not be materialized on disk.", + "type": "boolean" + }, + "forkedFromId": { + "description": "Source thread id when this thread was created by forking another thread.", + "type": [ + "string", + "null" + ] + }, + "gitInfo": { + "anyOf": [ + { + "$ref": "#/definitions/GitInfo" + }, + { + "type": "null" + } + ], + "description": "Optional Git metadata captured when the thread was created." + }, + "historyMode": { + "allOf": [ + { + "$ref": "#/definitions/ThreadHistoryMode" + } + ], + "default": "legacy", + "description": "Persisted thread history contract selected when this thread was created." + }, + "id": { + "description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.", + "type": "string" + }, + "modelProvider": { + "description": "Model provider used for this thread (for example, 'openai').", + "type": "string" + }, + "name": { + "description": "Optional user-facing thread title.", + "type": [ + "string", + "null" + ] + }, + "parentThreadId": { + "description": "The ID of the parent thread. This will only be set if this thread is a subagent.", + "type": [ + "string", + "null" + ] + }, + "path": { + "description": "[UNSTABLE] Path to the thread on disk.", + "type": [ + "string", + "null" + ] + }, + "preview": { + "description": "Usually the first user message in the thread, if available.", + "type": "string" + }, + "projectId": { + "description": "Canonical project assignment owned by app-server, if any.", + "type": [ + "string", + "null" + ] + }, + "recencyAt": { + "description": "Unix timestamp (in seconds) used for thread recency ordering.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "section": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSection" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The independently persisted section selected for this thread, if any." + }, + "sectionEnteredAt": { + "default": null, + "description": "Unix timestamp in seconds when the thread entered its current section.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "sessionId": { + "description": "Session id shared by threads that belong to the same session tree.", + "type": "string" + }, + "source": { + "allOf": [ + { + "$ref": "#/definitions/SessionSource" + } + ], + "description": "Origin of the thread (CLI, VSCode, codex exec, codex app-server, etc.)." + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/ThreadStatus" + } + ], + "description": "Current runtime status for the thread." + }, + "threadSource": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSource" + }, + { + "type": "null" + } + ], + "description": "Optional analytics source classification for this thread." + }, + "turns": { + "description": "Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", + "items": { + "$ref": "#/definitions/Turn" + }, + "type": "array" + }, + "updatedAt": { + "description": "Unix timestamp (in seconds) when the thread was last updated.", + "format": "int64", + "type": "integer" + } + }, + "required": [ + "cliVersion", + "createdAt", + "cwd", + "ephemeral", + "id", + "modelProvider", + "preview", + "projectId", + "sessionId", + "source", + "status", + "turns", + "updatedAt" + ], + "type": "object" + }, + "ThreadActiveFlag": { + "enum": [ + "waitingOnApproval", + "waitingOnUserInput" + ], + "type": "string" + }, + "ThreadApproveGuardianDeniedActionParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "event": { + "description": "Serialized `codex_protocol::protocol::GuardianAssessmentEvent`." + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "event", + "threadId" + ], + "title": "ThreadApproveGuardianDeniedActionParams", + "type": "object" + }, + "ThreadApproveGuardianDeniedActionResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ThreadApproveGuardianDeniedActionResponse", + "type": "object" + }, + "ThreadArchiveParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadArchiveParams", + "type": "object" + }, + "ThreadArchiveResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ThreadArchiveResponse", + "type": "object" + }, + "ThreadArchivedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadArchivedNotification", + "type": "object" + }, + "ThreadClosedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadClosedNotification", + "type": "object" + }, + "ThreadCompactStartParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadCompactStartParams", + "type": "object" + }, + "ThreadCompactStartResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ThreadCompactStartResponse", + "type": "object" + }, + "ThreadDeleteParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadDeleteParams", + "type": "object" + }, + "ThreadDeleteResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ThreadDeleteResponse", + "type": "object" + }, + "ThreadDeletedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadDeletedNotification", + "type": "object" + }, + "ThreadExtra": { + "description": "Extra app-server data for a thread.", + "type": "object" + }, + "ThreadForkParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "There are two ways to fork a thread: 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread. 2. By path: load the thread from disk by path and fork it into a new thread.\n\nIf using a non-empty path, the thread_id param will be ignored. Empty string path values are treated as absent.\n\nPrefer using thread_id whenever possible.", + "properties": { + "approvalPolicy": { + "anyOf": [ + { + "$ref": "#/definitions/AskForApproval" + }, + { + "type": "null" + } + ] + }, + "approvalsReviewer": { + "anyOf": [ + { + "$ref": "#/definitions/ApprovalsReviewer" + }, + { + "type": "null" + } + ], + "description": "Override where approval requests are routed for review on this thread and subsequent turns." + }, + "baseInstructions": { + "type": [ + "string", + "null" + ] + }, + "config": { + "additionalProperties": true, + "type": [ + "object", + "null" + ] + }, + "cwd": { + "type": [ + "string", + "null" + ] + }, + "developerInstructions": { + "type": [ + "string", + "null" + ] + }, + "ephemeral": { + "type": "boolean" + }, + "excludeTurns": { + "description": "When true, return only thread metadata and live fork state without populating `thread.turns`. This is useful when the client plans to call `thread/turns/list` immediately after forking. Full-history hydration is deprecated for paginated threads; use this with `thread/turns/list` and `thread/items/list` instead.", + "type": "boolean" + }, + "lastTurnId": { + "description": "Optional last turn id to fork through, inclusive.\n\nWhen specified, turns after `last_turn_id` are omitted from the fork. The referenced turn cannot be in progress.", + "type": [ + "string", + "null" + ] + }, + "model": { + "description": "Configuration overrides for the forked thread, if any.", + "type": [ + "string", + "null" + ] + }, + "modelProvider": { + "type": [ + "string", + "null" + ] + }, + "sandbox": { + "anyOf": [ + { + "$ref": "#/definitions/SandboxMode" + }, + { + "type": "null" + } + ] + }, + "serviceTier": { + "type": [ + "string", + "null" + ] + }, + "threadId": { + "type": "string" + }, + "threadSource": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSource" + }, + { + "type": "null" + } + ], + "description": "Optional client-supplied analytics source classification for this forked thread." + } + }, + "required": [ + "threadId" + ], + "title": "ThreadForkParams", + "type": "object" + }, + "ThreadForkResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "approvalPolicy": { + "$ref": "#/definitions/AskForApproval" + }, + "approvalsReviewer": { + "allOf": [ + { + "$ref": "#/definitions/ApprovalsReviewer" + } + ], + "description": "Reviewer currently used for approval requests on this thread." + }, + "cwd": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "instructionSources": { + "default": [], + "description": "Environment-native paths to instruction source files currently loaded for this thread.", + "items": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": "array" + }, + "model": { + "type": "string" + }, + "modelProvider": { + "type": "string" + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ] + }, + "sandbox": { + "allOf": [ + { + "$ref": "#/definitions/SandboxPolicy" + } + ], + "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `activePermissionProfile` for profile provenance." + }, + "serviceTier": { + "type": [ + "string", + "null" + ] + }, + "thread": { + "$ref": "#/definitions/Thread" + } + }, + "required": [ + "approvalPolicy", + "approvalsReviewer", + "cwd", + "model", + "modelProvider", + "sandbox", + "thread" + ], + "title": "ThreadForkResponse", + "type": "object" + }, + "ThreadGoal": { + "properties": { + "createdAt": { + "format": "int64", + "type": "integer" + }, + "objective": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/ThreadGoalStatus" + }, + "threadId": { + "type": "string" + }, + "timeUsedSeconds": { + "format": "int64", + "type": "integer" + }, + "tokenBudget": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "tokensUsed": { + "format": "int64", + "type": "integer" + }, + "updatedAt": { + "format": "int64", + "type": "integer" + } + }, + "required": [ + "createdAt", + "objective", + "status", + "threadId", + "timeUsedSeconds", + "tokensUsed", + "updatedAt" + ], + "type": "object" + }, + "ThreadGoalClearParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadGoalClearParams", + "type": "object" + }, + "ThreadGoalClearResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "cleared": { + "type": "boolean" + } + }, + "required": [ + "cleared" + ], + "title": "ThreadGoalClearResponse", + "type": "object" + }, + "ThreadGoalClearedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadGoalClearedNotification", + "type": "object" + }, + "ThreadGoalGetParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadGoalGetParams", + "type": "object" + }, + "ThreadGoalGetResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "goal": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadGoal" + }, + { + "type": "null" + } + ] + } + }, + "title": "ThreadGoalGetResponse", + "type": "object" + }, + "ThreadGoalSetParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "objective": { + "type": [ + "string", + "null" + ] + }, + "status": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadGoalStatus" + }, + { + "type": "null" + } + ] + }, + "threadId": { + "type": "string" + }, + "tokenBudget": { + "format": "int64", + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "threadId" + ], + "title": "ThreadGoalSetParams", + "type": "object" + }, + "ThreadGoalSetResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "goal": { + "$ref": "#/definitions/ThreadGoal" + } + }, + "required": [ + "goal" + ], + "title": "ThreadGoalSetResponse", + "type": "object" + }, + "ThreadGoalStatus": { + "enum": [ + "active", + "paused", + "blocked", + "usageLimited", + "budgetLimited", + "complete" + ], + "type": "string" + }, + "ThreadGoalUpdatedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "goal": { + "$ref": "#/definitions/ThreadGoal" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "goal", + "threadId" + ], + "title": "ThreadGoalUpdatedNotification", + "type": "object" + }, + "ThreadHistoryMode": { + "enum": [ + "legacy", + "paginated" + ], + "type": "string" + }, + "ThreadId": { + "type": "string" + }, + "ThreadInjectItemsParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "items": { + "description": "Raw Responses API items to append to the thread's model-visible history.", + "items": true, + "type": "array" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "items", + "threadId" + ], + "title": "ThreadInjectItemsParams", + "type": "object" + }, + "ThreadInjectItemsResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ThreadInjectItemsResponse", + "type": "object" + }, + "ThreadItem": { + "oneOf": [ + { + "properties": { + "clientId": { + "type": [ + "string", + "null" + ] + }, + "content": { + "items": { + "$ref": "#/definitions/UserInput" + }, + "type": "array" + }, + "id": { + "type": "string" + }, + "type": { + "enum": [ + "userMessage" + ], + "title": "UserMessageThreadItemType", + "type": "string" + } + }, + "required": [ + "content", + "id", + "type" + ], + "title": "UserMessageThreadItem", + "type": "object" + }, + { + "properties": { + "fragments": { + "items": { + "$ref": "#/definitions/HookPromptFragment" + }, + "type": "array" + }, + "id": { + "type": "string" + }, + "type": { + "enum": [ + "hookPrompt" + ], + "title": "HookPromptThreadItemType", + "type": "string" + } + }, + "required": [ + "fragments", + "id", + "type" + ], + "title": "HookPromptThreadItem", + "type": "object" + }, + { + "properties": { + "delivery": { + "anyOf": [ + { + "$ref": "#/definitions/AgentMessageDelivery" + }, + { + "type": "null" + } + ], + "default": null + }, + "id": { + "type": "string" + }, + "memoryCitation": { + "anyOf": [ + { + "$ref": "#/definitions/MemoryCitation" + }, + { + "type": "null" + } + ], + "default": null + }, + "phase": { + "anyOf": [ + { + "$ref": "#/definitions/MessagePhase" + }, + { + "type": "null" + } + ], + "default": null + }, + "text": { + "type": "string" + }, + "type": { + "enum": [ + "agentMessage" + ], + "title": "AgentMessageThreadItemType", + "type": "string" + } + }, + "required": [ + "id", + "text", + "type" + ], + "title": "AgentMessageThreadItem", + "type": "object" + }, + { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": [ + "string", + "null" + ] + }, + "output": { + "$ref": "#/definitions/FunctionCallOutputBody" + }, + "type": { + "enum": [ + "functionCallOutput" + ], + "title": "FunctionCallOutputThreadItemType", + "type": "string" + } + }, + "required": [ + "id", + "name", + "output", + "type" + ], + "title": "FunctionCallOutputThreadItem", + "type": "object" + }, + { + "description": "EXPERIMENTAL - proposed plan item content. The completed plan item is authoritative and may not match the concatenation of `PlanDelta` text.", + "properties": { + "id": { + "type": "string" + }, + "text": { + "type": "string" + }, + "type": { + "enum": [ + "plan" + ], + "title": "PlanThreadItemType", + "type": "string" + } + }, + "required": [ + "id", + "text", + "type" + ], + "title": "PlanThreadItem", + "type": "object" + }, + { + "properties": { + "content": { + "default": [], + "items": { + "type": "string" + }, + "type": "array" + }, + "id": { + "type": "string" + }, + "summary": { + "default": [], + "items": { + "type": "string" + }, + "type": "array" + }, + "type": { + "enum": [ + "reasoning" + ], + "title": "ReasoningThreadItemType", + "type": "string" + } + }, + "required": [ + "id", + "type" + ], + "title": "ReasoningThreadItem", + "type": "object" + }, + { + "properties": { + "aggregatedOutput": { + "description": "The command's output, aggregated from stdout and stderr.", + "type": [ + "string", + "null" + ] + }, + "command": { + "description": "The command to be executed.", + "type": "string" + }, + "commandActions": { + "description": "A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", + "items": { + "$ref": "#/definitions/CommandAction" + }, + "type": "array" + }, + "cwd": { + "allOf": [ + { + "$ref": "#/definitions/LegacyAppPathString" + } + ], + "description": "The command's working directory." + }, + "durationMs": { + "description": "The duration of the command execution in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "exitCode": { + "description": "The command's exit code.", + "format": "int32", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "pluginId": { + "default": null, + "description": "Trusted first-party plugin id when this command resolves to one plugin script.", + "type": [ + "string", + "null" + ] + }, + "processId": { + "description": "Identifier for the underlying PTY process (when available).", + "type": [ + "string", + "null" + ] + }, + "scriptPath": { + "default": null, + "description": "Safe plugin-relative path when this command resolves to one plugin script.", + "type": [ + "string", + "null" + ] + }, + "source": { + "allOf": [ + { + "$ref": "#/definitions/CommandExecutionSource" + } + ], + "default": "agent" + }, + "status": { + "$ref": "#/definitions/CommandExecutionStatus" + }, + "type": { + "enum": [ + "commandExecution" + ], + "title": "CommandExecutionThreadItemType", + "type": "string" + } + }, + "required": [ + "command", + "commandActions", + "cwd", + "id", + "status", + "type" + ], + "title": "CommandExecutionThreadItem", + "type": "object" + }, + { + "properties": { + "changes": { + "items": { + "$ref": "#/definitions/FileUpdateChange" + }, + "type": "array" + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/PatchApplyStatus" + }, + "type": { + "enum": [ + "fileChange" + ], + "title": "FileChangeThreadItemType", + "type": "string" + } + }, + "required": [ + "changes", + "id", + "status", + "type" + ], + "title": "FileChangeThreadItem", + "type": "object" + }, + { + "properties": { + "appContext": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallAppContext" + }, + { + "type": "null" + } + ] + }, + "arguments": true, + "durationMs": { + "description": "The duration of the MCP tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "error": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallError" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "string" + }, + "mcpAppResourceUri": { + "description": "Deprecated: use `appContext.resourceUri` instead.", + "type": [ + "string", + "null" + ] + }, + "pluginId": { + "type": [ + "string", + "null" + ] + }, + "readOnlyHint": { + "type": [ + "boolean", + "null" + ] + }, + "result": { + "anyOf": [ + { + "$ref": "#/definitions/McpToolCallResult" + }, + { + "type": "null" + } + ] + }, + "server": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/McpToolCallStatus" + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "mcpToolCall" + ], + "title": "McpToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "server", + "status", + "tool", + "type" + ], + "title": "McpToolCallThreadItem", + "type": "object" + }, + { + "properties": { + "arguments": true, + "contentItems": { + "items": { + "$ref": "#/definitions/DynamicToolCallOutputContentItem" + }, + "type": [ + "array", + "null" + ] + }, + "durationMs": { + "description": "The duration of the dynamic tool call in milliseconds.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "id": { + "type": "string" + }, + "namespace": { + "type": [ + "string", + "null" + ] + }, + "status": { + "$ref": "#/definitions/DynamicToolCallStatus" + }, + "success": { + "type": [ + "boolean", + "null" + ] + }, + "tool": { + "type": "string" + }, + "type": { + "enum": [ + "dynamicToolCall" + ], + "title": "DynamicToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "arguments", + "id", + "status", + "tool", + "type" + ], + "title": "DynamicToolCallThreadItem", + "type": "object" + }, + { + "properties": { + "agentsStates": { + "additionalProperties": { + "$ref": "#/definitions/CollabAgentState" + }, + "description": "Last known status of the target agents, when available.", + "type": "object" + }, + "id": { + "description": "Unique identifier for this collab tool call.", + "type": "string" + }, + "model": { + "description": "Model requested for the spawned agent, when applicable.", + "type": [ + "string", + "null" + ] + }, + "prompt": { + "description": "Prompt text sent as part of the collab tool call, when available.", + "type": [ + "string", + "null" + ] + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ], + "description": "Reasoning effort requested for the spawned agent, when applicable." + }, + "receiverThreadIds": { + "description": "Thread ID of the receiving agent, when applicable. In case of spawn operation, this corresponds to the newly spawned agent.", + "items": { + "type": "string" + }, + "type": "array" + }, + "senderThreadId": { + "description": "Thread ID of the agent issuing the collab request.", + "type": "string" + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/CollabAgentToolCallStatus" + } + ], + "description": "Current status of the collab tool call." + }, + "tool": { + "allOf": [ + { + "$ref": "#/definitions/CollabAgentTool" + } + ], + "description": "Name of the collab tool that was invoked." + }, + "type": { + "enum": [ + "collabAgentToolCall" + ], + "title": "CollabAgentToolCallThreadItemType", + "type": "string" + } + }, + "required": [ + "agentsStates", + "id", + "receiverThreadIds", + "senderThreadId", + "status", + "tool", + "type" + ], + "title": "CollabAgentToolCallThreadItem", + "type": "object" + }, + { + "properties": { + "agentPath": { + "type": "string" + }, + "agentThreadId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "kind": { + "$ref": "#/definitions/SubAgentActivityKind" + }, + "type": { + "enum": [ + "subAgentActivity" + ], + "title": "SubAgentActivityThreadItemType", + "type": "string" + } + }, + "required": [ + "agentPath", + "agentThreadId", + "id", + "kind", + "type" + ], + "title": "SubAgentActivityThreadItem", + "type": "object" + }, + { + "properties": { + "action": { + "anyOf": [ + { + "$ref": "#/definitions/WebSearchAction" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "string" + }, + "query": { + "type": "string" + }, + "results": { + "default": null, + "description": "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.", + "items": true, + "type": [ + "array", + "null" + ] + }, + "type": { + "enum": [ + "webSearch" + ], + "title": "WebSearchThreadItemType", + "type": "string" + } + }, + "required": [ + "id", + "query", + "type" + ], + "title": "WebSearchThreadItem", + "type": "object" + }, + { + "properties": { + "id": { + "type": "string" + }, + "path": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": { + "enum": [ + "imageView" + ], + "title": "ImageViewThreadItemType", + "type": "string" + } + }, + "required": [ + "id", + "path", + "type" + ], + "title": "ImageViewThreadItem", + "type": "object" + }, + { + "description": "Display item emitted by the interruptible `clock.sleep` tool.", + "properties": { + "durationMs": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + }, + "id": { + "type": "string" + }, + "type": { + "enum": [ + "sleep" + ], + "title": "SleepThreadItemType", + "type": "string" + } + }, + "required": [ + "durationMs", + "id", + "type" + ], + "title": "SleepThreadItem", + "type": "object" + }, + { + "properties": { + "failure": { + "anyOf": [ + { + "$ref": "#/definitions/ImageGenerationFailure" + }, + { + "type": "null" + } + ], + "default": null + }, + "id": { + "type": "string" + }, + "result": { + "type": "string" + }, + "revisedPrompt": { + "type": [ + "string", + "null" + ] + }, + "savedPath": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "status": { + "type": "string" + }, + "transparentBackground": { + "default": null, + "type": [ + "boolean", + "null" + ] + }, + "type": { + "enum": [ + "imageGeneration" + ], + "title": "ImageGenerationThreadItemType", + "type": "string" + } + }, + "required": [ + "id", + "result", + "status", + "type" + ], + "title": "ImageGenerationThreadItem", + "type": "object" + }, + { + "properties": { + "id": { + "type": "string" + }, + "review": { + "type": "string" + }, + "type": { + "enum": [ + "enteredReviewMode" + ], + "title": "EnteredReviewModeThreadItemType", + "type": "string" + } + }, + "required": [ + "id", + "review", + "type" + ], + "title": "EnteredReviewModeThreadItem", + "type": "object" + }, + { + "properties": { + "id": { + "type": "string" + }, + "review": { + "type": "string" + }, + "type": { + "enum": [ + "exitedReviewMode" + ], + "title": "ExitedReviewModeThreadItemType", + "type": "string" + } + }, + "required": [ + "id", + "review", + "type" + ], + "title": "ExitedReviewModeThreadItem", + "type": "object" + }, + { + "properties": { + "id": { + "type": "string" + }, + "type": { + "enum": [ + "contextCompaction" + ], + "title": "ContextCompactionThreadItemType", + "type": "string" + } + }, + "required": [ + "id", + "type" + ], + "title": "ContextCompactionThreadItem", + "type": "object" + } + ] + }, + "ThreadItemEntry": { + "properties": { + "item": { + "$ref": "#/definitions/ThreadItem" + }, + "turnId": { + "description": "Turn containing this item.", + "type": "string" + } + }, + "required": [ + "item", + "turnId" + ], + "type": "object" + }, + "ThreadItemsListParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "cursor": { + "description": "Opaque cursor to pass to the next call to continue after the last item.", + "type": [ + "string", + "null" + ] + }, + "limit": { + "description": "Optional item page size.", + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "sortDirection": { + "anyOf": [ + { + "$ref": "#/definitions/SortDirection" + }, + { + "type": "null" + } + ], + "description": "Optional item pagination direction; defaults to ascending." + }, + "threadId": { + "type": "string" + }, + "turnId": { + "description": "Optional turn id to filter by. When omitted, returns items across the thread.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "threadId" + ], + "title": "ThreadItemsListParams", + "type": "object" + }, + "ThreadItemsListResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "backwardsCursor": { + "description": "Opaque cursor to pass as `cursor` when reversing `sortDirection`. This is only populated when the page contains at least one item.", + "type": [ + "string", + "null" + ] + }, + "data": { + "items": { + "$ref": "#/definitions/ThreadItemEntry" + }, + "type": "array" + }, + "nextCursor": { + "description": "Opaque cursor to pass to the next call to continue after the last item. if None, there are no more items to return.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "data" + ], + "title": "ThreadItemsListResponse", + "type": "object" + }, + "ThreadListCwdFilter": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "ThreadListParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "archived": { + "description": "Optional archived filter; when set to true, only archived threads are returned. If false or null, only non-archived threads are returned.", + "type": [ + "boolean", + "null" + ] + }, + "cursor": { + "description": "Opaque pagination cursor returned by a previous call.", + "type": [ + "string", + "null" + ] + }, + "cwd": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadListCwdFilter" + }, + { + "type": "null" + } + ], + "description": "Optional cwd filter or filters; when set, only threads whose session cwd exactly matches one of these paths are returned." + }, + "limit": { + "description": "Optional page size; defaults to a reasonable server-side value.", + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "modelProviders": { + "description": "Optional provider filter; when set, only sessions recorded under these providers are returned. When present but empty, includes all providers.", + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "searchTerm": { + "description": "Optional substring filter for the extracted thread title.", + "type": [ + "string", + "null" + ] + }, + "sectionId": { + "description": "Omit to include every section, set to `null` for unsectioned threads, or provide a section ID to return only threads in that section.", + "type": [ + "string", + "null" + ] + }, + "sortDirection": { + "anyOf": [ + { + "$ref": "#/definitions/SortDirection" + }, + { + "type": "null" + } + ], + "description": "Optional sort direction; defaults to descending (newest first)." + }, + "sortKey": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSortKey" + }, + { + "type": "null" + } + ], + "description": "Optional sort key; defaults to created_at." + }, + "sourceKinds": { + "description": "Optional source filter; when set, only sessions from these source kinds are returned. When omitted or empty, defaults to interactive sources.", + "items": { + "$ref": "#/definitions/ThreadSourceKind" + }, + "type": [ + "array", + "null" + ] + }, + "useStateDbOnly": { + "description": "If true, return from the state DB without scanning JSONL rollouts to repair thread metadata. Omitted or false preserves scan-and-repair behavior.", + "type": "boolean" + } + }, + "title": "ThreadListParams", + "type": "object" + }, + "ThreadListResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "backwardsCursor": { + "description": "Opaque cursor to pass as `cursor` when reversing `sortDirection`. This is only populated when the page contains at least one thread. Use it with the opposite `sortDirection`; for timestamp sorts it anchors at the start of the page timestamp so same-second updates are not skipped.", + "type": [ + "string", + "null" + ] + }, + "data": { + "items": { + "$ref": "#/definitions/Thread" + }, + "type": "array" + }, + "nextCursor": { + "description": "Opaque cursor to pass to the next call to continue after the last item. if None, there are no more items to return.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "data" + ], + "title": "ThreadListResponse", + "type": "object" + }, + "ThreadLoadedListParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "cursor": { + "description": "Opaque pagination cursor returned by a previous call.", + "type": [ + "string", + "null" + ] + }, + "limit": { + "description": "Optional page size; defaults to no limit.", + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "title": "ThreadLoadedListParams", + "type": "object" + }, + "ThreadLoadedListResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "data": { + "description": "Thread ids for sessions currently loaded in memory.", + "items": { + "type": "string" + }, + "type": "array" + }, + "nextCursor": { + "description": "Opaque cursor to pass to the next call to continue after the last item. if None, there are no more items to return.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "data" + ], + "title": "ThreadLoadedListResponse", + "type": "object" + }, + "ThreadMemoryMode": { + "enum": [ + "enabled", + "disabled" + ], + "type": "string" + }, + "ThreadMetadataGitInfoUpdateParams": { + "properties": { + "branch": { + "description": "Omit to leave the stored branch unchanged, set to `null` to clear it, or provide a non-empty string to replace it.", + "type": [ + "string", + "null" + ] + }, + "originUrl": { + "description": "Omit to leave the stored origin URL unchanged, set to `null` to clear it, or provide a non-empty string to replace it.", + "type": [ + "string", + "null" + ] + }, + "sha": { + "description": "Omit to leave the stored commit unchanged, set to `null` to clear it, or provide a non-empty string to replace it.", + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "ThreadMetadataUpdateParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "gitInfo": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadMetadataGitInfoUpdateParams" + }, + { + "type": "null" + } + ], + "description": "Patch the stored Git metadata for this thread. Omit a field to leave it unchanged, set it to `null` to clear it, or provide a string to replace the stored value." + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadMetadataUpdateParams", + "type": "object" + }, + "ThreadMetadataUpdateResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "thread": { + "$ref": "#/definitions/Thread" + } + }, + "required": [ + "thread" + ], + "title": "ThreadMetadataUpdateResponse", + "type": "object" + }, + "ThreadNameUpdatedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + }, + "threadName": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "threadId" + ], + "title": "ThreadNameUpdatedNotification", + "type": "object" + }, + "ThreadProjectUpdatedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "projectId": { + "type": [ + "string", + "null" + ] + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "projectId", + "threadId" + ], + "title": "ThreadProjectUpdatedNotification", + "type": "object" + }, + "ThreadQueueChangedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadQueueChangedNotification", + "type": "object" + }, + "ThreadReadParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "includeTurns": { + "description": "When true, include turns and their items from rollout history. Full-history hydration is deprecated for paginated threads; prefer a metadata-only read and page with `thread/turns/list` and `thread/items/list`.", + "type": "boolean" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadReadParams", + "type": "object" + }, + "ThreadReadResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "thread": { + "$ref": "#/definitions/Thread" + } + }, + "required": [ + "thread" + ], + "title": "ThreadReadResponse", + "type": "object" + }, + "ThreadRealtimeAudioChunk": { + "description": "EXPERIMENTAL - thread realtime audio chunk.", + "properties": { + "data": { + "type": "string" + }, + "itemId": { + "type": [ + "string", + "null" + ] + }, + "numChannels": { + "format": "uint16", + "minimum": 0.0, + "type": "integer" + }, + "sampleRate": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "samplesPerChannel": { + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "data", + "numChannels", + "sampleRate" + ], + "type": "object" + }, + "ThreadRealtimeBemItemPresentation": { + "description": "EXPERIMENTAL - how an existing agent item appears in a realtime conversation.", + "oneOf": [ + { + "properties": { + "type": { + "enum": [ + "wholeItem" + ], + "title": "WholeItemThreadRealtimeBemItemPresentationType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "WholeItemThreadRealtimeBemItemPresentation", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "inlineMarkdown" + ], + "title": "InlineMarkdownThreadRealtimeBemItemPresentationType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "InlineMarkdownThreadRealtimeBemItemPresentation", + "type": "object" + }, + { + "properties": { + "index": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "type": { + "enum": [ + "inlineVisualization" + ], + "title": "InlineVisualizationThreadRealtimeBemItemPresentationType", + "type": "string" + } + }, + "required": [ + "index", + "type" + ], + "title": "InlineVisualizationThreadRealtimeBemItemPresentation", + "type": "object" + } + ] + }, + "ThreadRealtimeClosedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "EXPERIMENTAL - emitted when thread realtime transport closes.", + "properties": { + "reason": { + "type": [ + "string", + "null" + ] + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadRealtimeClosedNotification", + "type": "object" + }, + "ThreadRealtimeErrorNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "EXPERIMENTAL - emitted when thread realtime encounters an error.", + "properties": { + "message": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "message", + "threadId" + ], + "title": "ThreadRealtimeErrorNotification", + "type": "object" + }, + "ThreadRealtimeInitialItem": { + "description": "EXPERIMENTAL - role-bearing text item included when a realtime V3 session starts.", + "properties": { + "role": { + "$ref": "#/definitions/ConversationTextRole" + }, + "text": { + "type": "string" + } + }, + "required": [ + "role", + "text" + ], + "type": "object" + }, + "ThreadRealtimeItem": { + "description": "EXPERIMENTAL - a thread-scoped realtime item in the canonical timeline.", + "oneOf": [ + { + "properties": { + "type": { + "enum": [ + "realtimeSessionStarted" + ], + "title": "RealtimeSessionStartedThreadRealtimeItemType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "RealtimeSessionStartedThreadRealtimeItem", + "type": "object" + }, + { + "properties": { + "role": { + "$ref": "#/definitions/ThreadRealtimeTranscriptRole" + }, + "text": { + "type": "string" + }, + "type": { + "enum": [ + "transcriptSegment" + ], + "title": "TranscriptSegmentThreadRealtimeItemType", + "type": "string" + } + }, + "required": [ + "role", + "text", + "type" + ], + "title": "TranscriptSegmentThreadRealtimeItem", + "type": "object" + }, + { + "properties": { + "item_id": { + "type": "string" + }, + "presentation": { + "$ref": "#/definitions/ThreadRealtimeBemItemPresentation" + }, + "turn_id": { + "type": "string" + }, + "type": { + "enum": [ + "bemItemPromoted" + ], + "title": "BemItemPromotedThreadRealtimeItemType", + "type": "string" + } + }, + "required": [ + "item_id", + "presentation", + "turn_id", + "type" + ], + "title": "BemItemPromotedThreadRealtimeItem", + "type": "object" + }, + { + "properties": { + "outcome": { + "$ref": "#/definitions/ThreadRealtimeSessionOutcome" + }, + "type": { + "enum": [ + "realtimeSessionClosed" + ], + "title": "RealtimeSessionClosedThreadRealtimeItemType", + "type": "string" + } + }, + "required": [ + "outcome", + "type" + ], + "title": "RealtimeSessionClosedThreadRealtimeItem", + "type": "object" + } + ], + "properties": { + "id": { + "type": "string" + }, + "realtimeSessionId": { + "type": "string" + } + }, + "required": [ + "id", + "realtimeSessionId" + ], + "type": "object" + }, + "ThreadRealtimeItemAddedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "EXPERIMENTAL - raw non-audio thread realtime item emitted by the backend.", + "properties": { + "item": true, + "threadId": { + "type": "string" + } + }, + "required": [ + "item", + "threadId" + ], + "title": "ThreadRealtimeItemAddedNotification", + "type": "object" + }, + "ThreadRealtimeItemCompletedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "EXPERIMENTAL - a realtime timeline item published after canonical commit.", + "properties": { + "item": { + "$ref": "#/definitions/ThreadRealtimeItem" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "item", + "threadId" + ], + "title": "ThreadRealtimeItemCompletedNotification", + "type": "object" + }, + "ThreadRealtimeItemStartedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "EXPERIMENTAL - a realtime timeline item started before its content streams.", + "properties": { + "item": { + "$ref": "#/definitions/ThreadRealtimeItem" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "item", + "threadId" + ], + "title": "ThreadRealtimeItemStartedNotification", + "type": "object" + }, + "ThreadRealtimeItemTranscriptDeltaNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "EXPERIMENTAL - text appended to an active realtime transcript item.", + "properties": { + "delta": { + "type": "string" + }, + "itemId": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "delta", + "itemId", + "threadId" + ], + "title": "ThreadRealtimeItemTranscriptDeltaNotification", + "type": "object" + }, + "ThreadRealtimeOutputAudioDeltaNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "EXPERIMENTAL - streamed output audio emitted by thread realtime.", + "properties": { + "audio": { + "$ref": "#/definitions/ThreadRealtimeAudioChunk" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "audio", + "threadId" + ], + "title": "ThreadRealtimeOutputAudioDeltaNotification", + "type": "object" + }, + "ThreadRealtimeSdpNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "EXPERIMENTAL - emitted with the remote SDP for a WebRTC realtime session.", + "properties": { + "sdp": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "sdp", + "threadId" + ], + "title": "ThreadRealtimeSdpNotification", + "type": "object" + }, + "ThreadRealtimeSessionOutcome": { + "enum": [ + "ended", + "failed" + ], + "type": "string" + }, + "ThreadRealtimeStartTransport": { + "description": "EXPERIMENTAL - transport used by thread realtime.", + "oneOf": [ + { + "properties": { + "type": { + "enum": [ + "websocket" + ], + "title": "WebsocketThreadRealtimeStartTransportType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "WebsocketThreadRealtimeStartTransport", + "type": "object" + }, + { + "properties": { + "sdp": { + "description": "SDP offer generated by a WebRTC RTCPeerConnection after configuring audio and the realtime events data channel.", + "type": "string" + }, + "type": { + "enum": [ + "webrtc" + ], + "title": "WebrtcThreadRealtimeStartTransportType", + "type": "string" + } + }, + "required": [ + "sdp", + "type" + ], + "title": "WebrtcThreadRealtimeStartTransport", + "type": "object" + }, + { + "properties": { + "callId": { + "description": "Identifier of a realtime call already created and negotiated by the client.", + "type": "string" + }, + "type": { + "enum": [ + "existingCall" + ], + "title": "ExistingCallThreadRealtimeStartTransportType", + "type": "string" + } + }, + "required": [ + "callId", + "type" + ], + "title": "ExistingCallThreadRealtimeStartTransport", + "type": "object" + } + ] + }, + "ThreadRealtimeStartedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "EXPERIMENTAL - emitted when thread realtime startup is accepted.", + "properties": { + "realtimeSessionId": { + "type": [ + "string", + "null" + ] + }, + "threadId": { + "type": "string" + }, + "version": { + "$ref": "#/definitions/RealtimeConversationVersion" + } + }, + "required": [ + "threadId", + "version" + ], + "title": "ThreadRealtimeStartedNotification", + "type": "object" + }, + "ThreadRealtimeTranscriptDeltaNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "EXPERIMENTAL - flat transcript delta emitted whenever realtime transcript text changes.", + "properties": { + "delta": { + "description": "Live transcript delta from the realtime event.", + "type": "string" + }, + "role": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "delta", + "role", + "threadId" + ], + "title": "ThreadRealtimeTranscriptDeltaNotification", + "type": "object" + }, + "ThreadRealtimeTranscriptDoneNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "EXPERIMENTAL - final transcript text emitted when realtime completes a transcript part.", + "properties": { + "role": { + "type": "string" + }, + "text": { + "description": "Final complete text for the transcript part.", + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "role", + "text", + "threadId" + ], + "title": "ThreadRealtimeTranscriptDoneNotification", + "type": "object" + }, + "ThreadRealtimeTranscriptRole": { + "enum": [ + "user", + "assistant" + ], + "type": "string" + }, + "ThreadResumeInitialTurnsPageParams": { + "properties": { + "itemsView": { + "anyOf": [ + { + "$ref": "#/definitions/TurnItemsView" + }, + { + "type": "null" + } + ], + "description": "How much item detail to include for each returned turn; defaults to summary." + }, + "limit": { + "description": "Optional turn page size.", + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "sortDirection": { + "anyOf": [ + { + "$ref": "#/definitions/SortDirection" + }, + { + "type": "null" + } + ], + "description": "Optional turn pagination direction; defaults to descending." + } + }, + "type": "object" + }, + "ThreadResumeParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "There are three ways to resume a thread: 1. By thread_id: load the thread from disk by thread_id and resume it. 2. By history: instantiate the thread from memory and resume it. 3. By path: load the thread from disk by path and resume it.\n\nFor non-running threads, the precedence is: history > non-empty path > thread_id. If using history or a non-empty path for a non-running thread, the thread_id param will be ignored.\n\nIf thread_id identifies a running thread, app-server rejoins that thread and treats a non-empty path as a consistency check against the active rollout path. Empty string path values are treated as absent.\n\nPrefer using thread_id whenever possible.", + "properties": { + "approvalPolicy": { + "anyOf": [ + { + "$ref": "#/definitions/AskForApproval" + }, + { + "type": "null" + } + ] + }, + "approvalsReviewer": { + "anyOf": [ + { + "$ref": "#/definitions/ApprovalsReviewer" + }, + { + "type": "null" + } + ], + "description": "Override where approval requests are routed for review on this thread and subsequent turns." + }, + "baseInstructions": { + "type": [ + "string", + "null" + ] + }, + "config": { + "additionalProperties": true, + "type": [ + "object", + "null" + ] + }, + "cwd": { + "type": [ + "string", + "null" + ] + }, + "developerInstructions": { + "type": [ + "string", + "null" + ] + }, + "excludeTurns": { + "description": "When true, return only thread metadata and live-resume state without populating `thread.turns`. This is useful when the client plans to call `thread/turns/list` immediately after resuming. Full-history hydration is deprecated for paginated threads; use this with `thread/turns/list` and `thread/items/list` instead.", + "type": "boolean" + }, + "model": { + "description": "Configuration overrides for the resumed thread, if any.", + "type": [ + "string", + "null" + ] + }, + "modelProvider": { + "type": [ + "string", + "null" + ] + }, + "personality": { + "anyOf": [ + { + "$ref": "#/definitions/Personality" + }, + { + "type": "null" + } + ] + }, + "sandbox": { + "anyOf": [ + { + "$ref": "#/definitions/SandboxMode" + }, + { + "type": "null" + } + ] + }, + "serviceTier": { + "type": [ + "string", + "null" + ] + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadResumeParams", + "type": "object" + }, + "ThreadResumeResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "approvalPolicy": { + "$ref": "#/definitions/AskForApproval" + }, + "approvalsReviewer": { + "allOf": [ + { + "$ref": "#/definitions/ApprovalsReviewer" + } + ], + "description": "Reviewer currently used for approval requests on this thread." + }, + "cwd": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "instructionSources": { + "default": [], + "description": "Environment-native paths to instruction source files currently loaded for this thread.", + "items": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": "array" + }, + "itemsBackwardsCursor": { + "default": null, + "description": "Opaque cursor for hydrating paginated items backwards.\n\nPass this as `cursor` to `thread/items/list` with `sortDirection: \"desc\"`. The first page includes the item identified by the cursor.", + "type": [ + "string", + "null" + ] + }, + "model": { + "type": "string" + }, + "modelProvider": { + "type": "string" + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ] + }, + "sandbox": { + "allOf": [ + { + "$ref": "#/definitions/SandboxPolicy" + } + ], + "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `activePermissionProfile` for profile provenance." + }, + "serviceTier": { + "type": [ + "string", + "null" + ] + }, + "thread": { + "$ref": "#/definitions/Thread" + }, + "turnsBackwardsCursor": { + "default": null, + "description": "Opaque cursor for hydrating paginated turns backwards.\n\nPass this as `cursor` to `thread/turns/list` with `sortDirection: \"desc\"`. The first page includes the turn identified by the cursor.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "approvalPolicy", + "approvalsReviewer", + "cwd", + "model", + "modelProvider", + "sandbox", + "thread" + ], + "title": "ThreadResumeResponse", + "type": "object" + }, + "ThreadRevertParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Replace a paginated thread's durable history with the prefix before one turn.\n\nThis only changes persisted conversation history. It does not revert local file changes.", + "properties": { + "beforeTurnId": { + "description": "Turn excluded from the replacement history, together with every later turn.", + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "beforeTurnId", + "threadId" + ], + "title": "ThreadRevertParams", + "type": "object" + }, + "ThreadRevertResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "itemsBackwardsCursor": { + "description": "Opaque cursor for hydrating paginated items backwards.\n\nPass this as `cursor` to `thread/items/list` with `sortDirection: \"desc\"`. The first page includes the item identified by the cursor.", + "type": [ + "string", + "null" + ] + }, + "thread": { + "allOf": [ + { + "$ref": "#/definitions/Thread" + } + ], + "description": "Updated loaded thread metadata. `turns` is always empty; hydrate retained history through `thread/turns/list`." + }, + "turnsBackwardsCursor": { + "description": "Opaque cursor for hydrating paginated turns backwards.\n\nPass this as `cursor` to `thread/turns/list` with `sortDirection: \"desc\"`. The first page includes the turn identified by the cursor.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "thread" + ], + "title": "ThreadRevertResponse", + "type": "object" + }, + "ThreadRevertedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadRevertedNotification", + "type": "object" + }, + "ThreadRollbackParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "DEPRECATED: `thread/rollback` will be removed soon.", + "properties": { + "numTurns": { + "description": "The number of turns to drop from the end of the thread. Must be >= 1.\n\nThis only modifies the thread's history and does not revert local file changes that have been made by the agent. Clients are responsible for reverting these changes.", + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "numTurns", + "threadId" + ], + "title": "ThreadRollbackParams", + "type": "object" + }, + "ThreadRollbackResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "thread": { + "allOf": [ + { + "$ref": "#/definitions/Thread" + } + ], + "description": "The updated thread after applying the rollback, with `turns` populated.\n\nThe ThreadItems stored in each Turn are lossy since we explicitly do not persist all agent interactions, such as command executions. This is the same behavior as `thread/resume`." + } + }, + "required": [ + "thread" + ], + "title": "ThreadRollbackResponse", + "type": "object" + }, + "ThreadSearchResult": { + "properties": { + "snippet": { + "type": "string" + }, + "thread": { + "$ref": "#/definitions/Thread" + } + }, + "required": [ + "snippet", + "thread" + ], + "type": "object" + }, + "ThreadSearchSortKey": { + "enum": [ + "created_at", + "updated_at", + "recency_at" + ], + "type": "string" + }, + "ThreadSection": { + "description": "An independently persisted, user-visible thread section.", + "properties": { + "appearance": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSectionAppearance" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional appearance synchronized across clients." + }, + "id": { + "description": "Opaque UUIDv7 identity that remains stable when the section is renamed.", + "type": "string" + }, + "name": { + "description": "The current user-visible section name.", + "type": "string" + } + }, + "required": [ + "id", + "name" + ], + "type": "object" + }, + "ThreadSectionAppearance": { + "description": "Extensible visual presentation for a custom thread section.", + "properties": { + "color": { + "type": [ + "string", + "null" + ] + }, + "icon": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "ThreadSectionCreateParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Parameters for creating an independently persisted thread section.", + "properties": { + "appearance": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSectionAppearance" + }, + { + "type": "null" + } + ], + "default": null + }, + "name": { + "description": "The user-visible name of the section.", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "ThreadSectionCreateParams", + "type": "object" + }, + "ThreadSectionCreateResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "The independently persisted section created by the server.", + "properties": { + "section": { + "$ref": "#/definitions/ThreadSection" + } + }, + "required": [ + "section" + ], + "title": "ThreadSectionCreateResponse", + "type": "object" + }, + "ThreadSectionDeleteParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Parameters for deleting an independently persisted thread section.", + "properties": { + "sectionId": { + "description": "The stable, server-generated identity of the section to delete.", + "type": "string" + } + }, + "required": [ + "sectionId" + ], + "title": "ThreadSectionDeleteParams", + "type": "object" + }, + "ThreadSectionDeleteResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Successful deletion does not return additional section data.", + "title": "ThreadSectionDeleteResponse", + "type": "object" + }, + "ThreadSectionListParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Parameters for listing independently persisted thread sections.", + "properties": { + "cursor": { + "description": "Opaque pagination cursor returned by a previous call.", + "type": [ + "string", + "null" + ] + }, + "limit": { + "description": "Maximum number of sections to return.", + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "title": "ThreadSectionListParams", + "type": "object" + }, + "ThreadSectionListResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "One page of independently persisted thread sections.", + "properties": { + "data": { + "items": { + "$ref": "#/definitions/ThreadSection" + }, + "type": "array" + }, + "nextCursor": { + "description": "Opaque cursor for the next page, or `null` when no sections remain.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "data" + ], + "title": "ThreadSectionListResponse", + "type": "object" + }, + "ThreadSectionMoveParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Parameters for moving a thread within a server-owned section ordering.", + "properties": { + "beforeThreadId": { + "description": "Existing thread to insert before; omission or null appends to the section.", + "type": [ + "string", + "null" + ] + }, + "sectionId": { + "description": "Destination section, or `null` to remove the thread from its section.", + "type": [ + "string", + "null" + ] + }, + "threadId": { + "description": "Thread to move into, within, or out of a section.", + "type": "string" + } + }, + "required": [ + "sectionId", + "threadId" + ], + "title": "ThreadSectionMoveParams", + "type": "object" + }, + "ThreadSectionMoveResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ThreadSectionMoveResponse", + "type": "object" + }, + "ThreadSectionUpdateParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Parameters for updating an independently persisted thread section.", + "properties": { + "appearance": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSectionAppearance" + }, + { + "type": "null" + } + ], + "description": "Omit to preserve appearance, use `null` to clear it, or provide a replacement." + }, + "name": { + "description": "The updated user-visible name of the section.", + "type": "string" + }, + "sectionId": { + "description": "The stable, server-generated identity of the section to update.", + "type": "string" + } + }, + "required": [ + "name", + "sectionId" + ], + "title": "ThreadSectionUpdateParams", + "type": "object" + }, + "ThreadSectionUpdateResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "The independently persisted section after its name is updated.", + "properties": { + "section": { + "$ref": "#/definitions/ThreadSection" + } + }, + "required": [ + "section" + ], + "title": "ThreadSectionUpdateResponse", + "type": "object" + }, + "ThreadSetNameParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "name": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "name", + "threadId" + ], + "title": "ThreadSetNameParams", + "type": "object" + }, + "ThreadSetNameResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ThreadSetNameResponse", + "type": "object" + }, + "ThreadSettings": { + "properties": { + "activePermissionProfile": { + "anyOf": [ + { + "$ref": "#/definitions/ActivePermissionProfile" + }, + { + "type": "null" + } + ] + }, + "approvalPolicy": { + "$ref": "#/definitions/AskForApproval" + }, + "approvalsReviewer": { + "$ref": "#/definitions/ApprovalsReviewer" + }, + "collaborationMode": { + "$ref": "#/definitions/CollaborationMode" + }, + "cwd": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "effort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ] + }, + "model": { + "type": "string" + }, + "modelProvider": { + "type": "string" + }, + "personality": { + "anyOf": [ + { + "$ref": "#/definitions/Personality" + }, + { + "type": "null" + } + ] + }, + "sandboxPolicy": { + "$ref": "#/definitions/SandboxPolicy" + }, + "serviceTier": { + "type": [ + "string", + "null" + ] + }, + "summary": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningSummary" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "approvalPolicy", + "approvalsReviewer", + "collaborationMode", + "cwd", + "model", + "modelProvider", + "sandboxPolicy" + ], + "type": "object" + }, + "ThreadSettingsUpdatedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + }, + "threadSettings": { + "$ref": "#/definitions/ThreadSettings" + } + }, + "required": [ + "threadId", + "threadSettings" + ], + "title": "ThreadSettingsUpdatedNotification", + "type": "object" + }, + "ThreadShellCommandParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "command": { + "description": "Shell command string evaluated by the thread's configured shell. Unlike `command/exec`, this intentionally preserves shell syntax such as pipes, redirects, and quoting. This runs unsandboxed with full access rather than inheriting the thread sandbox policy.", + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "command", + "threadId" + ], + "title": "ThreadShellCommandParams", + "type": "object" + }, + "ThreadShellCommandResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ThreadShellCommandResponse", + "type": "object" + }, + "ThreadSortKey": { + "enum": [ + "created_at", + "updated_at", + "recency_at", + "section_position" + ], + "type": "string" + }, + "ThreadSource": { + "type": "string" + }, + "ThreadSourceKind": { + "enum": [ + "cli", + "vscode", + "exec", + "appServer", + "subAgent", + "subAgentReview", + "subAgentCompact", + "subAgentThreadSpawn", + "subAgentOther", + "unknown" + ], + "type": "string" + }, + "ThreadStartParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "approvalPolicy": { + "anyOf": [ + { + "$ref": "#/definitions/AskForApproval" + }, + { + "type": "null" + } + ] + }, + "approvalsReviewer": { + "anyOf": [ + { + "$ref": "#/definitions/ApprovalsReviewer" + }, + { + "type": "null" + } + ], + "description": "Override where approval requests are routed for review on this thread and subsequent turns." + }, + "baseInstructions": { + "type": [ + "string", + "null" + ] + }, + "config": { + "additionalProperties": true, + "type": [ + "object", + "null" + ] + }, + "cwd": { + "type": [ + "string", + "null" + ] + }, + "developerInstructions": { + "type": [ + "string", + "null" + ] + }, + "ephemeral": { + "type": [ + "boolean", + "null" + ] + }, + "model": { + "type": [ + "string", + "null" + ] + }, + "modelProvider": { + "type": [ + "string", + "null" + ] + }, + "personality": { + "anyOf": [ + { + "$ref": "#/definitions/Personality" + }, + { + "type": "null" + } + ] + }, + "sandbox": { + "anyOf": [ + { + "$ref": "#/definitions/SandboxMode" + }, + { + "type": "null" + } + ] + }, + "serviceName": { + "type": [ + "string", + "null" + ] + }, + "serviceTier": { + "type": [ + "string", + "null" + ] + }, + "sessionStartSource": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadStartSource" + }, + { + "type": "null" + } + ] + }, + "threadSource": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadSource" + }, + { + "type": "null" + } + ], + "description": "Optional client-supplied analytics source classification for this thread." + } + }, + "title": "ThreadStartParams", + "type": "object" + }, + "ThreadStartResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "approvalPolicy": { + "$ref": "#/definitions/AskForApproval" + }, + "approvalsReviewer": { + "allOf": [ + { + "$ref": "#/definitions/ApprovalsReviewer" + } + ], + "description": "Reviewer currently used for approval requests on this thread." + }, + "cwd": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "instructionSources": { + "default": [], + "description": "Environment-native paths to instruction source files currently loaded for this thread.", + "items": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": "array" + }, + "model": { + "type": "string" + }, + "modelProvider": { + "type": "string" + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ] + }, + "sandbox": { + "allOf": [ + { + "$ref": "#/definitions/SandboxPolicy" + } + ], + "description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `activePermissionProfile` for profile provenance." + }, + "serviceTier": { + "type": [ + "string", + "null" + ] + }, + "thread": { + "$ref": "#/definitions/Thread" + } + }, + "required": [ + "approvalPolicy", + "approvalsReviewer", + "cwd", + "model", + "modelProvider", + "sandbox", + "thread" + ], + "title": "ThreadStartResponse", + "type": "object" + }, + "ThreadStartSource": { + "enum": [ + "startup", + "clear" + ], + "type": "string" + }, + "ThreadStartedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "thread": { + "$ref": "#/definitions/Thread" + } + }, + "required": [ + "thread" + ], + "title": "ThreadStartedNotification", + "type": "object" + }, + "ThreadStatus": { + "oneOf": [ + { + "properties": { + "type": { + "enum": [ + "notLoaded" + ], + "title": "NotLoadedThreadStatusType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "NotLoadedThreadStatus", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "idle" + ], + "title": "IdleThreadStatusType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "IdleThreadStatus", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "systemError" + ], + "title": "SystemErrorThreadStatusType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "SystemErrorThreadStatus", + "type": "object" + }, + { + "properties": { + "activeFlags": { + "items": { + "$ref": "#/definitions/ThreadActiveFlag" + }, + "type": "array" + }, + "type": { + "enum": [ + "active" + ], + "title": "ActiveThreadStatusType", + "type": "string" + } + }, + "required": [ + "activeFlags", + "type" + ], + "title": "ActiveThreadStatus", + "type": "object" + } + ] + }, + "ThreadStatusChangedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "status": { + "$ref": "#/definitions/ThreadStatus" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "status", + "threadId" + ], + "title": "ThreadStatusChangedNotification", + "type": "object" + }, + "ThreadTimelineEntry": { + "description": "EXPERIMENTAL - one item or turn boundary in canonical rollout order.", + "oneOf": [ + { + "properties": { + "item": { + "$ref": "#/definitions/ThreadItem" + }, + "position": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + }, + "turnId": { + "type": "string" + }, + "type": { + "enum": [ + "item" + ], + "title": "ItemThreadTimelineEntryType", + "type": "string" + } + }, + "required": [ + "item", + "position", + "turnId", + "type" + ], + "title": "ItemThreadTimelineEntry", + "type": "object" + }, + { + "properties": { + "item": { + "$ref": "#/definitions/ThreadRealtimeItem" + }, + "position": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + }, + "type": { + "enum": [ + "realtime" + ], + "title": "RealtimeThreadTimelineEntryType", + "type": "string" + } + }, + "required": [ + "item", + "position", + "type" + ], + "title": "RealtimeThreadTimelineEntry", + "type": "object" + }, + { + "properties": { + "position": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + }, + "started_at": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "turn_id": { + "type": "string" + }, + "type": { + "enum": [ + "turnStarted" + ], + "title": "TurnStartedThreadTimelineEntryType", + "type": "string" + } + }, + "required": [ + "position", + "turn_id", + "type" + ], + "title": "TurnStartedThreadTimelineEntry", + "type": "object" + }, + { + "properties": { + "completed_at": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "duration_ms": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "error": { + "anyOf": [ + { + "$ref": "#/definitions/TurnError" + }, + { + "type": "null" + } + ] + }, + "position": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + }, + "started_at": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "status": { + "$ref": "#/definitions/TurnStatus" + }, + "turn_id": { + "type": "string" + }, + "type": { + "enum": [ + "turnCompleted" + ], + "title": "TurnCompletedThreadTimelineEntryType", + "type": "string" + } + }, + "required": [ + "position", + "status", + "turn_id", + "type" + ], + "title": "TurnCompletedThreadTimelineEntry", + "type": "object" + } + ] + }, + "ThreadTokenUsage": { + "properties": { + "last": { + "$ref": "#/definitions/TokenUsageBreakdown" + }, + "modelContextWindow": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "total": { + "$ref": "#/definitions/TokenUsageBreakdown" + } + }, + "required": [ + "last", + "total" + ], + "type": "object" + }, + "ThreadTokenUsageUpdatedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + }, + "tokenUsage": { + "$ref": "#/definitions/ThreadTokenUsage" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "threadId", + "tokenUsage", + "turnId" + ], + "title": "ThreadTokenUsageUpdatedNotification", + "type": "object" + }, + "ThreadTurnsListParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "cursor": { + "description": "Opaque cursor to pass to the next call to continue after the last turn.", + "type": [ + "string", + "null" + ] + }, + "itemsView": { + "anyOf": [ + { + "$ref": "#/definitions/TurnItemsView" + }, + { + "type": "null" + } + ], + "description": "How much item detail to include for each returned turn; defaults to summary." + }, + "limit": { + "description": "Optional turn page size.", + "format": "uint32", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "sortDirection": { + "anyOf": [ + { + "$ref": "#/definitions/SortDirection" + }, + { + "type": "null" + } + ], + "description": "Optional turn pagination direction; defaults to descending." + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadTurnsListParams", + "type": "object" + }, + "ThreadTurnsListResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "backwardsCursor": { + "description": "Opaque cursor to pass as `cursor` when reversing `sortDirection`. This is only populated when the page contains at least one turn. Use it with the opposite `sortDirection` to include the anchor turn again and catch updates to that turn.", + "type": [ + "string", + "null" + ] + }, + "data": { + "items": { + "$ref": "#/definitions/Turn" + }, + "type": "array" + }, + "nextCursor": { + "description": "Opaque cursor to pass to the next call to continue after the last turn. if None, there are no more turns to return.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "data" + ], + "title": "ThreadTurnsListResponse", + "type": "object" + }, + "ThreadUnarchiveParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadUnarchiveParams", + "type": "object" + }, + "ThreadUnarchiveResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "thread": { + "$ref": "#/definitions/Thread" + } + }, + "required": [ + "thread" + ], + "title": "ThreadUnarchiveResponse", + "type": "object" + }, + "ThreadUnarchivedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadUnarchivedNotification", + "type": "object" + }, + "ThreadUnsubscribeParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadUnsubscribeParams", + "type": "object" + }, + "ThreadUnsubscribeResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "status": { + "$ref": "#/definitions/ThreadUnsubscribeStatus" + } + }, + "required": [ + "status" + ], + "title": "ThreadUnsubscribeResponse", + "type": "object" + }, + "ThreadUnsubscribeStatus": { + "enum": [ + "notLoaded", + "notSubscribed", + "unsubscribed" + ], + "type": "string" + }, + "ThreadUsage": { + "properties": { + "estimatedUsageCreditsMicros": { + "format": "int64", + "type": "integer" + }, + "estimatedUsageUsdMicros": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "groups": { + "items": { + "$ref": "#/definitions/ThreadUsageBreakdownGroup" + }, + "type": "array" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "estimatedUsageCreditsMicros", + "groups", + "threadId" + ], + "type": "object" + }, + "ThreadUsageBreakdownGroup": { + "properties": { + "cachedInputTokens": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "estimatedUsageCreditsMicros": { + "format": "int64", + "type": "integer" + }, + "inputTokens": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "model": { + "type": [ + "string", + "null" + ] + }, + "netNewInputTokens": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "outputTokens": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "reasoningEffort": { + "type": [ + "string", + "null" + ] + }, + "speed": { + "type": [ + "string", + "null" + ] + }, + "totalTokens": { + "format": "int64", + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "estimatedUsageCreditsMicros" + ], + "type": "object" + }, + "TokenUsageBreakdown": { + "properties": { + "cacheWriteInputTokens": { + "default": 0, + "format": "int64", + "type": "integer" + }, + "cachedInputTokens": { + "format": "int64", + "type": "integer" + }, + "inputTokens": { + "format": "int64", + "type": "integer" + }, + "outputTokens": { + "format": "int64", + "type": "integer" + }, + "reasoningOutputTokens": { + "format": "int64", + "type": "integer" + }, + "totalTokens": { + "format": "int64", + "type": "integer" + } + }, + "required": [ + "cachedInputTokens", + "inputTokens", + "outputTokens", + "reasoningOutputTokens", + "totalTokens" + ], + "type": "object" + }, + "Tool": { + "description": "Definition for a tool the client can call.", + "properties": { + "_meta": true, + "annotations": true, + "description": { + "type": [ + "string", + "null" + ] + }, + "icons": { + "items": true, + "type": [ + "array", + "null" + ] + }, + "inputSchema": true, + "name": { + "type": "string" + }, + "outputSchema": true, + "title": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "inputSchema", + "name" + ], + "type": "object" + }, + "ToolsV2": { + "properties": { + "web_search": { + "anyOf": [ + { + "$ref": "#/definitions/WebSearchToolConfig" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "Turn": { + "properties": { + "completedAt": { + "description": "Unix timestamp (in seconds) when the turn completed.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "durationMs": { + "description": "Duration between turn start and completion in milliseconds, if known.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "error": { + "anyOf": [ + { + "$ref": "#/definitions/TurnError" + }, + { + "type": "null" + } + ], + "description": "Only populated when the Turn's status is failed." + }, + "id": { + "description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.", + "type": "string" + }, + "items": { + "description": "Thread items currently included in this turn payload.", + "items": { + "$ref": "#/definitions/ThreadItem" + }, + "type": "array" + }, + "itemsView": { + "allOf": [ + { + "$ref": "#/definitions/TurnItemsView" + } + ], + "default": "full", + "description": "Describes how much of `items` has been loaded for this turn." + }, + "startedAt": { + "description": "Unix timestamp (in seconds) when the turn started.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "status": { + "$ref": "#/definitions/TurnStatus" + } + }, + "required": [ + "id", + "items", + "status" + ], + "type": "object" + }, + "TurnCompletedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + }, + "turn": { + "$ref": "#/definitions/Turn" + } + }, + "required": [ + "threadId", + "turn" + ], + "title": "TurnCompletedNotification", + "type": "object" + }, + "TurnDiffUpdatedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Notification that the turn-level unified diff has changed. Contains the latest aggregated diff across all file changes in the turn.", + "properties": { + "diff": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "diff", + "threadId", + "turnId" + ], + "title": "TurnDiffUpdatedNotification", + "type": "object" + }, + "TurnEnvironmentParams": { + "properties": { + "cwd": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "environmentId": { + "type": "string" + }, + "runtimeWorkspaceRoots": { + "description": "Environment-native runtime workspace roots. Omitted defaults to `cwd`.", + "items": { + "$ref": "#/definitions/LegacyAppPathString" + }, + "type": [ + "array", + "null" + ] + } + }, + "required": [ + "cwd", + "environmentId" + ], + "type": "object" + }, + "TurnError": { + "properties": { + "additionalDetails": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "codexErrorInfo": { + "anyOf": [ + { + "$ref": "#/definitions/CodexErrorInfo" + }, + { + "type": "null" + } + ] + }, + "message": { + "type": "string" + }, + "misalignment": { + "anyOf": [ + { + "$ref": "#/definitions/MisalignmentErrorDetails" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional public explanation and continuation instruction for a misalignment block." + } + }, + "required": [ + "message" + ], + "type": "object" + }, + "TurnInterruptParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "threadId", + "turnId" + ], + "title": "TurnInterruptParams", + "type": "object" + }, + "TurnInterruptResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "TurnInterruptResponse", + "type": "object" + }, + "TurnItemsView": { + "oneOf": [ + { + "description": "`items` was not loaded for this turn. The field is intentionally empty.", + "enum": [ + "notLoaded" + ], + "type": "string" + }, + { + "description": "`items` contains only a display summary for this turn.", + "enum": [ + "summary" + ], + "type": "string" + }, + { + "description": "`items` contains every ThreadItem available from persisted app-server history for this turn.", + "enum": [ + "full" + ], + "type": "string" + } + ] + }, + "TurnModerationMetadataNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "metadata": true, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "metadata", + "threadId", + "turnId" + ], + "title": "TurnModerationMetadataNotification", + "type": "object" + }, + "TurnPlanStep": { + "properties": { + "status": { + "$ref": "#/definitions/TurnPlanStepStatus" + }, + "step": { + "type": "string" + } + }, + "required": [ + "status", + "step" + ], + "type": "object" + }, + "TurnPlanStepStatus": { + "enum": [ + "pending", + "inProgress", + "completed" + ], + "type": "string" + }, + "TurnPlanUpdatedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "explanation": { + "type": [ + "string", + "null" + ] + }, + "plan": { + "items": { + "$ref": "#/definitions/TurnPlanStep" + }, + "type": "array" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "plan", + "threadId", + "turnId" + ], + "title": "TurnPlanUpdatedNotification", + "type": "object" + }, + "TurnStartParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "approvalPolicy": { + "anyOf": [ + { + "$ref": "#/definitions/AskForApproval" + }, + { + "type": "null" + } + ], + "description": "Override the approval policy for this turn and subsequent turns." + }, + "approvalsReviewer": { + "anyOf": [ + { + "$ref": "#/definitions/ApprovalsReviewer" + }, + { + "type": "null" + } + ], + "description": "Override where approval requests are routed for review on this turn and subsequent turns." + }, + "clientUserMessageId": { + "type": [ + "string", + "null" + ] + }, + "cwd": { + "description": "Override the working directory for this turn and subsequent turns.", + "type": [ + "string", + "null" + ] + }, + "effort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ], + "description": "Override the reasoning effort for this turn and subsequent turns." + }, + "input": { + "items": { + "$ref": "#/definitions/UserInput" + }, + "type": "array" + }, + "model": { + "description": "Override the model for this turn and subsequent turns.", + "type": [ + "string", + "null" + ] + }, + "outputSchema": { + "description": "Optional JSON Schema used to constrain the final assistant message for this turn." + }, + "personality": { + "anyOf": [ + { + "$ref": "#/definitions/Personality" + }, + { + "type": "null" + } + ], + "description": "Override the personality for this turn and subsequent turns." + }, + "sandboxPolicy": { + "anyOf": [ + { + "$ref": "#/definitions/SandboxPolicy" + }, + { + "type": "null" + } + ], + "description": "Override the sandbox policy for this turn and subsequent turns." + }, + "serviceTier": { + "description": "Override the service tier for this turn and subsequent turns.", + "type": [ + "string", + "null" + ] + }, + "serviceTierForTurn": { + "description": "Override the service tier only when this request starts a new turn. Use \"default\" for standard speed. Omitted or null inherits the thread's tier. Does not change the thread's tier or a turn being steered.", + "type": [ + "string", + "null" + ] + }, + "summary": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningSummary" + }, + { + "type": "null" + } + ], + "description": "Override the reasoning summary for this turn and subsequent turns." + }, + "threadId": { + "type": "string" + }, + "toolOutput": { + "anyOf": [ + { + "$ref": "#/definitions/TurnToolOutput" + }, + { + "type": "null" + } + ] + }, + "turnTrigger": { + "description": "Optional source classification for the caller that starts this turn. Ignored when this request steers an already-active turn.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "input", + "threadId" + ], + "title": "TurnStartParams", + "type": "object" + }, + "TurnStartResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "turn": { + "$ref": "#/definitions/Turn" + } + }, + "required": [ + "turn" + ], + "title": "TurnStartResponse", + "type": "object" + }, + "TurnStartedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + }, + "turn": { + "$ref": "#/definitions/Turn" + } + }, + "required": [ + "threadId", + "turn" + ], + "title": "TurnStartedNotification", + "type": "object" + }, + "TurnStatus": { + "enum": [ + "completed", + "interrupted", + "failed", + "inProgress" + ], + "type": "string" + }, + "TurnSteerParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "clientUserMessageId": { + "type": [ + "string", + "null" + ] + }, + "expectedTurnId": { + "description": "Required active turn id precondition. The request fails when it does not match the currently active turn.", + "type": "string" + }, + "input": { + "items": { + "$ref": "#/definitions/UserInput" + }, + "type": "array" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "expectedTurnId", + "input", + "threadId" + ], + "title": "TurnSteerParams", + "type": "object" + }, + "TurnSteerResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "turnId": { + "type": "string" + } + }, + "required": [ + "turnId" + ], + "title": "TurnSteerResponse", + "type": "object" + }, + "TurnToolOutput": { + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": [ + "string", + "null" + ] + }, + "output": { + "$ref": "#/definitions/FunctionCallOutputBody" + } + }, + "required": [ + "name", + "output" + ], + "type": "object" + }, + "TurnsPage": { + "properties": { + "backwardsCursor": { + "type": [ + "string", + "null" + ] + }, + "data": { + "items": { + "$ref": "#/definitions/Turn" + }, + "type": "array" + }, + "nextCursor": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "data" + ], + "type": "object" + }, + "UserInput": { + "oneOf": [ + { + "properties": { + "text": { + "type": "string" + }, + "text_elements": { + "default": [], + "description": "UI-defined spans within `text` used to render or persist special elements.", + "items": { + "$ref": "#/definitions/TextElement" + }, + "type": "array" + }, + "type": { + "enum": [ + "text" + ], + "title": "TextUserInputType", + "type": "string" + } + }, + "required": [ + "text", + "type" + ], + "title": "TextUserInput", + "type": "object" + }, + { + "properties": { + "detail": { + "anyOf": [ + { + "$ref": "#/definitions/ImageDetail" + }, + { + "type": "null" + } + ], + "default": null + }, + "type": { + "enum": [ + "image" + ], + "title": "ImageUserInputType", + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "type", + "url" + ], + "title": "ImageUserInput", + "type": "object" + }, + { + "properties": { + "detail": { + "anyOf": [ + { + "$ref": "#/definitions/ImageDetail" + }, + { + "type": "null" + } + ], + "default": null + }, + "path": { + "type": "string" + }, + "type": { + "enum": [ + "localImage" + ], + "title": "LocalImageUserInputType", + "type": "string" + } + }, + "required": [ + "path", + "type" + ], + "title": "LocalImageUserInput", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "audio" + ], + "title": "AudioUserInputType", + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "type", + "url" + ], + "title": "AudioUserInput", + "type": "object" + }, + { + "properties": { + "path": { + "type": "string" + }, + "type": { + "enum": [ + "localAudio" + ], + "title": "LocalAudioUserInputType", + "type": "string" + } + }, + "required": [ + "path", + "type" + ], + "title": "LocalAudioUserInput", + "type": "object" + }, + { + "properties": { + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "enum": [ + "skill" + ], + "title": "SkillUserInputType", + "type": "string" + } + }, + "required": [ + "name", + "path", + "type" + ], + "title": "SkillUserInput", + "type": "object" + }, + { + "properties": { + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "enum": [ + "mention" + ], + "title": "MentionUserInputType", + "type": "string" + } + }, + "required": [ + "name", + "path", + "type" + ], + "title": "MentionUserInput", + "type": "object" + } + ] + }, + "Verbosity": { + "description": "Controls output length/detail on GPT-5 models via the Responses API. Serialized with lowercase values to match the OpenAI API.", + "enum": [ + "low", + "medium", + "high" + ], + "type": "string" + }, + "WarningNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "message": { + "description": "Concise warning message for the user.", + "type": "string" + }, + "threadId": { + "description": "Optional thread target when the warning applies to a specific thread.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "message" + ], + "title": "WarningNotification", + "type": "object" + }, + "WebSearchAction": { + "oneOf": [ + { + "properties": { + "queries": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "query": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "search" + ], + "title": "SearchWebSearchActionType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "SearchWebSearchAction", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "openPage" + ], + "title": "OpenPageWebSearchActionType", + "type": "string" + }, + "url": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "type" + ], + "title": "OpenPageWebSearchAction", + "type": "object" + }, + { + "properties": { + "pattern": { + "type": [ + "string", + "null" + ] + }, + "type": { + "enum": [ + "findInPage" + ], + "title": "FindInPageWebSearchActionType", + "type": "string" + }, + "url": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "type" + ], + "title": "FindInPageWebSearchAction", + "type": "object" + }, + { + "properties": { + "type": { + "enum": [ + "other" + ], + "title": "OtherWebSearchActionType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "OtherWebSearchAction", + "type": "object" + } + ] + }, + "WebSearchContextSize": { + "enum": [ + "low", + "medium", + "high" + ], + "type": "string" + }, + "WebSearchLocation": { + "additionalProperties": false, + "properties": { + "city": { + "type": [ + "string", + "null" + ] + }, + "country": { + "type": [ + "string", + "null" + ] + }, + "region": { + "type": [ + "string", + "null" + ] + }, + "timezone": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "WebSearchMode": { + "enum": [ + "disabled", + "cached", + "indexed", + "live" + ], + "type": "string" + }, + "WebSearchToolConfig": { + "additionalProperties": false, + "properties": { + "allowed_domains": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "context_size": { + "anyOf": [ + { + "$ref": "#/definitions/WebSearchContextSize" + }, + { + "type": "null" + } + ] + }, + "location": { + "anyOf": [ + { + "$ref": "#/definitions/WebSearchLocation" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "WindowsSandboxReadiness": { + "enum": [ + "ready", + "notConfigured", + "updateRequired" + ], + "type": "string" + }, + "WindowsSandboxReadinessResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "status": { + "$ref": "#/definitions/WindowsSandboxReadiness" + } + }, + "required": [ + "status" + ], + "title": "WindowsSandboxReadinessResponse", + "type": "object" + }, + "WindowsSandboxSetupCompletedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "error": { + "type": [ + "string", + "null" + ] + }, + "mode": { + "$ref": "#/definitions/WindowsSandboxSetupMode" + }, + "success": { + "type": "boolean" + } + }, + "required": [ + "mode", + "success" + ], + "title": "WindowsSandboxSetupCompletedNotification", + "type": "object" + }, + "WindowsSandboxSetupMode": { + "enum": [ + "elevated", + "unelevated" + ], + "type": "string" + }, + "WindowsSandboxSetupStartParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "cwd": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "mode": { + "$ref": "#/definitions/WindowsSandboxSetupMode" + } + }, + "required": [ + "mode" + ], + "title": "WindowsSandboxSetupStartParams", + "type": "object" + }, + "WindowsSandboxSetupStartResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "started": { + "type": "boolean" + } + }, + "required": [ + "started" + ], + "title": "WindowsSandboxSetupStartResponse", + "type": "object" + }, + "WindowsWorldWritableWarningNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "extraCount": { + "format": "uint", + "minimum": 0.0, + "type": "integer" + }, + "failedScan": { + "type": "boolean" + }, + "samplePaths": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "extraCount", + "failedScan", + "samplePaths" + ], + "title": "WindowsWorldWritableWarningNotification", + "type": "object" + }, + "WorkspaceMessage": { + "properties": { + "archivedAt": { + "description": "Unix timestamp (in seconds) when the message was archived.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "createdAt": { + "description": "Unix timestamp (in seconds) when the message was created.", + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "messageBody": { + "type": "string" + }, + "messageId": { + "type": "string" + }, + "messageType": { + "$ref": "#/definitions/WorkspaceMessageType" + } + }, + "required": [ + "messageBody", + "messageId", + "messageType" + ], + "type": "object" + }, + "WorkspaceMessageType": { + "enum": [ + "headline", + "announcement", + "unknown" + ], + "type": "string" + }, + "WriteStatus": { + "enum": [ + "ok", + "okOverridden" + ], + "type": "string" + } + }, + "title": "CodexAppServerProtocolV2", + "type": "object" +} \ No newline at end of file diff --git a/internal/agent/runtime/codex/protocolgen/subset.go b/internal/agent/runtime/codex/protocolgen/subset.go new file mode 100644 index 0000000000..84b517ccc6 --- /dev/null +++ b/internal/agent/runtime/codex/protocolgen/subset.go @@ -0,0 +1,82 @@ +package protocolgen + +// The generator emits only the v2 subset Memoh actually speaks. Unknown +// methods and variants are tolerated at runtime by design (decoded to raw +// envelopes), so growing this list later is purely additive: add the method, +// regenerate, and the new typed params appear. +// +// The two v1 legacy server requests (applyPatchApproval, execCommandApproval) +// are deliberately absent: the runtime is v2-only. + +// clientMethod is a request Memoh sends to the app-server. +type clientMethod struct { + Method string + // Response names the response definition. Empty means the method is + // handled outside the generated code (only `initialize`, whose response + // type is version-independent bootstrap and lives in the protocol + // package's hand-written core). + Response string +} + +var clientMethods = []clientMethod{ + {Method: "initialize"}, + {Method: "thread/start", Response: "ThreadStartResponse"}, + {Method: "thread/resume", Response: "ThreadResumeResponse"}, + {Method: "thread/fork", Response: "ThreadForkResponse"}, + {Method: "thread/read", Response: "ThreadReadResponse"}, + {Method: "thread/compact/start", Response: "ThreadCompactStartResponse"}, + {Method: "turn/start", Response: "TurnStartResponse"}, + {Method: "turn/steer", Response: "TurnSteerResponse"}, + {Method: "turn/interrupt", Response: "TurnInterruptResponse"}, + {Method: "model/list", Response: "ModelListResponse"}, + {Method: "account/read", Response: "GetAccountResponse"}, + {Method: "account/rateLimits/read", Response: "GetAccountRateLimitsResponse"}, + {Method: "account/login/start", Response: "LoginAccountResponse"}, + {Method: "account/login/cancel", Response: "CancelLoginAccountResponse"}, + {Method: "account/logout", Response: "LogoutAccountResponse"}, +} + +// serverRequestMethod is a request the app-server sends to Memoh and waits on +// (approvals, elicitation, auth refresh). Responses are standalone documents +// vendored next to the bundle. +type serverRequestMethod struct { + Method string + Response string +} + +var serverRequestMethods = []serverRequestMethod{ + {Method: "item/commandExecution/requestApproval", Response: "CommandExecutionRequestApprovalResponse"}, + {Method: "item/fileChange/requestApproval", Response: "FileChangeRequestApprovalResponse"}, + {Method: "item/permissions/requestApproval", Response: "PermissionsRequestApprovalResponse"}, + {Method: "item/tool/requestUserInput", Response: "ToolRequestUserInputResponse"}, + {Method: "mcpServer/elicitation/request", Response: "McpServerElicitationRequestResponse"}, + {Method: "account/chatgptAuthTokens/refresh", Response: "ChatgptAuthTokensRefreshResponse"}, +} + +// serverNotifications are the notifications Memoh decodes into typed params. +// Anything not listed still surfaces as a raw envelope. +var serverNotifications = []string{ + "error", + "warning", + "configWarning", + "deprecationNotice", + "thread/started", + "thread/status/changed", + "thread/tokenUsage/updated", + "thread/compacted", + "turn/started", + "turn/completed", + "turn/plan/updated", + "item/started", + "item/completed", + "item/agentMessage/delta", + "item/reasoning/textDelta", + "item/reasoning/summaryTextDelta", + "item/reasoning/summaryPartAdded", + "item/commandExecution/outputDelta", + "item/fileChange/outputDelta", + "account/updated", + "account/login/completed", + "account/rateLimits/updated", + "serverRequest/resolved", +} diff --git a/internal/agent/runtime/codex/tools.go b/internal/agent/runtime/codex/tools.go new file mode 100644 index 0000000000..6c0c746ccc --- /dev/null +++ b/internal/agent/runtime/codex/tools.go @@ -0,0 +1,155 @@ +// Memoh tool-gateway injection for codex threads. Each thread gets its own +// reverse-HTTP route into the workspace tools proxy, configured through the +// thread's `mcp_servers` config override at start/resume. The route outlives +// individual turns (thread config is start-time), so the tool session +// identity resolves live from the thread's active turn. +package codex + +import ( + "fmt" + "strings" + "sync" + + "github.com/felinics/memoh/internal/agent/runtime/external" + "github.com/felinics/memoh/internal/agent/runtime/toolmount" + "github.com/felinics/memoh/internal/agent/sessionmode" + "github.com/felinics/memoh/internal/mcp" + "github.com/felinics/memoh/internal/runtimefence" +) + +// memohMCPServerName is the config key codex shows in tool names +// (memoh__send_message and friends). +const memohMCPServerName = "memoh" + +// MemohToolTimeoutSec caps one Memoh gateway tool call on the codex side. +// Memoh tools can legitimately block on a human decision (approval cards, +// ask_user) for up to their wait windows; the toolmount timeout-ladder guard +// test pins this above them. +const MemohToolTimeoutSec = 900 + +// threadRef carries the thread id into the mount's session resolver; the id +// is only known after thread/start returns. +type threadRef struct { + mu sync.Mutex + id string +} + +func (r *threadRef) set(id string) { + r.mu.Lock() + r.id = id + r.mu.Unlock() +} + +func (r *threadRef) get() string { + r.mu.Lock() + defer r.mu.Unlock() + return r.id +} + +// prepareThreadTools mounts the gateway for a thread about to start or +// resume. The returned function binds the mount to the created thread or +// closes it when creation fails. +func (d *Driver) prepareThreadTools(srv *appServer, input external.PromptInput) (map[string]any, func(threadID string), error) { + baseURL := toolmount.ResolveBaseURL(srv.workspaceInfo, input.ToolHTTPURL) + if baseURL == "" { + return nil, nil, fmt.Errorf("resolve Memoh tool gateway URL for %s workspace", srv.workspaceInfo.Backend) + } + ref := &threadRef{} + mount, err := toolmount.Serve(srv.mountCtx, srv.client, baseURL, d.toolGateway, func() mcp.ToolSessionContext { + return srv.toolSessionForThread(ref.get()) + }) + if err != nil { + return nil, nil, fmt.Errorf("mount Memoh tool gateway: %w", err) + } + config := map[string]any{ + "features.default_mode_request_user_input": true, + "mcp_servers": map[string]any{ + memohMCPServerName: map[string]any{ + "url": mount.URL, + "tool_timeout_sec": MemohToolTimeoutSec, + }, + }, + } + return config, func(threadID string) { + if threadID == "" { + mount.Stop() + return + } + ref.set(threadID) + srv.registerToolMount(threadID, mount) + }, nil +} + +// registerToolMount records a thread's live gateway mount, replacing (and +// stopping) any previous one for the same thread. +func (s *appServer) registerToolMount(threadID string, mount *toolmount.Mount) { + s.mu.Lock() + previous := s.toolMounts[threadID] + s.toolMounts[threadID] = mount + s.mu.Unlock() + if previous != nil { + previous.Stop() + } +} + +// stopToolMounts tears down every thread mount; used on server close. +func (s *appServer) stopToolMounts() { + s.mu.Lock() + mounts := make([]*toolmount.Mount, 0, len(s.toolMounts)) + for _, mount := range s.toolMounts { + mounts = append(mounts, mount) + } + s.toolMounts = map[string]*toolmount.Mount{} + s.mu.Unlock() + for _, mount := range mounts { + mount.Stop() + } +} + +// toolSessionForThread resolves the trusted tool identity for a thread's MCP +// requests: bot identity always, plus the live per-turn fields while a turn +// runs. It never trusts anything from the HTTP request. The mount outlives +// turns, so RequireActiveRun keeps the idle window to tools/list — a call +// with no live turn has no run to own it and is refused at the gateway. +func (s *appServer) toolSessionForThread(threadID string) mcp.ToolSessionContext { + session := mcp.ToolSessionContext{ + BotID: s.botID, + ChatID: s.botID, + SessionType: sessionmode.Chat, + CanListUserInput: true, + RequireActiveRun: true, + } + if threadID == "" { + return session + } + turn := s.turnForThread(threadID) + if turn == nil { + return session + } + in := turn.input + overlay := func(dst *string, value string) { + if value = strings.TrimSpace(value); value != "" { + *dst = value + } + } + overlay(&session.ChatID, in.ChatID) + overlay(&session.SessionID, in.ThreadID) + overlay(&session.RunID, in.RunID) + overlay(&session.SessionType, in.SessionMode) + overlay(&session.RouteID, in.RouteID) + overlay(&session.CurrentPlatform, in.CurrentPlatform) + overlay(&session.ReplyTarget, in.ReplyTarget) + overlay(&session.ConversationType, in.ConversationType) + overlay(&session.ChannelIdentityID, in.ChannelIdentityID) + overlay(&session.SessionToken, in.SessionToken) + session.CanRequestUserInput = in.CanRequestUserInput + session.RuntimeActive = true + session.SupportsImageInput = true + session.ContextBudgetMaxTokens = in.ContextBudgetMaxTokens + session.ContextToolExchangePolicy = in.ContextToolExchangePolicy + if fence, ok := runtimefence.FromContext(turn.ctx); ok { + session.RuntimeFence = fence + } + session.RunContext = turn.ctx + return session +} diff --git a/internal/agent/runtime/codex/turn.go b/internal/agent/runtime/codex/turn.go new file mode 100644 index 0000000000..b23a816839 --- /dev/null +++ b/internal/agent/runtime/codex/turn.go @@ -0,0 +1,717 @@ +package codex + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "log/slog" + "strings" + "sync" + "time" + + sdk "github.com/felinics/twilight/sdk" + + "github.com/felinics/memoh/internal/agent/decision/approval" + "github.com/felinics/memoh/internal/agent/event" + "github.com/felinics/memoh/internal/agent/runtime/codex/protocol" + "github.com/felinics/memoh/internal/agent/runtime/external" +) + +// interruptSettleTimeout bounds how long an interrupted turn may take to +// deliver its terminal notification before the driver gives up waiting. +const interruptSettleTimeout = 10 * time.Second + +// turnState tracks one running turn: it receives routed notifications and +// server requests, translates them into stream events, and assembles the +// transcript. +type turnState struct { + input external.PromptInput + approval approval.FlowService + waiter func(approvalID string) func() + userInput UserInputService + // toolLookup reports whether a tool name exists on the Memoh gateway; the + // MCP consent branch uses it to recognize Memoh-owned tools. + toolLookup func(context.Context, string) bool + logger *slog.Logger + threadID string + + // ctx is the turn-scoped context: it outlives the caller's stream context + // only long enough to unwind cleanly — it is cancelled when Prompt + // returns, aborting any decision still waiting. + ctx context.Context + cancel context.CancelFunc + + done chan struct{} + + mu sync.Mutex + turnID string + events []event.StreamEvent + finalText string + usage *sdk.Usage + threadTotals *protocol.TokenUsageBreakdown + contextWindow *int64 + turn *protocol.Turn + turnErr *protocol.TurnError + toolNames map[string]string // item id → emitted tool name + // inflight tracks decision goroutines by server-request id so a + // serverRequest/resolved notification can withdraw them. + inflight map[string]context.CancelFunc + closed bool + + // queue decouples the shared connection read loop from sink delivery: a + // stalled consumer must not head-of-line block the bot's other threads. + queue []event.StreamEvent + queueCond *sync.Cond + doneOnce sync.Once + pumpDone chan struct{} +} + +func newTurnState(parent context.Context, input external.PromptInput, threadID string, approvalSvc approval.FlowService, waiter func(string) func(), userInput UserInputService, toolLookup func(context.Context, string) bool, logger *slog.Logger) *turnState { + ctx, cancel := context.WithCancel(context.WithoutCancel(parent)) + t := &turnState{ + input: input, + approval: approvalSvc, + waiter: waiter, + userInput: userInput, + toolLookup: toolLookup, + logger: logger, + threadID: threadID, + ctx: ctx, + cancel: cancel, + done: make(chan struct{}), + toolNames: map[string]string{}, + inflight: map[string]context.CancelFunc{}, + pumpDone: make(chan struct{}), + } + t.queueCond = sync.NewCond(&t.mu) + go t.pump() + return t +} + +// close stops event delivery and unwinds in-flight decisions. Called exactly +// once, by the driver, before Prompt returns. +func (t *turnState) close() { + t.mu.Lock() + if t.closed { + t.mu.Unlock() + return + } + t.closed = true + inflight := t.inflight + t.inflight = map[string]context.CancelFunc{} + t.queueCond.Broadcast() + t.mu.Unlock() + for _, cancel := range inflight { + cancel() + } + t.cancel() + <-t.pumpDone +} + +// emit records one stream event for the transcript and queues it for +// delivery. After close it is a no-op: the caller's event channel may already +// be gone, and a late decision outcome must never crash the stream. +func (t *turnState) emit(ev event.StreamEvent) { + t.mu.Lock() + if t.closed { + t.mu.Unlock() + return + } + t.events = append(t.events, ev) + t.queue = append(t.queue, ev) + t.queueCond.Signal() + t.mu.Unlock() +} + +// pump delivers queued events to the sink on its own goroutine. +func (t *turnState) pump() { + defer close(t.pumpDone) + for { + t.mu.Lock() + for len(t.queue) == 0 && !t.closed { + t.queueCond.Wait() + } + if len(t.queue) == 0 && t.closed { + t.mu.Unlock() + return + } + batch := t.queue + t.queue = nil + t.mu.Unlock() + for _, ev := range batch { + t.input.Sink.EmitStreamEvent(ev) + } + } +} + +func (t *turnState) finish() { + t.doneOnce.Do(func() { close(t.done) }) +} + +// setTurnID pins the turn id this state accepts notifications for. +func (t *turnState) setTurnID(turnID string) { + turnID = strings.TrimSpace(turnID) + if turnID == "" { + return + } + t.mu.Lock() + if t.turnID == "" { + t.turnID = turnID + } + t.mu.Unlock() +} + +func (t *turnState) currentTurnID() string { + t.mu.Lock() + defer t.mu.Unlock() + return t.turnID +} + +// acceptsTurn reports whether a notification carrying turnID belongs to this +// turn. Late notifications from a previous turn on the same thread must not +// leak in (a stale turn/completed would end the new turn instantly). +func (t *turnState) acceptsTurn(turnID string) bool { + turnID = strings.TrimSpace(turnID) + if turnID == "" { + return true + } + t.mu.Lock() + defer t.mu.Unlock() + return t.turnID == "" || t.turnID == turnID +} + +// handleNotification translates app-server notifications into stream events. +// It runs on the connection read loop and must stay non-blocking. +func (t *turnState) handleNotification(decoded any) { + switch params := decoded.(type) { + case *protocol.TurnStartedNotification: + t.setTurnID(params.Turn.ID) + case *protocol.AgentMessageDeltaNotification: + if !t.acceptsTurn(params.TurnID) { + return + } + t.emit(event.StreamEvent{Type: event.TextDelta, Delta: params.Delta}) + case *protocol.ReasoningTextDeltaNotification: + if !t.acceptsTurn(params.TurnID) { + return + } + t.emit(event.StreamEvent{Type: event.ReasoningDelta, Delta: params.Delta}) + case *protocol.ReasoningSummaryTextDeltaNotification: + if !t.acceptsTurn(params.TurnID) { + return + } + t.emit(event.StreamEvent{Type: event.ReasoningDelta, Delta: params.Delta}) + case *protocol.ReasoningSummaryPartAddedNotification: + if !t.acceptsTurn(params.TurnID) { + return + } + // Summary parts are separate paragraphs of the same reasoning stream. + t.emit(event.StreamEvent{Type: event.ReasoningDelta, Delta: "\n\n"}) + case *protocol.ItemStartedNotification: + if !t.acceptsTurn(params.TurnID) { + return + } + t.handleItemStarted(¶ms.Item) + case *protocol.ItemCompletedNotification: + if !t.acceptsTurn(params.TurnID) { + return + } + t.handleItemCompleted(¶ms.Item) + case *protocol.CommandExecutionOutputDeltaNotification: + if !t.acceptsTurn(params.TurnID) { + return + } + t.emit(event.StreamEvent{Type: event.ToolCallProgress, ToolCallID: params.ItemID, ToolName: t.toolName(params.ItemID), Progress: params.Delta}) + case *protocol.FileChangeOutputDeltaNotification: + if !t.acceptsTurn(params.TurnID) { + return + } + t.emit(event.StreamEvent{Type: event.ToolCallProgress, ToolCallID: params.ItemID, ToolName: t.toolName(params.ItemID), Progress: params.Delta}) + case *protocol.ThreadTokenUsageUpdatedNotification: + if !t.acceptsTurn(params.TurnID) { + return + } + // `last` is the most recent model request's usage and the turn spans + // several requests, so the turn total is the running sum of `last`. + t.mu.Lock() + if t.usage == nil { + t.usage = &sdk.Usage{} + } + last := params.TokenUsage.Last + t.usage.InputTokens += int(last.InputTokens) + t.usage.OutputTokens += int(last.OutputTokens) + t.usage.TotalTokens += int(last.TotalTokens) + t.usage.ReasoningTokens += int(last.ReasoningOutputTokens) + t.usage.CachedInputTokens += int(last.CachedInputTokens) + totals := params.TokenUsage.Total + t.threadTotals = &totals + t.contextWindow = params.TokenUsage.ModelContextWindow + t.mu.Unlock() + case *protocol.TurnCompletedNotification: + if !t.acceptsTurn(params.Turn.ID) { + return + } + t.mu.Lock() + turn := params.Turn + t.turn = &turn + t.mu.Unlock() + t.finish() + case *protocol.ErrorNotification: + if !t.acceptsTurn(params.TurnID) { + return + } + if params.WillRetry { + t.logger.Warn("codex turn error, retrying", slog.String("thread_id", t.threadID), slog.String("message", params.Error.Message)) + return + } + t.mu.Lock() + turnErr := params.Error + t.turnErr = &turnErr + t.mu.Unlock() + case *protocol.ServerRequestResolvedNotification: + // The server settled its own request (e.g. auto-review); withdraw the + // matching pending decision instead of leaving a dead approval card. + t.mu.Lock() + cancel := t.inflight[params.RequestID.Key()] + t.mu.Unlock() + if cancel != nil { + cancel() + } + case *protocol.ContextCompactedNotification: //nolint:staticcheck // still the wire shape for thread/compacted at the pinned version + t.logger.Info("codex compacted thread context", slog.String("thread_id", t.threadID)) + } +} + +func (t *turnState) handleItemStarted(item *protocol.ThreadItem) { + switch { + case item.CommandExecution != nil: + cmd := item.CommandExecution + t.rememberTool(cmd.ID, "exec") + t.emit(event.StreamEvent{ + Type: event.ToolCallStart, ToolCallID: cmd.ID, ToolName: "exec", + Input: map[string]any{"command": cmd.Command, "cwd": cmd.Cwd}, + }) + case item.FileChange != nil: + fc := item.FileChange + t.rememberTool(fc.ID, "write") + t.emit(event.StreamEvent{ + Type: event.ToolCallStart, ToolCallID: fc.ID, ToolName: "write", + Input: fileChangeInput(fc), + }) + case item.MCPToolCall != nil: + mcpCall := item.MCPToolCall + if isMemohMCPToolCall(mcpCall) { + return + } + name := mcpToolName(mcpCall) + t.rememberTool(mcpCall.ID, name) + t.emit(event.StreamEvent{ + Type: event.ToolCallStart, ToolCallID: mcpCall.ID, ToolName: name, + Input: mcpCall.Arguments, + }) + case item.WebSearch != nil: + search := item.WebSearch + t.rememberTool(search.ID, "web_search") + t.emit(event.StreamEvent{ + Type: event.ToolCallStart, ToolCallID: search.ID, ToolName: "web_search", + Input: map[string]any{"query": search.Query}, + }) + } +} + +func (t *turnState) handleItemCompleted(item *protocol.ThreadItem) { + switch { + case item.AgentMessage != nil: + t.mu.Lock() + if text := item.AgentMessage.Text; text != "" { + if t.finalText != "" { + t.finalText += "\n\n" + } + t.finalText += text + } + t.mu.Unlock() + case item.CommandExecution != nil: + cmd := item.CommandExecution + result := map[string]any{} + if cmd.ExitCode != nil { + result["exitCode"] = *cmd.ExitCode + } + if cmd.AggregatedOutput != nil { + result["output"] = *cmd.AggregatedOutput + } + ev := event.StreamEvent{Type: event.ToolCallEnd, ToolCallID: cmd.ID, ToolName: t.toolName(cmd.ID), Result: result} + if cmd.ExitCode != nil && *cmd.ExitCode != 0 { + ev.Status = "failed" + } + t.emit(ev) + case item.FileChange != nil: + fc := item.FileChange + t.emit(event.StreamEvent{Type: event.ToolCallEnd, ToolCallID: fc.ID, ToolName: t.toolName(fc.ID), Result: fileChangeInput(fc)}) + case item.MCPToolCall != nil: + mcpCall := item.MCPToolCall + if isMemohMCPToolCall(mcpCall) { + return + } + ev := event.StreamEvent{Type: event.ToolCallEnd, ToolCallID: mcpCall.ID, ToolName: t.toolName(mcpCall.ID)} + if mcpCall.Result != nil { + ev.Result = *mcpCall.Result + } + if mcpCall.Status == protocol.McpToolCallStatusFailed { + ev.Status = "failed" + if mcpCall.Error != nil { + ev.Error = mcpToolErrorMessage(mcpCall.Error) + } + } + t.emit(ev) + case item.WebSearch != nil: + search := item.WebSearch + t.emit(event.StreamEvent{Type: event.ToolCallEnd, ToolCallID: search.ID, ToolName: t.toolName(search.ID)}) + } +} + +func (t *turnState) rememberTool(itemID, name string) { + t.mu.Lock() + t.toolNames[itemID] = name + t.mu.Unlock() +} + +func (t *turnState) toolName(itemID string) string { + t.mu.Lock() + defer t.mu.Unlock() + if name := t.toolNames[itemID]; name != "" { + return name + } + return "exec" +} + +// handleServerRequest decides one approval request through the Memoh approval +// flow and answers the app-server. Runs on its own goroutine, bounded by the +// turn-scoped context and cancellable via serverRequest/resolved. +func (t *turnState) handleServerRequest(c *conn, req *protocol.Inbound, decoded any) { + ctx, cancel := context.WithCancel(t.ctx) + defer cancel() + key := req.ID.Key() + t.mu.Lock() + if t.closed { + t.mu.Unlock() + _ = c.RespondError(req.ID, -32000, "the turn ended before this request was decided") + return + } + t.inflight[key] = cancel + t.mu.Unlock() + defer func() { + t.mu.Lock() + delete(t.inflight, key) + t.mu.Unlock() + }() + + switch params := decoded.(type) { + case *protocol.CommandExecutionRequestApprovalParams: + input := map[string]any{} + if params.Command != nil { + input["command"] = *params.Command + } + if params.Cwd != nil { + input["cwd"] = *params.Cwd + } + if params.Reason != nil { + input["reason"] = *params.Reason + } + if len(params.ProposedExecpolicyAmendment) > 0 { + input["proposed_execpolicy_amendment"] = params.ProposedExecpolicyAmendment + } + if len(params.ProposedNetworkPolicyAmendments) > 0 { + input["proposed_network_policy_amendments"] = params.ProposedNetworkPolicyAmendments + } + result := t.decide(ctx, approvalCallID(params.ApprovalID, params.ItemID), "exec", input, commandApprovalOptions(params)) + decision := commandApprovalDecision(params, result) + _ = c.Respond(req.ID, protocol.CommandExecutionRequestApprovalResponse{Decision: decision}) + case *protocol.FileChangeRequestApprovalParams: + input := map[string]any{} + if params.Reason != nil { + input["reason"] = *params.Reason + } + if params.GrantRoot != nil { + input["grantRoot"] = *params.GrantRoot + } + result := t.decide(ctx, params.ItemID, "write", input, fileChangeApprovalOptions(params)) + decision := fileChangeApprovalDecision(result) + _ = c.Respond(req.ID, protocol.FileChangeRequestApprovalResponse{Decision: decision}) + case *protocol.PermissionsRequestApprovalParams: + // Granting permission-profile escalations needs a dedicated surface; + // until then the request is declined, not silently granted. + t.logger.Warn("codex: declining permission-profile escalation", slog.String("thread_id", t.threadID)) + _ = c.RespondError(req.ID, -32000, "memoh does not grant permission profile escalations yet") + case *protocol.ToolRequestUserInputParams: + _ = c.Respond(req.ID, t.requestUserInput(ctx, params)) + default: + _ = c.RespondError(req.ID, -32601, "memoh does not handle this request") + } +} + +// approvalCallID prefers the dedicated approval callback id: one item can +// raise several approvals (sub-command review, stdin writes) and the pair +// (session, tool_call_id) must stay unique per decision. +func approvalCallID(approvalID *string, itemID string) string { + if approvalID != nil && strings.TrimSpace(*approvalID) != "" { + return strings.TrimSpace(*approvalID) + } + return itemID +} + +func commandApprovalOptions(params *protocol.CommandExecutionRequestApprovalParams) []approval.PermissionOption { + options := []approval.PermissionOption{ + {ID: protocol.CommandExecutionApprovalDecisionUnitAccept, Name: "Accept", Kind: approval.OptionKindAllowOnce}, + {ID: protocol.CommandExecutionApprovalDecisionUnitAcceptForSession, Name: "Accept for session", Kind: approval.OptionKindAllowAlways}, + } + if len(params.ProposedExecpolicyAmendment) > 0 { + options = append(options, approval.PermissionOption{ + ID: "acceptWithExecpolicyAmendment", + Name: "Accept and remember: " + strings.Join(params.ProposedExecpolicyAmendment, " "), + Kind: approval.OptionKindAllowAlways, + }) + } + for i, amendment := range params.ProposedNetworkPolicyAmendments { + kind := approval.OptionKindAllowAlways + if amendment.Action == protocol.NetworkPolicyRuleActionDeny { + kind = approval.OptionKindRejectAlways + } + options = append(options, approval.PermissionOption{ + ID: fmt.Sprintf("applyNetworkPolicyAmendment:%d", i), + Name: fmt.Sprintf("%s %s", amendment.Action, amendment.Host), + Kind: kind, + }) + } + return append(options, approval.PermissionOption{ + ID: protocol.CommandExecutionApprovalDecisionUnitDecline, Name: "Decline", Kind: approval.OptionKindRejectOnce, + }) +} + +func commandApprovalDecision(params *protocol.CommandExecutionRequestApprovalParams, result approval.FlowResult) protocol.CommandExecutionApprovalDecision { + switch result.SelectedOptionID { + case protocol.CommandExecutionApprovalDecisionUnitAccept: + return protocol.CommandExecutionApprovalDecision{Unit: protocol.CommandExecutionApprovalDecisionUnitAccept} + case protocol.CommandExecutionApprovalDecisionUnitAcceptForSession: + return protocol.CommandExecutionApprovalDecision{Unit: protocol.CommandExecutionApprovalDecisionUnitAcceptForSession} + case protocol.CommandExecutionApprovalDecisionUnitDecline: + return protocol.CommandExecutionApprovalDecision{Unit: protocol.CommandExecutionApprovalDecisionUnitDecline} + case "acceptWithExecpolicyAmendment": + return protocol.CommandExecutionApprovalDecision{ + AcceptWithExecpolicyAmendment: &protocol.CommandExecutionApprovalDecisionAcceptWithExecpolicyAmendment{ + ExecpolicyAmendment: params.ProposedExecpolicyAmendment, + }, + } + } + for i, amendment := range params.ProposedNetworkPolicyAmendments { + if result.SelectedOptionID == fmt.Sprintf("applyNetworkPolicyAmendment:%d", i) { + return protocol.CommandExecutionApprovalDecision{ + ApplyNetworkPolicyAmendment: &protocol.CommandExecutionApprovalDecisionApplyNetworkPolicyAmendment{ + NetworkPolicyAmendment: amendment, + }, + } + } + } + if result.Approved { + return protocol.CommandExecutionApprovalDecision{Unit: protocol.CommandExecutionApprovalDecisionUnitAccept} + } + if strings.EqualFold(result.Status, approval.StatusRejected) { + return protocol.CommandExecutionApprovalDecision{Unit: protocol.CommandExecutionApprovalDecisionUnitDecline} + } + return protocol.CommandExecutionApprovalDecision{Unit: protocol.CommandExecutionApprovalDecisionUnitCancel} +} + +func fileChangeApprovalOptions(params *protocol.FileChangeRequestApprovalParams) []approval.PermissionOption { + options := []approval.PermissionOption{{ + ID: string(protocol.FileChangeApprovalDecisionAccept), Name: "Accept", Kind: approval.OptionKindAllowOnce, + }} + if params.GrantRoot != nil { + options = append(options, approval.PermissionOption{ + ID: string(protocol.FileChangeApprovalDecisionAcceptForSession), Name: "Accept for session", Kind: approval.OptionKindAllowAlways, + }) + } + return append(options, approval.PermissionOption{ + ID: string(protocol.FileChangeApprovalDecisionDecline), Name: "Decline", Kind: approval.OptionKindRejectOnce, + }) +} + +func fileChangeApprovalDecision(result approval.FlowResult) protocol.FileChangeApprovalDecision { + switch result.SelectedOptionID { + case string(protocol.FileChangeApprovalDecisionAccept): + return protocol.FileChangeApprovalDecisionAccept + case string(protocol.FileChangeApprovalDecisionAcceptForSession): + return protocol.FileChangeApprovalDecisionAcceptForSession + case string(protocol.FileChangeApprovalDecisionDecline): + return protocol.FileChangeApprovalDecisionDecline + } + if result.Approved { + return protocol.FileChangeApprovalDecisionAccept + } + if strings.EqualFold(result.Status, approval.StatusRejected) { + return protocol.FileChangeApprovalDecisionDecline + } + return protocol.FileChangeApprovalDecisionCancel +} + +// decide runs one approval through policy and, when needed, the interactive +// decision flow. Failures fail closed (cancelled). +func (t *turnState) decide(ctx context.Context, callID, toolName string, input map[string]any, options []approval.PermissionOption) approval.FlowResult { + if t.approval == nil { + return approval.FlowResult{Status: approval.StatusRejected, DecisionReason: "approval service unavailable"} + } + result, err := approval.RunFlow(ctx, t.approval, approval.FlowRequest{ + Input: approval.CreatePendingInput{ + BotID: t.input.BotID, + SessionID: t.input.ThreadID, + RouteID: t.input.RouteID, + ChannelIdentityID: t.input.ChannelIdentityID, + RequestedByChannelIdentityID: t.input.ChannelIdentityID, + ToolCallID: callID, + ToolName: toolName, + ToolInput: input, + Options: options, + }, + Interactive: t.input.CanRequestUserInput, + RegisterWaiter: t.waiter, + Emit: t.emitApprovalRequest, + CancelOnAbort: func(cancelCtx context.Context, req approval.Request, reason string) (approval.Request, error) { + return t.approval.Reject(cancelCtx, req.ID, "", reason) + }, + }) + if err != nil { + if ctx.Err() != nil { + return approval.FlowResult{Status: approval.StatusCancelled, DecisionReason: "the turn ended before a decision arrived"} + } + t.logger.Error("codex approval flow failed", slog.String("thread_id", t.threadID), slog.Any("error", err)) + return approval.FlowResult{Status: approval.StatusCancelled, DecisionReason: "approval flow failed"} + } + return result +} + +func (t *turnState) emitApprovalRequest(req approval.Request) bool { + t.emit(event.StreamEvent{ + Type: event.ToolApprovalRequest, + ToolCallID: req.ToolCallID, + ToolName: req.ToolName, + Input: req.ToolInput, + ApprovalID: req.ID, + ShortID: req.ShortID, + Status: approval.NormalizedStatus(req.Status), + Metadata: map[string]any{ + "approval": approval.RequestMetadata(req), + }, + }) + return true +} + +// result assembles the durable outcome after the turn settled. +func (t *turnState) result(newThreadID string) (external.PromptResult, error) { + t.mu.Lock() + defer t.mu.Unlock() + + out := external.PromptResult{ + Output: external.TranscriptFromEvents(t.events, t.finalText), + Text: t.finalText, + } + if t.usage != nil { + usage := *t.usage + out.Usage = &usage + } + if newThreadID != "" || t.threadTotals != nil { + out.RuntimeMetadata = map[string]any{} + if newThreadID != "" { + out.RuntimeMetadata[metadataThreadIDKey] = newThreadID + } + // Context-occupancy data for the session UI: the thread's cumulative + // token count against its model context window. + if t.threadTotals != nil { + out.RuntimeMetadata["codex_thread_total_tokens"] = t.threadTotals.TotalTokens + } + if t.contextWindow != nil { + out.RuntimeMetadata["codex_context_window"] = *t.contextWindow + } + } + + var turnErr *protocol.TurnError + status := protocol.TurnStatusFailed + if t.turn != nil { + status = t.turn.Status + turnErr = t.turn.Error + } + if turnErr == nil { + turnErr = t.turnErr + } + out.StopReason = string(status) + out.AgentTurnID = t.turnID + switch status { + case protocol.TurnStatusCompleted: + out.TurnCompleted = true + return out, nil + case protocol.TurnStatusInterrupted: + return out, nil + default: + message := "codex turn failed" + if turnErr != nil && strings.TrimSpace(turnErr.Message) != "" { + message = turnErr.Message + } + return out, errors.New(message) + } +} + +// buildTurnInput assembles the turn/start input items: the Memoh context +// document, the user's message, and any inline images as data URLs. +func buildTurnInput(input external.PromptInput) []protocol.UserInput { + items := make([]protocol.UserInput, 0, 2+len(input.Images)) + if text := strings.TrimSpace(input.ContextMarkdown); text != "" { + items = append(items, protocol.UserInput{Text: &protocol.TextUserInput{Text: text}}) + } + items = append(items, protocol.UserInput{Text: &protocol.TextUserInput{Text: input.Prompt}}) + for _, img := range input.Images { + if len(img.Data) == 0 { + continue + } + mime := img.MimeType + if mime == "" { + mime = "image/png" + } + url := "data:" + mime + ";base64," + base64.StdEncoding.EncodeToString(img.Data) + items = append(items, protocol.UserInput{Image: &protocol.ImageUserInput{URL: url}}) + } + return items +} + +// fileChangeInput summarizes a file-change item for tool-event display. +func fileChangeInput(fc *protocol.FileChangeThreadItem) map[string]any { + raw, err := json.Marshal(fc) + if err != nil { + return map[string]any{} + } + var out map[string]any + if err := json.Unmarshal(raw, &out); err != nil { + return map[string]any{} + } + delete(out, "id") + return out +} + +func mcpToolName(call *protocol.McpToolCallThreadItem) string { + server := strings.TrimSpace(call.Server) + tool := strings.TrimSpace(call.Tool) + if server == "" { + return tool + } + return fmt.Sprintf("%s.%s", server, tool) +} + +func isMemohMCPToolCall(call *protocol.McpToolCallThreadItem) bool { + return strings.EqualFold(strings.TrimSpace(call.Server), memohMCPServerName) +} + +func mcpToolErrorMessage(mcpErr *protocol.McpToolCallError) string { + raw, err := json.Marshal(mcpErr) + if err != nil { + return "MCP tool call failed" + } + return string(raw) +} diff --git a/internal/agent/runtime/codex/userinput.go b/internal/agent/runtime/codex/userinput.go new file mode 100644 index 0000000000..afcb719d00 --- /dev/null +++ b/internal/agent/runtime/codex/userinput.go @@ -0,0 +1,179 @@ +// Bridges codex `tool/requestUserInput` to Memoh's ask_user decision flow: +// codex questions render as ask_user cards, the answers map back to the +// question ids codex asked with, and a canceled or timed-out card degrades to +// the empty answer set codex treats as "proceed with defaults". +package codex + +import ( + "context" + "fmt" + "log/slog" + "strings" + + userinput "github.com/felinics/memoh/internal/agent/decision/input" + "github.com/felinics/memoh/internal/agent/event" + "github.com/felinics/memoh/internal/agent/runtime/codex/protocol" +) + +// UserInputService is the ask_user decision flow the driver routes +// tool/requestUserInput through. +type UserInputService interface { + userinput.FlowService +} + +// requestUserInput answers one tool/requestUserInput server request. It never +// fails the turn: any flow error or non-submitted outcome yields the empty +// answer set. +func (t *turnState) requestUserInput(ctx context.Context, params *protocol.ToolRequestUserInputParams) protocol.ToolRequestUserInputResponse { + answers := map[string]protocol.ToolRequestUserInputAnswer{} + if t.userInput == nil || len(params.Questions) == 0 { + return protocol.ToolRequestUserInputResponse{Answers: answers} + } + // ask_user renders a bounded number of questions per card; longer + // requests run as sequential cards and stop at the first card the user + // does not submit. + for start := 0; start < len(params.Questions); start += userinput.MaxQuestionsPerRequest { + end := min(start+userinput.MaxQuestionsPerRequest, len(params.Questions)) + chunkAnswers, submitted := t.runUserInputCard(ctx, params, start, params.Questions[start:end]) + for id, answer := range chunkAnswers { + answers[id] = answer + } + if !submitted { + break + } + } + return protocol.ToolRequestUserInputResponse{Answers: answers} +} + +func (t *turnState) runUserInputCard(ctx context.Context, params *protocol.ToolRequestUserInputParams, offset int, questions []protocol.ToolRequestUserInputQuestion) (map[string]protocol.ToolRequestUserInputAnswer, bool) { + payloadQuestions := make([]any, 0, len(questions)) + for _, question := range questions { + payloadQuestions = append(payloadQuestions, codexQuestionToAskUser(question)) + } + toolCallID := params.ItemID + if offset > 0 { + toolCallID = fmt.Sprintf("%s:%d", params.ItemID, offset) + } + flow, err := userinput.RunFlow(ctx, t.userInput, userinput.FlowRequest{ + Input: userinput.CreatePendingInput{ + BotID: t.input.BotID, + SessionID: t.input.ThreadID, + RouteID: t.input.RouteID, + ChannelIdentityID: t.input.ChannelIdentityID, + RequestedByChannelIdentityID: t.input.ChannelIdentityID, + ToolCallID: toolCallID, + ToolName: userinput.ToolNameAskUser, + Input: map[string]any{"questions": payloadQuestions}, + ProviderMetadata: map[string]any{ + "source": userinput.ProviderSourceCodexUserInput, + "thread_id": params.ThreadID, + "turn_id": params.TurnID, + "item_id": params.ItemID, + }, + SourcePlatform: "", + }, + ActorChannelIdentityID: t.input.ChannelIdentityID, + Interactive: t.input.CanRequestUserInput, + WaitTimeout: userinput.DefaultWaitTimeout, + Emit: t.emitUserInputRequest, + NonInteractiveReason: "codex requested user input without an interactive stream", + UndeliveredReason: "codex user input request was not delivered to the interactive stream", + TimeoutReason: "codex user input timed out", + AbortReason: "codex user input aborted", + }) + if err != nil { + if ctx.Err() == nil { + t.logger.Error("codex user input flow failed", slog.String("thread_id", t.threadID), slog.Any("error", err)) + } + return nil, false + } + if flow.Request.Status != userinput.StatusSubmitted { + return nil, false + } + + byQuestion := map[string]userinput.UIAnswer{} + for _, answer := range userinput.AnswersFromResult(flow.Request.Result) { + byQuestion[answer.QuestionID] = answer + } + out := make(map[string]protocol.ToolRequestUserInputAnswer, len(questions)) + for idx, question := range questions { + // ask_user generates ids positionally within the card. + answer, ok := byQuestion[fmt.Sprintf("q%d", idx+1)] + if !ok || answer.Skipped { + continue + } + values := make([]string, 0, len(answer.Selected)+1) + for _, selected := range answer.Selected { + values = append(values, selected.Label) + } + if custom := strings.TrimSpace(answer.CustomText); custom != "" { + values = append(values, custom) + } + if text := strings.TrimSpace(answer.Text); text != "" { + values = append(values, text) + } + if len(values) == 0 { + continue + } + out[question.ID] = protocol.ToolRequestUserInputAnswer{Answers: values} + } + return out, true +} + +// codexQuestionToAskUser maps one codex question onto the strict ask_user +// question schema. Questions with fewer than the minimum selectable options +// degrade to free text, and `isOther` becomes the custom-answer affordance. +func codexQuestionToAskUser(question protocol.ToolRequestUserInputQuestion) map[string]any { + text := strings.TrimSpace(question.Question) + if header := strings.TrimSpace(question.Header); header != "" && !strings.EqualFold(header, text) { + text = header + " — " + text + } + out := map[string]any{"text": text} + if len(question.Options) >= userinput.MinOptionsPerQuestion { + options := question.Options + allowCustom := question.IsOther != nil && *question.IsOther + if len(options) > userinput.MaxOptionsPerQuestion { + // Beyond the render limit the tail options stay reachable as a + // typed custom answer. + options = options[:userinput.MaxOptionsPerQuestion] + allowCustom = true + } + payloadOptions := make([]any, 0, len(options)) + for _, option := range options { + entry := map[string]any{"label": option.Label} + if description := strings.TrimSpace(option.Description); description != "" { + entry["description"] = description + } + payloadOptions = append(payloadOptions, entry) + } + out["kind"] = userinput.QuestionKindSingleSelect + out["options"] = payloadOptions + if allowCustom { + out["allow_custom"] = true + } + return out + } + out["kind"] = userinput.QuestionKindText + if len(question.Options) == 1 { + out["placeholder"] = strings.TrimSpace(question.Options[0].Label) + } + return out +} + +func (t *turnState) emitUserInputRequest(req userinput.Request) bool { + status := strings.TrimSpace(req.Status) + if status == "" { + status = userinput.StatusPending + } + t.emit(event.StreamEvent{ + Type: event.UserInputRequest, + ToolCallID: req.ToolCallID, + ToolName: req.ToolName, + Input: req.Input, + UserInputID: req.ID, + ShortID: req.ShortID, + Status: status, + Metadata: userinput.DeferredMetadata(req), + }) + return true +} diff --git a/internal/handlers/external_agent_codex.go b/internal/handlers/external_agent_codex.go new file mode 100644 index 0000000000..de315393ae --- /dev/null +++ b/internal/handlers/external_agent_codex.go @@ -0,0 +1,184 @@ +package handlers + +import ( + "context" + "log/slog" + "net/http" + "strings" + + "github.com/labstack/echo/v4" + + "github.com/felinics/memoh/internal/accounts" + codexruntime "github.com/felinics/memoh/internal/agent/runtime/codex" + "github.com/felinics/memoh/internal/apperror" + "github.com/felinics/memoh/internal/botagents" + "github.com/felinics/memoh/internal/bots" +) + +// codexLoginService is the slice of the codex driver the handler drives. +type codexService interface { + StartChatGPTDeviceLogin(ctx context.Context, botID, botAgentID string) (codexruntime.DeviceLoginStart, error) + PollDeviceLogin(botID, botAgentID, loginID string) codexruntime.DeviceLoginStatus + CompleteChatGPTDeviceLogin(ctx context.Context, ownerUserID, botID, botAgentID, loginID string) error + CancelDeviceLogin(ctx context.Context, botID, botAgentID, loginID string) error +} + +// ExternalAgentCodexHandler exposes the direct codex runtime's login flow: the +// ChatGPT subscription device-code login runs through the app-server protocol +// and its credentials are copied into the encrypted Agent credential store. +type ExternalAgentCodexHandler struct { + driver codexService + agents *botagents.Service + botService *bots.Service + accountService *accounts.Service + logger *slog.Logger +} + +// NewExternalAgentCodexHandler constructs the codex runtime handler. +func NewExternalAgentCodexHandler(log *slog.Logger, driver codexService, agents *botagents.Service, botService *bots.Service, accountService *accounts.Service) *ExternalAgentCodexHandler { + return &ExternalAgentCodexHandler{ + driver: driver, + agents: agents, + botService: botService, + accountService: accountService, + logger: log.With(slog.String("handler", "external_agent_codex")), + } +} + +// Register registers codex runtime routes. +func (h *ExternalAgentCodexHandler) Register(e *echo.Echo) { + g := e.Group("/bots/:bot_id/agents/:id/codex") + g.POST("/login/device/authorize", h.AuthorizeDevice) + g.POST("/login/device/poll", h.PollDevice) + g.POST("/login/device/cancel", h.CancelDevice) +} + +func (h *ExternalAgentCodexHandler) requireAgentAccess(c echo.Context) (string, string, string, error) { + botID := strings.TrimSpace(c.Param("bot_id")) + botAgentID := strings.TrimSpace(c.Param("id")) + channelIdentityID, err := RequireChannelIdentityID(c) + if err != nil { + return "", "", "", err + } + bot, err := AuthorizeBotAccessWithPermission(c.Request().Context(), h.botService, h.accountService, channelIdentityID, botID, bots.PermissionManage) + if err != nil { + return "", "", "", err + } + agent, err := h.agents.GetActive(c.Request().Context(), bot.ID, botAgentID) + if err != nil || agent.Runtime != botagents.RuntimeCodex { + return "", "", "", apperror.New(apperror.CodeBotAgentNotFound, nil) + } + return bot.ID, agent.ID, channelIdentityID, nil +} + +// CodexDeviceLoginAuthorizeResponse starts a device-code login. +type CodexDeviceLoginAuthorizeResponse struct { + LoginID string `json:"login_id" validate:"required"` + UserCode string `json:"user_code" validate:"required"` + VerificationURL string `json:"verification_url" validate:"required"` +} // @name externalagent.CodexDeviceLoginAuthorizeResponse + +// CodexDeviceLoginPollRequest identifies the login being polled or cancelled. +type CodexDeviceLoginPollRequest struct { + LoginID string `json:"login_id" validate:"required"` +} // @name externalagent.CodexDeviceLoginPollRequest + +// CodexDeviceLoginPollResponse reports the login state. +type CodexDeviceLoginPollResponse struct { + Status string `json:"status" validate:"required" enums:"pending,success,error,unknown"` +} // @name externalagent.CodexDeviceLoginPollResponse + +// AuthorizeDevice godoc +// @Summary Start a ChatGPT device-code login for the direct codex runtime +// @Tags external-agents +// @Param bot_id path string true "Bot ID" +// @Success 200 {object} CodexDeviceLoginAuthorizeResponse +// @Failure 400 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 503 {object} apperror.Problem +// @Param id path string true "Bot Agent ID" +// @Router /bots/{bot_id}/agents/{id}/codex/login/device/authorize [post]. +func (h *ExternalAgentCodexHandler) AuthorizeDevice(c echo.Context) error { + botID, botAgentID, _, err := h.requireAgentAccess(c) + if err != nil { + return err + } + start, err := h.driver.StartChatGPTDeviceLogin(c.Request().Context(), botID, botAgentID) + if err != nil { + h.logger.Error("codex device login start failed", slog.String("bot_id", botID), slog.Any("error", err)) + if apperror.CodeOf(err) != "" { + return err + } + return apperror.Wrap( + apperror.CodeExternalRuntimeUnavailable, + err, + map[string]string{"runtime": codexruntime.RuntimeType}, + ) + } + return c.JSON(http.StatusOK, CodexDeviceLoginAuthorizeResponse{ + LoginID: start.LoginID, + UserCode: start.UserCode, + VerificationURL: start.VerificationURL, + }) +} + +// PollDevice godoc +// @Summary Poll a pending codex device-code login +// @Tags external-agents +// @Param bot_id path string true "Bot ID" +// @Param body body CodexDeviceLoginPollRequest true "Login reference" +// @Success 200 {object} CodexDeviceLoginPollResponse +// @Failure 400 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Param id path string true "Bot Agent ID" +// @Router /bots/{bot_id}/agents/{id}/codex/login/device/poll [post]. +func (h *ExternalAgentCodexHandler) PollDevice(c echo.Context) error { + botID, botAgentID, ownerUserID, err := h.requireAgentAccess(c) + if err != nil { + return err + } + var req CodexDeviceLoginPollRequest + if err := c.Bind(&req); err != nil || strings.TrimSpace(req.LoginID) == "" { + return echo.NewHTTPError(http.StatusBadRequest, "login_id is required") + } + loginID := strings.TrimSpace(req.LoginID) + status := h.driver.PollDeviceLogin(botID, botAgentID, loginID) + if status.Status == "success" { + if err := h.driver.CompleteChatGPTDeviceLogin(c.Request().Context(), ownerUserID, botID, botAgentID, loginID); err != nil { + if apperror.CodeOf(err) != "" { + return err + } + return apperror.Wrap(apperror.CodeAgentCredentialMaterializationFailed, err, nil) + } + } + return c.JSON(http.StatusOK, CodexDeviceLoginPollResponse{Status: status.Status}) +} + +// CancelDevice godoc +// @Summary Cancel a pending codex device-code login +// @Tags external-agents +// @Param bot_id path string true "Bot ID" +// @Param body body CodexDeviceLoginPollRequest true "Login reference" +// @Success 204 +// @Failure 400 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Param id path string true "Bot Agent ID" +// @Router /bots/{bot_id}/agents/{id}/codex/login/device/cancel [post]. +func (h *ExternalAgentCodexHandler) CancelDevice(c echo.Context) error { + botID, botAgentID, _, err := h.requireAgentAccess(c) + if err != nil { + return err + } + var req CodexDeviceLoginPollRequest + if err := c.Bind(&req); err != nil || strings.TrimSpace(req.LoginID) == "" { + return echo.NewHTTPError(http.StatusBadRequest, "login_id is required") + } + if err := h.driver.CancelDeviceLogin(c.Request().Context(), botID, botAgentID, strings.TrimSpace(req.LoginID)); err != nil { + return apperror.Wrap( + apperror.CodeExternalRuntimeUnavailable, + err, + map[string]string{"runtime": codexruntime.RuntimeType}, + ) + } + return c.NoContent(http.StatusNoContent) +} diff --git a/mise.toml b/mise.toml index 326434bec6..a5bba70488 100644 --- a/mise.toml +++ b/mise.toml @@ -63,6 +63,14 @@ description = "Generate SQL code" # version silently rewrites every generated file's version stamp. run = "go run github.com/sqlc-dev/sqlc/cmd/sqlc@v1.31.1 generate" +[tasks.codex-protocol-generate] +description = "Generate Go types for the codex app-server v2 protocol from the vendored schema snapshot" +run = "go run ./cmd/gen-codex-protocol" + +[tasks.codex-schema-sync] +description = "Refresh the vendored codex app-server schema snapshot from the local codex CLI and report drift" +run = "bash scripts/codex-schema-sync.sh" + [tasks.grpc-generate] description = "Generate internal gRPC protocol code" run = """ diff --git a/scripts/codex-schema-sync.sh b/scripts/codex-schema-sync.sh new file mode 100755 index 0000000000..1103239d51 --- /dev/null +++ b/scripts/codex-schema-sync.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# Refresh the vendored codex app-server schema snapshot from the local codex +# CLI and report drift. Any diff means the pinned binary and the snapshot +# disagree: review the schema diff, bump VERSION.json, rerun +# `mise run codex-protocol-generate`, and re-review the generated Go. +set -euo pipefail + +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SCHEMA_DIR="${PROJECT_ROOT}/internal/agent/runtime/codex/protocolgen/schema" + +if ! command -v codex >/dev/null 2>&1; then + echo "codex CLI not found on PATH" >&2 + exit 1 +fi + +pinned="$(sed -n 's/.*"codexVersion": *"\([^"]*\)".*/\1/p' "${SCHEMA_DIR}/VERSION.json")" +current="$(codex --version | awk '{print $2}')" +if [ "${pinned}" != "${current}" ]; then + echo "note: local codex is ${current}, snapshot is pinned to ${pinned}; syncing to ${current}" +fi + +tmp="$(mktemp -d)" +trap 'rm -rf "${tmp}"' EXIT +codex app-server generate-json-schema --out "${tmp}" >/dev/null + +files=( + codex_app_server_protocol.v2.schemas.json + ServerRequest.json + ClientNotification.json + CommandExecutionRequestApprovalResponse.json + FileChangeRequestApprovalResponse.json + PermissionsRequestApprovalResponse.json + ToolRequestUserInputResponse.json + McpServerElicitationRequestResponse.json + ChatgptAuthTokensRefreshResponse.json +) +for f in "${files[@]}"; do + cp "${tmp}/${f}" "${SCHEMA_DIR}/${f}" +done + +sed -i.bak "s/\"codexVersion\": *\"[^\"]*\"/\"codexVersion\": \"${current}\"/" "${SCHEMA_DIR}/VERSION.json" +rm -f "${SCHEMA_DIR}/VERSION.json.bak" + +cd "${PROJECT_ROOT}" +if git diff --quiet -- "${SCHEMA_DIR}"; then + echo "snapshot is in sync with codex ${current}" +else + echo "snapshot changed:" + git diff --stat -- "${SCHEMA_DIR}" + echo + echo "next: review the diff, then run 'mise run codex-protocol-generate' and fix any fallout." +fi