Building on the Giant: SDKs, APIs, and What's Next

Standing on the Shoulders of Giants

We've climbed seven mountains together.

In Article 1, we saw the 10,000-foot view of the four buildings that make up Codex. In Article 2, we traced the nervous system: the message queues, the submission queue, and the event queue that keep everything alive. Article 3 dissected the brain itself, the orchestrator that reasons about code and makes decisions. Article 4 showed us how the sandbox keeps the dangerous parts locked up. Article 5 revealed the four frontends, the terminal executor, the MCP server, and the fplugin framework. Article 6 explored the tools: file operations, command execution, code generation, and memory. Article 7 taught us about the safety mechanisms: approval policies, audit logs, and security boundaries.

All of that power, the orchestration, the reasoning, the sandboxing, the tools, would be useless if it were locked away in a Rust binary that only spoke directly to a terminal or IDE.

This is where Building 3: The SDK comes in.

The SDKs are the translation layer. They take everything we've learned in the previous seven articles and package it into clean, language-specific APIs that developers can actually use. TypeScript developers get a JavaScript-native interface. Python developers get Pydantic models and async generators. Anyone can extend Codex with custom MCP servers.

In this final article, we're going to see how it all connects, how a developer uses the SDK to spin up a thread, send a prompt, listen to events, and process results. We'll build a small automation script together. And then we'll talk about what comes next: the future of an architecture this well-designed.



The TypeScript SDK: From High-Level API to Streaming Events

Let's start with concrete code. Here's the simplest way to use Codex from TypeScript:

import { Codex } from '@openai/codex';

const codex = new Codex({
  apiKey: process.env.OPENAI_API_KEY,
  baseUrl: 'https://api.openai.com/v1',
});

const thread = codex.startThread();
const result = await thread.run("Add a login form to my React app");

console.log(result.finalResponse);
result.items.forEach(item => {
  console.log(`[${item.type}]`, item);
});

This looks simple from the outside. But it's worth understanding what's happening underneath.

The Class Hierarchy

The Codex class is the entry point. When you construct it, it creates a CodexExec instance, a wrapper around the actual CLI binary. The CLI binary lives in an npm package (@openai/codex-darwin-arm64, @openai/codex-linux-x64, etc.) that contains the compiled Rust app-server for your platform.

export class Codex {
  private exec: CodexExec;

  constructor(options: CodexOptions = {}) {
    const { codexPathOverride, env, config } = options;
    this.exec = new CodexExec(codexPathOverride, env, config);
    this.options = options;
  }

  startThread(options: ThreadOptions = {}): Thread {
    return new Thread(this.exec, this.options, options);
  }

  resumeThread(id: string, options: ThreadOptions = {}): Thread {
    return new Thread(this.exec, this.options, options, id);
  }
}

When you call startThread() or resumeThread(), you get a Thread object. This is where the real action happens.

Thread: The Workhorse

The Thread class has two primary methods:

  1. run(input, options) : Send a prompt and wait for a complete result. Useful for synchronous workflows.
  2. runStreamed(input, options) : Send a prompt and listen to events as they stream in. Useful for progress updates and real-time monitoring.

Here's what runStreamed() looks like:

async runStreamed(
  input: Input,
  turnOptions: TurnOptions = {}
): Promise<StreamedTurn> {
  return { events: this.runStreamedInternal(input, turnOptions) };
}

private async *runStreamedInternal(
  input: Input,
  turnOptions: TurnOptions = {},
): AsyncGenerator<ThreadEvent> {
  // Create temporary output schema file if needed
  const { schemaPath, cleanup } = await createOutputSchemaFile(
    turnOptions.outputSchema
  );

  // Normalize the input (text and/or images)
  const { prompt, images } = normalizeInput(input);

  // Spawn the CLI with `exec --experimental-json`
  const generator = this._exec.run({
    input: prompt,
    threadId: this._id,
    images,
    model: options?.model,
    sandboxMode: options?.sandboxMode,
    // ... more options
  });

  try {
    for await (const item of generator) {
      let parsed: ThreadEvent;
      try {
        parsed = JSON.parse(item) as ThreadEvent;
      } catch (error) {
        throw new Error(`Failed to parse item: ${item}`, { cause: error });
      }

      // Capture the thread ID from the first event
      if (parsed.type === "thread.started") {
        this._id = parsed.thread_id;
      }

      yield parsed;
    }
  } finally {
    await cleanup();
  }
}

The magic is in the spawning. When you call this._exec.run(), it spawns a subprocess:

