Skip to content

Commit fa1b75c

Browse files
committed
feat(codex): add reusable coding agent workflows
1 parent a042b8f commit fa1b75c

97 files changed

Lines changed: 27850 additions & 60 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
---
2+
title: Codex Coding Agent
3+
description: Run OpenAI Codex in an isolated repository sandbox to produce an implementation plan or create a pull request.
4+
---
5+
6+
import { Callout } from 'fumadocs-ui/components/callout'
7+
import { BlockPreview } from '@/components/workflow-preview'
8+
9+
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.
10+
11+
<BlockPreview type="codex" />
12+
13+
## Modes
14+
15+
### Plan
16+
17+
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.
18+
19+
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.
20+
21+
### Create PR
22+
23+
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:
24+
25+
1. Verify that repository-local Git configuration did not change during the Codex turn.
26+
2. Stage and commit the changes without an OpenAI or GitHub credential in scope.
27+
3. Capture the changed files and a bounded unified diff.
28+
4. Push the new branch with the GitHub token.
29+
5. Create the pull request through Sim's GitHub integration.
30+
31+
If Codex makes no changes, the block returns successfully without pushing a branch or opening a pull request.
32+
33+
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.
34+
35+
## Agent instances and session reuse
36+
37+
Each block is its own agent by default. Repeating that block in a loop continues the same sandbox, checkout, and native Codex thread.
38+
39+
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.
40+
41+
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.
42+
43+
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.
44+
45+
## Configuration
46+
47+
Codex configuration is a sparse overlay, similar to a Kustomize patch. Resolution runs in this order:
48+
49+
1. Workspace profile
50+
2. Workflow defaults
51+
3. Agent settings
52+
4. Step override
53+
54+
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.
55+
56+
- **Workspace profile** — shared defaults managed under **Settings → Codex**.
57+
- **Workflow defaults / Agent settings** — opened from **Configure** below the Agent picker.
58+
- **Task** — what Codex should plan or implement.
59+
- **Agent** — choose a workflow agent to reuse, or create a new independent one. Sim manages its internal ID.
60+
- **Mode / Model / Repository / Base Branch / Agent Shell Network** — stable layered settings, normally configured on the Agent or inherited from the Workflow and Workspace.
61+
- **OpenAI API Key** — your key, entered on the block or stored as OpenAI BYOK. Sim never substitutes a hosted model key for this block.
62+
- **GitHub Token** — clone access for Plan; clone, push, and pull-request write access for Create PR.
63+
- **Reasoning Effort (Step Override)** *(advanced)*`low`, `medium`, `high`, or `xhigh`; leave blank to inherit the Agent/Workflow/Workspace value.
64+
- **Branch Name / Draft / PR Title / PR Body** *(advanced)* — optional step-local pull-request delivery settings, used in Create PR mode.
65+
66+
## Isolation
67+
68+
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`.
69+
70+
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.
71+
72+
<Callout type="warn">
73+
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.
74+
</Callout>
75+
76+
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.
77+
78+
## Outputs
79+
80+
| Output | Description |
81+
| --- | --- |
82+
| `<codex.content>` | Final Codex message or Markdown plan |
83+
| `<codex.model>` | Model selected for the run |
84+
| `<codex.runStatus>` | Terminal status (`completed` for returned outputs; failures fail the block) |
85+
| `<codex.agentId>` | Resolved logical agent instance ID |
86+
| `<codex.sessionReused>` | Whether this turn continued an existing instance |
87+
| `<codex.turnNumber>` | One-based turn number within the instance |
88+
| `<codex.threadId>` | Native Codex thread ID resumed by later turns in this execution |
89+
| `<codex.commands>` | Bounded command, patch, and tool summaries |
90+
| `<codex.changedFiles>` | Files changed in Create PR mode |
91+
| `<codex.diff>` | Bounded unified diff in Create PR mode |
92+
| `<codex.branch>` | Branch pushed in Create PR mode |
93+
| `<codex.prUrl>` | Pull request URL in Create PR mode |
94+
| `<codex.tokens>` | Input, cache, output, and reasoning token counts |
95+
| `<codex.cost>` | Sim-attributed model cost; zero because this block is BYOK-only |
96+
| `<codex.providerTiming>` | Start time, end time, and duration |
97+
98+
## Self-hosted setup
99+
100+
Build the dedicated image after setting the provider API key:
101+
102+
```bash
103+
bun run apps/sim/scripts/build-codex-e2b-template.ts --name sim-codex
104+
bun run apps/sim/scripts/build-codex-daytona-snapshot.ts --name sim-codex:<tag>
105+
```
106+
107+
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`.
108+
109+
The image pins `@openai/codex@0.146.0`. Upgrade the package contract, JSONL fixtures, parser tests, and both provider images together.

