Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions apps/docs/content/docs/en/workflows/blocks/codex.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
---
title: Codex Coding Agent
description: Run OpenAI Codex in an isolated repository sandbox to produce an implementation plan or create a pull request.
---

import { Callout } from 'fumadocs-ui/components/callout'
import { BlockPreview } from '@/components/workflow-preview'

The **Codex Coding Agent** block runs a pinned OpenAI Codex CLI in an isolated E2B or Daytona sandbox. A logical agent can keep its native Codex thread and repository checkout across multiple blocks or loop rounds, inspect a GitHub repository and return a plan, or implement a task and maintain a pull request.

<BlockPreview type="codex" />

## Modes

### Plan

Plan clones the repository into a disposable checkout, removes the authenticated Git remote, and lets Codex read files, search, and run checks. It returns a Markdown plan and performs no commit, push, pull request, or other GitHub write.

Later turns for the same agent continue the native Codex thread in the same checkout, so one step can investigate and another can refine or challenge the plan without rebuilding context.

### Create PR

Create PR clones the selected base branch, asks Codex to edit the checkout, and waits for a successful `turn.completed` event. Sim then performs the credentialed delivery steps separately:

1. Verify that repository-local Git configuration did not change during the Codex turn.
2. Stage and commit the changes without an OpenAI or GitHub credential in scope.
3. Capture the changed files and a bounded unified diff.
4. Push the new branch with the GitHub token.
5. Create the pull request through Sim's GitHub integration.

If Codex makes no changes, the block returns successfully without pushing a branch or opening a pull request.

Later turns for the same agent continue editing the same local branch. Sim pushes the new commit to the existing branch and returns the existing pull request instead of creating another one.

## Agent instances and session reuse

Each block is its own agent by default. Repeating that block in a loop continues the same sandbox, checkout, and native Codex thread.

Choose an existing **Agent** when multiple Codex blocks should address one logical agent. The picker uses friendly labels such as Agent 1 and Agent 2; internal IDs are generated and managed automatically. Steps with the same agent share the instance and their turns run serially. Different agents create isolated instances and may run concurrently. Stable runtime configuration belongs to that logical Agent, so every step resolves the same mode, model, repository, and base branch.

Choose **New agent** to split a step into an independent sandbox and Codex thread. Copying a block also creates an independent agent by default. When several blocks that share an agent are copied together, the copied group keeps sharing with itself but not with the original group.

Agent instances are execution-scoped: Sim closes all of them when the uninterrupted workflow execution succeeds, fails, pauses, or is cancelled. A later independent workflow execution starts fresh. Durable reuse across independent executions requires a persistent runner and is not inferred from a thread ID alone, because Codex resume also requires its local rollout state.

## Configuration

Codex configuration is a sparse overlay, similar to a Kustomize patch. Resolution runs in this order:

1. Workspace profile
2. Workflow defaults
3. Agent settings
4. Step override

Only keys explicitly set at a layer are stored there; missing keys inherit. A Workspace change therefore reaches every Workflow, Agent, and Step that has not overridden that field. Sim freezes the resolved layers for an uninterrupted execution, so a settings edit cannot change an Agent halfway through a run.

- **Workspace profile** — shared defaults managed under **Settings → Codex**.
- **Workflow defaults / Agent settings** — opened from **Configure** below the Agent picker.
- **Task** — what Codex should plan or implement.
- **Agent** — choose a workflow agent to reuse, or create a new independent one. Sim manages its internal ID.
- **Mode / Model / Repository / Base Branch / Agent Shell Network** — stable layered settings, normally configured on the Agent or inherited from the Workflow and Workspace.
- **OpenAI API Key** — your key, entered on the block or stored as OpenAI BYOK. Sim never substitutes a hosted model key for this block.
- **GitHub Token** — clone access for Plan; clone, push, and pull-request write access for Create PR.
- **Reasoning Effort (Step Override)** *(advanced)* — `low`, `medium`, `high`, or `xhigh`; leave blank to inherit the Agent/Workflow/Workspace value.
- **Branch Name / Draft / PR Title / PR Body** *(advanced)* — optional step-local pull-request delivery settings, used in Create PR mode.