codex exec --experimental-json \
  --model gpt-4o \
  --config approval_policy=auto \
  --output-schema /tmp/schema.json \
  "Add a login form to my React app"

The --experimental-json flag tells the CLI to emit JSONL (JSON Lines) format instead of text. Each line is a complete JSON object representing an event.

Event Types and Item Types

Every event that comes back is strongly typed. Here's the event union:

export type ThreadEvent =
  | ThreadStartedEvent
  | TurnStartedEvent
  | TurnCompletedEvent
  | TurnFailedEvent
  | ItemStartedEvent
  | ItemUpdatedEvent
  | ItemCompletedEvent
  | ThreadErrorEvent;

And here's what you'll see inside the ItemStartedEvent:

export type ThreadItem =
  | AgentMessageItem
  | ReasoningItem
  | CommandExecutionItem
  | FileChangeItem
  | McpToolCallItem
  | WebSearchItem
  | TodoListItem
  | ErrorItem;

Each item type carries detailed information. A CommandExecutionItem includes the command line, stdout/stderr, and exit code. A FileChangeItem includes the list of file paths that changed and whether the patch succeeded. An AgentMessageItem contains the final response text or structured output.

Building a Simple Automation Script

Let's build a practical example: a script that watches for TODO comments in code and auto-fixes them.

import { Codex } from '@openai/codex';
import * as fs from 'fs/promises';

async function fixTodos() {
  const codex = new Codex();

  // Find all TODO comments in the codebase
  const todoPattern = /TODO: (.+?)$/gm;
  const files = await fs.readdir('.', { recursive: true });
  const todos: { file: string; comment: string }[] = [];

  for (const file of files) {
    if (file.includes('node_modules')) continue;
    try {
      const content = await fs.readFile(file, 'utf-8');
      let match;
      while ((match = todoPattern.exec(content)) !== null) {
        todos.push({ file, comment: match[1] });
      }
    } catch {
      // Not a text file, skip
    }
  }

  if (todos.length === 0) {
    console.log('No TODOs found');
    return;
  }

  // Create a summary for Codex
  const prompt = `
I found ${todos.length} TODO comments in the codebase:
${todos.map(t => `- ${t.file}: ${t.comment}`).join('\n')}

Please fix these issues one by one. For each TODO:
1. Read the file
2. Understand the issue
3. Implement a fix
4. Test the changes
5. Move to the next TODO
  `.trim();

  console.log('Starting Codex thread...');
  const thread = codex.startThread({
    workingDirectory: process.cwd(),
    sandboxMode: 'write',
  });

  // Listen to events
  const result = await thread.runStreamed(prompt);

  let todoListItem: any = null;
  let fileChanges = 0;

  for await (const event of result.events) {
    switch (event.type) {
      case 'item.started':
        if (event.item.type === 'command_execution') {
          console.log(`Running: ${event.item.command}`);
        }
        if (event.item.type === 'todo_list') {
          console.log('\nTodo list updated:');
        }
        break;

      case 'item.updated':
        if (event.item.type === 'command_execution') {
          console.log(`Output: ${event.item.aggregated_output.slice(0, 100)}...`);
        }
        if (event.item.type === 'todo_list') {
          todoListItem = event.item;
        }
        break;

      case 'item.completed':
        if (event.item.type === 'file_change') {
          fileChanges++;
          console.log(`File changes: ${fileChanges}`);
        }
        break;

      case 'turn.completed':
        console.log(`\nTurn completed. Tokens used: ${event.usage.input_tokens} in, ${event.usage.output_tokens} out`);
        break;

      case 'error':
        console.error(`Error: ${event.message}`);
        break;
    }
  }

  if (todoListItem) {
    console.log('\nFinal progress:');
    todoListItem.items.forEach((item: any) => {
      console.log(`${item.completed ? '✓' : '○'} ${item.text}`);
    });
  }
}

fixTodos().catch(console.error);

This script demonstrates the power of the streaming API: you can watch progress in real-time, update a UI, or make decisions based on what's happening.


The Python SDK: JSON-RPC Over Stdio

The Python SDK takes a different approach. Instead of spawning the CLI with --experimental-json, the Python SDK launches the app-server binary directly and communicates with it via JSON-RPC 2.0 over stdio.

Why the difference? The Python SDK is designed for deeper integration, it can send multiple turns to the same thread without respawning the process, and it has access to lower-level primitives like approval handlers.


The Client Architecture

from codex_app_server import Codex

