From 050a2594ba141241076282a598c1fb588b2a1bbc Mon Sep 17 00:00:00 2001 From: Frank Bria <136862992+frankbria@users.noreply.github.com> Date: Fri, 31 Jul 2026 21:18:43 -0700 Subject: [PATCH 1/2] fix(engines): rewrite the Codex adapter against the real app-server protocol (#914) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The adapter spoke an invented protocol: `initialize` without the required `clientInfo`, id-less `thread/start`/`turn/start` with made-up params, and an event loop waiting on `session_started`/`notification`/`tool_call` — none of which exist in `ServerNotification`. Every run died at the handshake. Verified against the live `codex app-server` (codex-cli 0.141.0) and its generated JSON Schema: - handshake is `initialize` (with clientInfo) -> `initialized` notification -> `thread/start` -> `turn/start`, all id-carrying requests correlated by id, with the threadId taken from the thread/start response instead of a uuid; JSON-RPC error responses are surfaced verbatim - approvals arrive as server *requests* and are answered by their own id; any unimplemented server request gets an explicit -32601 reply so the turn can never hang waiting on us - terminal state comes from `turn/completed`'s `turn.status`, tokens from `thread/tokenUsage/updated`, output from the final agentMessage item Transport: a single reader thread drains stdout into a queue, replacing the `select()`-plus-buffered-`TextIOWrapper` mix that stranded a burst of lines until the 5-minute stall timeout. Malformed JSON is logged and skipped, now distinct from EOF, so one stray log line no longer aborts a live turn. Tests: a burst-then-silence case over a real OS pipe asserts no stall, a stray non-JSON line is proven non-fatal, and a contract test validates every outbound message against a checked-in `generate-json-schema` fixture. --- codeframe/core/adapters/codex.py | 489 +- pyproject.toml | 1 + .../codex_app_server/ClientNotification.json | 22 + .../codex_app_server/ClientRequest.json | 6744 +++++++++++++++++ ...mmandExecutionRequestApprovalResponse.json | 116 + .../FileChangeRequestApprovalResponse.json | 47 + .../fixtures/codex_app_server/README.md | 18 + tests/core/adapters/test_codex.py | 713 +- uv.lock | 2 + 9 files changed, 7663 insertions(+), 489 deletions(-) create mode 100644 tests/core/adapters/fixtures/codex_app_server/ClientNotification.json create mode 100644 tests/core/adapters/fixtures/codex_app_server/ClientRequest.json create mode 100644 tests/core/adapters/fixtures/codex_app_server/CommandExecutionRequestApprovalResponse.json create mode 100644 tests/core/adapters/fixtures/codex_app_server/FileChangeRequestApprovalResponse.json create mode 100644 tests/core/adapters/fixtures/codex_app_server/README.md diff --git a/codeframe/core/adapters/codex.py b/codeframe/core/adapters/codex.py index 2f792b89..a3a53153 100644 --- a/codeframe/core/adapters/codex.py +++ b/codeframe/core/adapters/codex.py @@ -1,26 +1,38 @@ -"""Codex adapter using the app-server JSON-RPC protocol. - -Speaks the JSON-RPC protocol that OpenAI's Codex app-server exposes over -stdio. Unlike the simple stdin-to-stdout adapters (Claude Code, OpenCode), -this adapter maintains a bidirectional conversation with the subprocess: - - initialize -> initialized - thread/start - turn/start -> (stream of events) -> turn/completed | turn/failed +"""Codex adapter speaking the real ``codex app-server`` protocol. + +The protocol below is not invented — it is what ``codex app-server`` actually +speaks over stdio, cross-checked against the schema emitted by +``codex app-server generate-json-schema`` (a trimmed copy is checked in at +``tests/core/adapters/fixtures/codex_app_server/``):: + + -> {"id":1,"method":"initialize","params":{"clientInfo":{...}}} + <- {"id":1,"result":{...}} + -> {"method":"initialized"} + -> {"id":2,"method":"thread/start","params":{"cwd",...}} + <- {"id":2,"result":{"thread":{"id":...}}} + -> {"id":3,"method":"turn/start","params":{"threadId","input":[...]}} + <- {"method":"item/started"|"item/completed"|... } (notifications) + <- {"id":N,"method":"item/*/requestApproval",...} (server requests) + <- {"method":"turn/completed","params":{"turn":{"status":...}}} + +Two details bite if you assume plain JSON-RPC 2.0: the wire format carries **no** +``jsonrpc`` field, and approvals arrive as *requests* that must be answered by +their own id — an unanswered one hangs the turn. """ from __future__ import annotations import json -import selectors +import logging +import queue import shutil import subprocess import threading import time -import uuid from pathlib import Path from typing import Any, Callable +from codeframe import __version__ as _codeframe_version from codeframe.core.adapters.agent_adapter import ( AdapterTokenUsage, AgentEvent, @@ -28,29 +40,82 @@ ) from codeframe.core.adapters.git_utils import detect_modified_files +logger = logging.getLogger(__name__) -_TIMEOUT = object() # Sentinel for read timeout (distinct from EOF/None) +_TIMEOUT = object() # No message within the read window (process still alive) +_EOF = object() # stdout closed — the process is gone +# Server requests we know how to answer. Everything else gets a JSON-RPC +# "method not found" reply: these are the v2 approval callbacks, and the v1 +# ones (applyPatchApproval / execCommandApproval) use a different decision +# enum, so guessing at them would send an invalid response. +_APPROVAL_METHODS = ( + "item/commandExecution/requestApproval", + "item/fileChange/requestApproval", +) + +_METHOD_NOT_FOUND = -32601 -class CodexAdapter: - """Adapter that delegates code execution to OpenAI Codex via app-server protocol. - The Codex CLI is launched with ``app-server`` subcommand, producing a - JSON-RPC-over-stdio channel. The adapter performs a four-step handshake - then streams turn events until a terminal event arrives. +class _ProtocolError(RuntimeError): + """The app-server said something that ends the run.""" + + +class _MessageReader: + """Drain a subprocess stdout on its own thread into a queue. + + A single thread owns the stream and does nothing but ``readline`` + parse, + which is what keeps a burst of lines from stranding: buffering happens in + exactly one place instead of being split between ``select()`` on the fd and + a ``TextIOWrapper`` that has already swallowed the bytes. """ - # Default timeouts + def __init__(self, stdout: Any) -> None: + self._queue: queue.Queue = queue.Queue() + self._thread = threading.Thread(target=self._pump, args=(stdout,), daemon=True) + self._thread.start() + + def _pump(self, stdout: Any) -> None: + try: + for line in stdout: + line = line.strip() + if not line: + continue + try: + self._queue.put(json.loads(line)) + except json.JSONDecodeError: + # Codex writes the odd non-JSON log line; skipping it is not + # the same event as the stream ending (#914). + logger.debug("codex: skipping non-JSON line: %s", line[:200]) + except (ValueError, OSError): # stream closed mid-read + pass + finally: + self._queue.put(_EOF) + + def recv(self, timeout_s: float) -> Any: + """Next message, or ``_TIMEOUT`` / ``_EOF``.""" + try: + return self._queue.get(timeout=timeout_s) + except queue.Empty: + return _TIMEOUT + + +class CodexAdapter: + """Delegate task execution to OpenAI Codex via the app-server protocol.""" + DEFAULT_TURN_TIMEOUT_MS = 3_600_000 # 1 hour - DEFAULT_READ_TIMEOUT_MS = 30_000 # 30 s per line - DEFAULT_STALL_TIMEOUT_MS = 300_000 # 5 min no-progress + DEFAULT_READ_TIMEOUT_MS = 30_000 # 30 s per read window + DEFAULT_STALL_TIMEOUT_MS = 300_000 # 5 min with no messages at all def __init__( self, *, codex_command: str = "codex", approval_policy: str = "auto", - sandbox_mode: str | None = None, + # The adapter exists to write code into the workspace, so ask for a + # workspace-writable sandbox rather than inheriting whatever the + # operator's ~/.codex/config.toml defaults to. + sandbox_mode: str | None = "workspace-write", turn_timeout_ms: int = DEFAULT_TURN_TIMEOUT_MS, read_timeout_ms: int = DEFAULT_READ_TIMEOUT_MS, stall_timeout_ms: int = DEFAULT_STALL_TIMEOUT_MS, @@ -61,6 +126,7 @@ def __init__( self._turn_timeout_ms = turn_timeout_ms self._read_timeout_ms = read_timeout_ms self._stall_timeout_ms = stall_timeout_ms + self._next_id = 0 resolved = shutil.which(codex_command) if resolved is None: @@ -92,11 +158,11 @@ def run( ) -> AgentResult: """Execute a task via the Codex app-server protocol.""" start = time.monotonic() + self._next_id = 0 try: - cmd = [self._binary_path, "app-server"] process = subprocess.Popen( - cmd, + [self._binary_path, "app-server"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, @@ -109,12 +175,8 @@ def run( error=f"Binary '{self._binary}' not found during execution", ) except OSError as e: - return AgentResult( - status="failed", - error=f"Failed to start '{self._binary}': {e}", - ) + return AgentResult(status="failed", error=f"Failed to start '{self._binary}': {e}") - # Drain stderr in background to prevent deadlock stderr_chunks: list[str] = [] def _drain_stderr() -> None: @@ -124,132 +186,122 @@ def _drain_stderr() -> None: stderr_thread = threading.Thread(target=_drain_stderr, daemon=True) stderr_thread.start() + reader = _MessageReader(process.stdout) try: - ok = self._handshake( - process.stdin, process.stdout, prompt=prompt, workspace_path=workspace_path - ) - if not ok: - self._kill(process) - return AgentResult( - status="failed", - error="Codex app-server handshake failed (no initialized response)", - ) - - result = self._stream_turn(process.stdout, on_event=on_event, stdin=process.stdin) - except Exception as exc: - self._kill(process) - return AgentResult(status="failed", error=str(exc)) + thread_id = self._handshake(process.stdin, reader, workspace_path) + self._start_turn(process.stdin, thread_id, prompt, workspace_path) + result = self._stream_turn(reader, process.stdin, on_event=on_event) + except _ProtocolError as exc: + result = AgentResult(status="failed", error=str(exc)) + except Exception as exc: # unexpected — still report, never leak the process + result = AgentResult(status="failed", error=str(exc)) finally: - stderr_thread.join(timeout=5) self._kill(process) + stderr_thread.join(timeout=5) + + if result.status == "failed" and stderr_chunks and stderr_chunks[0].strip(): + result.error = f"{result.error}\nstderr: {stderr_chunks[0].strip()[-2000:]}" result.modified_files = self._detect_modified_files(workspace_path) result.duration_ms = int((time.monotonic() - start) * 1000) return result # ------------------------------------------------------------------ - # JSON-RPC framing + # Framing # ------------------------------------------------------------------ - def _send( - self, - stdin: Any, - method: str, - params: dict, - msg_id: int | None = None, - ) -> None: - """Write a single JSON-RPC message to the subprocess stdin.""" - msg: dict[str, Any] = {"jsonrpc": "2.0", "method": method, "params": params} - if msg_id is not None: - msg["id"] = msg_id - stdin.write(json.dumps(msg) + "\n") + def _send(self, stdin: Any, message: dict) -> None: + stdin.write(json.dumps(message) + "\n") stdin.flush() - def _recv_line(self, stdout: Any, timeout_s: float) -> dict | object | None: - """Read one JSON-RPC line from stdout with enforced timeout. + def _request(self, stdin: Any, reader: _MessageReader, method: str, params: dict) -> dict: + """Send an id-carrying request and return its ``result`` payload.""" + self._next_id += 1 + msg_id = self._next_id + self._send(stdin, {"id": msg_id, "method": method, "params": params}) - Uses ``selectors`` to wait for data availability before reading, - preventing indefinite blocking if the subprocess stops writing. + deadline = time.monotonic() + self._read_timeout_ms / 1000 + while True: + remaining = deadline - time.monotonic() + if remaining <= 0: + raise _ProtocolError(f"Codex app-server timed out responding to '{method}'") - Returns: - Parsed dict on success, ``_TIMEOUT`` sentinel on timeout (caller - should loop and re-check stall/turn timeouts), or ``None`` on EOF. - """ - # Use selectors for real timeout enforcement on file-based stdout. - # Mock objects (in tests) won't have fileno(), so fall back to - # direct readline for those. - if hasattr(stdout, "fileno"): - sel = selectors.DefaultSelector() - try: - sel.register(stdout, selectors.EVENT_READ) - ready = sel.select(timeout=timeout_s) - finally: - sel.close() - if not ready: - return _TIMEOUT - - line = stdout.readline() - if not line: - return None - try: - return json.loads(line) - except json.JSONDecodeError: - return None + msg = reader.recv(timeout_s=remaining) + if msg is _TIMEOUT: + continue + if msg is _EOF: + raise _ProtocolError(f"Codex app-server exited during '{method}' (EOF)") - # ------------------------------------------------------------------ - # Handshake - # ------------------------------------------------------------------ + if msg.get("id") != msg_id: + # Notifications and server requests interleave with responses. + if "method" in msg and "id" in msg: + self._answer_server_request(stdin, msg) + continue - def _handshake( - self, - stdin: Any, - stdout: Any, - *, - prompt: str, - workspace_path: Path, - ) -> bool: - """Perform the 4-step initialization handshake. + if "error" in msg: + error = msg["error"] or {} + raise _ProtocolError( + f"Codex app-server rejected '{method}': " + f"{error.get('message', error)} (code {error.get('code')})" + ) + return msg.get("result") or {} - 1. Send ``initialize`` with capabilities - 2. Wait for ``initialized`` response - 3. Send ``thread/start`` - 4. Send ``turn/start`` with the task prompt + def _notify(self, stdin: Any, method: str, params: dict | None = None) -> None: + message: dict[str, Any] = {"method": method} + if params is not None: + message["params"] = params + self._send(stdin, message) - Returns True on success, False on timeout/failure. - """ - thread_id = str(uuid.uuid4()) - turn_id = str(uuid.uuid4()) + # ------------------------------------------------------------------ + # Handshake + # ------------------------------------------------------------------ - # Step 1: initialize - init_params: dict[str, Any] = {"capabilities": {}} + def _handshake(self, stdin: Any, reader: _MessageReader, workspace_path: Path) -> str: + """initialize -> initialized -> thread/start. Returns the thread id.""" + self._request( + stdin, + reader, + "initialize", + { + "clientInfo": { + "name": "codeframe", + "title": "CodeFRAME", + "version": _codeframe_version, + } + }, + ) + self._notify(stdin, "initialized") + + params: dict[str, Any] = { + "cwd": str(workspace_path), + # "never" = don't interrupt an unattended run for approval. Any + # approval that still arrives is answered in _answer_server_request. + "approvalPolicy": "never" if self._approval_policy == "auto" else "on-request", + } if self._sandbox_mode: - init_params["sandbox_mode"] = self._sandbox_mode - self._send(stdin, "initialize", init_params, msg_id=1) - - # Step 2: wait for initialized - timeout_s = self._read_timeout_ms / 1000 - response = self._recv_line(stdout, timeout_s=timeout_s) - if response is _TIMEOUT or response is None: - return False - - # Accept either method="initialized" or a result response to id=1 - method = response.get("method", "") - if method != "initialized" and "result" not in response: - return False - - # Step 3: thread/start - self._send(stdin, "thread/start", { - "thread_id": thread_id, - "workspace": str(workspace_path), - }) - - # Step 4: turn/start - self._send(stdin, "turn/start", { - "turn_id": turn_id, - "prompt": prompt, - }) - - return True + params["sandbox"] = self._sandbox_mode + + result = self._request(stdin, reader, "thread/start", params) + thread_id = (result.get("thread") or {}).get("id") + if not thread_id: + raise _ProtocolError("Codex app-server returned no thread id from 'thread/start'") + return thread_id + + def _start_turn(self, stdin: Any, thread_id: str, prompt: str, workspace_path: Path) -> None: + """Send turn/start. The response is a plain ack; events arrive as notifications.""" + self._next_id += 1 + self._send( + stdin, + { + "id": self._next_id, + "method": "turn/start", + "params": { + "threadId": thread_id, + "cwd": str(workspace_path), + "input": [{"type": "text", "text": prompt}], + }, + }, + ) # ------------------------------------------------------------------ # Turn streaming @@ -257,126 +309,151 @@ def _handshake( def _stream_turn( self, - stdout: Any, + reader: _MessageReader, + stdin: Any, *, on_event: Callable[[AgentEvent], None] | None = None, - stdin: Any = None, ) -> AgentResult: - """Stream turn events until a terminal event or timeout.""" - last_event_time = time.monotonic() + """Consume notifications until ``turn/completed`` (or a timeout).""" + last_message = time.monotonic() turn_start = time.monotonic() stall_timeout_s = self._stall_timeout_ms / 1000 turn_timeout_s = self._turn_timeout_ms / 1000 read_timeout_s = self._read_timeout_ms / 1000 + output_parts: list[str] = [] + usage = (0, 0) + + def emit(type_: str, message: str, data: dict | None = None) -> None: + if on_event: + on_event(AgentEvent(type=type_, message=message, data=data or {})) + while True: - # Check stall timeout - if stall_timeout_s > 0 and (time.monotonic() - last_event_time) > stall_timeout_s: + if stall_timeout_s > 0 and (time.monotonic() - last_message) > stall_timeout_s: return AgentResult( status="failed", error=f"Stall timeout: no events for {self._stall_timeout_ms}ms", + output="\n".join(output_parts), ) - - # Check turn timeout if turn_timeout_s > 0 and (time.monotonic() - turn_start) > turn_timeout_s: return AgentResult( status="failed", error=f"Turn timeout: exceeded {self._turn_timeout_ms}ms", + output="\n".join(output_parts), ) - msg = self._recv_line(stdout, timeout_s=read_timeout_s) + msg = reader.recv(timeout_s=read_timeout_s) if msg is _TIMEOUT: - # Read timed out — loop back to check stall/turn timeouts continue - if msg is None: - # EOF — process likely terminated + if msg is _EOF: return AgentResult( status="failed", - error="Process terminated unexpectedly (EOF)", + error="Codex app-server terminated unexpectedly (EOF)", + output="\n".join(output_parts), ) - last_event_time = time.monotonic() + last_message = time.monotonic() method = msg.get("method", "") - params = msg.get("params", {}) - - if method == "session_started": - if on_event: - on_event(AgentEvent(type="progress", message="Session started")) - - elif method == "notification": - message = params.get("message", "") - if on_event: - on_event(AgentEvent(type="progress", message=message)) - - elif method == "tool_call": - if stdin: - self._handle_approval(stdin, msg, on_event=on_event) - - elif method == "turn/completed": - input_t, output_t = self._extract_token_usage(msg) + params = msg.get("params") or {} + + if "id" in msg: + if method: + self._answer_server_request(stdin, msg, on_event=on_event) + continue # a late response to one of our requests: nothing to do + + if method == "turn/completed": + turn = params.get("turn") or {} + status = turn.get("status", "completed") + output = "\n".join(output_parts) + if status == "completed": + return AgentResult( + status="completed", + output=output, + token_usage=AdapterTokenUsage( + input_tokens=usage[0], output_tokens=usage[1] + ), + ) + error = turn.get("error") or {} return AgentResult( - status="completed", + status="failed", + output=output, + error=error.get("message") or f"Turn ended with status '{status}'", token_usage=AdapterTokenUsage( - input_tokens=input_t, - output_tokens=output_t, + input_tokens=usage[0], output_tokens=usage[1] ), ) - elif method == "turn/failed": - error_msg = params.get("error", "Turn failed") - return AgentResult(status="failed", error=error_msg) + if method == "thread/tokenUsage/updated": + usage = self._extract_token_usage(params) + + elif method == "item/started": + item = params.get("item") or {} + emit("progress", f"Started {item.get('type', 'item')}", item) - elif method == "turn/cancelled": - return AgentResult(status="failed", error="Turn cancelled by Codex") + elif method == "item/completed": + item = params.get("item") or {} + if item.get("type") == "agentMessage" and item.get("text"): + output_parts.append(item["text"]) + emit("progress", f"Completed {item.get('type', 'item')}", item) + + elif method == "error": + error = params.get("error") or {} + emit("error", error.get("message", "Codex reported an error"), params) # ------------------------------------------------------------------ - # Approval handling + # Server requests # ------------------------------------------------------------------ - def _handle_approval( + def _answer_server_request( self, stdin: Any, - event: dict, + request: dict, *, on_event: Callable[[AgentEvent], None] | None = None, ) -> None: - """Handle a tool_call event that requires approval.""" - params = event.get("params", {}) - tool_id = params.get("id", "unknown") - tool_name = params.get("name", "unknown") + """Answer a server->client request by its own id. - if self._approval_policy == "auto": - self._send(stdin, "tool_call/approved", {"id": tool_id}) - if on_event: - on_event(AgentEvent( - type="progress", - message=f"Auto-approved tool call: {tool_name}", - )) - else: - self._send(stdin, "tool_call/rejected", {"id": tool_id}) - if on_event: - on_event(AgentEvent( - type="progress", - message=f"Rejected tool call (policy={self._approval_policy}): {tool_name}", - )) - - # ------------------------------------------------------------------ - # Token usage - # ------------------------------------------------------------------ - - def _extract_token_usage(self, event: dict) -> tuple[int, int]: - """Extract (input_tokens, output_tokens) from a turn_completed event.""" - params = event.get("params", {}) - usage = params.get("usage", {}) - return ( - usage.get("input_tokens", 0), - usage.get("output_tokens", 0), - ) + Leaving one unanswered wedges the turn, so unknown requests get an + explicit "method not found" rather than silence. + """ + msg_id = request.get("id") + method = request.get("method", "") + + if method not in _APPROVAL_METHODS: + self._send( + stdin, + { + "id": msg_id, + "error": { + "code": _METHOD_NOT_FOUND, + "message": f"codeframe does not implement '{method}'", + }, + }, + ) + return + + decision = "accept" if self._approval_policy == "auto" else "decline" + self._send(stdin, {"id": msg_id, "result": {"decision": decision}}) + if on_event: + on_event( + AgentEvent( + type="tool_call", + message=f"{decision}ed approval request: {method}", + data=request.get("params") or {}, + ) + ) # ------------------------------------------------------------------ # Helpers # ------------------------------------------------------------------ + @staticmethod + def _extract_token_usage(params: dict) -> tuple[int, int]: + """Pull (input, output) tokens from a thread/tokenUsage/updated payload.""" + token_usage = params.get("tokenUsage") or {} + bucket = token_usage.get("total") or token_usage.get("last") or {} + return (bucket.get("inputTokens", 0) or 0, bucket.get("outputTokens", 0) or 0) + @staticmethod def _kill(process: subprocess.Popen) -> None: """Terminate the subprocess if still running.""" diff --git a/pyproject.toml b/pyproject.toml index ac28a740..aac3f11c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,6 +95,7 @@ dev = [ "pre-commit>=3.5.0", "hypothesis>=6.0.0", "e2b>=2.0.0", + "jsonschema>=4.0.0", # codex app-server protocol contract test (#914) ] [project.scripts] diff --git a/tests/core/adapters/fixtures/codex_app_server/ClientNotification.json b/tests/core/adapters/fixtures/codex_app_server/ClientNotification.json new file mode 100644 index 00000000..a9be2746 --- /dev/null +++ b/tests/core/adapters/fixtures/codex_app_server/ClientNotification.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ClientNotification", + "oneOf": [ + { + "type": "object", + "required": [ + "method" + ], + "properties": { + "method": { + "type": "string", + "enum": [ + "initialized" + ], + "title": "InitializedNotificationMethod" + } + }, + "title": "InitializedNotification" + } + ] +} \ No newline at end of file diff --git a/tests/core/adapters/fixtures/codex_app_server/ClientRequest.json b/tests/core/adapters/fixtures/codex_app_server/ClientRequest.json new file mode 100644 index 00000000..ef6a75bd --- /dev/null +++ b/tests/core/adapters/fixtures/codex_app_server/ClientRequest.json @@ -0,0 +1,6744 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ClientRequest", + "description": "Request from the client to the server.", + "oneOf": [ + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "initialize" + ], + "title": "InitializeRequestMethod" + }, + "params": { + "$ref": "#/definitions/InitializeParams" + } + }, + "title": "InitializeRequest" + }, + { + "description": "NEW APIs", + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "thread/start" + ], + "title": "Thread/startRequestMethod" + }, + "params": { + "$ref": "#/definitions/ThreadStartParams" + } + }, + "title": "Thread/startRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "thread/resume" + ], + "title": "Thread/resumeRequestMethod" + }, + "params": { + "$ref": "#/definitions/ThreadResumeParams" + } + }, + "title": "Thread/resumeRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "thread/fork" + ], + "title": "Thread/forkRequestMethod" + }, + "params": { + "$ref": "#/definitions/ThreadForkParams" + } + }, + "title": "Thread/forkRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "thread/archive" + ], + "title": "Thread/archiveRequestMethod" + }, + "params": { + "$ref": "#/definitions/ThreadArchiveParams" + } + }, + "title": "Thread/archiveRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "thread/delete" + ], + "title": "Thread/deleteRequestMethod" + }, + "params": { + "$ref": "#/definitions/ThreadDeleteParams" + } + }, + "title": "Thread/deleteRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "thread/unsubscribe" + ], + "title": "Thread/unsubscribeRequestMethod" + }, + "params": { + "$ref": "#/definitions/ThreadUnsubscribeParams" + } + }, + "title": "Thread/unsubscribeRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "thread/name/set" + ], + "title": "Thread/name/setRequestMethod" + }, + "params": { + "$ref": "#/definitions/ThreadSetNameParams" + } + }, + "title": "Thread/name/setRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "thread/goal/set" + ], + "title": "Thread/goal/setRequestMethod" + }, + "params": { + "$ref": "#/definitions/ThreadGoalSetParams" + } + }, + "title": "Thread/goal/setRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "thread/goal/get" + ], + "title": "Thread/goal/getRequestMethod" + }, + "params": { + "$ref": "#/definitions/ThreadGoalGetParams" + } + }, + "title": "Thread/goal/getRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "thread/goal/clear" + ], + "title": "Thread/goal/clearRequestMethod" + }, + "params": { + "$ref": "#/definitions/ThreadGoalClearParams" + } + }, + "title": "Thread/goal/clearRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "thread/metadata/update" + ], + "title": "Thread/metadata/updateRequestMethod" + }, + "params": { + "$ref": "#/definitions/ThreadMetadataUpdateParams" + } + }, + "title": "Thread/metadata/updateRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "thread/unarchive" + ], + "title": "Thread/unarchiveRequestMethod" + }, + "params": { + "$ref": "#/definitions/ThreadUnarchiveParams" + } + }, + "title": "Thread/unarchiveRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "thread/compact/start" + ], + "title": "Thread/compact/startRequestMethod" + }, + "params": { + "$ref": "#/definitions/ThreadCompactStartParams" + } + }, + "title": "Thread/compact/startRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "thread/shellCommand" + ], + "title": "Thread/shellCommandRequestMethod" + }, + "params": { + "$ref": "#/definitions/ThreadShellCommandParams" + } + }, + "title": "Thread/shellCommandRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "thread/approveGuardianDeniedAction" + ], + "title": "Thread/approveGuardianDeniedActionRequestMethod" + }, + "params": { + "$ref": "#/definitions/ThreadApproveGuardianDeniedActionParams" + } + }, + "title": "Thread/approveGuardianDeniedActionRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "thread/rollback" + ], + "title": "Thread/rollbackRequestMethod" + }, + "params": { + "$ref": "#/definitions/ThreadRollbackParams" + } + }, + "title": "Thread/rollbackRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "thread/list" + ], + "title": "Thread/listRequestMethod" + }, + "params": { + "$ref": "#/definitions/ThreadListParams" + } + }, + "title": "Thread/listRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "thread/loaded/list" + ], + "title": "Thread/loaded/listRequestMethod" + }, + "params": { + "$ref": "#/definitions/ThreadLoadedListParams" + } + }, + "title": "Thread/loaded/listRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "thread/read" + ], + "title": "Thread/readRequestMethod" + }, + "params": { + "$ref": "#/definitions/ThreadReadParams" + } + }, + "title": "Thread/readRequest" + }, + { + "description": "Append raw Responses API items to the thread history without starting a user turn.", + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "thread/inject_items" + ], + "title": "Thread/injectItemsRequestMethod" + }, + "params": { + "$ref": "#/definitions/ThreadInjectItemsParams" + } + }, + "title": "Thread/injectItemsRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "skills/list" + ], + "title": "Skills/listRequestMethod" + }, + "params": { + "$ref": "#/definitions/SkillsListParams" + } + }, + "title": "Skills/listRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "skills/extraRoots/set" + ], + "title": "Skills/extraRoots/setRequestMethod" + }, + "params": { + "$ref": "#/definitions/SkillsExtraRootsSetParams" + } + }, + "title": "Skills/extraRoots/setRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "hooks/list" + ], + "title": "Hooks/listRequestMethod" + }, + "params": { + "$ref": "#/definitions/HooksListParams" + } + }, + "title": "Hooks/listRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "marketplace/add" + ], + "title": "Marketplace/addRequestMethod" + }, + "params": { + "$ref": "#/definitions/MarketplaceAddParams" + } + }, + "title": "Marketplace/addRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "marketplace/remove" + ], + "title": "Marketplace/removeRequestMethod" + }, + "params": { + "$ref": "#/definitions/MarketplaceRemoveParams" + } + }, + "title": "Marketplace/removeRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "marketplace/upgrade" + ], + "title": "Marketplace/upgradeRequestMethod" + }, + "params": { + "$ref": "#/definitions/MarketplaceUpgradeParams" + } + }, + "title": "Marketplace/upgradeRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "plugin/list" + ], + "title": "Plugin/listRequestMethod" + }, + "params": { + "$ref": "#/definitions/PluginListParams" + } + }, + "title": "Plugin/listRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "plugin/installed" + ], + "title": "Plugin/installedRequestMethod" + }, + "params": { + "$ref": "#/definitions/PluginInstalledParams" + } + }, + "title": "Plugin/installedRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "plugin/read" + ], + "title": "Plugin/readRequestMethod" + }, + "params": { + "$ref": "#/definitions/PluginReadParams" + } + }, + "title": "Plugin/readRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "plugin/skill/read" + ], + "title": "Plugin/skill/readRequestMethod" + }, + "params": { + "$ref": "#/definitions/PluginSkillReadParams" + } + }, + "title": "Plugin/skill/readRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "plugin/share/save" + ], + "title": "Plugin/share/saveRequestMethod" + }, + "params": { + "$ref": "#/definitions/PluginShareSaveParams" + } + }, + "title": "Plugin/share/saveRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "plugin/share/updateTargets" + ], + "title": "Plugin/share/updateTargetsRequestMethod" + }, + "params": { + "$ref": "#/definitions/PluginShareUpdateTargetsParams" + } + }, + "title": "Plugin/share/updateTargetsRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "plugin/share/list" + ], + "title": "Plugin/share/listRequestMethod" + }, + "params": { + "$ref": "#/definitions/PluginShareListParams" + } + }, + "title": "Plugin/share/listRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "plugin/share/checkout" + ], + "title": "Plugin/share/checkoutRequestMethod" + }, + "params": { + "$ref": "#/definitions/PluginShareCheckoutParams" + } + }, + "title": "Plugin/share/checkoutRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "plugin/share/delete" + ], + "title": "Plugin/share/deleteRequestMethod" + }, + "params": { + "$ref": "#/definitions/PluginShareDeleteParams" + } + }, + "title": "Plugin/share/deleteRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "app/list" + ], + "title": "App/listRequestMethod" + }, + "params": { + "$ref": "#/definitions/AppsListParams" + } + }, + "title": "App/listRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "fs/readFile" + ], + "title": "Fs/readFileRequestMethod" + }, + "params": { + "$ref": "#/definitions/FsReadFileParams" + } + }, + "title": "Fs/readFileRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "fs/writeFile" + ], + "title": "Fs/writeFileRequestMethod" + }, + "params": { + "$ref": "#/definitions/FsWriteFileParams" + } + }, + "title": "Fs/writeFileRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "fs/createDirectory" + ], + "title": "Fs/createDirectoryRequestMethod" + }, + "params": { + "$ref": "#/definitions/FsCreateDirectoryParams" + } + }, + "title": "Fs/createDirectoryRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "fs/getMetadata" + ], + "title": "Fs/getMetadataRequestMethod" + }, + "params": { + "$ref": "#/definitions/FsGetMetadataParams" + } + }, + "title": "Fs/getMetadataRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "fs/readDirectory" + ], + "title": "Fs/readDirectoryRequestMethod" + }, + "params": { + "$ref": "#/definitions/FsReadDirectoryParams" + } + }, + "title": "Fs/readDirectoryRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "fs/remove" + ], + "title": "Fs/removeRequestMethod" + }, + "params": { + "$ref": "#/definitions/FsRemoveParams" + } + }, + "title": "Fs/removeRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "fs/copy" + ], + "title": "Fs/copyRequestMethod" + }, + "params": { + "$ref": "#/definitions/FsCopyParams" + } + }, + "title": "Fs/copyRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "fs/watch" + ], + "title": "Fs/watchRequestMethod" + }, + "params": { + "$ref": "#/definitions/FsWatchParams" + } + }, + "title": "Fs/watchRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "fs/unwatch" + ], + "title": "Fs/unwatchRequestMethod" + }, + "params": { + "$ref": "#/definitions/FsUnwatchParams" + } + }, + "title": "Fs/unwatchRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "skills/config/write" + ], + "title": "Skills/config/writeRequestMethod" + }, + "params": { + "$ref": "#/definitions/SkillsConfigWriteParams" + } + }, + "title": "Skills/config/writeRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "plugin/install" + ], + "title": "Plugin/installRequestMethod" + }, + "params": { + "$ref": "#/definitions/PluginInstallParams" + } + }, + "title": "Plugin/installRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "plugin/uninstall" + ], + "title": "Plugin/uninstallRequestMethod" + }, + "params": { + "$ref": "#/definitions/PluginUninstallParams" + } + }, + "title": "Plugin/uninstallRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "turn/start" + ], + "title": "Turn/startRequestMethod" + }, + "params": { + "$ref": "#/definitions/TurnStartParams" + } + }, + "title": "Turn/startRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "turn/steer" + ], + "title": "Turn/steerRequestMethod" + }, + "params": { + "$ref": "#/definitions/TurnSteerParams" + } + }, + "title": "Turn/steerRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "turn/interrupt" + ], + "title": "Turn/interruptRequestMethod" + }, + "params": { + "$ref": "#/definitions/TurnInterruptParams" + } + }, + "title": "Turn/interruptRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "review/start" + ], + "title": "Review/startRequestMethod" + }, + "params": { + "$ref": "#/definitions/ReviewStartParams" + } + }, + "title": "Review/startRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "model/list" + ], + "title": "Model/listRequestMethod" + }, + "params": { + "$ref": "#/definitions/ModelListParams" + } + }, + "title": "Model/listRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "modelProvider/capabilities/read" + ], + "title": "ModelProvider/capabilities/readRequestMethod" + }, + "params": { + "$ref": "#/definitions/ModelProviderCapabilitiesReadParams" + } + }, + "title": "ModelProvider/capabilities/readRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "experimentalFeature/list" + ], + "title": "ExperimentalFeature/listRequestMethod" + }, + "params": { + "$ref": "#/definitions/ExperimentalFeatureListParams" + } + }, + "title": "ExperimentalFeature/listRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "permissionProfile/list" + ], + "title": "PermissionProfile/listRequestMethod" + }, + "params": { + "$ref": "#/definitions/PermissionProfileListParams" + } + }, + "title": "PermissionProfile/listRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "experimentalFeature/enablement/set" + ], + "title": "ExperimentalFeature/enablement/setRequestMethod" + }, + "params": { + "$ref": "#/definitions/ExperimentalFeatureEnablementSetParams" + } + }, + "title": "ExperimentalFeature/enablement/setRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "mcpServer/oauth/login" + ], + "title": "McpServer/oauth/loginRequestMethod" + }, + "params": { + "$ref": "#/definitions/McpServerOauthLoginParams" + } + }, + "title": "McpServer/oauth/loginRequest" + }, + { + "type": "object", + "required": [ + "id", + "method" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "config/mcpServer/reload" + ], + "title": "Config/mcpServer/reloadRequestMethod" + }, + "params": { + "type": "null" + } + }, + "title": "Config/mcpServer/reloadRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "mcpServerStatus/list" + ], + "title": "McpServerStatus/listRequestMethod" + }, + "params": { + "$ref": "#/definitions/ListMcpServerStatusParams" + } + }, + "title": "McpServerStatus/listRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "mcpServer/resource/read" + ], + "title": "McpServer/resource/readRequestMethod" + }, + "params": { + "$ref": "#/definitions/McpResourceReadParams" + } + }, + "title": "McpServer/resource/readRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "mcpServer/tool/call" + ], + "title": "McpServer/tool/callRequestMethod" + }, + "params": { + "$ref": "#/definitions/McpServerToolCallParams" + } + }, + "title": "McpServer/tool/callRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "windowsSandbox/setupStart" + ], + "title": "WindowsSandbox/setupStartRequestMethod" + }, + "params": { + "$ref": "#/definitions/WindowsSandboxSetupStartParams" + } + }, + "title": "WindowsSandbox/setupStartRequest" + }, + { + "type": "object", + "required": [ + "id", + "method" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "windowsSandbox/readiness" + ], + "title": "WindowsSandbox/readinessRequestMethod" + }, + "params": { + "type": "null" + } + }, + "title": "WindowsSandbox/readinessRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "account/login/start" + ], + "title": "Account/login/startRequestMethod" + }, + "params": { + "$ref": "#/definitions/LoginAccountParams" + } + }, + "title": "Account/login/startRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "account/login/cancel" + ], + "title": "Account/login/cancelRequestMethod" + }, + "params": { + "$ref": "#/definitions/CancelLoginAccountParams" + } + }, + "title": "Account/login/cancelRequest" + }, + { + "type": "object", + "required": [ + "id", + "method" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "account/logout" + ], + "title": "Account/logoutRequestMethod" + }, + "params": { + "type": "null" + } + }, + "title": "Account/logoutRequest" + }, + { + "type": "object", + "required": [ + "id", + "method" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "account/rateLimits/read" + ], + "title": "Account/rateLimits/readRequestMethod" + }, + "params": { + "type": "null" + } + }, + "title": "Account/rateLimits/readRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "account/rateLimitResetCredit/consume" + ], + "title": "Account/rateLimitResetCredit/consumeRequestMethod" + }, + "params": { + "$ref": "#/definitions/ConsumeAccountRateLimitResetCreditParams" + } + }, + "title": "Account/rateLimitResetCredit/consumeRequest" + }, + { + "type": "object", + "required": [ + "id", + "method" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "account/usage/read" + ], + "title": "Account/usage/readRequestMethod" + }, + "params": { + "type": "null" + } + }, + "title": "Account/usage/readRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "account/sendAddCreditsNudgeEmail" + ], + "title": "Account/sendAddCreditsNudgeEmailRequestMethod" + }, + "params": { + "$ref": "#/definitions/SendAddCreditsNudgeEmailParams" + } + }, + "title": "Account/sendAddCreditsNudgeEmailRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "feedback/upload" + ], + "title": "Feedback/uploadRequestMethod" + }, + "params": { + "$ref": "#/definitions/FeedbackUploadParams" + } + }, + "title": "Feedback/uploadRequest" + }, + { + "description": "Execute a standalone command (argv vector) under the server's sandbox.", + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "command/exec" + ], + "title": "Command/execRequestMethod" + }, + "params": { + "$ref": "#/definitions/CommandExecParams" + } + }, + "title": "Command/execRequest" + }, + { + "description": "Write stdin bytes to a running `command/exec` session or close stdin.", + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "command/exec/write" + ], + "title": "Command/exec/writeRequestMethod" + }, + "params": { + "$ref": "#/definitions/CommandExecWriteParams" + } + }, + "title": "Command/exec/writeRequest" + }, + { + "description": "Terminate a running `command/exec` session by client-supplied `processId`.", + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "command/exec/terminate" + ], + "title": "Command/exec/terminateRequestMethod" + }, + "params": { + "$ref": "#/definitions/CommandExecTerminateParams" + } + }, + "title": "Command/exec/terminateRequest" + }, + { + "description": "Resize a running PTY-backed `command/exec` session by client-supplied `processId`.", + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "command/exec/resize" + ], + "title": "Command/exec/resizeRequestMethod" + }, + "params": { + "$ref": "#/definitions/CommandExecResizeParams" + } + }, + "title": "Command/exec/resizeRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "config/read" + ], + "title": "Config/readRequestMethod" + }, + "params": { + "$ref": "#/definitions/ConfigReadParams" + } + }, + "title": "Config/readRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "externalAgentConfig/detect" + ], + "title": "ExternalAgentConfig/detectRequestMethod" + }, + "params": { + "$ref": "#/definitions/ExternalAgentConfigDetectParams" + } + }, + "title": "ExternalAgentConfig/detectRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "externalAgentConfig/import" + ], + "title": "ExternalAgentConfig/importRequestMethod" + }, + "params": { + "$ref": "#/definitions/ExternalAgentConfigImportParams" + } + }, + "title": "ExternalAgentConfig/importRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "config/value/write" + ], + "title": "Config/value/writeRequestMethod" + }, + "params": { + "$ref": "#/definitions/ConfigValueWriteParams" + } + }, + "title": "Config/value/writeRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "config/batchWrite" + ], + "title": "Config/batchWriteRequestMethod" + }, + "params": { + "$ref": "#/definitions/ConfigBatchWriteParams" + } + }, + "title": "Config/batchWriteRequest" + }, + { + "type": "object", + "required": [ + "id", + "method" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "configRequirements/read" + ], + "title": "ConfigRequirements/readRequestMethod" + }, + "params": { + "type": "null" + } + }, + "title": "ConfigRequirements/readRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "account/read" + ], + "title": "Account/readRequestMethod" + }, + "params": { + "$ref": "#/definitions/GetAccountParams" + } + }, + "title": "Account/readRequest" + }, + { + "type": "object", + "required": [ + "id", + "method", + "params" + ], + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "type": "string", + "enum": [ + "fuzzyFileSearch" + ], + "title": "FuzzyFileSearchRequestMethod" + }, + "params": { + "$ref": "#/definitions/FuzzyFileSearchParams" + } + }, + "title": "FuzzyFileSearchRequest" + } + ], + "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" + }, + "AddCreditsNudgeCreditType": { + "type": "string", + "enum": [ + "credits", + "usage_limit" + ] + }, + "AdditionalContextEntry": { + "type": "object", + "required": [ + "kind", + "value" + ], + "properties": { + "kind": { + "$ref": "#/definitions/AdditionalContextKind" + }, + "value": { + "type": "string" + } + } + }, + "AdditionalContextKind": { + "type": "string", + "enum": [ + "untrusted", + "application" + ] + }, + "AgentMessageInputContent": { + "oneOf": [ + { + "type": "object", + "required": [ + "text", + "type" + ], + "properties": { + "text": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "input_text" + ], + "title": "InputTextAgentMessageInputContentType" + } + }, + "title": "InputTextAgentMessageInputContent" + }, + { + "type": "object", + "required": [ + "encrypted_content", + "type" + ], + "properties": { + "encrypted_content": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "encrypted_content" + ], + "title": "EncryptedContentAgentMessageInputContentType" + } + }, + "title": "EncryptedContentAgentMessageInputContent" + } + ] + }, + "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.", + "type": "string", + "enum": [ + "user", + "auto_review", + "guardian_subagent" + ] + }, + "AppsListParams": { + "description": "EXPERIMENTAL - list available apps/connectors.", + "type": "object", + "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.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "threadId": { + "description": "Optional thread id used to evaluate app feature gating from that thread's config.", + "type": [ + "string", + "null" + ] + } + } + }, + "AskForApproval": { + "oneOf": [ + { + "type": "string", + "enum": [ + "untrusted", + "on-failure", + "on-request", + "never" + ] + }, + { + "type": "object", + "required": [ + "granular" + ], + "properties": { + "granular": { + "type": "object", + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "properties": { + "mcp_elicitations": { + "type": "boolean" + }, + "request_permissions": { + "default": false, + "type": "boolean" + }, + "rules": { + "type": "boolean" + }, + "sandbox_approval": { + "type": "boolean" + }, + "skill_approval": { + "default": false, + "type": "boolean" + } + } + } + }, + "additionalProperties": false, + "title": "GranularAskForApproval" + } + ] + }, + "ByteRange": { + "type": "object", + "required": [ + "end", + "start" + ], + "properties": { + "end": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "start": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + } + } + }, + "CancelLoginAccountParams": { + "type": "object", + "required": [ + "loginId" + ], + "properties": { + "loginId": { + "type": "string" + } + } + }, + "CapabilityRootLocation": { + "description": "Location used to resolve a selected capability root.", + "oneOf": [ + { + "description": "A path owned by an execution environment.", + "type": "object", + "required": [ + "environmentId", + "path", + "type" + ], + "properties": { + "environmentId": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "environment" + ], + "title": "EnvironmentCapabilityRootLocationType" + } + }, + "title": "EnvironmentCapabilityRootLocation" + } + ] + }, + "ClientInfo": { + "type": "object", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "type": "string" + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "version": { + "type": "string" + } + } + }, + "CollaborationMode": { + "description": "Collaboration mode for a Codex session.", + "type": "object", + "required": [ + "mode", + "settings" + ], + "properties": { + "mode": { + "$ref": "#/definitions/ModeKind" + }, + "settings": { + "$ref": "#/definitions/Settings" + } + } + }, + "WindowsSandboxSetupStartParams": { + "type": "object", + "required": [ + "mode" + ], + "properties": { + "cwd": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "mode": { + "$ref": "#/definitions/WindowsSandboxSetupMode" + } + } + }, + "CommandExecParams": { + "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.", + "type": "object", + "required": [ + "command" + ], + "properties": { + "command": { + "description": "Command argv vector. Empty arrays are rejected.", + "type": "array", + "items": { + "type": "string" + } + }, + "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": { + "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" + ], + "additionalProperties": { + "type": [ + "string", + "null" + ] + } + }, + "outputBytesCap": { + "description": "Optional per-stream stdout/stderr capture cap in bytes.\n\nWhen omitted, the server default applies. Cannot be combined with `disableOutputCap`.", + "type": [ + "integer", + "null" + ], + "format": "uint", + "minimum": 0.0 + }, + "tty": { + "description": "Enable PTY mode.\n\nThis implies `streamStdin` and `streamStdoutStderr`.", + "type": "boolean" + }, + "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": { + "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`.", + "anyOf": [ + { + "$ref": "#/definitions/SandboxPolicy" + }, + { + "type": "null" + } + ] + }, + "size": { + "description": "Optional initial PTY size in character cells. Only valid when `tty` is true.", + "anyOf": [ + { + "$ref": "#/definitions/CommandExecTerminalSize" + }, + { + "type": "null" + } + ] + }, + "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`.", + "type": [ + "integer", + "null" + ], + "format": "int64" + } + } + }, + "CommandExecResizeParams": { + "description": "Resize a running PTY-backed `command/exec` session.", + "type": "object", + "required": [ + "processId", + "size" + ], + "properties": { + "processId": { + "description": "Client-supplied, connection-scoped `processId` from the original `command/exec` request.", + "type": "string" + }, + "size": { + "description": "New PTY size in character cells.", + "allOf": [ + { + "$ref": "#/definitions/CommandExecTerminalSize" + } + ] + } + } + }, + "CommandExecTerminalSize": { + "description": "PTY size in character cells for `command/exec` PTY sessions.", + "type": "object", + "required": [ + "cols", + "rows" + ], + "properties": { + "cols": { + "description": "Terminal width in character cells.", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + }, + "rows": { + "description": "Terminal height in character cells.", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + } + } + }, + "CommandExecTerminateParams": { + "description": "Terminate a running `command/exec` session.", + "type": "object", + "required": [ + "processId" + ], + "properties": { + "processId": { + "description": "Client-supplied, connection-scoped `processId` from the original `command/exec` request.", + "type": "string" + } + } + }, + "CommandExecWriteParams": { + "description": "Write stdin bytes to a running `command/exec` session, close stdin, or both.", + "type": "object", + "required": [ + "processId" + ], + "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" + } + } + }, + "CommandMigration": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + } + } + }, + "ConfigBatchWriteParams": { + "type": "object", + "required": [ + "edits" + ], + "properties": { + "edits": { + "type": "array", + "items": { + "$ref": "#/definitions/ConfigEdit" + } + }, + "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 the updated user config into all loaded threads after writing.", + "type": "boolean" + } + } + }, + "ConfigEdit": { + "type": "object", + "required": [ + "keyPath", + "mergeStrategy", + "value" + ], + "properties": { + "keyPath": { + "type": "string" + }, + "mergeStrategy": { + "$ref": "#/definitions/MergeStrategy" + }, + "value": true + } + }, + "ConfigReadParams": { + "type": "object", + "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" + } + } + }, + "ConfigValueWriteParams": { + "type": "object", + "required": [ + "keyPath", + "mergeStrategy", + "value" + ], + "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 + } + }, + "ConsumeAccountRateLimitResetCreditParams": { + "type": "object", + "required": [ + "idempotencyKey" + ], + "properties": { + "idempotencyKey": { + "description": "Identifies one logical reset attempt. A UUID is recommended; reuse the same value when retrying that attempt.", + "type": "string" + } + } + }, + "ContentItem": { + "oneOf": [ + { + "type": "object", + "required": [ + "text", + "type" + ], + "properties": { + "text": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "input_text" + ], + "title": "InputTextContentItemType" + } + }, + "title": "InputTextContentItem" + }, + { + "type": "object", + "required": [ + "image_url", + "type" + ], + "properties": { + "detail": { + "anyOf": [ + { + "$ref": "#/definitions/ImageDetail" + }, + { + "type": "null" + } + ] + }, + "image_url": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "input_image" + ], + "title": "InputImageContentItemType" + } + }, + "title": "InputImageContentItem" + }, + { + "type": "object", + "required": [ + "text", + "type" + ], + "properties": { + "text": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "output_text" + ], + "title": "OutputTextContentItemType" + } + }, + "title": "OutputTextContentItem" + } + ] + }, + "ConversationTextRole": { + "type": "string", + "enum": [ + "user", + "developer" + ] + }, + "DynamicToolNamespaceTool": { + "oneOf": [ + { + "type": "object", + "required": [ + "description", + "inputSchema", + "name", + "type" + ], + "properties": { + "deferLoading": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "inputSchema": true, + "name": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "function" + ], + "title": "FunctionDynamicToolNamespaceToolType" + } + }, + "title": "FunctionDynamicToolNamespaceTool" + } + ] + }, + "DynamicToolSpec": { + "oneOf": [ + { + "type": "object", + "required": [ + "description", + "inputSchema", + "name", + "type" + ], + "properties": { + "deferLoading": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "inputSchema": true, + "name": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "function" + ], + "title": "FunctionDynamicToolSpecType" + } + }, + "title": "FunctionDynamicToolSpec" + }, + { + "type": "object", + "required": [ + "description", + "name", + "tools", + "type" + ], + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "tools": { + "type": "array", + "items": { + "$ref": "#/definitions/DynamicToolNamespaceTool" + } + }, + "type": { + "type": "string", + "enum": [ + "namespace" + ], + "title": "NamespaceDynamicToolSpecType" + } + }, + "title": "NamespaceDynamicToolSpec" + } + ] + }, + "WindowsSandboxSetupMode": { + "type": "string", + "enum": [ + "elevated", + "unelevated" + ] + }, + "ExperimentalFeatureEnablementSetParams": { + "type": "object", + "required": [ + "enablement" + ], + "properties": { + "enablement": { + "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", + "additionalProperties": { + "type": "boolean" + } + } + } + }, + "ExperimentalFeatureListParams": { + "type": "object", + "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.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "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" + ] + } + } + }, + "ExternalAgentConfigDetectParams": { + "type": "object", + "properties": { + "cwds": { + "description": "Zero or more working directories to include for repo-scoped detection.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "includeHome": { + "description": "If true, include detection under the user's home (~/.claude, ~/.codex, etc.).", + "type": "boolean" + } + } + }, + "ExternalAgentConfigImportParams": { + "type": "object", + "required": [ + "migrationItems" + ], + "properties": { + "migrationItems": { + "type": "array", + "items": { + "$ref": "#/definitions/ExternalAgentConfigMigrationItem" + } + } + } + }, + "ExternalAgentConfigMigrationItem": { + "type": "object", + "required": [ + "description", + "itemType" + ], + "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" + } + } + }, + "ExternalAgentConfigMigrationItemType": { + "type": "string", + "enum": [ + "AGENTS_MD", + "CONFIG", + "SKILLS", + "PLUGINS", + "MCP_SERVER_CONFIG", + "SUBAGENTS", + "HOOKS", + "COMMANDS", + "SESSIONS" + ] + }, + "FeedbackUploadParams": { + "type": "object", + "required": [ + "classification" + ], + "properties": { + "classification": { + "type": "string" + }, + "extraLogFiles": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "includeLogs": { + "type": "boolean" + }, + "reason": { + "type": [ + "string", + "null" + ] + }, + "tags": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "threadId": { + "type": [ + "string", + "null" + ] + } + } + }, + "FsCopyParams": { + "description": "Copy a file or directory tree on the host filesystem.", + "type": "object", + "required": [ + "destinationPath", + "sourcePath" + ], + "properties": { + "destinationPath": { + "description": "Absolute destination path.", + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ] + }, + "recursive": { + "description": "Required for directory copies; ignored for file copies.", + "type": "boolean" + }, + "sourcePath": { + "description": "Absolute source path.", + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ] + } + } + }, + "FsCreateDirectoryParams": { + "description": "Create a directory on the host filesystem.", + "type": "object", + "required": [ + "path" + ], + "properties": { + "path": { + "description": "Absolute directory path to create.", + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ] + }, + "recursive": { + "description": "Whether parent directories should also be created. Defaults to `true`.", + "type": [ + "boolean", + "null" + ] + } + } + }, + "FsGetMetadataParams": { + "description": "Request metadata for an absolute path.", + "type": "object", + "required": [ + "path" + ], + "properties": { + "path": { + "description": "Absolute path to inspect.", + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ] + } + } + }, + "FsReadDirectoryParams": { + "description": "List direct child names for a directory.", + "type": "object", + "required": [ + "path" + ], + "properties": { + "path": { + "description": "Absolute directory path to read.", + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ] + } + } + }, + "FsReadFileParams": { + "description": "Read a file from the host filesystem.", + "type": "object", + "required": [ + "path" + ], + "properties": { + "path": { + "description": "Absolute path to read.", + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ] + } + } + }, + "FsRemoveParams": { + "description": "Remove a file or directory tree from the host filesystem.", + "type": "object", + "required": [ + "path" + ], + "properties": { + "force": { + "description": "Whether missing paths should be ignored. Defaults to `true`.", + "type": [ + "boolean", + "null" + ] + }, + "path": { + "description": "Absolute path to remove.", + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ] + }, + "recursive": { + "description": "Whether directory removal should recurse. Defaults to `true`.", + "type": [ + "boolean", + "null" + ] + } + } + }, + "FsUnwatchParams": { + "description": "Stop filesystem watch notifications for a prior `fs/watch`.", + "type": "object", + "required": [ + "watchId" + ], + "properties": { + "watchId": { + "description": "Watch identifier previously provided to `fs/watch`.", + "type": "string" + } + } + }, + "FsWatchParams": { + "description": "Start filesystem watch notifications for an absolute path.", + "type": "object", + "required": [ + "path", + "watchId" + ], + "properties": { + "path": { + "description": "Absolute file or directory path to watch.", + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ] + }, + "watchId": { + "description": "Connection-scoped watch identifier used for `fs/unwatch` and `fs/changed`.", + "type": "string" + } + } + }, + "FsWriteFileParams": { + "description": "Write a file on the host filesystem.", + "type": "object", + "required": [ + "dataBase64", + "path" + ], + "properties": { + "dataBase64": { + "description": "File contents encoded as base64.", + "type": "string" + }, + "path": { + "description": "Absolute path to write.", + "allOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + } + ] + } + } + }, + "FunctionCallOutputBody": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/FunctionCallOutputContentItem" + } + } + ] + }, + "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": [ + { + "type": "object", + "required": [ + "text", + "type" + ], + "properties": { + "text": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "input_text" + ], + "title": "InputTextFunctionCallOutputContentItemType" + } + }, + "title": "InputTextFunctionCallOutputContentItem" + }, + { + "type": "object", + "required": [ + "image_url", + "type" + ], + "properties": { + "detail": { + "anyOf": [ + { + "$ref": "#/definitions/ImageDetail" + }, + { + "type": "null" + } + ] + }, + "image_url": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "input_image" + ], + "title": "InputImageFunctionCallOutputContentItemType" + } + }, + "title": "InputImageFunctionCallOutputContentItem" + }, + { + "type": "object", + "required": [ + "encrypted_content", + "type" + ], + "properties": { + "encrypted_content": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "encrypted_content" + ], + "title": "EncryptedContentFunctionCallOutputContentItemType" + } + }, + "title": "EncryptedContentFunctionCallOutputContentItem" + } + ] + }, + "FuzzyFileSearchParams": { + "type": "object", + "required": [ + "query", + "roots" + ], + "properties": { + "cancellationToken": { + "type": [ + "string", + "null" + ] + }, + "query": { + "type": "string" + }, + "roots": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "UserInput": { + "oneOf": [ + { + "type": "object", + "required": [ + "text", + "type" + ], + "properties": { + "text": { + "type": "string" + }, + "text_elements": { + "description": "UI-defined spans within `text` used to render or persist special elements.", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/TextElement" + } + }, + "type": { + "type": "string", + "enum": [ + "text" + ], + "title": "TextUserInputType" + } + }, + "title": "TextUserInput" + }, + { + "type": "object", + "required": [ + "type", + "url" + ], + "properties": { + "detail": { + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/ImageDetail" + }, + { + "type": "null" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "image" + ], + "title": "ImageUserInputType" + }, + "url": { + "type": "string" + } + }, + "title": "ImageUserInput" + }, + { + "type": "object", + "required": [ + "path", + "type" + ], + "properties": { + "detail": { + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/ImageDetail" + }, + { + "type": "null" + } + ] + }, + "path": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "localImage" + ], + "title": "LocalImageUserInputType" + } + }, + "title": "LocalImageUserInput" + }, + { + "type": "object", + "required": [ + "name", + "path", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "skill" + ], + "title": "SkillUserInputType" + } + }, + "title": "SkillUserInput" + }, + { + "type": "object", + "required": [ + "name", + "path", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "mention" + ], + "title": "MentionUserInputType" + } + }, + "title": "MentionUserInput" + } + ] + }, + "TurnSteerParams": { + "type": "object", + "required": [ + "expectedTurnId", + "input", + "threadId" + ], + "properties": { + "threadId": { + "type": "string" + }, + "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": { + "type": "array", + "items": { + "$ref": "#/definitions/UserInput" + } + } + } + }, + "TurnStartParams": { + "type": "object", + "required": [ + "input", + "threadId" + ], + "properties": { + "sandboxPolicy": { + "description": "Override the sandbox policy for this turn and subsequent turns.", + "anyOf": [ + { + "$ref": "#/definitions/SandboxPolicy" + }, + { + "type": "null" + } + ] + }, + "approvalPolicy": { + "description": "Override the approval policy for this turn and subsequent turns.", + "anyOf": [ + { + "$ref": "#/definitions/AskForApproval" + }, + { + "type": "null" + } + ] + }, + "approvalsReviewer": { + "description": "Override where approval requests are routed for review on this turn and subsequent turns.", + "anyOf": [ + { + "$ref": "#/definitions/ApprovalsReviewer" + }, + { + "type": "null" + } + ] + }, + "clientUserMessageId": { + "type": [ + "string", + "null" + ] + }, + "serviceTier": { + "description": "Override the service tier for this turn and subsequent turns.", + "type": [ + "string", + "null" + ] + }, + "cwd": { + "description": "Override the working directory for this turn and subsequent turns.", + "type": [ + "string", + "null" + ] + }, + "effort": { + "description": "Override the reasoning effort for this turn and subsequent turns.", + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ] + }, + "summary": { + "description": "Override the reasoning summary for this turn and subsequent turns.", + "anyOf": [ + { + "$ref": "#/definitions/ReasoningSummary" + }, + { + "type": "null" + } + ] + }, + "input": { + "type": "array", + "items": { + "$ref": "#/definitions/UserInput" + } + }, + "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." + }, + "threadId": { + "type": "string" + }, + "personality": { + "description": "Override the personality for this turn and subsequent turns.", + "anyOf": [ + { + "$ref": "#/definitions/Personality" + }, + { + "type": "null" + } + ] + } + } + }, + "GetAccountParams": { + "type": "object", + "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" + } + } + }, + "HookMigration": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + } + } + }, + "HooksListParams": { + "type": "object", + "properties": { + "cwds": { + "description": "When empty, defaults to the current session working directory.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "ImageDetail": { + "type": "string", + "enum": [ + "auto", + "low", + "high", + "original" + ] + }, + "InitializeCapabilities": { + "description": "Client-declared capabilities negotiated during initialize.", + "type": "object", + "properties": { + "experimentalApi": { + "description": "Opt into receiving experimental API methods and fields.", + "default": false, + "type": "boolean" + }, + "optOutNotificationMethods": { + "description": "Exact notification method names that should be suppressed for this connection (for example `thread/started`).", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "requestAttestation": { + "description": "Opt into `attestation/generate` requests for upstream `x-oai-attestation`.", + "default": false, + "type": "boolean" + } + } + }, + "InitializeParams": { + "type": "object", + "required": [ + "clientInfo" + ], + "properties": { + "capabilities": { + "anyOf": [ + { + "$ref": "#/definitions/InitializeCapabilities" + }, + { + "type": "null" + } + ] + }, + "clientInfo": { + "$ref": "#/definitions/ClientInfo" + } + } + }, + "ListMcpServerStatusParams": { + "type": "object", + "properties": { + "cursor": { + "description": "Opaque pagination cursor returned by a previous call.", + "type": [ + "string", + "null" + ] + }, + "detail": { + "description": "Controls how much MCP inventory data to fetch for each server. Defaults to `Full` when omitted.", + "anyOf": [ + { + "$ref": "#/definitions/McpServerStatusDetail" + }, + { + "type": "null" + } + ] + }, + "limit": { + "description": "Optional page size; defaults to a server-defined value.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "threadId": { + "type": [ + "string", + "null" + ] + } + } + }, + "LocalShellAction": { + "oneOf": [ + { + "type": "object", + "required": [ + "command", + "type" + ], + "properties": { + "command": { + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "timeout_ms": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "type": { + "type": "string", + "enum": [ + "exec" + ], + "title": "ExecLocalShellActionType" + }, + "user": { + "type": [ + "string", + "null" + ] + }, + "working_directory": { + "type": [ + "string", + "null" + ] + } + }, + "title": "ExecLocalShellAction" + } + ] + }, + "LocalShellStatus": { + "type": "string", + "enum": [ + "completed", + "in_progress", + "incomplete" + ] + }, + "LoginAccountParams": { + "oneOf": [ + { + "type": "object", + "required": [ + "apiKey", + "type" + ], + "properties": { + "apiKey": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "apiKey" + ], + "title": "ApiKeyLoginAccountParamsType" + } + }, + "title": "ApiKeyLoginAccountParams" + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "codexStreamlinedLogin": { + "type": "boolean" + }, + "type": { + "type": "string", + "enum": [ + "chatgpt" + ], + "title": "ChatgptLoginAccountParamsType" + } + }, + "title": "ChatgptLoginAccountParams" + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "chatgptDeviceCode" + ], + "title": "ChatgptDeviceCodeLoginAccountParamsType" + } + }, + "title": "ChatgptDeviceCodeLoginAccountParams" + }, + { + "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.", + "type": "object", + "required": [ + "accessToken", + "chatgptAccountId", + "type" + ], + "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": { + "type": "string", + "enum": [ + "chatgptAuthTokens" + ], + "title": "ChatgptAuthTokensLoginAccountParamsType" + } + }, + "title": "ChatgptAuthTokensLoginAccountParams" + } + ] + }, + "MarketplaceAddParams": { + "type": "object", + "required": [ + "source" + ], + "properties": { + "refName": { + "type": [ + "string", + "null" + ] + }, + "source": { + "type": "string" + }, + "sparsePaths": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + } + }, + "MarketplaceRemoveParams": { + "type": "object", + "required": [ + "marketplaceName" + ], + "properties": { + "marketplaceName": { + "type": "string" + } + } + }, + "MarketplaceUpgradeParams": { + "type": "object", + "properties": { + "marketplaceName": { + "type": [ + "string", + "null" + ] + } + } + }, + "McpResourceReadParams": { + "type": "object", + "required": [ + "server", + "uri" + ], + "properties": { + "server": { + "type": "string" + }, + "threadId": { + "type": [ + "string", + "null" + ] + }, + "uri": { + "type": "string" + } + } + }, + "McpServerMigration": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + } + } + }, + "McpServerOauthLoginParams": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "scopes": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "timeoutSecs": { + "type": [ + "integer", + "null" + ], + "format": "int64" + } + } + }, + "McpServerStatusDetail": { + "type": "string", + "enum": [ + "full", + "toolsAndAuthOnly" + ] + }, + "McpServerToolCallParams": { + "type": "object", + "required": [ + "server", + "threadId", + "tool" + ], + "properties": { + "_meta": true, + "arguments": true, + "server": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "tool": { + "type": "string" + } + } + }, + "MergeStrategy": { + "type": "string", + "enum": [ + "replace", + "upsert" + ] + }, + "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.", + "type": "string", + "enum": [ + "commentary" + ] + }, + { + "description": "The assistant's terminal answer text for the current turn.", + "type": "string", + "enum": [ + "final_answer" + ] + } + ] + }, + "MigrationDetails": { + "type": "object", + "properties": { + "commands": { + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/CommandMigration" + } + }, + "hooks": { + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/HookMigration" + } + }, + "mcpServers": { + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/McpServerMigration" + } + }, + "plugins": { + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/PluginsMigration" + } + }, + "sessions": { + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/SessionMigration" + } + }, + "subagents": { + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/SubagentMigration" + } + } + } + }, + "TurnItemsView": { + "oneOf": [ + { + "description": "`items` was not loaded for this turn. The field is intentionally empty.", + "type": "string", + "enum": [ + "notLoaded" + ] + }, + { + "description": "`items` contains only a display summary for this turn.", + "type": "string", + "enum": [ + "summary" + ] + }, + { + "description": "`items` contains every ThreadItem available from persisted app-server history for this turn.", + "type": "string", + "enum": [ + "full" + ] + } + ] + }, + "ModeKind": { + "description": "Initial collaboration mode to use when the TUI starts.", + "type": "string", + "enum": [ + "plan", + "default" + ] + }, + "ModelListParams": { + "type": "object", + "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.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + } + }, + "ModelProviderCapabilitiesReadParams": { + "type": "object" + }, + "NetworkAccess": { + "type": "string", + "enum": [ + "restricted", + "enabled" + ] + }, + "PermissionProfileListParams": { + "type": "object", + "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.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + } + }, + "Personality": { + "type": "string", + "enum": [ + "none", + "friendly", + "pragmatic" + ] + }, + "PluginInstallParams": { + "type": "object", + "required": [ + "pluginName" + ], + "properties": { + "marketplacePath": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "pluginName": { + "type": "string" + }, + "remoteMarketplaceName": { + "type": [ + "string", + "null" + ] + } + } + }, + "PluginInstalledParams": { + "type": "object", + "properties": { + "cwds": { + "description": "Optional working directories used to discover repo marketplaces.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + } + }, + "installSuggestionPluginNames": { + "description": "Additional uninstalled plugin names that should be returned when present locally. This is used by mention surfaces that intentionally expose install entrypoints.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + } + }, + "PluginListMarketplaceKind": { + "type": "string", + "enum": [ + "local", + "vertical", + "workspace-directory", + "shared-with-me", + "created-by-me-remote" + ] + }, + "PluginListParams": { + "type": "object", + "properties": { + "cwds": { + "description": "Optional working directories used to discover repo marketplaces. When omitted, only home-scoped marketplaces and the official curated marketplace are considered.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + } + }, + "marketplaceKinds": { + "description": "Optional marketplace kind filter. When omitted, only local marketplaces are queried, plus the default remote catalog when enabled by feature flag.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/PluginListMarketplaceKind" + } + } + } + }, + "PluginReadParams": { + "type": "object", + "required": [ + "pluginName" + ], + "properties": { + "marketplacePath": { + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + }, + "pluginName": { + "type": "string" + }, + "remoteMarketplaceName": { + "type": [ + "string", + "null" + ] + } + } + }, + "PluginShareCheckoutParams": { + "type": "object", + "required": [ + "remotePluginId" + ], + "properties": { + "remotePluginId": { + "type": "string" + } + } + }, + "PluginShareDeleteParams": { + "type": "object", + "required": [ + "remotePluginId" + ], + "properties": { + "remotePluginId": { + "type": "string" + } + } + }, + "PluginShareDiscoverability": { + "type": "string", + "enum": [ + "LISTED", + "UNLISTED", + "PRIVATE" + ] + }, + "PluginShareListParams": { + "type": "object" + }, + "PluginSharePrincipalType": { + "type": "string", + "enum": [ + "user", + "group", + "workspace" + ] + }, + "PluginShareSaveParams": { + "type": "object", + "required": [ + "pluginPath" + ], + "properties": { + "discoverability": { + "anyOf": [ + { + "$ref": "#/definitions/PluginShareDiscoverability" + }, + { + "type": "null" + } + ] + }, + "pluginPath": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "remotePluginId": { + "type": [ + "string", + "null" + ] + }, + "shareTargets": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/PluginShareTarget" + } + } + } + }, + "PluginShareTarget": { + "type": "object", + "required": [ + "principalId", + "principalType", + "role" + ], + "properties": { + "principalId": { + "type": "string" + }, + "principalType": { + "$ref": "#/definitions/PluginSharePrincipalType" + }, + "role": { + "$ref": "#/definitions/PluginShareTargetRole" + } + } + }, + "PluginShareTargetRole": { + "type": "string", + "enum": [ + "reader", + "editor" + ] + }, + "PluginShareUpdateDiscoverability": { + "type": "string", + "enum": [ + "UNLISTED", + "PRIVATE" + ] + }, + "PluginShareUpdateTargetsParams": { + "type": "object", + "required": [ + "discoverability", + "remotePluginId", + "shareTargets" + ], + "properties": { + "discoverability": { + "$ref": "#/definitions/PluginShareUpdateDiscoverability" + }, + "remotePluginId": { + "type": "string" + }, + "shareTargets": { + "type": "array", + "items": { + "$ref": "#/definitions/PluginShareTarget" + } + } + } + }, + "PluginSkillReadParams": { + "type": "object", + "required": [ + "remoteMarketplaceName", + "remotePluginId", + "skillName" + ], + "properties": { + "remoteMarketplaceName": { + "type": "string" + }, + "remotePluginId": { + "type": "string" + }, + "skillName": { + "type": "string" + } + } + }, + "PluginUninstallParams": { + "type": "object", + "required": [ + "pluginId" + ], + "properties": { + "pluginId": { + "type": "string" + } + } + }, + "PluginsMigration": { + "type": "object", + "required": [ + "marketplaceName", + "pluginNames" + ], + "properties": { + "marketplaceName": { + "type": "string" + }, + "pluginNames": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "TurnInterruptParams": { + "type": "object", + "required": [ + "threadId", + "turnId" + ], + "properties": { + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + } + }, + "TurnEnvironmentParams": { + "type": "object", + "required": [ + "cwd", + "environmentId" + ], + "properties": { + "cwd": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "environmentId": { + "type": "string" + } + } + }, + "ThreadUnsubscribeParams": { + "type": "object", + "required": [ + "threadId" + ], + "properties": { + "threadId": { + "type": "string" + } + } + }, + "ProcessTerminalSize": { + "description": "PTY size in character cells for `process/spawn` PTY sessions.", + "type": "object", + "required": [ + "cols", + "rows" + ], + "properties": { + "cols": { + "description": "Terminal width in character cells.", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + }, + "rows": { + "description": "Terminal height in character cells.", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + } + } + }, + "ThreadUnarchiveParams": { + "type": "object", + "required": [ + "threadId" + ], + "properties": { + "threadId": { + "type": "string" + } + } + }, + "RealtimeConversationArchitecture": { + "type": "string", + "enum": [ + "realtimeapi", + "avas" + ] + }, + "RealtimeConversationVersion": { + "type": "string", + "enum": [ + "v1", + "v2" + ] + }, + "RealtimeOutputModality": { + "type": "string", + "enum": [ + "text", + "audio" + ] + }, + "RealtimeVoice": { + "type": "string", + "enum": [ + "alloy", + "arbor", + "ash", + "ballad", + "breeze", + "cedar", + "coral", + "cove", + "echo", + "ember", + "juniper", + "maple", + "marin", + "sage", + "shimmer", + "sol", + "spruce", + "vale", + "verse" + ] + }, + "ReasoningEffort": { + "description": "A non-empty reasoning effort value advertised by the model.", + "type": "string", + "minLength": 1 + }, + "ReasoningItemContent": { + "oneOf": [ + { + "type": "object", + "required": [ + "text", + "type" + ], + "properties": { + "text": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "reasoning_text" + ], + "title": "ReasoningTextReasoningItemContentType" + } + }, + "title": "ReasoningTextReasoningItemContent" + }, + { + "type": "object", + "required": [ + "text", + "type" + ], + "properties": { + "text": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "text" + ], + "title": "TextReasoningItemContentType" + } + }, + "title": "TextReasoningItemContent" + } + ] + }, + "ReasoningItemReasoningSummary": { + "oneOf": [ + { + "type": "object", + "required": [ + "text", + "type" + ], + "properties": { + "text": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "summary_text" + ], + "title": "SummaryTextReasoningItemReasoningSummaryType" + } + }, + "title": "SummaryTextReasoningItemReasoningSummary" + } + ] + }, + "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": [ + { + "type": "string", + "enum": [ + "auto", + "concise", + "detailed" + ] + }, + { + "description": "Option to disable reasoning summaries.", + "type": "string", + "enum": [ + "none" + ] + } + ] + }, + "ThreadResumeParams": { + "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.", + "type": "object", + "required": [ + "threadId" + ], + "properties": { + "approvalPolicy": { + "anyOf": [ + { + "$ref": "#/definitions/AskForApproval" + }, + { + "type": "null" + } + ] + }, + "approvalsReviewer": { + "description": "Override where approval requests are routed for review on this thread and subsequent turns.", + "anyOf": [ + { + "$ref": "#/definitions/ApprovalsReviewer" + }, + { + "type": "null" + } + ] + }, + "baseInstructions": { + "type": [ + "string", + "null" + ] + }, + "config": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "cwd": { + "type": [ + "string", + "null" + ] + }, + "developerInstructions": { + "type": [ + "string", + "null" + ] + }, + "threadId": { + "type": "string" + }, + "sandbox": { + "anyOf": [ + { + "$ref": "#/definitions/SandboxMode" + }, + { + "type": "null" + } + ] + }, + "serviceTier": { + "type": [ + "string", + "null" + ] + }, + "model": { + "description": "Configuration overrides for the resumed thread, if any.", + "type": [ + "string", + "null" + ] + }, + "modelProvider": { + "type": [ + "string", + "null" + ] + }, + "personality": { + "anyOf": [ + { + "$ref": "#/definitions/Personality" + }, + { + "type": "null" + } + ] + } + } + }, + "ThreadResumeInitialTurnsPageParams": { + "type": "object", + "properties": { + "itemsView": { + "description": "How much item detail to include for each returned turn; defaults to summary.", + "anyOf": [ + { + "$ref": "#/definitions/TurnItemsView" + }, + { + "type": "null" + } + ] + }, + "limit": { + "description": "Optional turn page size.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "sortDirection": { + "description": "Optional turn pagination direction; defaults to descending.", + "anyOf": [ + { + "$ref": "#/definitions/SortDirection" + }, + { + "type": "null" + } + ] + } + } + }, + "ThreadStartSource": { + "type": "string", + "enum": [ + "startup", + "clear" + ] + }, + "RemoteControlDisableParams": { + "type": "object", + "properties": { + "ephemeral": { + "type": "boolean" + } + } + }, + "RemoteControlEnableParams": { + "type": "object", + "properties": { + "ephemeral": { + "type": "boolean" + } + } + }, + "ThreadStartParams": { + "type": "object", + "properties": { + "approvalPolicy": { + "anyOf": [ + { + "$ref": "#/definitions/AskForApproval" + }, + { + "type": "null" + } + ] + }, + "approvalsReviewer": { + "description": "Override where approval requests are routed for review on this thread and subsequent turns.", + "anyOf": [ + { + "$ref": "#/definitions/ApprovalsReviewer" + }, + { + "type": "null" + } + ] + }, + "baseInstructions": { + "type": [ + "string", + "null" + ] + }, + "config": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "cwd": { + "type": [ + "string", + "null" + ] + }, + "developerInstructions": { + "type": [ + "string", + "null" + ] + }, + "serviceTier": { + "type": [ + "string", + "null" + ] + }, + "serviceName": { + "type": [ + "string", + "null" + ] + }, + "ephemeral": { + "type": [ + "boolean", + "null" + ] + }, + "sessionStartSource": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadStartSource" + }, + { + "type": "null" + } + ] + }, + "threadSource": { + "description": "Optional client-supplied analytics source classification for this thread.", + "anyOf": [ + { + "$ref": "#/definitions/ThreadSource" + }, + { + "type": "null" + } + ] + }, + "model": { + "type": [ + "string", + "null" + ] + }, + "modelProvider": { + "type": [ + "string", + "null" + ] + }, + "sandbox": { + "anyOf": [ + { + "$ref": "#/definitions/SandboxMode" + }, + { + "type": "null" + } + ] + }, + "personality": { + "anyOf": [ + { + "$ref": "#/definitions/Personality" + }, + { + "type": "null" + } + ] + } + } + }, + "ThreadSourceKind": { + "type": "string", + "enum": [ + "cli", + "vscode", + "exec", + "appServer", + "subAgent", + "subAgentReview", + "subAgentCompact", + "subAgentThreadSpawn", + "subAgentOther", + "unknown" + ] + }, + "RequestId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer", + "format": "int64" + } + ] + }, + "ResponseItem": { + "oneOf": [ + { + "type": "object", + "required": [ + "content", + "role", + "type" + ], + "properties": { + "content": { + "type": "array", + "items": { + "$ref": "#/definitions/ContentItem" + } + }, + "id": { + "writeOnly": true, + "type": [ + "string", + "null" + ] + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, + "phase": { + "anyOf": [ + { + "$ref": "#/definitions/MessagePhase" + }, + { + "type": "null" + } + ] + }, + "role": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "message" + ], + "title": "MessageResponseItemType" + } + }, + "title": "MessageResponseItem" + }, + { + "type": "object", + "required": [ + "author", + "content", + "recipient", + "type" + ], + "properties": { + "author": { + "type": "string" + }, + "content": { + "type": "array", + "items": { + "$ref": "#/definitions/AgentMessageInputContent" + } + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, + "recipient": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "agent_message" + ], + "title": "AgentMessageResponseItemType" + } + }, + "title": "AgentMessageResponseItem" + }, + { + "type": "object", + "required": [ + "summary", + "type" + ], + "properties": { + "content": { + "default": null, + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/ReasoningItemContent" + } + }, + "encrypted_content": { + "type": [ + "string", + "null" + ] + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, + "summary": { + "type": "array", + "items": { + "$ref": "#/definitions/ReasoningItemReasoningSummary" + } + }, + "type": { + "type": "string", + "enum": [ + "reasoning" + ], + "title": "ReasoningResponseItemType" + } + }, + "title": "ReasoningResponseItem" + }, + { + "type": "object", + "required": [ + "action", + "status", + "type" + ], + "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.", + "writeOnly": true, + "type": [ + "string", + "null" + ] + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, + "status": { + "$ref": "#/definitions/LocalShellStatus" + }, + "type": { + "type": "string", + "enum": [ + "local_shell_call" + ], + "title": "LocalShellCallResponseItemType" + } + }, + "title": "LocalShellCallResponseItem" + }, + { + "type": "object", + "required": [ + "arguments", + "call_id", + "name", + "type" + ], + "properties": { + "arguments": { + "type": "string" + }, + "call_id": { + "type": "string" + }, + "id": { + "writeOnly": true, + "type": [ + "string", + "null" + ] + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, + "name": { + "type": "string" + }, + "namespace": { + "type": [ + "string", + "null" + ] + }, + "type": { + "type": "string", + "enum": [ + "function_call" + ], + "title": "FunctionCallResponseItemType" + } + }, + "title": "FunctionCallResponseItem" + }, + { + "type": "object", + "required": [ + "arguments", + "execution", + "type" + ], + "properties": { + "arguments": true, + "call_id": { + "type": [ + "string", + "null" + ] + }, + "execution": { + "type": "string" + }, + "id": { + "writeOnly": true, + "type": [ + "string", + "null" + ] + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, + "status": { + "type": [ + "string", + "null" + ] + }, + "type": { + "type": "string", + "enum": [ + "tool_search_call" + ], + "title": "ToolSearchCallResponseItemType" + } + }, + "title": "ToolSearchCallResponseItem" + }, + { + "type": "object", + "required": [ + "call_id", + "output", + "type" + ], + "properties": { + "call_id": { + "type": "string" + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, + "output": { + "$ref": "#/definitions/FunctionCallOutputBody" + }, + "type": { + "type": "string", + "enum": [ + "function_call_output" + ], + "title": "FunctionCallOutputResponseItemType" + } + }, + "title": "FunctionCallOutputResponseItem" + }, + { + "type": "object", + "required": [ + "call_id", + "input", + "name", + "type" + ], + "properties": { + "call_id": { + "type": "string" + }, + "id": { + "writeOnly": true, + "type": [ + "string", + "null" + ] + }, + "input": { + "type": "string" + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, + "name": { + "type": "string" + }, + "status": { + "type": [ + "string", + "null" + ] + }, + "type": { + "type": "string", + "enum": [ + "custom_tool_call" + ], + "title": "CustomToolCallResponseItemType" + } + }, + "title": "CustomToolCallResponseItem" + }, + { + "type": "object", + "required": [ + "call_id", + "output", + "type" + ], + "properties": { + "call_id": { + "type": "string" + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "output": { + "$ref": "#/definitions/FunctionCallOutputBody" + }, + "type": { + "type": "string", + "enum": [ + "custom_tool_call_output" + ], + "title": "CustomToolCallOutputResponseItemType" + } + }, + "title": "CustomToolCallOutputResponseItem" + }, + { + "type": "object", + "required": [ + "execution", + "status", + "tools", + "type" + ], + "properties": { + "call_id": { + "type": [ + "string", + "null" + ] + }, + "execution": { + "type": "string" + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, + "status": { + "type": "string" + }, + "tools": { + "type": "array", + "items": true + }, + "type": { + "type": "string", + "enum": [ + "tool_search_output" + ], + "title": "ToolSearchOutputResponseItemType" + } + }, + "title": "ToolSearchOutputResponseItem" + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "action": { + "anyOf": [ + { + "$ref": "#/definitions/ResponsesApiWebSearchAction" + }, + { + "type": "null" + } + ] + }, + "id": { + "writeOnly": true, + "type": [ + "string", + "null" + ] + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, + "status": { + "type": [ + "string", + "null" + ] + }, + "type": { + "type": "string", + "enum": [ + "web_search_call" + ], + "title": "WebSearchCallResponseItemType" + } + }, + "title": "WebSearchCallResponseItem" + }, + { + "type": "object", + "required": [ + "id", + "result", + "status", + "type" + ], + "properties": { + "id": { + "type": "string" + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, + "result": { + "type": "string" + }, + "revised_prompt": { + "type": [ + "string", + "null" + ] + }, + "status": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "image_generation_call" + ], + "title": "ImageGenerationCallResponseItemType" + } + }, + "title": "ImageGenerationCallResponseItem" + }, + { + "type": "object", + "required": [ + "encrypted_content", + "type" + ], + "properties": { + "encrypted_content": { + "type": "string" + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "compaction" + ], + "title": "CompactionResponseItemType" + } + }, + "title": "CompactionResponseItem" + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "compaction_trigger" + ], + "title": "CompactionTriggerResponseItemType" + } + }, + "title": "CompactionTriggerResponseItem" + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "encrypted_content": { + "type": [ + "string", + "null" + ] + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/definitions/ResponseItemMetadata" + }, + { + "type": "null" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "context_compaction" + ], + "title": "ContextCompactionResponseItemType" + } + }, + "title": "ContextCompactionResponseItem" + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "other" + ], + "title": "OtherResponseItemType" + } + }, + "title": "OtherResponseItem" + } + ] + }, + "ResponseItemMetadata": { + "type": "object", + "properties": { + "turn_id": { + "type": [ + "string", + "null" + ] + } + } + }, + "ResponsesApiWebSearchAction": { + "oneOf": [ + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "queries": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "query": { + "type": [ + "string", + "null" + ] + }, + "type": { + "type": "string", + "enum": [ + "search" + ], + "title": "SearchResponsesApiWebSearchActionType" + } + }, + "title": "SearchResponsesApiWebSearchAction" + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "open_page" + ], + "title": "OpenPageResponsesApiWebSearchActionType" + }, + "url": { + "type": [ + "string", + "null" + ] + } + }, + "title": "OpenPageResponsesApiWebSearchAction" + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "pattern": { + "type": [ + "string", + "null" + ] + }, + "type": { + "type": "string", + "enum": [ + "find_in_page" + ], + "title": "FindInPageResponsesApiWebSearchActionType" + }, + "url": { + "type": [ + "string", + "null" + ] + } + }, + "title": "FindInPageResponsesApiWebSearchAction" + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "other" + ], + "title": "OtherResponsesApiWebSearchActionType" + } + }, + "title": "OtherResponsesApiWebSearchAction" + } + ] + }, + "ReviewDelivery": { + "type": "string", + "enum": [ + "inline", + "detached" + ] + }, + "ReviewStartParams": { + "type": "object", + "required": [ + "target", + "threadId" + ], + "properties": { + "delivery": { + "description": "Where to run the review: inline (default) on the current thread or detached on a new thread (returned in `reviewThreadId`).", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/ReviewDelivery" + }, + { + "type": "null" + } + ] + }, + "target": { + "$ref": "#/definitions/ReviewTarget" + }, + "threadId": { + "type": "string" + } + } + }, + "ReviewTarget": { + "oneOf": [ + { + "description": "Review the working tree: staged, unstaged, and untracked files.", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "uncommittedChanges" + ], + "title": "UncommittedChangesReviewTargetType" + } + }, + "title": "UncommittedChangesReviewTarget" + }, + { + "description": "Review changes between the current branch and the given base branch.", + "type": "object", + "required": [ + "branch", + "type" + ], + "properties": { + "branch": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "baseBranch" + ], + "title": "BaseBranchReviewTargetType" + } + }, + "title": "BaseBranchReviewTarget" + }, + { + "description": "Review the changes introduced by a specific commit.", + "type": "object", + "required": [ + "sha", + "type" + ], + "properties": { + "sha": { + "type": "string" + }, + "title": { + "description": "Optional human-readable label (e.g., commit subject) for UIs.", + "type": [ + "string", + "null" + ] + }, + "type": { + "type": "string", + "enum": [ + "commit" + ], + "title": "CommitReviewTargetType" + } + }, + "title": "CommitReviewTarget" + }, + { + "description": "Arbitrary instructions, equivalent to the old free-form prompt.", + "type": "object", + "required": [ + "instructions", + "type" + ], + "properties": { + "instructions": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "custom" + ], + "title": "CustomReviewTargetType" + } + }, + "title": "CustomReviewTarget" + } + ] + }, + "SandboxMode": { + "type": "string", + "enum": [ + "read-only", + "workspace-write", + "danger-full-access" + ] + }, + "SandboxPolicy": { + "oneOf": [ + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "dangerFullAccess" + ], + "title": "DangerFullAccessSandboxPolicyType" + } + }, + "title": "DangerFullAccessSandboxPolicy" + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "networkAccess": { + "default": false, + "type": "boolean" + }, + "type": { + "type": "string", + "enum": [ + "readOnly" + ], + "title": "ReadOnlySandboxPolicyType" + } + }, + "title": "ReadOnlySandboxPolicy" + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "networkAccess": { + "default": "restricted", + "allOf": [ + { + "$ref": "#/definitions/NetworkAccess" + } + ] + }, + "type": { + "type": "string", + "enum": [ + "externalSandbox" + ], + "title": "ExternalSandboxSandboxPolicyType" + } + }, + "title": "ExternalSandboxSandboxPolicy" + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "excludeSlashTmp": { + "default": false, + "type": "boolean" + }, + "excludeTmpdirEnvVar": { + "default": false, + "type": "boolean" + }, + "networkAccess": { + "default": false, + "type": "boolean" + }, + "type": { + "type": "string", + "enum": [ + "workspaceWrite" + ], + "title": "WorkspaceWriteSandboxPolicyType" + }, + "writableRoots": { + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + } + } + }, + "title": "WorkspaceWriteSandboxPolicy" + } + ] + }, + "SelectedCapabilityRoot": { + "description": "A user-selected root that can expose one or more runtime capabilities.", + "type": "object", + "required": [ + "id", + "location" + ], + "properties": { + "id": { + "description": "Stable identifier supplied by the capability selection platform.", + "type": "string" + }, + "location": { + "description": "Where the selected root can be resolved.", + "allOf": [ + { + "$ref": "#/definitions/CapabilityRootLocation" + } + ] + } + } + }, + "SendAddCreditsNudgeEmailParams": { + "type": "object", + "required": [ + "creditType" + ], + "properties": { + "creditType": { + "$ref": "#/definitions/AddCreditsNudgeCreditType" + } + } + }, + "SessionMigration": { + "type": "object", + "required": [ + "cwd", + "path" + ], + "properties": { + "cwd": { + "type": "string" + }, + "path": { + "type": "string" + }, + "title": { + "type": [ + "string", + "null" + ] + } + } + }, + "Settings": { + "description": "Settings for a collaboration mode.", + "type": "object", + "required": [ + "model" + ], + "properties": { + "developer_instructions": { + "type": [ + "string", + "null" + ] + }, + "model": { + "type": "string" + }, + "reasoning_effort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ] + } + } + }, + "SkillsConfigWriteParams": { + "type": "object", + "required": [ + "enabled" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "name": { + "description": "Name-based selector.", + "type": [ + "string", + "null" + ] + }, + "path": { + "description": "Path-based selector.", + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] + } + } + }, + "SkillsExtraRootsSetParams": { + "type": "object", + "required": [ + "extraRoots" + ], + "properties": { + "extraRoots": { + "type": "array", + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + } + } + } + }, + "SkillsListParams": { + "type": "object", + "properties": { + "cwds": { + "description": "When empty, defaults to the current session working directory.", + "type": "array", + "items": { + "type": "string" + } + }, + "forceReload": { + "description": "When true, bypass the skills cache and re-scan skills from disk.", + "type": "boolean" + } + } + }, + "SortDirection": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + }, + "SubagentMigration": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + } + } + }, + "TextElement": { + "type": "object", + "required": [ + "byteRange" + ], + "properties": { + "byteRange": { + "description": "Byte range in the parent `text` buffer that this element occupies.", + "allOf": [ + { + "$ref": "#/definitions/ByteRange" + } + ] + }, + "placeholder": { + "description": "Optional human-readable placeholder for the element, displayed in the UI.", + "type": [ + "string", + "null" + ] + } + } + }, + "ThreadApproveGuardianDeniedActionParams": { + "type": "object", + "required": [ + "event", + "threadId" + ], + "properties": { + "event": { + "description": "Serialized `codex_protocol::protocol::GuardianAssessmentEvent`." + }, + "threadId": { + "type": "string" + } + } + }, + "ThreadArchiveParams": { + "type": "object", + "required": [ + "threadId" + ], + "properties": { + "threadId": { + "type": "string" + } + } + }, + "ThreadSource": { + "type": "string" + }, + "ThreadSortKey": { + "type": "string", + "enum": [ + "created_at", + "updated_at" + ] + }, + "ThreadShellCommandParams": { + "type": "object", + "required": [ + "command", + "threadId" + ], + "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" + } + } + }, + "ThreadCompactStartParams": { + "type": "object", + "required": [ + "threadId" + ], + "properties": { + "threadId": { + "type": "string" + } + } + }, + "ThreadRealtimeAudioChunk": { + "description": "EXPERIMENTAL - thread realtime audio chunk.", + "type": "object", + "required": [ + "data", + "numChannels", + "sampleRate" + ], + "properties": { + "data": { + "type": "string" + }, + "itemId": { + "type": [ + "string", + "null" + ] + }, + "numChannels": { + "type": "integer", + "format": "uint16", + "minimum": 0.0 + }, + "sampleRate": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "samplesPerChannel": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + } + }, + "ThreadDeleteParams": { + "type": "object", + "required": [ + "threadId" + ], + "properties": { + "threadId": { + "type": "string" + } + } + }, + "ThreadForkParams": { + "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.", + "type": "object", + "required": [ + "threadId" + ], + "properties": { + "approvalPolicy": { + "anyOf": [ + { + "$ref": "#/definitions/AskForApproval" + }, + { + "type": "null" + } + ] + }, + "approvalsReviewer": { + "description": "Override where approval requests are routed for review on this thread and subsequent turns.", + "anyOf": [ + { + "$ref": "#/definitions/ApprovalsReviewer" + }, + { + "type": "null" + } + ] + }, + "baseInstructions": { + "type": [ + "string", + "null" + ] + }, + "config": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "cwd": { + "type": [ + "string", + "null" + ] + }, + "developerInstructions": { + "type": [ + "string", + "null" + ] + }, + "ephemeral": { + "type": "boolean" + }, + "serviceTier": { + "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" + } + ] + }, + "threadId": { + "type": "string" + }, + "threadSource": { + "description": "Optional client-supplied analytics source classification for this forked thread.", + "anyOf": [ + { + "$ref": "#/definitions/ThreadSource" + }, + { + "type": "null" + } + ] + } + } + }, + "ThreadGoalClearParams": { + "type": "object", + "required": [ + "threadId" + ], + "properties": { + "threadId": { + "type": "string" + } + } + }, + "ThreadGoalGetParams": { + "type": "object", + "required": [ + "threadId" + ], + "properties": { + "threadId": { + "type": "string" + } + } + }, + "ThreadGoalSetParams": { + "type": "object", + "required": [ + "threadId" + ], + "properties": { + "objective": { + "type": [ + "string", + "null" + ] + }, + "status": { + "anyOf": [ + { + "$ref": "#/definitions/ThreadGoalStatus" + }, + { + "type": "null" + } + ] + }, + "threadId": { + "type": "string" + }, + "tokenBudget": { + "type": [ + "integer", + "null" + ], + "format": "int64" + } + } + }, + "ThreadGoalStatus": { + "type": "string", + "enum": [ + "active", + "paused", + "blocked", + "usageLimited", + "budgetLimited", + "complete" + ] + }, + "ThreadSetNameParams": { + "type": "object", + "required": [ + "name", + "threadId" + ], + "properties": { + "name": { + "type": "string" + }, + "threadId": { + "type": "string" + } + } + }, + "ThreadInjectItemsParams": { + "type": "object", + "required": [ + "items", + "threadId" + ], + "properties": { + "items": { + "description": "Raw Responses API items to append to the thread's model-visible history.", + "type": "array", + "items": true + }, + "threadId": { + "type": "string" + } + } + }, + "ThreadListCwdFilter": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "ThreadListParams": { + "type": "object", + "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": { + "description": "Optional cwd filter or filters; when set, only threads whose session cwd exactly matches one of these paths are returned.", + "anyOf": [ + { + "$ref": "#/definitions/ThreadListCwdFilter" + }, + { + "type": "null" + } + ] + }, + "limit": { + "description": "Optional page size; defaults to a reasonable server-side value.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "modelProviders": { + "description": "Optional provider filter; when set, only sessions recorded under these providers are returned. When present but empty, includes all providers.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "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" + }, + "searchTerm": { + "description": "Optional substring filter for the extracted thread title.", + "type": [ + "string", + "null" + ] + }, + "sortDirection": { + "description": "Optional sort direction; defaults to descending (newest first).", + "anyOf": [ + { + "$ref": "#/definitions/SortDirection" + }, + { + "type": "null" + } + ] + }, + "sortKey": { + "description": "Optional sort key; defaults to created_at.", + "anyOf": [ + { + "$ref": "#/definitions/ThreadSortKey" + }, + { + "type": "null" + } + ] + }, + "sourceKinds": { + "description": "Optional source filter; when set, only sessions from these source kinds are returned. When omitted or empty, defaults to interactive sources.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/ThreadSourceKind" + } + } + } + }, + "ThreadLoadedListParams": { + "type": "object", + "properties": { + "cursor": { + "description": "Opaque pagination cursor returned by a previous call.", + "type": [ + "string", + "null" + ] + }, + "limit": { + "description": "Optional page size; defaults to no limit.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + } + }, + "ThreadMemoryMode": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "ThreadRealtimeStartTransport": { + "description": "EXPERIMENTAL - transport used by thread realtime.", + "oneOf": [ + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "websocket" + ], + "title": "WebsocketThreadRealtimeStartTransportType" + } + }, + "title": "WebsocketThreadRealtimeStartTransport" + }, + { + "type": "object", + "required": [ + "sdp", + "type" + ], + "properties": { + "sdp": { + "description": "SDP offer generated by a WebRTC RTCPeerConnection after configuring audio and the realtime events data channel.", + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "webrtc" + ], + "title": "WebrtcThreadRealtimeStartTransportType" + } + }, + "title": "WebrtcThreadRealtimeStartTransport" + } + ] + }, + "ThreadMetadataGitInfoUpdateParams": { + "type": "object", + "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" + ] + } + } + }, + "ThreadMetadataUpdateParams": { + "type": "object", + "required": [ + "threadId" + ], + "properties": { + "gitInfo": { + "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.", + "anyOf": [ + { + "$ref": "#/definitions/ThreadMetadataGitInfoUpdateParams" + }, + { + "type": "null" + } + ] + }, + "threadId": { + "type": "string" + } + } + }, + "ThreadReadParams": { + "type": "object", + "required": [ + "threadId" + ], + "properties": { + "includeTurns": { + "description": "When true, include turns and their items from rollout history.", + "type": "boolean" + }, + "threadId": { + "type": "string" + } + } + }, + "ThreadRollbackParams": { + "type": "object", + "required": [ + "numTurns", + "threadId" + ], + "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.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "threadId": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/tests/core/adapters/fixtures/codex_app_server/CommandExecutionRequestApprovalResponse.json b/tests/core/adapters/fixtures/codex_app_server/CommandExecutionRequestApprovalResponse.json new file mode 100644 index 00000000..60036c05 --- /dev/null +++ b/tests/core/adapters/fixtures/codex_app_server/CommandExecutionRequestApprovalResponse.json @@ -0,0 +1,116 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CommandExecutionRequestApprovalResponse", + "type": "object", + "required": [ + "decision" + ], + "properties": { + "decision": { + "$ref": "#/definitions/CommandExecutionApprovalDecision" + } + }, + "definitions": { + "CommandExecutionApprovalDecision": { + "oneOf": [ + { + "description": "User approved the command.", + "type": "string", + "enum": [ + "accept" + ] + }, + { + "description": "User approved the command and future prompts in the same session-scoped approval cache should run without prompting.", + "type": "string", + "enum": [ + "acceptForSession" + ] + }, + { + "description": "User approved the command, and wants to apply the proposed execpolicy amendment so future matching commands can run without prompting.", + "type": "object", + "required": [ + "acceptWithExecpolicyAmendment" + ], + "properties": { + "acceptWithExecpolicyAmendment": { + "type": "object", + "required": [ + "execpolicy_amendment" + ], + "properties": { + "execpolicy_amendment": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "additionalProperties": false, + "title": "AcceptWithExecpolicyAmendmentCommandExecutionApprovalDecision" + }, + { + "description": "User chose a persistent network policy rule (allow/deny) for this host.", + "type": "object", + "required": [ + "applyNetworkPolicyAmendment" + ], + "properties": { + "applyNetworkPolicyAmendment": { + "type": "object", + "required": [ + "network_policy_amendment" + ], + "properties": { + "network_policy_amendment": { + "$ref": "#/definitions/NetworkPolicyAmendment" + } + } + } + }, + "additionalProperties": false, + "title": "ApplyNetworkPolicyAmendmentCommandExecutionApprovalDecision" + }, + { + "description": "User denied the command. The agent will continue the turn.", + "type": "string", + "enum": [ + "decline" + ] + }, + { + "description": "User denied the command. The turn will also be immediately interrupted.", + "type": "string", + "enum": [ + "cancel" + ] + } + ] + }, + "NetworkPolicyAmendment": { + "type": "object", + "required": [ + "action", + "host" + ], + "properties": { + "action": { + "$ref": "#/definitions/NetworkPolicyRuleAction" + }, + "host": { + "type": "string" + } + } + }, + "NetworkPolicyRuleAction": { + "type": "string", + "enum": [ + "allow", + "deny" + ] + } + } +} \ No newline at end of file diff --git a/tests/core/adapters/fixtures/codex_app_server/FileChangeRequestApprovalResponse.json b/tests/core/adapters/fixtures/codex_app_server/FileChangeRequestApprovalResponse.json new file mode 100644 index 00000000..ace77406 --- /dev/null +++ b/tests/core/adapters/fixtures/codex_app_server/FileChangeRequestApprovalResponse.json @@ -0,0 +1,47 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "FileChangeRequestApprovalResponse", + "type": "object", + "required": [ + "decision" + ], + "properties": { + "decision": { + "$ref": "#/definitions/FileChangeApprovalDecision" + } + }, + "definitions": { + "FileChangeApprovalDecision": { + "oneOf": [ + { + "description": "User approved the file changes.", + "type": "string", + "enum": [ + "accept" + ] + }, + { + "description": "User approved the file changes and future changes to the same files should run without prompting.", + "type": "string", + "enum": [ + "acceptForSession" + ] + }, + { + "description": "User denied the file changes. The agent will continue the turn.", + "type": "string", + "enum": [ + "decline" + ] + }, + { + "description": "User denied the file changes. The turn will also be immediately interrupted.", + "type": "string", + "enum": [ + "cancel" + ] + } + ] + } + } +} \ No newline at end of file diff --git a/tests/core/adapters/fixtures/codex_app_server/README.md b/tests/core/adapters/fixtures/codex_app_server/README.md new file mode 100644 index 00000000..71e1cf8a --- /dev/null +++ b/tests/core/adapters/fixtures/codex_app_server/README.md @@ -0,0 +1,18 @@ +# Codex app-server protocol fixture + +Checked-in JSON Schemas for the `codex app-server` stdio protocol, used by +`tests/core/adapters/test_codex.py` to validate every message `CodexAdapter` +writes to the subprocess (issue #914). + +Generated with **codex-cli 0.141.0**: + +```bash +codex app-server generate-json-schema --out /tmp/codex-schema +cp /tmp/codex-schema/{ClientRequest,ClientNotification,\ +CommandExecutionRequestApprovalResponse,FileChangeRequestApprovalResponse}.json \ + tests/core/adapters/fixtures/codex_app_server/ +``` + +Only the four schemas the adapter actually emits are kept. Refresh them when +bumping the supported Codex CLI version; a contract-test failure after a refresh +means the adapter's outbound messages drifted from the protocol. diff --git a/tests/core/adapters/test_codex.py b/tests/core/adapters/test_codex.py index 305fd7bf..28e19467 100644 --- a/tests/core/adapters/test_codex.py +++ b/tests/core/adapters/test_codex.py @@ -1,6 +1,13 @@ -"""Tests for Codex adapter (app-server JSON-RPC protocol).""" +"""Tests for the Codex adapter (app-server JSON-RPC protocol, #914). + +The wire protocol here is not invented: it is the one emitted by +``codex app-server`` and described by the checked-in schema fixture in +``fixtures/codex_app_server/`` (see that directory's README). +""" import json +import os +import time from pathlib import Path from unittest.mock import MagicMock, patch @@ -9,385 +16,525 @@ pytestmark = pytest.mark.v2 +FIXTURES = Path(__file__).parent / "fixtures" / "codex_app_server" -def _make_jsonrpc(method: str, params: dict | None = None, id_: int | None = None) -> str: - """Build a JSON-RPC line as the mock Codex process would emit.""" - msg: dict = {"jsonrpc": "2.0", "method": method} - if params is not None: - msg["params"] = params - if id_ is not None: - msg["id"] = id_ - return json.dumps(msg) + "\n" +# ---------------------------------------------------------------------- +# Helpers +# ---------------------------------------------------------------------- -class _FakeStdout: - """Simulate a subprocess stdout that yields pre-scripted JSON-RPC lines.""" - def __init__(self, lines: list[str]) -> None: - self._lines = iter(lines) +def _response(msg_id: int, result: dict) -> str: + """A successful JSON-RPC response as the real server writes it (no `jsonrpc`).""" + return json.dumps({"id": msg_id, "result": result}) + "\n" - def readline(self) -> str: - try: - return next(self._lines) - except StopIteration: - return "" +def _error_response(msg_id: int, code: int, message: str) -> str: + return json.dumps({"id": msg_id, "error": {"code": code, "message": message}}) + "\n" -class TestCodexAdapterImport: - """Verify module can be imported and conforms to protocol.""" - def test_import(self) -> None: - from codeframe.core.adapters.codex import CodexAdapter # noqa: F401 +def _notification(method: str, params: dict | None = None) -> str: + msg: dict = {"method": method} + if params is not None: + msg["params"] = params + return json.dumps(msg) + "\n" - def test_conforms_to_protocol(self) -> None: - from codeframe.core.adapters.codex import CodexAdapter - with patch("shutil.which", return_value="/usr/bin/codex"): - adapter = CodexAdapter() - assert isinstance(adapter, AgentAdapter) +def _server_request(msg_id: int, method: str, params: dict) -> str: + return json.dumps({"id": msg_id, "method": method, "params": params}) + "\n" - def test_name(self) -> None: - from codeframe.core.adapters.codex import CodexAdapter - with patch("shutil.which", return_value="/usr/bin/codex"): - adapter = CodexAdapter() - assert adapter.name == "codex" +def _thread_started(thread_id: str = "th-1") -> str: + return _response(2, {"thread": {"id": thread_id}}) - def test_raises_if_binary_not_found(self) -> None: - from codeframe.core.adapters.codex import CodexAdapter - with patch("shutil.which", return_value=None): - with pytest.raises(EnvironmentError, match="not found on PATH"): - CodexAdapter() +def _turn_completed(status: str = "completed", error: dict | None = None) -> str: + turn: dict = {"id": "turn-1", "items": [], "status": status} + if error is not None: + turn["error"] = error + return _notification("turn/completed", {"threadId": "th-1", "turn": turn}) -class TestCodexJsonRpc: - """Test JSON-RPC message framing helpers.""" +def _handshake_lines(thread_id: str = "th-1") -> list[str]: + """The three server messages a successful handshake consumes.""" + return [ + _response(1, {"userAgent": "codeframe/0.141.0"}), + _notification("remoteControl/status/changed", {"status": "disabled"}), + _thread_started(thread_id), + ] - def test_send_writes_json_line(self) -> None: - from codeframe.core.adapters.codex import CodexAdapter - with patch("shutil.which", return_value="/usr/bin/codex"): - adapter = CodexAdapter() +class _PipeStdout: + """A real OS pipe standing in for subprocess stdout. - stdin = MagicMock() - adapter._send(stdin, "initialize", {"capabilities": {}}, msg_id=1) + Unlike a scripted list, this exercises the actual blocking-read behaviour: + lines written in a burst, then silence with the pipe still open. + """ - written = stdin.write.call_args[0][0] - parsed = json.loads(written.strip()) - assert parsed["jsonrpc"] == "2.0" - assert parsed["method"] == "initialize" - assert parsed["id"] == 1 - stdin.flush.assert_called_once() + def __init__(self) -> None: + read_fd, self._write_fd = os.pipe() + self.reader = os.fdopen(read_fd, "r") + self._writer = os.fdopen(self._write_fd, "w") - def test_send_without_id(self) -> None: - from codeframe.core.adapters.codex import CodexAdapter + def write_lines(self, lines: list[str]) -> None: + for line in lines: + self._writer.write(line) + self._writer.flush() - with patch("shutil.which", return_value="/usr/bin/codex"): - adapter = CodexAdapter() + def close(self) -> None: + try: + self._writer.close() + except ValueError: + pass - stdin = MagicMock() - adapter._send(stdin, "thread/start", {"thread_id": "t1"}) - written = stdin.write.call_args[0][0] - parsed = json.loads(written.strip()) - assert "id" not in parsed +def _make_adapter(**kwargs): + from codeframe.core.adapters.codex import CodexAdapter - def test_recv_line_parses_json(self) -> None: - from codeframe.core.adapters.codex import CodexAdapter + with patch("shutil.which", return_value="/usr/bin/codex"): + return CodexAdapter(**kwargs) - with patch("shutil.which", return_value="/usr/bin/codex"): - adapter = CodexAdapter() - line = _make_jsonrpc("initialized", {"session_id": "s1"}) - stdout = _FakeStdout([line]) +def _run_with_script(adapter, lines: list[str], *, close_stdout: bool = True, **run_kwargs): + """Run the adapter against a scripted stdout, returning (result, sent_messages).""" + pipe = _PipeStdout() + pipe.write_lines(lines) + if close_stdout: + pipe.close() - result = adapter._recv_line(stdout, timeout_s=5.0) - assert result is not None - assert result["method"] == "initialized" + sent: list[str] = [] + stdin = MagicMock() + stdin.write.side_effect = sent.append - def test_recv_line_returns_none_on_eof(self) -> None: - from codeframe.core.adapters.codex import CodexAdapter + process = MagicMock() + process.stdin = stdin + process.stdout = pipe.reader + process.stderr = MagicMock() + process.stderr.read.return_value = "" + process.poll.return_value = None - with patch("shutil.which", return_value="/usr/bin/codex"): - adapter = CodexAdapter() + try: + with patch("subprocess.Popen", return_value=process): + with patch.object(adapter, "_detect_modified_files", return_value=[]): + result = adapter.run( + run_kwargs.pop("task_id", "task-1"), + run_kwargs.pop("prompt", "fix the bug"), + run_kwargs.pop("workspace_path", Path("/tmp/repo")), + **run_kwargs, + ) + finally: + pipe.close() - stdout = _FakeStdout([]) - result = adapter._recv_line(stdout, timeout_s=1.0) - assert result is None + return result, [json.loads(m) for m in sent] -class TestCodexHandshake: - """Test the 4-step initialization handshake.""" +# ---------------------------------------------------------------------- +# Basics +# ---------------------------------------------------------------------- - def test_successful_handshake(self) -> None: - from codeframe.core.adapters.codex import CodexAdapter - with patch("shutil.which", return_value="/usr/bin/codex"): - adapter = CodexAdapter() +class TestCodexAdapterImport: + def test_conforms_to_protocol(self) -> None: + assert isinstance(_make_adapter(), AgentAdapter) - # Mock stdin - stdin = MagicMock() + def test_name(self) -> None: + assert _make_adapter().name == "codex" - # Mock stdout: expect initialized response after initialize is sent - stdout = _FakeStdout([ - _make_jsonrpc("initialized", {"session_id": "s1"}), - ]) + def test_raises_if_binary_not_found(self) -> None: + from codeframe.core.adapters.codex import CodexAdapter - success = adapter._handshake( - stdin, stdout, prompt="fix the bug", workspace_path=Path("/tmp/repo") - ) - assert success is True + with patch("shutil.which", return_value=None): + with pytest.raises(EnvironmentError, match="not found on PATH"): + CodexAdapter() - # Verify 3 messages were sent: initialize, thread/start, turn/start - assert stdin.write.call_count == 3 - def test_handshake_fails_on_timeout(self) -> None: - from codeframe.core.adapters.codex import CodexAdapter +# ---------------------------------------------------------------------- +# Handshake — must match the generated schema +# ---------------------------------------------------------------------- - with patch("shutil.which", return_value="/usr/bin/codex"): - adapter = CodexAdapter(read_timeout_ms=100) - stdin = MagicMock() - # Empty stdout = no initialized response - stdout = _FakeStdout([]) +class TestCodexHandshake: + def test_initialize_carries_client_info(self) -> None: + adapter = _make_adapter() + result, sent = _run_with_script(adapter, _handshake_lines() + [_turn_completed()]) - success = adapter._handshake( - stdin, stdout, prompt="fix the bug", workspace_path=Path("/tmp/repo") + assert result.status == "completed" + init = sent[0] + assert init["method"] == "initialize" + assert init["id"] == 1 + # InitializeParams requires clientInfo{name,version} — the old adapter + # sent {"capabilities": {}} and the server rejected the handshake. + assert init["params"]["clientInfo"]["name"] + assert init["params"]["clientInfo"]["version"] + + def test_sends_initialized_notification(self) -> None: + adapter = _make_adapter() + _, sent = _run_with_script(adapter, _handshake_lines() + [_turn_completed()]) + + notif = [m for m in sent if m.get("method") == "initialized"] + assert len(notif) == 1 + assert "id" not in notif[0] + + def test_thread_start_and_turn_start_are_requests(self) -> None: + adapter = _make_adapter() + workspace = Path("/tmp/repo") + _, sent = _run_with_script( + adapter, + _handshake_lines() + [_turn_completed()], + workspace_path=workspace, + prompt="do the thing", ) - assert success is False - -class TestCodexTurnStreaming: - """Test turn event streaming and routing.""" + thread_start = next(m for m in sent if m["method"] == "thread/start") + assert isinstance(thread_start["id"], int) + assert thread_start["params"]["cwd"] == str(workspace) + + turn_start = next(m for m in sent if m["method"] == "turn/start") + assert isinstance(turn_start["id"], int) + # threadId comes from the thread/start *response*, not an invented uuid. + assert turn_start["params"]["threadId"] == "th-1" + assert turn_start["params"]["input"] == [{"type": "text", "text": "do the thing"}] + + def test_jsonrpc_error_response_is_surfaced(self) -> None: + adapter = _make_adapter() + result, _ = _run_with_script( + adapter, + [_error_response(1, -32602, "clientInfo is required")], + ) - def _make_adapter(self, **kwargs): - from codeframe.core.adapters.codex import CodexAdapter + assert result.status == "failed" + assert "clientInfo is required" in (result.error or "") - with patch("shutil.which", return_value="/usr/bin/codex"): - return CodexAdapter(**kwargs) + def test_handshake_timeout_fails(self) -> None: + adapter = _make_adapter(read_timeout_ms=200) + # Pipe stays open but nothing is ever written. + result, _ = _run_with_script(adapter, [], close_stdout=False) - def test_turn_completed(self) -> None: - adapter = self._make_adapter() - events: list[AgentEvent] = [] + assert result.status == "failed" + assert "timed out" in (result.error or "").lower() - stdout = _FakeStdout([ - _make_jsonrpc("session_started", {"session_id": "s1"}), - _make_jsonrpc("turn/completed", { - "usage": {"input_tokens": 100, "output_tokens": 50} - }), - ]) - result = adapter._stream_turn(stdout, on_event=events.append) - assert result.status == "completed" - assert result.token_usage is not None - assert result.token_usage.input_tokens == 100 - assert result.token_usage.output_tokens == 50 - # Should have received a progress event for session_started - assert any(e.message == "Session started" for e in events) +# ---------------------------------------------------------------------- +# Turn streaming +# ---------------------------------------------------------------------- - def test_turn_failed(self) -> None: - adapter = self._make_adapter() - stdout = _FakeStdout([ - _make_jsonrpc("turn/failed", {"error": "syntax error in file"}), - ]) +class TestCodexTurnStreaming: + def test_turn_completed_reports_success(self) -> None: + adapter = _make_adapter() + result, _ = _run_with_script(adapter, _handshake_lines() + [_turn_completed()]) + assert result.status == "completed" - result = adapter._stream_turn(stdout, on_event=None) + def test_turn_failed_reports_error_message(self) -> None: + adapter = _make_adapter() + result, _ = _run_with_script( + adapter, + _handshake_lines() + + [_turn_completed("failed", {"message": "model refused the request"})], + ) assert result.status == "failed" - assert "syntax error" in (result.error or "") - - def test_turn_cancelled(self) -> None: - adapter = self._make_adapter() + assert "model refused" in (result.error or "") - stdout = _FakeStdout([ - _make_jsonrpc("turn/cancelled", {}), - ]) - - result = adapter._stream_turn(stdout, on_event=None) + def test_interrupted_turn_reports_failure(self) -> None: + adapter = _make_adapter() + result, _ = _run_with_script( + adapter, _handshake_lines() + [_turn_completed("interrupted")] + ) assert result.status == "failed" - assert "cancelled" in (result.error or "").lower() + assert "interrupted" in (result.error or "").lower() + + def test_token_usage_from_thread_token_usage_updated(self) -> None: + adapter = _make_adapter() + result, _ = _run_with_script( + adapter, + _handshake_lines() + + [ + _notification( + "thread/tokenUsage/updated", + { + "threadId": "th-1", + "tokenUsage": { + "total": {"inputTokens": 38510, "outputTokens": 165}, + "last": {"inputTokens": 19324, "outputTokens": 66}, + }, + }, + ), + _turn_completed(), + ], + ) + assert result.token_usage is not None + assert result.token_usage.input_tokens == 38510 + assert result.token_usage.output_tokens == 165 + + def test_agent_message_becomes_output(self) -> None: + adapter = _make_adapter() + result, _ = _run_with_script( + adapter, + _handshake_lines() + + [ + _notification( + "item/completed", + { + "item": { + "type": "agentMessage", + "id": "msg-1", + "text": "Created hello.txt", + } + }, + ), + _turn_completed(), + ], + ) + assert "Created hello.txt" in result.output - def test_notification_emits_progress(self) -> None: - adapter = self._make_adapter() + def test_item_events_emit_progress(self) -> None: + adapter = _make_adapter() events: list[AgentEvent] = [] + _run_with_script( + adapter, + _handshake_lines() + + [ + _notification( + "item/started", + {"item": {"type": "commandExecution", "id": "i1", "command": "ls"}}, + ), + _turn_completed(), + ], + on_event=events.append, + ) + assert any("commandExecution" in e.message for e in events) - stdout = _FakeStdout([ - _make_jsonrpc("notification", {"message": "Reading file main.py"}), - _make_jsonrpc("turn/completed", {"usage": {}}), - ]) - - result = adapter._stream_turn(stdout, on_event=events.append) - assert result.status == "completed" - assert any("Reading file" in e.message for e in events) - - def test_eof_detected_as_failure(self) -> None: - adapter = self._make_adapter() - - # Empty stdout = process terminated (EOF) - stdout = _FakeStdout([]) + def test_error_notification_is_reported(self) -> None: + adapter = _make_adapter() + events: list[AgentEvent] = [] + result, _ = _run_with_script( + adapter, + _handshake_lines() + + [ + _notification( + "error", + { + "threadId": "th-1", + "turnId": "turn-1", + "willRetry": False, + "error": {"message": "stream disconnected"}, + }, + ), + _turn_completed("failed", {"message": "stream disconnected"}), + ], + on_event=events.append, + ) + assert result.status == "failed" + assert any(e.type == "error" for e in events) - result = adapter._stream_turn(stdout, on_event=None) + def test_eof_before_terminal_event_fails(self) -> None: + adapter = _make_adapter() + result, _ = _run_with_script(adapter, _handshake_lines()) assert result.status == "failed" assert "eof" in (result.error or "").lower() -class TestCodexApproval: - """Test tool_call approval handling.""" - - def _make_adapter(self, **kwargs): - from codeframe.core.adapters.codex import CodexAdapter +# ---------------------------------------------------------------------- +# Transport: the burst-then-silence and malformed-line regressions +# ---------------------------------------------------------------------- - with patch("shutil.which", return_value="/usr/bin/codex"): - return CodexAdapter(**kwargs) - def test_auto_approve_sends_approved(self) -> None: - adapter = self._make_adapter(approval_policy="auto") - events: list[AgentEvent] = [] +class TestCodexTransport: + def test_burst_ending_in_terminal_event_does_not_stall(self) -> None: + """A burst of lines followed by silence must be consumed, not stranded. - stdin = MagicMock() - event = {"method": "tool_call", "params": {"id": "tc-1", "name": "write_file"}} + The old adapter mixed ``select()`` on the raw fd with a buffered + ``TextIOWrapper``: once the burst was slurped into the Python buffer, + ``select`` reported "no data" and the run sat until the stall timeout. + """ + adapter = _make_adapter(stall_timeout_ms=2_000, read_timeout_ms=200) - adapter._handle_approval(stdin, event, on_event=events.append) - - # Verify approved message sent - written = stdin.write.call_args[0][0] - parsed = json.loads(written.strip()) - assert parsed["method"] == "tool_call/approved" - assert parsed["params"]["id"] == "tc-1" - - def test_auto_approve_emits_event(self) -> None: - adapter = self._make_adapter(approval_policy="auto") - events: list[AgentEvent] = [] + pipe = _PipeStdout() + burst = _handshake_lines() + [ + _notification("item/started", {"item": {"type": "reasoning", "id": f"i{i}"}}) + for i in range(50) + ] + burst.append(_turn_completed()) + pipe.write_lines(burst) + # Deliberately do NOT close: the process is alive and simply silent. stdin = MagicMock() - event = {"method": "tool_call", "params": {"id": "tc-1", "name": "run_command"}} + process = MagicMock() + process.stdin = stdin + process.stdout = pipe.reader + process.stderr = MagicMock() + process.stderr.read.return_value = "" + process.poll.return_value = None + + started = time.monotonic() + try: + with patch("subprocess.Popen", return_value=process): + with patch.object(adapter, "_detect_modified_files", return_value=[]): + result = adapter.run("task-1", "prompt", Path("/tmp/repo")) + finally: + pipe.close() - adapter._handle_approval(stdin, event, on_event=events.append) - assert any("auto-approved" in e.message.lower() for e in events) + elapsed = time.monotonic() - started + assert result.status == "completed" + assert elapsed < 2.0, f"stalled for {elapsed:.1f}s despite a terminal event" - def test_non_auto_policy_rejects_tool_call(self) -> None: - adapter = self._make_adapter(approval_policy="require") - events: list[AgentEvent] = [] + def test_malformed_line_is_skipped_not_fatal(self) -> None: + adapter = _make_adapter() + lines = _handshake_lines() + lines.append("2026-07-31T21:00:00Z INFO some non-JSON log line\n") + lines.append(_turn_completed()) - stdin = MagicMock() - event = {"method": "tool_call", "params": {"id": "tc-2", "name": "write_file"}} + result, _ = _run_with_script(adapter, lines) + assert result.status == "completed" - adapter._handle_approval(stdin, event, on_event=events.append) + def test_malformed_line_is_distinct_from_eof(self) -> None: + """A stray log line must not be reported as the process dying.""" + adapter = _make_adapter() + lines = _handshake_lines() + lines.append("garbage\n") - written = stdin.write.call_args[0][0] - parsed = json.loads(written.strip()) - assert parsed["method"] == "tool_call/rejected" - assert parsed["params"]["id"] == "tc-2" - assert any("rejected" in e.message.lower() for e in events) + result, _ = _run_with_script(adapter, lines) + # EOF still ends the run, but only because the stream actually ended. + assert result.status == "failed" + assert "eof" in (result.error or "").lower() -class TestCodexFullRun: - """Integration test: full run() with mock subprocess.""" +# ---------------------------------------------------------------------- +# Approvals — answered by request id +# ---------------------------------------------------------------------- - def test_successful_run(self) -> None: - from codeframe.core.adapters.codex import CodexAdapter - with patch("shutil.which", return_value="/usr/bin/codex"): - adapter = CodexAdapter() - - mock_process = MagicMock() - mock_process.stdin = MagicMock() - mock_process.stderr = MagicMock() - mock_process.stderr.read.return_value = "" - - # Script the stdout: handshake response + turn events - lines = [ - _make_jsonrpc("initialized", {"session_id": "s1"}), - _make_jsonrpc("session_started", {"session_id": "s1"}), - _make_jsonrpc("notification", {"message": "Editing file"}), - _make_jsonrpc("turn/completed", { - "usage": {"input_tokens": 500, "output_tokens": 200} - }), - ] - mock_process.stdout = _FakeStdout(lines) - mock_process.returncode = 0 - mock_process.poll.return_value = None - mock_process.wait.return_value = None +class TestCodexApproval: + def _approval_run(self, method: str, params: dict, **kwargs): + adapter = _make_adapter(**kwargs) + return _run_with_script( + adapter, + _handshake_lines() + [_server_request(77, method, params), _turn_completed()], + ) - events: list[AgentEvent] = [] + def test_command_approval_answered_by_request_id(self) -> None: + result, sent = self._approval_run( + "item/commandExecution/requestApproval", + {"threadId": "th-1", "turnId": "turn-1", "itemId": "i1", + "startedAtMs": 1, "command": "ls"}, + ) + assert result.status == "completed" + reply = next(m for m in sent if m.get("id") == 77 and "method" not in m) + assert reply["result"]["decision"] == "accept" - with patch("subprocess.Popen", return_value=mock_process): - with patch.object(adapter, "_detect_modified_files", return_value=["src/main.py"]): - result = adapter.run( - "task-1", "fix the bug", Path("/tmp/repo"), - on_event=events.append, - ) + def test_file_change_approval_answered_by_request_id(self) -> None: + _, sent = self._approval_run( + "item/fileChange/requestApproval", + {"threadId": "th-1", "turnId": "turn-1", "itemId": "i2", "startedAtMs": 1}, + ) + reply = next(m for m in sent if m.get("id") == 77 and "method" not in m) + assert reply["result"]["decision"] == "accept" + + def test_non_auto_policy_declines(self) -> None: + _, sent = self._approval_run( + "item/fileChange/requestApproval", + {"threadId": "th-1", "turnId": "turn-1", "itemId": "i2", "startedAtMs": 1}, + approval_policy="require", + ) + reply = next(m for m in sent if m.get("id") == 77 and "method" not in m) + assert reply["result"]["decision"] == "decline" + def test_unsupported_server_request_gets_error_reply(self) -> None: + """Never leave a server request unanswered — that hangs the turn.""" + result, sent = self._approval_run("attestation/generate", {"nonce": "x"}) assert result.status == "completed" - assert result.modified_files == ["src/main.py"] - assert result.token_usage is not None - assert result.token_usage.input_tokens == 500 + reply = next(m for m in sent if m.get("id") == 77 and "method" not in m) + assert reply["error"]["code"] == -32601 + + +# ---------------------------------------------------------------------- +# Contract: every outbound message validates against the generated schema +# ---------------------------------------------------------------------- + + +class TestCodexSchemaContract: + """AC #3 — outbound messages validated against the checked-in schema fixture.""" + + @staticmethod + def _validator(name: str): + jsonschema = pytest.importorskip("jsonschema") + schema = json.loads((FIXTURES / f"{name}.json").read_text()) + return jsonschema.Draft7Validator(schema) + + def _all_outbound(self) -> list[dict]: + adapter = _make_adapter() + lines = _handshake_lines() + [ + _server_request( + 55, + "item/commandExecution/requestApproval", + {"threadId": "th-1", "turnId": "turn-1", "itemId": "i1", + "startedAtMs": 1, "command": "ls"}, + ), + _server_request( + 56, + "item/fileChange/requestApproval", + {"threadId": "th-1", "turnId": "turn-1", "itemId": "i2", "startedAtMs": 1}, + ), + _turn_completed(), + ] + _, sent = _run_with_script(adapter, lines) + assert sent, "adapter wrote nothing" + return sent - def test_failed_run_handshake_fails(self) -> None: - from codeframe.core.adapters.codex import CodexAdapter + def test_requests_and_notifications_match_schema(self) -> None: + req_validator = self._validator("ClientRequest") + notif_validator = self._validator("ClientNotification") - with patch("shutil.which", return_value="/usr/bin/codex"): - adapter = CodexAdapter(read_timeout_ms=100) + for msg in self._all_outbound(): + if "method" not in msg: + continue # responses are covered by the next test + validator = req_validator if "id" in msg else notif_validator + errors = sorted(validator.iter_errors(msg), key=lambda e: e.path) + assert not errors, f"{msg['method']} violates schema: {errors[0].message}" - mock_process = MagicMock() - mock_process.stdin = MagicMock() - mock_process.stderr = MagicMock() - mock_process.stderr.read.return_value = "" - mock_process.stdout = _FakeStdout([]) # No handshake response - mock_process.returncode = None - mock_process.poll.return_value = None - mock_process.wait.return_value = None - mock_process.kill.return_value = None + def test_approval_responses_match_schema(self) -> None: + by_id = { + 55: self._validator("CommandExecutionRequestApprovalResponse"), + 56: self._validator("FileChangeRequestApprovalResponse"), + } + replies = [m for m in self._all_outbound() if "method" not in m and "result" in m] + assert len(replies) == 2 - with patch("subprocess.Popen", return_value=mock_process): - result = adapter.run("task-1", "fix the bug", Path("/tmp/repo")) + for reply in replies: + validator = by_id[reply["id"]] + errors = sorted(validator.iter_errors(reply["result"]), key=lambda e: e.path) + assert not errors, f"approval reply violates schema: {errors[0].message}" - assert result.status == "failed" - assert "handshake" in (result.error or "").lower() - def test_binary_not_found_during_execution(self) -> None: - from codeframe.core.adapters.codex import CodexAdapter +# ---------------------------------------------------------------------- +# Process lifecycle +# ---------------------------------------------------------------------- - with patch("shutil.which", return_value="/usr/bin/codex"): - adapter = CodexAdapter() +class TestCodexProcessErrors: + def test_binary_not_found_during_execution(self) -> None: + adapter = _make_adapter() with patch("subprocess.Popen", side_effect=FileNotFoundError("codex not found")): result = adapter.run("task-1", "fix the bug", Path("/tmp/repo")) assert result.status == "failed" assert "not found" in (result.error or "").lower() + def test_modified_files_are_detected(self) -> None: + adapter = _make_adapter() + pipe = _PipeStdout() + pipe.write_lines(_handshake_lines() + [_turn_completed()]) + pipe.close() -class TestCodexTokenExtraction: - """Test token usage extraction from Codex events.""" - - def _make_adapter(self): - from codeframe.core.adapters.codex import CodexAdapter - - with patch("shutil.which", return_value="/usr/bin/codex"): - return CodexAdapter() - - def test_extracts_tokens_from_usage(self) -> None: - adapter = self._make_adapter() - event = {"params": {"usage": {"input_tokens": 1000, "output_tokens": 500}}} - - input_t, output_t = adapter._extract_token_usage(event) - assert input_t == 1000 - assert output_t == 500 - - def test_returns_zero_on_missing_usage(self) -> None: - adapter = self._make_adapter() - event = {"params": {}} - - input_t, output_t = adapter._extract_token_usage(event) - assert input_t == 0 - assert output_t == 0 + process = MagicMock() + process.stdin = MagicMock() + process.stdout = pipe.reader + process.stderr = MagicMock() + process.stderr.read.return_value = "" + process.poll.return_value = None - def test_returns_zero_on_missing_params(self) -> None: - adapter = self._make_adapter() - event = {} + with patch("subprocess.Popen", return_value=process): + with patch.object(adapter, "_detect_modified_files", return_value=["src/a.py"]): + result = adapter.run("task-1", "prompt", Path("/tmp/repo")) - input_t, output_t = adapter._extract_token_usage(event) - assert input_t == 0 - assert output_t == 0 + assert result.modified_files == ["src/a.py"] diff --git a/uv.lock b/uv.lock index 9e53c75c..39f97544 100644 --- a/uv.lock +++ b/uv.lock @@ -636,6 +636,7 @@ dev = [ { name = "black" }, { name = "e2b" }, { name = "hypothesis" }, + { name = "jsonschema" }, { name = "mypy" }, { name = "pre-commit" }, { name = "pytest" }, @@ -670,6 +671,7 @@ requires-dist = [ { name = "hypothesis", marker = "extra == 'dev'", specifier = ">=6.0.0" }, { name = "idna", specifier = ">=3.15" }, { name = "jinja2", specifier = ">=3.1.6" }, + { name = "jsonschema", marker = "extra == 'dev'", specifier = ">=4.0.0" }, { name = "keyring", specifier = ">=24.0.0" }, { name = "mcp", specifier = ">=1.23.0" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.8.0" }, From fe93933442b20bbd924db58652873990535a4950 Mon Sep 17 00:00:00 2001 From: Frank Bria <136862992+frankbria@users.noreply.github.com> Date: Fri, 31 Jul 2026 21:25:54 -0700 Subject: [PATCH 2/2] fix(engines): surface a rejected turn/start instead of waiting out the timeouts (#914) codex review [P2]: a JSON-RPC error on the turn/start id was treated as a harmless late ack, so an invalid cwd/threadId or an auth/model refusal sat until the stall or turn timeout and lost the server's reason. Verified live: a bad threadId returns {"id":3,"error":{"code":-32600,...}} and no turn/completed ever follows. --- codeframe/core/adapters/codex.py | 32 ++++++++++++++++++++++++++----- tests/core/adapters/test_codex.py | 20 +++++++++++++++++++ 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/codeframe/core/adapters/codex.py b/codeframe/core/adapters/codex.py index a3a53153..cbb11d98 100644 --- a/codeframe/core/adapters/codex.py +++ b/codeframe/core/adapters/codex.py @@ -189,8 +189,10 @@ def _drain_stderr() -> None: reader = _MessageReader(process.stdout) try: thread_id = self._handshake(process.stdin, reader, workspace_path) - self._start_turn(process.stdin, thread_id, prompt, workspace_path) - result = self._stream_turn(reader, process.stdin, on_event=on_event) + turn_request_id = self._start_turn(process.stdin, thread_id, prompt, workspace_path) + result = self._stream_turn( + reader, process.stdin, turn_request_id=turn_request_id, on_event=on_event + ) except _ProtocolError as exc: result = AgentResult(status="failed", error=str(exc)) except Exception as exc: # unexpected — still report, never leak the process @@ -287,8 +289,14 @@ def _handshake(self, stdin: Any, reader: _MessageReader, workspace_path: Path) - raise _ProtocolError("Codex app-server returned no thread id from 'thread/start'") return thread_id - def _start_turn(self, stdin: Any, thread_id: str, prompt: str, workspace_path: Path) -> None: - """Send turn/start. The response is a plain ack; events arrive as notifications.""" + def _start_turn(self, stdin: Any, thread_id: str, prompt: str, workspace_path: Path) -> int: + """Send turn/start and return its request id. + + The success response is a plain ack (events arrive as notifications), + but a rejected turn — bad cwd, bad threadId, auth or model refusal — + comes back as a JSON-RPC error on this id and never produces a + ``turn/completed``, so the caller has to watch for it. + """ self._next_id += 1 self._send( stdin, @@ -302,6 +310,7 @@ def _start_turn(self, stdin: Any, thread_id: str, prompt: str, workspace_path: P }, }, ) + return self._next_id # ------------------------------------------------------------------ # Turn streaming @@ -312,6 +321,7 @@ def _stream_turn( reader: _MessageReader, stdin: Any, *, + turn_request_id: int | None = None, on_event: Callable[[AgentEvent], None] | None = None, ) -> AgentResult: """Consume notifications until ``turn/completed`` (or a timeout).""" @@ -359,7 +369,19 @@ def emit(type_: str, message: str, data: dict | None = None) -> None: if "id" in msg: if method: self._answer_server_request(stdin, msg, on_event=on_event) - continue # a late response to one of our requests: nothing to do + elif msg.get("id") == turn_request_id and "error" in msg: + # A rejected turn never emits turn/completed — surface the + # server's reason now instead of waiting out the timeouts. + error = msg["error"] or {} + return AgentResult( + status="failed", + output="\n".join(output_parts), + error=( + f"Codex app-server rejected 'turn/start': " + f"{error.get('message', error)} (code {error.get('code')})" + ), + ) + continue # any other response to one of our requests: nothing to do if method == "turn/completed": turn = params.get("turn") or {} diff --git a/tests/core/adapters/test_codex.py b/tests/core/adapters/test_codex.py index 28e19467..b0df4cad 100644 --- a/tests/core/adapters/test_codex.py +++ b/tests/core/adapters/test_codex.py @@ -327,6 +327,26 @@ def test_error_notification_is_reported(self) -> None: assert result.status == "failed" assert any(e.type == "error" for e in events) + def test_rejected_turn_start_fails_immediately(self) -> None: + """A rejected turn never emits turn/completed — don't wait out the timeouts. + + Verified against the real server: a bad threadId/cwd comes back as + ``{"id":3,"error":{"code":-32600,"message":"invalid thread id: ..."}}``. + """ + adapter = _make_adapter(stall_timeout_ms=30_000, turn_timeout_ms=30_000) + + started = time.monotonic() + result, _ = _run_with_script( + adapter, + _handshake_lines() + [_error_response(3, -32600, "invalid thread id")], + close_stdout=False, + ) + elapsed = time.monotonic() - started + + assert result.status == "failed" + assert "invalid thread id" in (result.error or "") + assert elapsed < 2.0, f"waited {elapsed:.1f}s for a rejection already on the wire" + def test_eof_before_terminal_event_fails(self) -> None: adapter = _make_adapter() result, _ = _run_with_script(adapter, _handshake_lines())