## Isolation

Every agent instance receives a private `CODEX_HOME`. Its rollout files are retained only while that workflow execution is active so later turns can use `codex exec resume`. The runtime ignores user config and execpolicy rules, disables hooks, plugins, apps, collaboration, skill discovery, and persisted goals, and runs with the `workspace-write` sandbox. Headless Codex runs never request approval. The shell environment is restricted so model-generated commands do not inherit `OPENAI_API_KEY`.

The GitHub token is present only during clone and push or in the host-side pull-request API call. It is never placed in the Codex process environment.

<Callout type="warn">
Repository contents are untrusted instructions. Keep **Agent Shell Network** off unless the task needs it, use narrowly scoped credentials, and review every generated pull request before merging.
</Callout>

The MVP does not resume Codex threads across independent workflow executions and does not expose Sim tools, MCP servers, plugins, hooks, or mid-turn human approval. Those capabilities require the persistent app-server runner planned for a later phase.

## Outputs

| Output | Description |
| --- | --- |
| `<codex.content>` | Final Codex message or Markdown plan |
| `<codex.model>` | Model selected for the run |
| `<codex.runStatus>` | Terminal status (`completed` for returned outputs; failures fail the block) |
| `<codex.agentId>` | Resolved logical agent instance ID |
| `<codex.sessionReused>` | Whether this turn continued an existing instance |
| `<codex.turnNumber>` | One-based turn number within the instance |
| `<codex.threadId>` | Native Codex thread ID resumed by later turns in this execution |
| `<codex.commands>` | Bounded command, patch, and tool summaries |
| `<codex.changedFiles>` | Files changed in Create PR mode |
| `<codex.diff>` | Bounded unified diff in Create PR mode |
| `<codex.branch>` | Branch pushed in Create PR mode |
| `<codex.prUrl>` | Pull request URL in Create PR mode |
| `<codex.tokens>` | Input, cache, output, and reasoning token counts |
| `<codex.cost>` | Sim-attributed model cost; zero because this block is BYOK-only |
| `<codex.providerTiming>` | Start time, end time, and duration |

## Self-hosted setup

Build the dedicated image after setting the provider API key:

```bash
bun run apps/sim/scripts/build-codex-e2b-template.ts --name sim-codex
bun run apps/sim/scripts/build-codex-daytona-snapshot.ts --name sim-codex:<tag>
```

For E2B, set `SANDBOX_PROVIDER=e2b`, `E2B_API_KEY`, and `E2B_CODEX_TEMPLATE_ID`. For Daytona, set `SANDBOX_PROVIDER=daytona`, `DAYTONA_API_KEY`, and `DAYTONA_CODEX_SNAPSHOT_ID`.

The image pins `@openai/codex@0.146.0`. Upgrade the package contract, JSONL fixtures, parser tests, and both provider images together.
3 changes: 3 additions & 0 deletions apps/sim/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,16 @@ NEXT_PUBLIC_APP_URL=http://localhost:3000
# E2B_FUNCTION_TEMPLATE_ID=<sim-function-template>:<sim-function-build-id> # Copy the exact ref printed by the builder
# E2B_FUNCTION_TEMPLATE_GENERATION=<release-epoch-ms> # Copy the monotonic generation printed by the builder
# MOTHERSHIP_E2B_TEMPLATE_ID= # Mothership shell template ref, required when Mothership runs code on E2B
# E2B_CODEX_TEMPLATE_ID=sim-codex # Build with apps/sim/scripts/build-codex-e2b-template.ts
# CODEX_SANDBOX_LIFETIME_MS= # Optional lower lifetime ceiling; values below 32 minutes are raised
#
# Daytona
# Build from an accepted E2B parity manifest with: bun run apps/sim/scripts/build-function-daytona-snapshot.ts --name <name> --parity-manifest <path>
# SANDBOX_PROVIDER=daytona
# DAYTONA_API_KEY=
# DAYTONA_FUNCTION_SNAPSHOT_ID=<snapshot-uuid> # Copy the immutable ID printed by the Daytona builder
# DAYTONA_SHELL_SNAPSHOT_ID= # Mothership shell snapshot ref, required when Mothership runs code on Daytona
# DAYTONA_CODEX_SNAPSHOT_ID=sim-codex:<tag> # Build with apps/sim/scripts/build-codex-daytona-snapshot.ts