# Use as a context manager
with Codex() as codex:
    thread = codex.thread_start()

    # Send a turn
    turn_result = codex.thread_turn(
        thread_id=thread.thread_id,
        input="Fix this bug in my Python code",
    )

    # Process the result
    for item in turn_result.items:
        if item.type == "command_execution":
            print(f"Command: {item.command}")
            print(f"Exit code: {item.exit_code}")
        elif item.type == "agent_message":
            print(f"Response: {item.text}")

The Python SDK uses Pydantic models (auto-generated from the schema in /codex-rs/app-server-protocol/). This means you get full type hints and validation automatically.

Here's what's happening under the hood:

  1. The Codex() context manager spawns a subprocess running the app-server binary
  2. It opens stdin and stdout pipes to that process
  3. All communication uses JSON-RPC 2.0 messages
  4. When you call thread_start(), it sends an RPC message like:
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "thread/start",
  "params": {
    "model": "gpt-4o"
  }
}
  1. The app-server processes it, orchestrates a new thread, and streams back notifications
  2. The Python SDK buffers these and returns them as Python objects

Auto-Generated Types from the Protocol

The Python SDK is built on a foundation of protocol definitions. In /codex-rs/app-server-protocol/schema/v2.json, there's a complete OpenAPI/JSON Schema definition of all types.

A build script reads this schema and generates:

  • v2_types.py: All the data types as Pydantic models
  • v2_all.py: Convenience imports
  • notification_registry.py: A mapping of notification type names to their Python classes

This means when the app-server sends back a notification like:

{
  "method": "notification/item_completed",
  "params": {
    "item": {
      "type": "file_change",
      "id": "item-123",
      "changes": [{"path": "main.py", "kind": "update"}],
      "status": "completed"
    }
  }
}

The Python SDK automatically deserializes it into a FileChangeItem object with full type safety.

A Real Example: Batch Code Generation

Let's build a script that reads a CSV of feature requests and generates code for each:

from codex_app_server import Codex, ThreadStartParams
import csv
import json

def process_features(csv_file: str):
    """Process a CSV of feature requests and generate code for each."""

    features = []
    with open(csv_file) as f:
        reader = csv.DictReader(f)
        features = list(reader)

    results = {}

    with Codex() as codex:
        for feature in features:
            feature_id = feature['id']
            description = feature['description']
            priority = feature['priority']

            print(f"\nProcessing feature {feature_id}: {description}")

            # Start a new thread for each feature
            thread = codex.thread_start(
                ThreadStartParams(
                    model='gpt-4o',
                    sandbox_mode='write',
                )
            )

            # Build the prompt
            prompt = f"""
Feature ID: {feature_id}
Priority: {priority}
Description: {description}

Please implement this feature. When done:
1. Write the code
2. Add tests
3. Update documentation
4. Commit the changes with a clear message
            """.strip()

            # Send the turn
            turn = codex.thread_turn(
                thread_id=thread.thread_id,
                input=prompt,
            )

            # Extract results
            result = {
                'feature_id': feature_id,
                'files_modified': 0,
                'commands_run': 0,
                'final_response': '',
                'errors': [],
            }

            for item in turn.items:
                if item.type == 'file_change':
                    result['files_modified'] += len(item.changes)
                elif item.type == 'command_execution':
                    result['commands_run'] += 1
                    if item.exit_code != 0:
                        result['errors'].append(item.aggregated_output)
                elif item.type == 'agent_message':
                    result['final_response'] = item.text

            results[feature_id] = result

            # Print summary
            print(f"  Files modified: {result['files_modified']}")
            print(f"  Commands run: {result['commands_run']}")
            if result['errors']:
                print(f"  Errors: {len(result['errors'])}")

    # Output results as JSON
    with open('results.json', 'w') as f:
        json.dump(results, f, indent=2)

    print(f"\nProcessed {len(features)} features. Results saved to results.json")

if __name__ == '__main__':
    process_features('features.csv')

This script demonstrates several key features of the Python SDK:

  • Persistent connection: The Codex() context manager keeps a single app-server process alive
  • Multiple threads: Each feature gets its own thread, but they share the same app-server
  • Type-safe models: The turn result automatically has all the items with proper types
  • Structured data: You can easily extract metrics and results from the items

Extending Codex with Custom MCP Servers

Now here's where it gets really powerful. The architecture isn't just about using Codex as-is. It's about extending it.

MCP (Model Context Protocol) is OpenAI's standard for tools and resources. If you want Codex to do something special, call your internal APIs, query your database, interact with your CI/CD system, you build an MCP server for it.

Anatomy of a Custom MCP Server

