@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 +213 -0
- package/assets/e2b-codex-banner.svg +100 -0
- package/dist/codex-app-server.d.ts +32 -0
- package/dist/codex-app-server.js +93 -0
- package/dist/env.d.ts +2 -0
- package/dist/env.js +42 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/sandbox.d.ts +34 -0
- package/dist/sandbox.js +178 -0
- package/dist/template.d.ts +1 -0
- package/dist/template.js +45 -0
- package/package.json +36 -0
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
|
+

|
|
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
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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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,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
|
+
}>;
|
package/dist/sandbox.js
ADDED
|
@@ -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;
|
package/dist/template.js
ADDED
|
@@ -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
|
+
}
|