@denieler/e2b-codex 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,213 @@
1
+ # @denieler/e2e-codex
2
+
3
+ Use a shared E2B template with Codex preinstalled, then create fresh sandboxes from that template and talk to `codex app-server` over websocket.
4
+
5
+ ![e2b-codex banner](./assets/e2b-codex-banner.svg)
6
+
7
+ This project covers two jobs:
8
+
9
+ - runtime use: create a sandbox, connect to Codex, send prompts, continue conversations
10
+ - template development: build and publish the shared E2B template
11
+
12
+ ## Runtime Use
13
+
14
+ Install the package:
15
+
16
+ ```bash
17
+ npm install @denieler/e2e-codex
18
+ ```
19
+
20
+ ### What you need
21
+
22
+ - `E2B_API_KEY`
23
+ - `OPENAI_API_KEY`
24
+ - `E2B_TEMPLATE_ID`
25
+
26
+ `E2B_TEMPLATE_ID` must point to a template that already has Codex installed.
27
+
28
+ ### Run one prompt
29
+
30
+ If you just want to verify the full flow, run:
31
+
32
+ ```bash
33
+ E2B_TEMPLATE_ID=<template-id> doppler run -- npm run example:prompt -- "Reply with exactly: test-ok"
34
+ ```
35
+
36
+ What this does:
37
+
38
+ 1. Creates a new sandbox from `E2B_TEMPLATE_ID`
39
+ 2. Starts `codex app-server` inside the sandbox
40
+ 3. Opens an authenticated websocket connection
41
+ 4. Starts a Codex thread
42
+ 5. Sends one prompt
43
+ 6. Returns the final reply
44
+
45
+ ### Create a ready sandbox in code
46
+
47
+ ```ts
48
+ import { createReadyCodexSandbox } from "@denieler/e2e-codex";
49
+
50
+ const ready = await createReadyCodexSandbox({
51
+ e2bApiKey: process.env.E2B_API_KEY!,
52
+ templateId: process.env.E2B_TEMPLATE_ID!,
53
+ openAiApiKey: process.env.OPENAI_API_KEY!,
54
+ userId: "user-123",
55
+ });
56
+ ```
57
+
58
+ The returned object includes:
59
+
60
+ - `sandboxId`
61
+ - `websocketUrl`
62
+ - `authToken`
63
+ - `workspaceRoot`
64
+
65
+ ### Connect to Codex over websocket
66
+
67
+ ```ts
68
+ import { connectCodexClient } from "@denieler/e2e-codex";
69
+
70
+ const client = await connectCodexClient(ready);
71
+ ```
72
+
73
+ ### Start a conversation
74
+
75
+ ```ts
76
+ const started = await client.request("thread/start", {
77
+ model: "gpt-5.3-codex",
78
+ cwd: ready.workspaceRoot,
79
+ });
80
+
81
+ const threadId = String((started.thread as { id?: string }).id);
82
+ ```
83
+
84
+ ### Send turns on the same thread
85
+
86
+ ```ts
87
+ await client.request("turn/start", {
88
+ threadId,
89
+ input: [{ type: "text", text: "First message" }],
90
+ cwd: ready.workspaceRoot,
91
+ model: "gpt-5.3-codex",
92
+ effort: "medium",
93
+ approvalPolicy: "never",
94
+ sandboxPolicy: {
95
+ type: "workspaceWrite",
96
+ writableRoots: [ready.workspaceRoot],
97
+ networkAccess: true,
98
+ },
99
+ summary: "concise",
100
+ });
101
+ ```
102
+
103
+ To continue the same conversation:
104
+
105
+ - keep the sandbox alive
106
+ - reuse the same `threadId`
107
+ - send another `turn/start`
108
+
109
+ If your websocket connection drops, reconnect using the same `websocketUrl` and `authToken`, then continue using the same `threadId`.
110
+
111
+ ### One-call helper
112
+
113
+ If you want one fresh sandbox and one prompt, use:
114
+
115
+ ```ts
116
+ import { createReadyCodexSandbox, runPrompt } from "@denieler/e2e-codex";
117
+
118
+ const sandbox = await createReadyCodexSandbox({
119
+ e2bApiKey: process.env.E2B_API_KEY!,
120
+ templateId: process.env.E2B_TEMPLATE_ID!,
121
+ openAiApiKey: process.env.OPENAI_API_KEY!,
122
+ userId: "user-123",
123
+ });
124
+
125
+ const result = await runPrompt({
126
+ sandbox,
127
+ prompt: "Summarize this workspace in one sentence.",
128
+ });
129
+
130
+ console.log(result.reply);
131
+ ```
132
+
133
+ ## Template Development
134
+
135
+ Use this section if you are maintaining the template itself.
136
+
137
+ ### Install
138
+
139
+ ```bash
140
+ npm install
141
+ ```
142
+
143
+ ### Build the template
144
+
145
+ ```bash
146
+ doppler run -- npm run build:template
147
+ ```
148
+
149
+ The build prints:
150
+
151
+ - template name
152
+ - template id
153
+ - build id
154
+
155
+ Use the printed template id as `E2B_TEMPLATE_ID` for runtime use.
156
+
157
+ ### What the build does
158
+
159
+ The template build:
160
+
161
+ - starts from E2B `base`
162
+ - installs a small set of system packages
163
+ - downloads the Codex Linux binary from OpenAI GitHub releases
164
+ - installs it to `/usr/local/bin/codex`
165
+
166
+ Codex is installed at template build time, not at sandbox startup.
167
+
168
+ ### Useful commands
169
+
170
+ ```bash
171
+ npm install
172
+ npm run typecheck
173
+ doppler run -- npm run build:template
174
+ E2B_TEMPLATE_ID=<template-id> doppler run -- npm run example:prompt -- "Hello"
175
+ ```
176
+
177
+ ## Runtime Design
178
+
179
+ At sandbox startup, the runtime:
180
+
181
+ - writes a websocket capability token into the sandbox
182
+ - starts `codex app-server` as an E2B background process
183
+ - waits for it to stay alive
184
+ - opens the websocket connection
185
+
186
+ `codex app-server` is started at runtime, not stored as a pre-running process in the template.
187
+
188
+ ## Notes
189
+
190
+ - The template is shared. Sandboxes are ephemeral.
191
+ - Secrets are injected when the sandbox is created.
192
+ - The websocket token file is written to `/tmp/e2b-codex-ws-token`.
193
+ - The example uses `approvalPolicy: "never"` and `workspaceWrite`.
194
+
195
+ ## Publish
196
+
197
+ Build the package:
198
+
199
+ ```bash
200
+ npm run build
201
+ ```
202
+
203
+ Pack it locally:
204
+
205
+ ```bash
206
+ npm pack
207
+ ```
208
+
209
+ Publish when ready:
210
+
211
+ ```bash
212
+ npm publish
213
+ ```
@@ -0,0 +1,100 @@
1
+ <svg width="1200" height="420" viewBox="0 0 1200 420" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <defs>
3
+ <linearGradient id="bg" x1="120" y1="48" x2="1080" y2="372" gradientUnits="userSpaceOnUse">
4
+ <stop stop-color="#0B1220"/>
5
+ <stop offset="1" stop-color="#111C2F"/>
6
+ </linearGradient>
7
+ <linearGradient id="panel" x1="226" y1="92" x2="978" y2="330" gradientUnits="userSpaceOnUse">
8
+ <stop stop-color="#121B2D"/>
9
+ <stop offset="1" stop-color="#18253D"/>
10
+ </linearGradient>
11
+ <linearGradient id="accent" x1="268" y1="124" x2="934" y2="296" gradientUnits="userSpaceOnUse">
12
+ <stop stop-color="#38BDF8"/>
13
+ <stop offset="1" stop-color="#22C55E"/>
14
+ </linearGradient>
15
+ <linearGradient id="cubeFaceA" x1="0" y1="0" x2="1" y2="1">
16
+ <stop stop-color="#3DD5F3"/>
17
+ <stop offset="1" stop-color="#23B3D6"/>
18
+ </linearGradient>
19
+ <linearGradient id="cubeFaceB" x1="0" y1="0" x2="1" y2="1">
20
+ <stop stop-color="#1F8FB5"/>
21
+ <stop offset="1" stop-color="#176A8C"/>
22
+ </linearGradient>
23
+ <linearGradient id="cubeFaceC" x1="0" y1="0" x2="1" y2="1">
24
+ <stop stop-color="#6EE7B7"/>
25
+ <stop offset="1" stop-color="#22C55E"/>
26
+ </linearGradient>
27
+ <filter id="shadow" x="186" y="82" width="830" height="266" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
28
+ <feFlood flood-opacity="0" result="BackgroundImageFix"/>
29
+ <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
30
+ <feOffset dy="10"/>
31
+ <feGaussianBlur stdDeviation="20"/>
32
+ <feComposite in2="hardAlpha" operator="out"/>
33
+ <feColorMatrix type="matrix" values="0 0 0 0 0.0156863 0 0 0 0 0.0470588 0 0 0 0 0.121569 0 0 0 0.38 0"/>
34
+ <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_1"/>
35
+ <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_0_1" result="shape"/>
36
+ </filter>
37
+ </defs>
38
+
39
+ <rect width="1200" height="420" rx="28" fill="url(#bg)"/>
40
+
41
+ <g opacity="0.2">
42
+ <path d="M110 84H1090" stroke="url(#accent)" stroke-width="1"/>
43
+ <path d="M110 336H1090" stroke="url(#accent)" stroke-width="1"/>
44
+ <path d="M190 48V372" stroke="#2B3A56" stroke-width="1"/>
45
+ <path d="M1010 48V372" stroke="#2B3A56" stroke-width="1"/>
46
+ </g>
47
+
48
+ <g opacity="0.22" fill="#7DD3FC">
49
+ <circle cx="126" cy="96" r="2"/>
50
+ <circle cx="156" cy="126" r="2"/>
51
+ <circle cx="108" cy="154" r="2"/>
52
+ <circle cx="1074" cy="112" r="2"/>
53
+ <circle cx="1048" cy="146" r="2"/>
54
+ <circle cx="1090" cy="174" r="2"/>
55
+ <circle cx="101" cy="286" r="2"/>
56
+ <circle cx="142" cy="316" r="2"/>
57
+ <circle cx="1087" cy="288" r="2"/>
58
+ <circle cx="1058" cy="324" r="2"/>
59
+ </g>
60
+
61
+ <g filter="url(#shadow)">
62
+ <rect x="226" y="92" width="748" height="226" rx="24" fill="url(#panel)" stroke="#2A3C5F"/>
63
+ </g>
64
+
65
+ <rect x="256" y="122" width="204" height="166" rx="20" fill="#0E1727" stroke="#243551"/>
66
+ <rect x="276" y="142" width="164" height="18" rx="9" fill="#172338"/>
67
+ <circle cx="295" cy="151" r="4" fill="#38BDF8"/>
68
+ <circle cx="311" cy="151" r="4" fill="#22C55E"/>
69
+ <circle cx="327" cy="151" r="4" fill="#F59E0B"/>
70
+
71
+ <path d="M302 198L328 224L302 250" stroke="#7DD3FC" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"/>
72
+ <rect x="346" y="244" width="48" height="10" rx="5" fill="#6EE7B7"/>
73
+ <rect x="346" y="208" width="62" height="10" rx="5" fill="#2A3C5F"/>
74
+ <rect x="346" y="190" width="42" height="10" rx="5" fill="#2A3C5F"/>
75
+
76
+ <g>
77
+ <path d="M584 138L632 164V217L584 243L536 217V164L584 138Z" fill="url(#cubeFaceA)"/>
78
+ <path d="M584 138L632 164L584 190L536 164L584 138Z" fill="#8BE9FC"/>
79
+ <path d="M632 164V217L584 243V190L632 164Z" fill="url(#cubeFaceB)"/>
80
+ <path d="M536 164V217L584 243V190L536 164Z" fill="url(#cubeFaceC)"/>
81
+ <path d="M584 156L614 173V205L584 222L554 205V173L584 156Z" stroke="#E0FBFF" stroke-width="4"/>
82
+ </g>
83
+
84
+ <path d="M664 192H730" stroke="url(#accent)" stroke-width="6" stroke-linecap="round"/>
85
+ <circle cx="748" cy="192" r="10" fill="#38BDF8"/>
86
+ <path d="M758 192H818" stroke="url(#accent)" stroke-width="6" stroke-linecap="round"/>
87
+ <circle cx="836" cy="192" r="10" fill="#22C55E"/>
88
+ <path d="M846 192H904" stroke="url(#accent)" stroke-width="6" stroke-linecap="round"/>
89
+
90
+ <g>
91
+ <rect x="858" y="142" width="78" height="100" rx="16" fill="#0E1727" stroke="#24405A"/>
92
+ <rect x="874" y="160" width="46" height="8" rx="4" fill="#38BDF8"/>
93
+ <rect x="874" y="178" width="34" height="8" rx="4" fill="#2A3C5F"/>
94
+ <rect x="874" y="212" width="28" height="8" rx="4" fill="#6EE7B7"/>
95
+ <path d="M884 201L897 188L910 201" stroke="#E0FBFF" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
96
+ </g>
97
+
98
+ <text x="502" y="287" fill="#F7FAFC" font-family="ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="56" font-weight="700" letter-spacing="-0.03em">e2b-codex</text>
99
+ <text x="504" y="325" fill="#9FB4D0" font-family="ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="22" font-weight="500">Build once. Spin sandboxes fast. Talk to Codex over websocket.</text>
100
+ </svg>
@@ -0,0 +1,32 @@
1
+ type JsonRpcError = {
2
+ code: number;
3
+ message: string;
4
+ };
5
+ type JsonRpcMessage = {
6
+ id?: number;
7
+ method?: string;
8
+ params?: Record<string, unknown>;
9
+ result?: Record<string, unknown>;
10
+ error?: JsonRpcError;
11
+ };
12
+ type NotificationHandler = (message: Required<Pick<JsonRpcMessage, "method">> & JsonRpcMessage) => void;
13
+ export declare class CodexAppServerClient {
14
+ private readonly socket;
15
+ private readonly pending;
16
+ private readonly notificationHandlers;
17
+ private nextId;
18
+ private constructor();
19
+ static connect(url: string, authToken?: string): Promise<CodexAppServerClient>;
20
+ onNotification(handler: NotificationHandler): () => boolean;
21
+ initialize(clientInfo?: {
22
+ name?: string;
23
+ title?: string;
24
+ version?: string;
25
+ }): Promise<void>;
26
+ request(method: string, params: Record<string, unknown>): Promise<Record<string, unknown>>;
27
+ notify(method: string, params: Record<string, unknown>): void;
28
+ close(): void;
29
+ private handleIncomingMessage;
30
+ private rejectAllPending;
31
+ }
32
+ export {};
@@ -0,0 +1,93 @@
1
+ export class CodexAppServerClient {
2
+ socket;
3
+ pending = new Map();
4
+ notificationHandlers = new Set();
5
+ nextId = 0;
6
+ constructor(socket) {
7
+ this.socket = socket;
8
+ this.socket.addEventListener("message", (event) => {
9
+ this.handleIncomingMessage(typeof event.data === "string" ? event.data : "");
10
+ });
11
+ this.socket.addEventListener("error", (event) => {
12
+ const error = event instanceof ErrorEvent ? event.error : new Error("Codex websocket error");
13
+ this.rejectAllPending(error);
14
+ });
15
+ this.socket.addEventListener("close", () => {
16
+ this.rejectAllPending(new Error("Codex websocket connection closed."));
17
+ });
18
+ }
19
+ static async connect(url, authToken) {
20
+ const WebSocketConstructor = WebSocket;
21
+ const socket = new WebSocketConstructor(url, {
22
+ headers: authToken
23
+ ? {
24
+ Authorization: `Bearer ${authToken}`,
25
+ }
26
+ : undefined,
27
+ });
28
+ await new Promise((resolve, reject) => {
29
+ socket.addEventListener("open", () => resolve(), { once: true });
30
+ socket.addEventListener("error", () => reject(new Error("Unable to open Codex websocket.")), { once: true });
31
+ });
32
+ return new CodexAppServerClient(socket);
33
+ }
34
+ onNotification(handler) {
35
+ this.notificationHandlers.add(handler);
36
+ return () => this.notificationHandlers.delete(handler);
37
+ }
38
+ async initialize(clientInfo) {
39
+ await this.request("initialize", {
40
+ clientInfo: {
41
+ name: clientInfo?.name ?? "e2b_codex",
42
+ title: clientInfo?.title ?? "E2B Codex",
43
+ version: clientInfo?.version ?? "0.1.0",
44
+ },
45
+ });
46
+ this.notify("initialized", {});
47
+ }
48
+ async request(method, params) {
49
+ const id = this.nextId++;
50
+ const payload = { id, method, params };
51
+ const resultPromise = new Promise((resolve, reject) => {
52
+ this.pending.set(id, { resolve, reject });
53
+ });
54
+ this.socket.send(JSON.stringify(payload));
55
+ return resultPromise;
56
+ }
57
+ notify(method, params) {
58
+ this.socket.send(JSON.stringify({ method, params }));
59
+ }
60
+ close() {
61
+ this.socket.close();
62
+ }
63
+ handleIncomingMessage(raw) {
64
+ if (!raw) {
65
+ return;
66
+ }
67
+ const message = JSON.parse(raw);
68
+ if (typeof message.id === "number") {
69
+ const pending = this.pending.get(message.id);
70
+ if (!pending) {
71
+ return;
72
+ }
73
+ this.pending.delete(message.id);
74
+ if (message.error) {
75
+ pending.reject(new Error(message.error.message));
76
+ return;
77
+ }
78
+ pending.resolve(message.result ?? {});
79
+ return;
80
+ }
81
+ if (message.method) {
82
+ for (const handler of this.notificationHandlers) {
83
+ handler(message);
84
+ }
85
+ }
86
+ }
87
+ rejectAllPending(error) {
88
+ for (const pending of this.pending.values()) {
89
+ pending.reject(error);
90
+ }
91
+ this.pending.clear();
92
+ }
93
+ }
package/dist/env.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare function loadLocalEnvFile(): void;
2
+ export declare function requireEnvVar(name: string): string;
package/dist/env.js ADDED
@@ -0,0 +1,42 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ function parseEnvFile(content) {
4
+ const entries = new Map();
5
+ for (const rawLine of content.split(/\r?\n/)) {
6
+ const line = rawLine.trim();
7
+ if (!line || line.startsWith("#")) {
8
+ continue;
9
+ }
10
+ const separatorIndex = line.indexOf("=");
11
+ if (separatorIndex <= 0) {
12
+ continue;
13
+ }
14
+ const key = line.slice(0, separatorIndex).trim();
15
+ let value = line.slice(separatorIndex + 1).trim();
16
+ if ((value.startsWith('"') && value.endsWith('"')) ||
17
+ (value.startsWith("'") && value.endsWith("'"))) {
18
+ value = value.slice(1, -1);
19
+ }
20
+ entries.set(key, value);
21
+ }
22
+ return entries;
23
+ }
24
+ export function loadLocalEnvFile() {
25
+ const envPath = path.join(process.cwd(), ".env.local");
26
+ if (!fs.existsSync(envPath)) {
27
+ return;
28
+ }
29
+ const parsed = parseEnvFile(fs.readFileSync(envPath, "utf8"));
30
+ for (const [key, value] of parsed.entries()) {
31
+ if (!process.env[key]) {
32
+ process.env[key] = value;
33
+ }
34
+ }
35
+ }
36
+ export function requireEnvVar(name) {
37
+ const value = process.env[name];
38
+ if (!value) {
39
+ throw new Error(`Missing required environment variable: ${name}`);
40
+ }
41
+ return value;
42
+ }
@@ -0,0 +1,5 @@
1
+ export { CodexAppServerClient } from "./codex-app-server.js";
2
+ export { loadLocalEnvFile, requireEnvVar } from "./env.js";
3
+ export { template } from "./template.js";
4
+ export { connectCodexClient, createReadyCodexSandbox, runPrompt, } from "./sandbox.js";
5
+ export type { CreateCodexSandboxOptions, ReadyCodexSandbox } from "./sandbox.js";
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { CodexAppServerClient } from "./codex-app-server.js";
2
+ export { loadLocalEnvFile, requireEnvVar } from "./env.js";
3
+ export { template } from "./template.js";
4
+ export { connectCodexClient, createReadyCodexSandbox, runPrompt, } from "./sandbox.js";
@@ -0,0 +1,34 @@
1
+ import { Sandbox } from "e2b";
2
+ import { CodexAppServerClient } from "./codex-app-server.js";
3
+ export type CreateCodexSandboxOptions = {
4
+ e2bApiKey: string;
5
+ templateId: string;
6
+ openAiApiKey: string;
7
+ userId: string;
8
+ timeoutMs?: number;
9
+ allowInternetAccess?: boolean;
10
+ port?: number;
11
+ workspaceRoot?: string;
12
+ metadata?: Record<string, string>;
13
+ };
14
+ export type ReadyCodexSandbox = {
15
+ sandbox: Sandbox;
16
+ sandboxId: string;
17
+ websocketUrl: string;
18
+ authToken: string;
19
+ port: number;
20
+ workspaceRoot: string;
21
+ };
22
+ export declare function createReadyCodexSandbox(options: CreateCodexSandboxOptions): Promise<ReadyCodexSandbox>;
23
+ export declare function connectCodexClient(readySandbox: Pick<ReadyCodexSandbox, "websocketUrl" | "authToken">): Promise<CodexAppServerClient>;
24
+ export declare function runPrompt(options: {
25
+ sandbox: ReadyCodexSandbox;
26
+ prompt: string;
27
+ cwd?: string;
28
+ model?: string;
29
+ effort?: "low" | "medium" | "high" | "xhigh";
30
+ summary?: "auto" | "concise" | "detailed";
31
+ }): Promise<{
32
+ threadId: string;
33
+ reply: string;
34
+ }>;
@@ -0,0 +1,178 @@
1
+ import { createHash } from "node:crypto";
2
+ import { Sandbox } from "e2b";
3
+ import { CodexAppServerClient } from "./codex-app-server.js";
4
+ function createAppServerToken(userId) {
5
+ return createHash("sha256")
6
+ .update("e2b-codex")
7
+ .update(":")
8
+ .update(userId)
9
+ .digest("hex");
10
+ }
11
+ function getTokenFilePath() {
12
+ return "/tmp/e2b-codex-ws-token";
13
+ }
14
+ async function ensureAppServerRunning(sandbox, options) {
15
+ const port = options.port ?? 4571;
16
+ const workspaceRoot = options.workspaceRoot ?? "/workspace";
17
+ const tokenFile = getTokenFilePath();
18
+ const token = createAppServerToken(options.userId);
19
+ const processPattern = `[c]odex app-server --listen ws://0.0.0.0:${port}`;
20
+ await sandbox.files.write(tokenFile, token);
21
+ await sandbox.commands.run(`mkdir -p ${workspaceRoot}`, {
22
+ timeoutMs: 10_000,
23
+ });
24
+ const existing = await sandbox.commands.run(`bash -lc 'ps -ef | grep "${processPattern}" || true'`, {
25
+ timeoutMs: 10_000,
26
+ });
27
+ if (existing.stdout.trim()) {
28
+ return;
29
+ }
30
+ await sandbox.commands.run(`codex app-server --listen ws://0.0.0.0:${port} --ws-auth capability-token --ws-token-file ${tokenFile}`, {
31
+ background: true,
32
+ envs: {
33
+ OPENAI_API_KEY: options.openAiApiKey,
34
+ },
35
+ timeoutMs: 15_000,
36
+ });
37
+ await new Promise((resolve) => setTimeout(resolve, 3_000));
38
+ const started = await sandbox.commands.run(`bash -lc 'ps -ef | grep "${processPattern}" || true'`, {
39
+ timeoutMs: 10_000,
40
+ });
41
+ if (!started.stdout.trim()) {
42
+ throw new Error("codex app-server did not remain running after startup.");
43
+ }
44
+ }
45
+ export async function createReadyCodexSandbox(options) {
46
+ const port = options.port ?? 4571;
47
+ const workspaceRoot = options.workspaceRoot ?? "/workspace";
48
+ const sandbox = await Sandbox.create(options.templateId, {
49
+ apiKey: options.e2bApiKey,
50
+ timeoutMs: options.timeoutMs ?? 300_000,
51
+ allowInternetAccess: options.allowInternetAccess ?? true,
52
+ envs: {
53
+ OPENAI_API_KEY: options.openAiApiKey,
54
+ },
55
+ metadata: {
56
+ product: "e2b-codex",
57
+ userId: options.userId,
58
+ ...(options.metadata ?? {}),
59
+ },
60
+ });
61
+ await ensureAppServerRunning(sandbox, {
62
+ openAiApiKey: options.openAiApiKey,
63
+ port,
64
+ workspaceRoot,
65
+ userId: options.userId,
66
+ });
67
+ return {
68
+ sandbox,
69
+ sandboxId: sandbox.sandboxId,
70
+ websocketUrl: `wss://${sandbox.getHost(port)}`,
71
+ authToken: createAppServerToken(options.userId),
72
+ port,
73
+ workspaceRoot,
74
+ };
75
+ }
76
+ export async function connectCodexClient(readySandbox) {
77
+ let lastError = null;
78
+ for (let attempt = 0; attempt < 20; attempt += 1) {
79
+ try {
80
+ const client = await CodexAppServerClient.connect(readySandbox.websocketUrl, readySandbox.authToken);
81
+ await client.initialize();
82
+ return client;
83
+ }
84
+ catch (error) {
85
+ lastError = error;
86
+ await new Promise((resolve) => setTimeout(resolve, 500));
87
+ }
88
+ }
89
+ throw lastError instanceof Error
90
+ ? lastError
91
+ : new Error("Unable to open Codex websocket.");
92
+ }
93
+ export async function runPrompt(options) {
94
+ const client = await connectCodexClient(options.sandbox);
95
+ const cwd = options.cwd ?? options.sandbox.workspaceRoot;
96
+ const model = options.model ?? "gpt-5.3-codex";
97
+ const effort = options.effort ?? "medium";
98
+ const summary = options.summary ?? "concise";
99
+ try {
100
+ const started = await client.request("thread/start", {
101
+ model,
102
+ cwd,
103
+ });
104
+ const threadId = String(started.thread?.id ?? "");
105
+ if (!threadId) {
106
+ throw new Error("Codex did not return a thread id.");
107
+ }
108
+ const replyParts = [];
109
+ await new Promise((resolve, reject) => {
110
+ const unsubscribe = client.onNotification((message) => {
111
+ const params = (message.params ?? {});
112
+ if (message.method === "item/agentMessage/delta" && typeof params.delta === "string") {
113
+ replyParts.push(params.delta);
114
+ return;
115
+ }
116
+ if (message.method === "item/completed") {
117
+ const item = (params.item ?? {});
118
+ const content = Array.isArray(item.content) ? item.content : [];
119
+ const completedText = content
120
+ .map((part) => typeof part === "object" && part && typeof part.text === "string"
121
+ ? String(part.text)
122
+ : "")
123
+ .join("")
124
+ .trim();
125
+ if (item.type === "agentMessage" && completedText && replyParts.length === 0) {
126
+ replyParts.push(completedText);
127
+ }
128
+ return;
129
+ }
130
+ if (message.method === "error") {
131
+ const error = (params.error ?? {});
132
+ const errorMessage = String(error.message ?? "Codex turn failed.");
133
+ const willRetry = Boolean(params.willRetry);
134
+ if (willRetry) {
135
+ return;
136
+ }
137
+ unsubscribe();
138
+ reject(new Error(errorMessage));
139
+ return;
140
+ }
141
+ if (message.method === "turn/completed") {
142
+ unsubscribe();
143
+ const turn = (params.turn ?? {});
144
+ if (String(turn.status ?? "") === "failed") {
145
+ reject(new Error(String(turn.error?.message ??
146
+ "Codex turn failed.")));
147
+ return;
148
+ }
149
+ resolve();
150
+ }
151
+ });
152
+ client.request("turn/start", {
153
+ threadId,
154
+ input: [{ type: "text", text: options.prompt }],
155
+ cwd,
156
+ model,
157
+ effort,
158
+ approvalPolicy: "never",
159
+ sandboxPolicy: {
160
+ type: "workspaceWrite",
161
+ writableRoots: [cwd],
162
+ networkAccess: true,
163
+ },
164
+ summary,
165
+ }).catch((error) => {
166
+ unsubscribe();
167
+ reject(error);
168
+ });
169
+ });
170
+ return {
171
+ threadId,
172
+ reply: replyParts.join("").trim(),
173
+ };
174
+ }
175
+ finally {
176
+ client.close();
177
+ }
178
+ }
@@ -0,0 +1 @@
1
+ export declare const template: import("e2b").TemplateBuilder;
@@ -0,0 +1,45 @@
1
+ import { Template } from "e2b";
2
+ const codexInstallScript = `
3
+ set -eux
4
+ ARCH="$(uname -m)"
5
+ case "$ARCH" in
6
+ x86_64) TARGET="x86_64-unknown-linux-musl" ;;
7
+ aarch64|arm64) TARGET="aarch64-unknown-linux-musl" ;;
8
+ *)
9
+ echo "Unsupported architecture: $ARCH" >&2
10
+ exit 1
11
+ ;;
12
+ esac
13
+
14
+ VERSION="\${CODEX_VERSION:-latest}"
15
+ ASSET="codex-$TARGET.tar.gz"
16
+ BASE_URL="https://github.com/openai/codex/releases"
17
+
18
+ if [ "$VERSION" = "latest" ]; then
19
+ DOWNLOAD_URL="$BASE_URL/latest/download/$ASSET"
20
+ else
21
+ DOWNLOAD_URL="$BASE_URL/download/$VERSION/$ASSET"
22
+ fi
23
+
24
+ curl -fsSL "$DOWNLOAD_URL" -o /tmp/codex.tar.gz
25
+ mkdir -p /tmp/codex-extract
26
+ tar -xzf /tmp/codex.tar.gz -C /tmp/codex-extract
27
+
28
+ BIN_PATH="/tmp/codex-extract/codex-$TARGET"
29
+ if [ ! -f "$BIN_PATH" ]; then
30
+ echo "Codex binary not found after extraction." >&2
31
+ exit 1
32
+ fi
33
+
34
+ install -m 0755 "$BIN_PATH" /usr/local/bin/codex
35
+ codex --version
36
+ `;
37
+ export const template = Template()
38
+ .fromTemplate("base")
39
+ .setUser("root")
40
+ .aptInstall(["ca-certificates", "curl", "git", "tar", "unzip"], {
41
+ noInstallRecommends: true,
42
+ })
43
+ .makeDir("/workspace", { mode: 0o755 })
44
+ .runCmd(codexInstallScript)
45
+ .runCmd("sh -lc 'command -v codex && codex --version'");
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@denieler/e2b-codex",
3
+ "version": "0.1.0",
4
+ "description": "Build and use E2B sandboxes with Codex preinstalled and codex app-server ready over websocket.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md",
18
+ "assets"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsc -p tsconfig.build.json",
22
+ "build:template": "tsx scripts/build-template.ts",
23
+ "clean": "rm -rf dist",
24
+ "example:prompt": "tsx examples/run-prompt.ts",
25
+ "prepublishOnly": "npm run build",
26
+ "typecheck": "tsc --noEmit"
27
+ },
28
+ "dependencies": {
29
+ "e2b": "^2.18.0"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^24.0.0",
33
+ "tsx": "^4.19.2",
34
+ "typescript": "^5.8.2"
35
+ }
36
+ }