An MCP server is a subprocess that:

  1. Speaks JSON-RPC 2.0 over stdio
  2. Implements the MCP protocol (list tools, call tools, etc.)
  3. Connects to Codex's agent process

Here's a minimal example: an MCP server that queries your company's database.

Recommended by LinkedIn

from mcp.server import Server
from mcp.types import Tool, TextContent
import sqlite3

server = Server("company-db-server")

@server.call_tool()
async def query_database(query: str) -> str:
    """Execute a SQL query against the company database."""
    try:
        conn = sqlite3.connect('/company/db.sqlite')
        cursor = conn.cursor()

        # Safety: only allow SELECT queries
        if not query.strip().upper().startswith('SELECT'):
            raise ValueError("Only SELECT queries are allowed")

        cursor.execute(query)
        rows = cursor.fetchall()
        conn.close()

        return json.dumps(rows)
    except Exception as e:
        return json.dumps({"error": str(e)})

@server.list_tools()
async def list_tools():
    return [
        Tool(
            name="query_database",
            description="Execute a SELECT query against the company database",
            input_schema={
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "SQL SELECT query"}
                },
                "required": ["query"]
            }
        )
    ]

if __name__ == "__main__":
    server.run()

Then you register it with Codex:

codex --mcp-server company-db-server="python /path/to/mcp_server.py"

Now, when Codex encounters a task that needs to query the database, it can call your MCP server. The sandboxing still applies, the MCP server runs in a controlled environment, and its outputs are validated.

You can build MCP servers for:

  • Internal APIs: Product management systems, customer databases, billing systems
  • Specialized tools: Image generation, ML model inference, video processing
  • Domain-specific knowledge: Your company's conventions, frameworks, or standards
  • Secrets management: Securely passing credentials to tools without exposing them in the filesystem

Architecture Patterns Worth Stealing

Over the course of these eight articles, we've seen some genuinely clever design patterns. Even if you never touch Codex's code, these patterns are worth understanding, they'll make you a better architect.


1. SQ/EQ Decoupling: Separation of Concerns

The Submission Queue and Event Queue are separate. When you submit a prompt, it doesn't immediately execute. The orchestrator:

  1. Accepts the submission
  2. Breaks it into logical steps
  3. Executes those steps one by one
  4. Emits events for each step

This is not the classic request-response pattern. It's more like an async state machine. The benefits:

  • Pausable workflows: You can pause a long-running task and resume it later
  • Observable execution: Every step generates an event, so you know exactly what's happening
  • Recoverable: If something fails, you can see the failure event and retry
  • Nestable: A step can itself spawn sub-steps with their own events

If you're building complex workflows or orchestration systems, this pattern is gold.

2. Platform-Specific Sandbox Abstraction

Codex runs on macOS, Linux, and Windows. Each platform has different sandboxing capabilities:

  • Linux: Uses namespaces and seccomp (best-in-class security)
  • macOS: Uses sandbox profiles (good, but less granular)
  • Windows: Uses job objects and capability restrictions (emerging)

The code doesn't have platform-specific conditionals scattered everywhere. Instead, there's a Sandbox trait that different platforms implement. The orchestrator talks to the trait, not the implementation.

pub trait Sandbox {
  fn execute(&self, command: &str) -> Result<CommandOutput>;
  fn read_file(&self, path: &Path) -> Result<Vec<u8>>;
  fn write_file(&self, path: &Path, contents: &[u8]) -> Result<()>;
}

impl Sandbox for LinuxSandbox { /* ... */ }
impl Sandbox for MacOSSandbox { /* ... */ }
impl Sandbox for WindowsSandbox { /* ... */ }

This is the Adapter pattern in action. If you're building cross-platform systems, this approach prevents your codebase from becoming a maze of #[cfg(...)] attributes.

3. Policy-as-Code: ExecPolicy

The approval policy isn't hard-coded. It's a data structure that the orchestrator evaluates at decision points.

An ExecPolicy can say things like:

  • "Auto-approve all file reads, but require human approval for file writes"
  • "Auto-approve commands in specific directories, but block command execution outside them"
  • "Log all network access but allow it"

At runtime, when the orchestrator reaches a decision point ("Should I execute this command?"), it evaluates the policy. The policy can be configured via CLI flags, config files, or environment variables.

This is declarative security. It's easier to audit than code, easier to override for testing, and easier to update without recompiling.

4. Hexagonal Architecture: Ports and Adapters