# Security (Required)
ENCRYPTION_KEY=your_encryption_key # Use `openssl rand -hex 32` to generate, used to encrypt environment variables
Expand Down
84 changes: 84 additions & 0 deletions apps/sim/app/api/workflows/[id]/codex-config/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { db } from '@sim/db'
import { workflow } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { permissionSatisfies } from '@sim/platform-authz/workspace'
import { getErrorMessage } from '@sim/utils/errors'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { updateWorkflowCodexConfigContract } from '@/lib/api/contracts/codex-config'
import { parseRequest } from '@/lib/api/server'
import { getSession } from '@/lib/auth'
import { compactCodexWorkflowConfig, parseCodexWorkflowConfig } from '@/lib/codex/config'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'

const logger = createLogger('WorkflowCodexConfigAPI')

async function loadAuthorizedWorkflow(workflowId: string, requireWrite: boolean) {
const session = await getSession()
if (!session?.user?.id)
return { response: NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) }

const [row] = await db
.select({
id: workflow.id,
userId: workflow.userId,
workspaceId: workflow.workspaceId,
config: workflow.codexConfig,
})
.from(workflow)
.where(eq(workflow.id, workflowId))
.limit(1)
if (!row) return { response: NextResponse.json({ error: 'Workflow not found' }, { status: 404 }) }

if (!row.workspaceId) {
if (row.userId !== session.user.id) {
return { response: NextResponse.json({ error: 'Workflow not found' }, { status: 404 }) }
}
return { row, userId: session.user.id }
}

const permission = await getUserEntityPermissions(session.user.id, 'workspace', row.workspaceId)
if (!permission || (requireWrite && !permissionSatisfies(permission, 'write'))) {
return { response: NextResponse.json({ error: 'Workflow not found' }, { status: 404 }) }
}
return { row, userId: session.user.id }
}

export const GET = withRouteHandler(
async (_request: NextRequest, { params }: { params: Promise<{ id: string }> }) => {
const workflowId = (await params).id
const auth = await loadAuthorizedWorkflow(workflowId, false)
if (auth.response) return auth.response
return NextResponse.json({ config: parseCodexWorkflowConfig(auth.row.config) })
}
)

export const PUT = withRouteHandler(
async (request: NextRequest, context: { params: Promise<{ id: string }> }) => {
const workflowId = (await context.params).id
const auth = await loadAuthorizedWorkflow(workflowId, true)
if (auth.response) return auth.response

const parsed = await parseRequest(updateWorkflowCodexConfigContract, request, context)
if (!parsed.success) return parsed.response

try {
const config = compactCodexWorkflowConfig(parseCodexWorkflowConfig(parsed.data.body.config))
const [updated] = await db
.update(workflow)
.set({ codexConfig: config, updatedAt: new Date() })
.where(eq(workflow.id, workflowId))
.returning({ config: workflow.codexConfig })
if (!updated) return NextResponse.json({ error: 'Workflow not found' }, { status: 404 })
return NextResponse.json({ config: parseCodexWorkflowConfig(updated.config) })
} catch (error) {
logger.error('Failed to update workflow Codex configuration', {
workflowId,
userId: auth.userId,
error: getErrorMessage(error),
})
return NextResponse.json({ error: 'Failed to update Codex configuration' }, { status: 500 })
}
}
)
73 changes: 73 additions & 0 deletions apps/sim/app/api/workspaces/[id]/codex-config/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { db } from '@sim/db'
import { workspace } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { permissionSatisfies } from '@sim/platform-authz/workspace'
import { getErrorMessage } from '@sim/utils/errors'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { updateWorkspaceCodexConfigContract } from '@/lib/api/contracts/codex-config'
import { parseRequest } from '@/lib/api/server'
import { getSession } from '@/lib/auth'
import { parseCodexConfigPatch } from '@/lib/codex/config'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'