apps/sim/.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,16 @@ NEXT_PUBLIC_APP_URL=http://localhost:3000
4242
# E2B_FUNCTION_TEMPLATE_ID=<sim-function-template>:<sim-function-build-id> # Copy the exact ref printed by the builder
4343
# E2B_FUNCTION_TEMPLATE_GENERATION=<release-epoch-ms> # Copy the monotonic generation printed by the builder
4444
# MOTHERSHIP_E2B_TEMPLATE_ID= # Mothership shell template ref, required when Mothership runs code on E2B
45+
# E2B_CODEX_TEMPLATE_ID=sim-codex # Build with apps/sim/scripts/build-codex-e2b-template.ts
46+
# CODEX_SANDBOX_LIFETIME_MS= # Optional lower lifetime ceiling; values below 32 minutes are raised
4547
#
4648
# Daytona
4749
# Build from an accepted E2B parity manifest with: bun run apps/sim/scripts/build-function-daytona-snapshot.ts --name <name> --parity-manifest <path>
4850
# SANDBOX_PROVIDER=daytona
4951
# DAYTONA_API_KEY=
5052
# DAYTONA_FUNCTION_SNAPSHOT_ID=<snapshot-uuid> # Copy the immutable ID printed by the Daytona builder
5153
# DAYTONA_SHELL_SNAPSHOT_ID= # Mothership shell snapshot ref, required when Mothership runs code on Daytona
54+
# DAYTONA_CODEX_SNAPSHOT_ID=sim-codex:<tag> # Build with apps/sim/scripts/build-codex-daytona-snapshot.ts
5255

5356
# Security (Required)
5457
ENCRYPTION_KEY=your_encryption_key # Use `openssl rand -hex 32` to generate, used to encrypt environment variables
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { db } from '@sim/db'
2+
import { workflow } from '@sim/db/schema'
3+
import { createLogger } from '@sim/logger'
4+
import { permissionSatisfies } from '@sim/platform-authz/workspace'
5+
import { getErrorMessage } from '@sim/utils/errors'
6+
import { eq } from 'drizzle-orm'
7+
import { type NextRequest, NextResponse } from 'next/server'
8+
import { updateWorkflowCodexConfigContract } from '@/lib/api/contracts/codex-config'
9+
import { parseRequest } from '@/lib/api/server'
10+
import { getSession } from '@/lib/auth'
11+
import { compactCodexWorkflowConfig, parseCodexWorkflowConfig } from '@/lib/codex/config'
12+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
13+
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
14+
15+
const logger = createLogger('WorkflowCodexConfigAPI')
16+
17+
async function loadAuthorizedWorkflow(workflowId: string, requireWrite: boolean) {
18+
const session = await getSession()
19+
if (!session?.user?.id)
20+
return { response: NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) }
21+
22+
const [row] = await db
23+
.select({
24+
id: workflow.id,
25+
userId: workflow.userId,
26+
workspaceId: workflow.workspaceId,
27+
config: workflow.codexConfig,
28+
})
29+
.from(workflow)
30+
.where(eq(workflow.id, workflowId))
31+
.limit(1)
32+
if (!row) return { response: NextResponse.json({ error: 'Workflow not found' }, { status: 404 }) }
33+
34+
if (!row.workspaceId) {
35+
if (row.userId !== session.user.id) {
36+
return { response: NextResponse.json({ error: 'Workflow not found' }, { status: 404 }) }
37+
}
38+
return { row, userId: session.user.id }
39+
}
40+
41+
const permission = await getUserEntityPermissions(session.user.id, 'workspace', row.workspaceId)
42+
if (!permission || (requireWrite && !permissionSatisfies(permission, 'write'))) {
43+
return { response: NextResponse.json({ error: 'Workflow not found' }, { status: 404 }) }
44+
}
45+
return { row, userId: session.user.id }
46+
}
47+
48+
export const GET = withRouteHandler(
49+
async (_request: NextRequest, { params }: { params: Promise<{ id: string }> }) => {
50+
const workflowId = (await params).id
51+
const auth = await loadAuthorizedWorkflow(workflowId, false)
52+
if (auth.response) return auth.response
53+
return NextResponse.json({ config: parseCodexWorkflowConfig(auth.row.config) })
54+
}
55+
)
56+
57+
export const PUT = withRouteHandler(
58+
async (request: NextRequest, context: { params: Promise<{ id: string }> }) => {
59+
const workflowId = (await context.params).id
60+
const auth = await loadAuthorizedWorkflow(workflowId, true)
61+
if (auth.response) return auth.response
62+
63+
const parsed = await parseRequest(updateWorkflowCodexConfigContract, request, context)
64+
if (!parsed.success) return parsed.response
65+
66+
try {
67+
const config = compactCodexWorkflowConfig(parseCodexWorkflowConfig(parsed.data.body.config))
68+
const [updated] = await db
69+
.update(workflow)
70+
.set({ codexConfig: config, updatedAt: new Date() })
71+
.where(eq(workflow.id, workflowId))
72+
.returning({ config: workflow.codexConfig })
73+
if (!updated) return NextResponse.json({ error: 'Workflow not found' }, { status: 404 })
74+
return NextResponse.json({ config: parseCodexWorkflowConfig(updated.config) })
75+
} catch (error) {
76+
logger.error('Failed to update workflow Codex configuration', {
77+
workflowId,
78+
userId: auth.userId,
79+
error: getErrorMessage(error),
80+
})
81+
return NextResponse.json({ error: 'Failed to update Codex configuration' }, { status: 500 })
82+
}
83+
}
84+
)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { db } from '@sim/db'
2+
import { workspace } from '@sim/db/schema'
3+
import { createLogger } from '@sim/logger'
4+
import { permissionSatisfies } from '@sim/platform-authz/workspace'
5+
import { getErrorMessage } from '@sim/utils/errors'
6+
import { eq } from 'drizzle-orm'
7+
import { type NextRequest, NextResponse } from 'next/server'
8+
import { updateWorkspaceCodexConfigContract } from '@/lib/api/contracts/codex-config'
9+
import { parseRequest } from '@/lib/api/server'
10+
import { getSession } from '@/lib/auth'
11+
import { parseCodexConfigPatch } from '@/lib/codex/config'
12+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
13+
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
14+
15+
const logger = createLogger('WorkspaceCodexConfigAPI')
16+
17+
async function authorize(workspaceId: string, requireWrite: boolean) {
18+
const session = await getSession()
19+
if (!session?.user?.id)
20+
return { response: NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) }
21+
22+
const permission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId)
23+
if (!permission || (requireWrite && !permissionSatisfies(permission, 'write'))) {
24+
return { response: NextResponse.json({ error: 'Workspace not found' }, { status: 404 }) }
25+
}
26+
return { userId: session.user.id }
27+
}
28+
29+
export const GET = withRouteHandler(
30+
async (_request: NextRequest, { params }: { params: Promise<{ id: string }> }) => {
31+
const workspaceId = (await params).id
32+
const auth = await authorize(workspaceId, false)
33+
if (auth.response) return auth.response
34+
35+
const [row] = await db
36+
.select({ config: workspace.codexConfig })
37+
.from(workspace)
38+
.where(eq(workspace.id, workspaceId))
39+
.limit(1)
40+
if (!row) return NextResponse.json({ error: 'Workspace not found' }, { status: 404 })
41+
42+
return NextResponse.json({ config: parseCodexConfigPatch(row.config) })
43+
}
44+
)
45+
46+
export const PUT = withRouteHandler(
47+
async (request: NextRequest, context: { params: Promise<{ id: string }> }) => {
48+
const workspaceId = (await context.params).id
49+
const auth = await authorize(workspaceId, true)
50+
if (auth.response) return auth.response
51+
52+
const parsed = await parseRequest(updateWorkspaceCodexConfigContract, request, context)
53+
if (!parsed.success) return parsed.response
54+
55+
try {
56+
const config = parseCodexConfigPatch(parsed.data.body.config)
57+
const [updated] = await db
58+
.update(workspace)
59+
.set({ codexConfig: config, updatedAt: new Date() })
60+
.where(eq(workspace.id, workspaceId))
61+
.returning({ config: workspace.codexConfig })
62+
if (!updated) return NextResponse.json({ error: 'Workspace not found' }, { status: 404 })
63+
return NextResponse.json({ config: parseCodexConfigPatch(updated.config) })
64+
} catch (error) {
65+
logger.error('Failed to update workspace Codex configuration', {
66+
workspaceId,
67+
userId: auth.userId,
68+
error: getErrorMessage(error),
69+
})
70+
return NextResponse.json({ error: 'Failed to update Codex configuration' }, { status: 500 })
71+
}
72+
}
73+
)

apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const WORKSPACE_SECTION_MAP: Partial<Record<SettingsSection, WorkspaceSettingsSe
3636
secrets: 'secrets',
3737
'credential-groups': 'credential-groups',
3838
byok: 'byok',
39+
codex: 'codex',
3940
sandboxes: 'sandboxes',
4041
'custom-tools': 'custom-tools',
4142
mcp: 'mcp',

apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ const ApiKeys = dynamic(() =>
2525
const BYOK = dynamic(() =>
2626
import('@/app/workspace/[workspaceId]/settings/components/byok/byok').then((m) => m.BYOK)
2727
)
28+
const CodexSettings = dynamic(() =>
29+
import('@/app/workspace/[workspaceId]/settings/components/codex/codex').then(
30+
(m) => m.CodexSettings
31+
)
32+
)
2833
const Forks = dynamic(() => import('@/ee/workspace-forking/components/forks').then((m) => m.Forks))
2934
const Secrets = dynamic(() =>
3035
import('@/app/workspace/[workspaceId]/settings/components/secrets/secrets').then((m) => m.Secrets)
@@ -201,6 +206,7 @@ export function SettingsPage({ section }: SettingsPageProps) {
201206
<WhitelabelingSettings organizationId={organizationId} />
202207
)}
203208
{effectiveSection === 'byok' && <BYOK />}
209+
{effectiveSection === 'codex' && <CodexSettings />}
204210
{effectiveSection === 'sandboxes' && <Sandboxes />}
205211
{effectiveSection === 'mcp' && <MCP />}
206212
{effectiveSection === 'forks' && <Forks />}

0 commit comments

Comments
 (0)