Codex's codebase is organized around the hexagon:

  • Core domain (center): The orchestrator, the AI reasoning, the algorithms
  • Ports (interfaces): What the core needs from the outside (a way to read files, a way to call APIs, etc.)
  • Adapters (implementations): Different ways to fulfill the ports

For example:

  • Port: "I need to execute a shell command"
  • Adapters: Linux implementation (uses seccomp), macOS implementation (uses sandbox profile), and a test mock (returns canned responses)

The core orchestrator doesn't care which adapter is plugged in. This makes the code:

  • Testable: Plug in a mock adapter for unit tests
  • Portable: Add a new platform by adding a new adapter
  • Focused: The core logic isn't tangled with implementation details

The Series Retrospective: What We've Built

Let's take a step back. Over eight articles, what have we actually learned?

Article 1: The 10,000-Foot View introduced you to Codex as a concept and showed you the four buildings.

Article 2: The Nervous System taught you about the Submission Queue and Event Queue, the message-passing backbone that keeps everything alive.

Article 3: The Brain dissected the orchestrator itself, how it reasons about code, makes decisions, and coordinates everything.

Article 4: The Vault showed you the sandboxing layer, how dangerous operations are isolated, and what happens inside a sandbox.

Article 5: Four Windows revealed the different frontends, how you can interact with Codex via terminal, headless execution, MCP server, or IDE plugins.

Article 6: The Swiss Army Knife explored the tools: reading and writing files, executing commands, calling the AI, web search, and memory management.

Article 7: The Safety Net deep-dived into safety mechanisms: approval policies, audit logging, and the constraints that keep everything secure.

Article 8: Building on the Giant (this article) showed you the SDKs and how to extend Codex with custom MCP servers.

Together, these eight articles form a complete mental model of how a sophisticated AI coding agent works.

You understand:

  • Why it's built in Rust (performance and safety)
  • How it communicates internally (message queues and events)
  • How it makes decisions (orchestration and state machines)
  • How it stays secure (sandboxing and approval policies)
  • How you can use it (SDKs and APIs)
  • How you can extend it (MCP servers)

The Next Frontier: What Comes Next

Codex is built on a foundation that's designed to scale. The architecture isn't just "here's an AI coding agent." It's "here's a pattern for building trustworthy AI systems that work locally on your machine."

What comes next?

Better reasoning models. As language models improve, not just in scale, but in reasoning capability, Codex will be able to tackle harder problems. The orchestration layer is already there to handle more complex decision-making.

Richer tool ecosystems. The MCP standard is extensible. Over time, we'll see thousands of MCP servers built by the community. Your codebase will be able to integrate with everything: your company's internal tools, open-source services, specialized domain tools.

Multi-agent coordination. What happens when you have multiple AI agents working together? The same queue-based architecture that coordinates a single agent's actions can coordinate multiple agents. Imagine a code generation agent, a testing agent, and a documentation agent working in concert.

Fine-tuning and specialization. The architecture is designed so that the core reasoning engine can be swapped out. Need a model specialized for your domain? Plug it in. Need offline capability? Use a smaller model locally.

Formal verification. Some tasks don't need an AI agent; they need a verified algorithm. The same approval policies that let humans check AI-generated code can let formal verification systems check it too.

Decentralized computing. The architecture is designed for a single machine, but it could scale to a network. Imagine spawning sandboxed execution environments across multiple machines, with the orchestrator coordinating them.

The beautiful thing about a well-designed architecture is that it's forward-compatible. Codex's core patterns, message queues, sandboxing, policy evaluation, modular adapters, don't depend on any specific technology. As the AI landscape evolves, the architecture will evolve with it.


A Satisfying Conclusion (Not an Ending)

We began this series with a simple image: "an incredibly talented software engineer in a padded room."

By now, you understand what that really means. It's not magic. It's careful orchestration. It's a Rust core that reasons about code and plans actions. It's a queue that serializes requests and events. It's a sandbox that isolates dangerous operations. It's approval policies that let humans stay in control. It's SDKs that make it accessible. It's MCP servers that extend it.

Codex CLI is open-source. The code is on GitHub. You can read it, understand it, modify it, and build on it. Some of you will. You'll add support for new languages, new tools, new frontends. You'll integrate it into your IDEs, your CI/CD pipelines, your internal tools.

That's the real gift of an architecture this well-designed: it doesn't try to solve everything. It solves one thing (orchestrating a sophisticated AI agent) in a way that's extensible, understandable, and trustworthy.

The future of AI coding isn't in cloud-hosted black boxes. It's in local, transparent systems that you understand and control.

And now, you do.

Originally published on LinkedIn.