const logger = createLogger('WorkspaceCodexConfigAPI')

async function authorize(workspaceId: string, requireWrite: boolean) {
const session = await getSession()
if (!session?.user?.id)
return { response: NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) }

const permission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId)
if (!permission || (requireWrite && !permissionSatisfies(permission, 'write'))) {
return { response: NextResponse.json({ error: 'Workspace not found' }, { status: 404 }) }
}
return { userId: session.user.id }
}

export const GET = withRouteHandler(
async (_request: NextRequest, { params }: { params: Promise<{ id: string }> }) => {
const workspaceId = (await params).id
const auth = await authorize(workspaceId, false)
if (auth.response) return auth.response

const [row] = await db
.select({ config: workspace.codexConfig })
.from(workspace)
.where(eq(workspace.id, workspaceId))
.limit(1)
if (!row) return NextResponse.json({ error: 'Workspace not found' }, { status: 404 })

return NextResponse.json({ config: parseCodexConfigPatch(row.config) })
}
)

export const PUT = withRouteHandler(
async (request: NextRequest, context: { params: Promise<{ id: string }> }) => {
const workspaceId = (await context.params).id
const auth = await authorize(workspaceId, true)
if (auth.response) return auth.response

const parsed = await parseRequest(updateWorkspaceCodexConfigContract, request, context)
if (!parsed.success) return parsed.response

try {
const config = parseCodexConfigPatch(parsed.data.body.config)
const [updated] = await db
.update(workspace)
.set({ codexConfig: config, updatedAt: new Date() })
.where(eq(workspace.id, workspaceId))
.returning({ config: workspace.codexConfig })
if (!updated) return NextResponse.json({ error: 'Workspace not found' }, { status: 404 })
return NextResponse.json({ config: parseCodexConfigPatch(updated.config) })
} catch (error) {
logger.error('Failed to update workspace Codex configuration', {
workspaceId,
userId: auth.userId,
error: getErrorMessage(error),
})
return NextResponse.json({ error: 'Failed to update Codex configuration' }, { status: 500 })
}
}
)
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const WORKSPACE_SECTION_MAP: Partial<Record<SettingsSection, WorkspaceSettingsSe
secrets: 'secrets',
'credential-groups': 'credential-groups',
byok: 'byok',
codex: 'codex',
sandboxes: 'sandboxes',
'custom-tools': 'custom-tools',
mcp: 'mcp',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ const ApiKeys = dynamic(() =>
const BYOK = dynamic(() =>
import('@/app/workspace/[workspaceId]/settings/components/byok/byok').then((m) => m.BYOK)
)
const CodexSettings = dynamic(() =>
import('@/app/workspace/[workspaceId]/settings/components/codex/codex').then(
(m) => m.CodexSettings
)
)
const Forks = dynamic(() => import('@/ee/workspace-forking/components/forks').then((m) => m.Forks))
const Secrets = dynamic(() =>
import('@/app/workspace/[workspaceId]/settings/components/secrets/secrets').then((m) => m.Secrets)
Expand Down Expand Up @@ -201,6 +206,7 @@ export function SettingsPage({ section }: SettingsPageProps) {
<WhitelabelingSettings organizationId={organizationId} />
)}
{effectiveSection === 'byok' && <BYOK />}
{effectiveSection === 'codex' && <CodexSettings />}
{effectiveSection === 'sandboxes' && <Sandboxes />}
{effectiveSection === 'mcp' && <MCP />}
{effectiveSection === 'forks' && <Forks />}
Expand Down
Loading