mirror of
https://github.com/openai/codex.git
synced 2026-10-24 18:18:41 +00:00
552 lines
16 KiB
HTML
552 lines
16 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Codex WASM Harness Prototype</title>
|
|
<style>
|
|
:root {
|
|
color-scheme: light;
|
|
font-family:
|
|
ui-serif, "Iowan Old Style", "Palatino Linotype", Georgia, serif;
|
|
background: #f2eddf;
|
|
color: #1f2a24;
|
|
}
|
|
|
|
body {
|
|
margin: 0;
|
|
min-height: 100vh;
|
|
background:
|
|
radial-gradient(circle at 20% 10%, #ffc96b 0 9rem, transparent 9rem),
|
|
linear-gradient(135deg, #f8f2e3 0%, #dfecdc 48%, #c7dde7 100%);
|
|
}
|
|
|
|
main {
|
|
box-sizing: border-box;
|
|
max-width: 960px;
|
|
margin: 0 auto;
|
|
padding: 48px 20px;
|
|
}
|
|
|
|
h1 {
|
|
max-width: 760px;
|
|
margin: 0 0 12px;
|
|
font-size: clamp(2.4rem, 7vw, 5.5rem);
|
|
line-height: 0.92;
|
|
letter-spacing: -0.06em;
|
|
}
|
|
|
|
p {
|
|
max-width: 720px;
|
|
font-size: 1.1rem;
|
|
}
|
|
|
|
form {
|
|
display: grid;
|
|
grid-template-columns: 1fr auto;
|
|
gap: 12px;
|
|
margin: 32px 0;
|
|
}
|
|
|
|
input,
|
|
button {
|
|
border: 2px solid #1f2a24;
|
|
border-radius: 999px;
|
|
font: inherit;
|
|
font-size: 1.05rem;
|
|
padding: 14px 18px;
|
|
}
|
|
|
|
input {
|
|
background: #fffaf0;
|
|
}
|
|
|
|
button {
|
|
background: #1f2a24;
|
|
color: #fffaf0;
|
|
cursor: pointer;
|
|
}
|
|
|
|
label {
|
|
display: grid;
|
|
gap: 6px;
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
font-size: 0.82rem;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.api-key-panel {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
gap: 14px;
|
|
align-items: end;
|
|
margin: 28px 0 0;
|
|
border: 2px solid rgb(31 42 36 / 0.26);
|
|
border-radius: 28px;
|
|
background: rgb(255 250 240 / 0.68);
|
|
padding: 18px;
|
|
}
|
|
|
|
.api-key-panel h2 {
|
|
margin: 0 0 8px;
|
|
font-size: 1.1rem;
|
|
letter-spacing: -0.03em;
|
|
}
|
|
|
|
.api-key-panel p {
|
|
margin: 0;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.api-key-panel label {
|
|
grid-column: 1 / -1;
|
|
}
|
|
|
|
.api-key-actions {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
grid-column: 1 / -1;
|
|
gap: 10px;
|
|
align-items: center;
|
|
}
|
|
|
|
.secondary-button {
|
|
background: transparent;
|
|
color: #1f2a24;
|
|
}
|
|
|
|
.api-key-status {
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
#events {
|
|
display: grid;
|
|
gap: 12px;
|
|
}
|
|
|
|
.event {
|
|
border: 2px solid rgb(31 42 36 / 0.22);
|
|
border-radius: 20px;
|
|
background: rgb(255 250 240 / 0.75);
|
|
padding: 14px 16px;
|
|
box-shadow: 0 16px 40px rgb(31 42 36 / 0.08);
|
|
}
|
|
|
|
.event strong {
|
|
display: block;
|
|
margin-bottom: 6px;
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
font-size: 0.82rem;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
pre {
|
|
margin: 0;
|
|
overflow: auto;
|
|
white-space: pre-wrap;
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
form,
|
|
.api-key-panel {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main>
|
|
<h1>Codex WASM harness prototype</h1>
|
|
<p>
|
|
This page exercises the real Codex browser boundary: submit a prompt,
|
|
stream Codex protocol events from WASM, and resolve when the turn
|
|
completes.
|
|
</p>
|
|
<section class="api-key-panel" aria-labelledby="api-key-title">
|
|
<div>
|
|
<h2 id="api-key-title">Responses API key</h2>
|
|
<p>
|
|
Local prototype only. The key is stored in this browser's
|
|
localStorage and is sent directly from this page to the Responses
|
|
API when you run a turn.
|
|
</p>
|
|
</div>
|
|
<label>
|
|
API key
|
|
<input
|
|
id="api-key"
|
|
type="password"
|
|
placeholder="sk-..."
|
|
autocomplete="off"
|
|
spellcheck="false"
|
|
/>
|
|
</label>
|
|
<div class="api-key-actions">
|
|
<button id="clear-api-key" class="secondary-button" type="button">
|
|
Forget key
|
|
</button>
|
|
<output id="api-key-status" class="api-key-status"></output>
|
|
</div>
|
|
</section>
|
|
<form id="prompt-form">
|
|
<input
|
|
id="prompt"
|
|
value="write hello world in JavaScript, run it with exec, and tell me the result"
|
|
autocomplete="off"
|
|
/>
|
|
<button type="submit">Run turn</button>
|
|
</form>
|
|
<section id="events" aria-live="polite"></section>
|
|
</main>
|
|
<script type="module">
|
|
import init, {
|
|
BrowserAppServer,
|
|
} from "../pkg/codex_wasm_harness.js?v=20260416a";
|
|
|
|
await init("../pkg/codex_wasm_harness_bg.wasm?v=20260416a");
|
|
|
|
const form = document.querySelector("#prompt-form");
|
|
const input = document.querySelector("#prompt");
|
|
const events = document.querySelector("#events");
|
|
const apiKeyInput = document.querySelector("#api-key");
|
|
const apiKeyStatus = document.querySelector("#api-key-status");
|
|
const clearApiKeyButton = document.querySelector("#clear-api-key");
|
|
const API_KEY_STORAGE_KEY = "codexWasmHarness.apiKey";
|
|
const app = new BrowserAppServer(readStoredApiKey());
|
|
const sandbox = createSandboxKernel();
|
|
let nextRequestId = 1;
|
|
let threadId = null;
|
|
const browserSessionOptions = {
|
|
cwd: "/workspace",
|
|
instructions: {
|
|
developer: [
|
|
"Code mode runs inside AppKernel, exposed in this demo through a browser iframe sandbox.",
|
|
"Browser network APIs and OPFS are available to code running in AppKernel.",
|
|
"Approved OPFS path layout:",
|
|
"- /workspace: active workspace files and entrypoints",
|
|
"- /workspace/cache: fetched artifacts and caches",
|
|
"- /workspace/repos: cloned or mirrored source trees",
|
|
"Approved GitHub URL prefixes:",
|
|
"- https://github.com/openai/",
|
|
"- https://raw.githubusercontent.com/openai/",
|
|
"Fetch, cache, and search source code yourself using the available browser APIs instead of assuming the harness preloads repository contents.",
|
|
].join("\n"),
|
|
},
|
|
};
|
|
|
|
apiKeyInput.value = readStoredApiKey();
|
|
updateApiKeyStatus();
|
|
app.setSessionOptions(browserSessionOptions);
|
|
app.setEventHandler(handleServerEvent);
|
|
app.set_code_executor(async (requestJson) =>
|
|
sandbox.execute(requestJson),
|
|
);
|
|
await app.notify({ method: "initialized" });
|
|
renderEvent({ type: "page_ready" });
|
|
|
|
apiKeyInput.addEventListener("input", () => {
|
|
const apiKey = apiKeyInput.value.trim();
|
|
writeStoredApiKey(apiKey);
|
|
threadId = null;
|
|
app.set_api_key(apiKey);
|
|
updateApiKeyStatus();
|
|
});
|
|
|
|
clearApiKeyButton.addEventListener("click", () => {
|
|
apiKeyInput.value = "";
|
|
writeStoredApiKey("");
|
|
threadId = null;
|
|
app.set_api_key("");
|
|
updateApiKeyStatus();
|
|
});
|
|
|
|
form.addEventListener("submit", async (event) => {
|
|
event.preventDefault();
|
|
events.replaceChildren();
|
|
renderEvent({ type: "submit_clicked" });
|
|
try {
|
|
const currentThreadId = await ensureThread();
|
|
await request("turn/start", {
|
|
threadId: currentThreadId,
|
|
input: [{ type: "text", text: input.value }],
|
|
});
|
|
} catch (error) {
|
|
renderEvent({
|
|
type: "page_error",
|
|
message: error instanceof Error ? error.message : String(error),
|
|
});
|
|
}
|
|
});
|
|
|
|
window.addEventListener("error", (event) => {
|
|
renderEvent({
|
|
type: "window_error",
|
|
message: event.message,
|
|
stack:
|
|
event.error instanceof Error && event.error.stack
|
|
? event.error.stack
|
|
: null,
|
|
});
|
|
});
|
|
|
|
window.addEventListener("unhandledrejection", (event) => {
|
|
renderEvent({
|
|
type: "unhandled_rejection",
|
|
message:
|
|
event.reason instanceof Error
|
|
? event.reason.message
|
|
: String(event.reason),
|
|
stack:
|
|
event.reason instanceof Error && event.reason.stack
|
|
? event.reason.stack
|
|
: null,
|
|
});
|
|
});
|
|
|
|
function readStoredApiKey() {
|
|
try {
|
|
return localStorage.getItem(API_KEY_STORAGE_KEY) ?? "";
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function writeStoredApiKey(apiKey) {
|
|
try {
|
|
if (apiKey) {
|
|
localStorage.setItem(API_KEY_STORAGE_KEY, apiKey);
|
|
} else {
|
|
localStorage.removeItem(API_KEY_STORAGE_KEY);
|
|
}
|
|
} catch {
|
|
apiKeyStatus.textContent =
|
|
"localStorage is unavailable in this browser context.";
|
|
}
|
|
}
|
|
|
|
function updateApiKeyStatus() {
|
|
apiKeyStatus.textContent = apiKeyInput.value
|
|
? "API key stored in localStorage for this browser."
|
|
: "No API key stored. Enter a key to run the real Codex harness.";
|
|
}
|
|
|
|
async function ensureThread() {
|
|
if (threadId) {
|
|
return threadId;
|
|
}
|
|
const response = await request("thread/start", {});
|
|
threadId = response.thread.id;
|
|
return threadId;
|
|
}
|
|
|
|
async function request(method, params) {
|
|
const payload = { method, id: nextRequestId++, params };
|
|
renderEvent({ type: "client_request", payload });
|
|
const response = await app.request(payload);
|
|
renderEvent({ type: "client_response", method, response });
|
|
return response;
|
|
}
|
|
|
|
function handleServerEvent(event) {
|
|
if (event?.method === "thread/started") {
|
|
threadId = event.params?.thread?.id ?? threadId;
|
|
}
|
|
renderEvent(event);
|
|
}
|
|
|
|
function renderEvent(event) {
|
|
const card = document.createElement("article");
|
|
card.className = "event";
|
|
const title = document.createElement("strong");
|
|
title.textContent = event.method ?? event.type ?? "event";
|
|
const body = document.createElement("pre");
|
|
body.textContent = JSON.stringify(event, null, 2);
|
|
card.append(title, body);
|
|
events.append(card);
|
|
}
|
|
|
|
function createSandboxKernel() {
|
|
const iframe = document.createElement("iframe");
|
|
iframe.setAttribute("sandbox", "allow-scripts allow-same-origin");
|
|
iframe.style.display = "none";
|
|
iframe.srcdoc = `
|
|
<!doctype html>
|
|
<html>
|
|
<body>
|
|
<script>
|
|
const EXIT_SIGNAL = Symbol("codexExit");
|
|
|
|
function formatValue(value) {
|
|
if (typeof value === "string") {
|
|
return value;
|
|
}
|
|
if (value === undefined || value === null) {
|
|
return "";
|
|
}
|
|
try {
|
|
return JSON.stringify(value);
|
|
} catch {
|
|
return String(value);
|
|
}
|
|
}
|
|
|
|
function cloneJson(value) {
|
|
return JSON.parse(JSON.stringify(value ?? null));
|
|
}
|
|
|
|
window.addEventListener("message", async (event) => {
|
|
const data = event.data;
|
|
if (!data || data.type !== "exec_js") {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const request = JSON.parse(data.requestJson);
|
|
const outputParts = [];
|
|
const storedValues = cloneJson(request.stored_values) ?? {};
|
|
const toolsMetadata = Array.isArray(request.enabled_tools)
|
|
? request.enabled_tools
|
|
: [];
|
|
const consoleProxy = {
|
|
log: (...args) => outputParts.push(args.map(formatValue).join(" ")),
|
|
error: (...args) => outputParts.push(args.map(formatValue).join(" ")),
|
|
};
|
|
const text = (value) => {
|
|
const formatted = formatValue(value);
|
|
if (formatted) {
|
|
outputParts.push(formatted);
|
|
}
|
|
};
|
|
const notify = text;
|
|
const store = (key, value) => {
|
|
if (typeof key !== "string" || !key) {
|
|
throw new Error("store(key, value) requires a non-empty string key");
|
|
}
|
|
storedValues[key] = cloneJson(value);
|
|
};
|
|
const load = (key) => storedValues[key];
|
|
const tools = new Proxy(
|
|
{},
|
|
{
|
|
get(_target, prop) {
|
|
return async () => {
|
|
throw new Error(
|
|
"Nested tool " +
|
|
String(prop) +
|
|
" is unavailable in the browser prototype",
|
|
);
|
|
};
|
|
},
|
|
},
|
|
);
|
|
let errorText = null;
|
|
const runner = new Function(
|
|
"tools",
|
|
"text",
|
|
"output_text",
|
|
"image",
|
|
"store",
|
|
"load",
|
|
"notify",
|
|
"yield_control",
|
|
"exit",
|
|
"ALL_TOOLS",
|
|
"console",
|
|
"return (async () => {\\n" + request.source + "\\n})();",
|
|
);
|
|
|
|
try {
|
|
const result = await runner(
|
|
tools,
|
|
text,
|
|
text,
|
|
text,
|
|
store,
|
|
load,
|
|
notify,
|
|
async () => {},
|
|
() => {
|
|
throw EXIT_SIGNAL;
|
|
},
|
|
toolsMetadata,
|
|
consoleProxy,
|
|
);
|
|
const formattedResult = formatValue(result);
|
|
if (formattedResult) {
|
|
outputParts.push(formattedResult);
|
|
}
|
|
} catch (error) {
|
|
if (error !== EXIT_SIGNAL) {
|
|
errorText =
|
|
error instanceof Error ? error.message : String(error);
|
|
}
|
|
}
|
|
|
|
parent.postMessage(
|
|
{
|
|
type: "exec_js_result",
|
|
requestId: data.requestId,
|
|
ok: true,
|
|
output: outputParts.join("\\n"),
|
|
storedValues,
|
|
errorText,
|
|
},
|
|
"*",
|
|
);
|
|
} catch (error) {
|
|
parent.postMessage(
|
|
{
|
|
type: "exec_js_result",
|
|
requestId: data.requestId,
|
|
ok: false,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
},
|
|
"*",
|
|
);
|
|
}
|
|
});
|
|
<\/script>
|
|
</body>
|
|
</html>`;
|
|
document.body.append(iframe);
|
|
|
|
let nextRequestId = 0;
|
|
const pending = new Map();
|
|
|
|
window.addEventListener("message", (event) => {
|
|
const data = event.data;
|
|
if (!data || data.type !== "exec_js_result") {
|
|
return;
|
|
}
|
|
const entry = pending.get(data.requestId);
|
|
if (!entry) {
|
|
return;
|
|
}
|
|
pending.delete(data.requestId);
|
|
if (data.ok) {
|
|
entry.resolve(data);
|
|
} else {
|
|
entry.reject(new Error(data.error ?? "sandbox execution failed"));
|
|
}
|
|
});
|
|
|
|
return {
|
|
execute(requestJson) {
|
|
return new Promise((resolve, reject) => {
|
|
nextRequestId += 1;
|
|
pending.set(nextRequestId, { resolve, reject });
|
|
iframe.contentWindow.postMessage(
|
|
{ type: "exec_js", requestId: nextRequestId, requestJson },
|
|
"*",
|
|
);
|
|
});
|
|
},
|
|
};
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|