codex-webapp 0.1.6 → 0.1.8
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/ACKNOWLEDGEMENTS.md +11 -37
- package/README.ja.md +56 -101
- package/README.md +64 -103
- package/docs/architecture.md +85 -0
- package/docs/clean-release-verification.md +95 -0
- package/docs/codex-app-install.md +5 -0
- package/docs/distribution-boundary.md +37 -0
- package/docs/i18n/README.ko.md +11 -65
- package/docs/i18n/README.zh-CN.md +11 -65
- package/package.json +9 -2
- package/scripts/check-public-package-boundary.mjs +248 -0
- package/scripts/verify-clean-release.mjs +492 -0
- package/src/appServerBridge.js +150 -0
- package/src/appServerMessageCodec.js +12 -0
- package/src/auditEvidenceHook.js +18 -0
- package/src/bridgeEventEnvelope.js +29 -0
- package/src/browserPreload.js +176 -0
- package/src/browserSmoke.js +2 -2
- package/src/codexAppRenderer.js +12 -0
- package/src/codexWeb.js +7 -14
- package/src/commands.js +40 -33
- package/src/electronBridge.js +324 -0
- package/src/localServer.js +184 -0
- package/src/projectionManifest.js +8 -0
- package/src/rendererAssetSource.js +278 -0
- package/docs/assets/codex-webapp-readme.png +0 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
const APP_SESSION_ID = "codex-webapp-browser-session";
|
|
3
|
+
const REQUEST_TIMEOUT_MS = 10_000;
|
|
4
|
+
const sharedObjects = new Map([
|
|
5
|
+
["host_config", { id: "local", display_name: "Local", kind: "local" }],
|
|
6
|
+
["remote_connections", []],
|
|
7
|
+
["remote_control_connections", []],
|
|
8
|
+
]);
|
|
9
|
+
const pending = new Map();
|
|
10
|
+
const queue = [];
|
|
11
|
+
let nextId = 1;
|
|
12
|
+
let socket = null;
|
|
13
|
+
let socketOpen = false;
|
|
14
|
+
const nativeFetch = window.fetch?.bind(window);
|
|
15
|
+
|
|
16
|
+
if (nativeFetch) {
|
|
17
|
+
window.fetch = function codexWebappFetch(input, init) {
|
|
18
|
+
const url = typeof input === "string" ? input : input?.url;
|
|
19
|
+
if (String(url || "").startsWith("sentry-ipc://")) {
|
|
20
|
+
return Promise.resolve(new Response("{}", {
|
|
21
|
+
status: 200,
|
|
22
|
+
headers: { "content-type": "application/json" },
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
return nativeFetch(input, init);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function connect() {
|
|
30
|
+
if (socket) return;
|
|
31
|
+
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
32
|
+
socket = new WebSocket(`${protocol}//${window.location.host}/__backend/ipc`);
|
|
33
|
+
socket.addEventListener("open", () => {
|
|
34
|
+
socketOpen = true;
|
|
35
|
+
while (queue.length > 0) socket.send(queue.shift());
|
|
36
|
+
});
|
|
37
|
+
socket.addEventListener("message", (event) => {
|
|
38
|
+
let message;
|
|
39
|
+
try {
|
|
40
|
+
message = JSON.parse(String(event.data));
|
|
41
|
+
} catch {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (message && message.id && pending.has(message.id)) {
|
|
45
|
+
const deferred = pending.get(message.id);
|
|
46
|
+
pending.delete(message.id);
|
|
47
|
+
clearTimeout(deferred.timer);
|
|
48
|
+
if (message.ok === false) {
|
|
49
|
+
deferred.reject(new Error(message.error || "Codex WebApp IPC request failed"));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
deferred.resolve(message.result);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (message && message.type === "event") {
|
|
56
|
+
receiveFromHost(message.payload);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
socket.addEventListener("close", () => {
|
|
60
|
+
socketOpen = false;
|
|
61
|
+
socket = null;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function request(kind, payload) {
|
|
66
|
+
connect();
|
|
67
|
+
const id = String(nextId++);
|
|
68
|
+
const frame = JSON.stringify({ id, kind, payload });
|
|
69
|
+
if (socketOpen && socket?.readyState === WebSocket.OPEN) {
|
|
70
|
+
socket.send(frame);
|
|
71
|
+
} else {
|
|
72
|
+
queue.push(frame);
|
|
73
|
+
}
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
const timer = setTimeout(() => {
|
|
76
|
+
pending.delete(id);
|
|
77
|
+
reject(new Error(`Codex WebApp IPC timed out: ${kind}`));
|
|
78
|
+
}, REQUEST_TIMEOUT_MS);
|
|
79
|
+
pending.set(id, { resolve, reject, timer });
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function receiveFromHost(payload) {
|
|
84
|
+
if (!payload || typeof payload !== "object") return;
|
|
85
|
+
if (payload.type === "shared-object-updated" && payload.key) {
|
|
86
|
+
sharedObjects.set(payload.key, payload.value);
|
|
87
|
+
}
|
|
88
|
+
window.dispatchEvent(new MessageEvent("message", {
|
|
89
|
+
data: payload,
|
|
90
|
+
origin: window.location.origin,
|
|
91
|
+
source: window,
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function sendMessageFromView(message) {
|
|
96
|
+
if (message?.type === "shared-object-set" && message.key) {
|
|
97
|
+
sharedObjects.set(message.key, message.value);
|
|
98
|
+
receiveFromHost({ type: "shared-object-updated", key: message.key, value: message.value });
|
|
99
|
+
}
|
|
100
|
+
return request("view-message", message).catch(() => undefined);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function sendWorkerMessageFromView(workerId, message) {
|
|
104
|
+
return request("worker-message", { workerId, message }).catch(() => undefined);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function subscribeToWorkerMessages(workerId, callback) {
|
|
108
|
+
const handler = (event) => {
|
|
109
|
+
const message = event.data;
|
|
110
|
+
if (message?.type === `codex_desktop:worker:${workerId}:for-view`) {
|
|
111
|
+
callback(message.payload);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
window.addEventListener("message", handler);
|
|
115
|
+
return () => window.removeEventListener("message", handler);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getSystemThemeVariant() {
|
|
119
|
+
return window.matchMedia?.("(prefers-color-scheme: dark)")?.matches ? "dark" : "light";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function subscribeToSystemThemeVariant(callback) {
|
|
123
|
+
const query = window.matchMedia?.("(prefers-color-scheme: dark)");
|
|
124
|
+
if (!query) return () => {};
|
|
125
|
+
const listener = () => callback(getSystemThemeVariant());
|
|
126
|
+
query.addEventListener?.("change", listener);
|
|
127
|
+
return () => query.removeEventListener?.("change", listener);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
window.codexWindowType = "electron";
|
|
131
|
+
window.electronBridge = {
|
|
132
|
+
windowType: "electron",
|
|
133
|
+
sendMessageFromView,
|
|
134
|
+
getPathForFile(file) {
|
|
135
|
+
return file?.path || file?.name || "";
|
|
136
|
+
},
|
|
137
|
+
sendWorkerMessageFromView,
|
|
138
|
+
subscribeToWorkerMessages,
|
|
139
|
+
showContextMenu(options) {
|
|
140
|
+
return request("show-context-menu", options).catch(() => null);
|
|
141
|
+
},
|
|
142
|
+
showApplicationMenu(options) {
|
|
143
|
+
return request("show-application-menu", options).catch(() => null);
|
|
144
|
+
},
|
|
145
|
+
getFastModeRolloutMetrics() {
|
|
146
|
+
return Promise.resolve(null);
|
|
147
|
+
},
|
|
148
|
+
getSharedObjectSnapshotValue(key) {
|
|
149
|
+
return sharedObjects.get(key);
|
|
150
|
+
},
|
|
151
|
+
getSystemThemeVariant,
|
|
152
|
+
subscribeToSystemThemeVariant,
|
|
153
|
+
triggerSentryTestError() {
|
|
154
|
+
throw new Error("Codex WebApp browser Sentry test");
|
|
155
|
+
},
|
|
156
|
+
getSentryInitOptions() {
|
|
157
|
+
return {
|
|
158
|
+
appVersion: "0.0.0",
|
|
159
|
+
buildFlavor: "prod",
|
|
160
|
+
buildNumber: "0",
|
|
161
|
+
dsn: null,
|
|
162
|
+
environment: "browser",
|
|
163
|
+
codexAppSessionId: APP_SESSION_ID,
|
|
164
|
+
release: null,
|
|
165
|
+
};
|
|
166
|
+
},
|
|
167
|
+
getAppSessionId() {
|
|
168
|
+
return APP_SESSION_ID;
|
|
169
|
+
},
|
|
170
|
+
getBuildFlavor() {
|
|
171
|
+
return "prod";
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
connect();
|
|
176
|
+
})();
|
package/src/browserSmoke.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
|
|
4
|
-
const DEFAULT_TEXT = "
|
|
4
|
+
const DEFAULT_TEXT = "Codex";
|
|
5
5
|
const DEFAULT_MAX_RESPONSE_MS = 5_000;
|
|
6
6
|
|
|
7
7
|
export function parseSmokeArgs(args = []) {
|
|
@@ -66,7 +66,7 @@ async function runBrowserSmokeWithOptions(options) {
|
|
|
66
66
|
waitUntil: "networkidle",
|
|
67
67
|
timeout: options.timeoutMs,
|
|
68
68
|
});
|
|
69
|
-
await page.
|
|
69
|
+
await page.waitForFunction((needle) => document.body?.innerText.includes(needle), options.text, {
|
|
70
70
|
timeout: options.timeoutMs,
|
|
71
71
|
});
|
|
72
72
|
if (options.screenshot) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export {
|
|
2
|
+
BROWSER_PRELOAD_ROUTE,
|
|
3
|
+
DEFAULT_CODEX_APP_ASAR,
|
|
4
|
+
DEFAULT_CODEX_APP_PATH,
|
|
5
|
+
DEFAULT_RENDERER_CACHE_ROOT,
|
|
6
|
+
createStaticRendererAssetSource as createStaticRenderer,
|
|
7
|
+
headersForAsset,
|
|
8
|
+
isUnsafeRendererPath,
|
|
9
|
+
matchRendererAsset,
|
|
10
|
+
prepareRendererAssetSource as prepareCodexAppRenderer,
|
|
11
|
+
streamAsset,
|
|
12
|
+
} from "./rendererAssetSource.js";
|
package/src/codexWeb.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
export const CODEX_WEB_UPSTREAM = "github:0xcaff/codex-web";
|
|
2
|
-
export const CODEX_WEB_COMMIT = "585613f5a3a355af5aefc388ca4e31b07a472cda";
|
|
3
|
-
export const CODEX_WEB_REFERENCE = `${CODEX_WEB_UPSTREAM}#${CODEX_WEB_COMMIT}`;
|
|
4
1
|
export const DEFAULT_WEB_HOST = "127.0.0.1";
|
|
5
2
|
export const DEFAULT_WEB_PORT = 8214;
|
|
6
3
|
|
|
@@ -21,21 +18,17 @@ export function buildWebUrl({ host = DEFAULT_WEB_HOST, port = DEFAULT_WEB_PORT }
|
|
|
21
18
|
export function assertSafeHost(host, { allowNonLoopback = false } = {}) {
|
|
22
19
|
if (allowNonLoopback || isLoopbackHost(host)) return;
|
|
23
20
|
throw new Error(
|
|
24
|
-
"Refusing to bind
|
|
21
|
+
"Refusing to bind Codex WebApp to a non-loopback host without --allow-non-loopback. Put Tailscale, Cloudflare Access, WireGuard, SSH tunneling, or an equivalent trusted boundary in front before remote access.",
|
|
25
22
|
);
|
|
26
23
|
}
|
|
27
24
|
|
|
28
|
-
export function
|
|
29
|
-
return
|
|
30
|
-
"--yes",
|
|
31
|
-
"--package",
|
|
32
|
-
CODEX_WEB_REFERENCE,
|
|
33
|
-
"codex-web",
|
|
34
|
-
"--host",
|
|
25
|
+
export function buildLocalServerSummary({ host = DEFAULT_WEB_HOST, port = DEFAULT_WEB_PORT } = {}) {
|
|
26
|
+
return {
|
|
35
27
|
host,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
port,
|
|
29
|
+
url: buildWebUrl({ host, port }),
|
|
30
|
+
runtime: "package-owned Codex App renderer bridge",
|
|
31
|
+
};
|
|
39
32
|
}
|
|
40
33
|
|
|
41
34
|
export function isLoopbackHost(host) {
|
package/src/commands.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { createServer } from "node:net";
|
|
3
3
|
import { createInterface } from "node:readline/promises";
|
|
4
4
|
import { stdin as input, stdout as output } from "node:process";
|
|
@@ -9,10 +9,10 @@ import {
|
|
|
9
9
|
MIN_CODEX_VERSION,
|
|
10
10
|
} from "./version.js";
|
|
11
11
|
import { runSmoke } from "./browserSmoke.js";
|
|
12
|
+
import { startLocalServer } from "./localServer.js";
|
|
12
13
|
import {
|
|
13
|
-
CODEX_WEB_REFERENCE,
|
|
14
14
|
assertSafeHost,
|
|
15
|
-
|
|
15
|
+
buildLocalServerSummary,
|
|
16
16
|
buildWebUrl,
|
|
17
17
|
parseStartArgs,
|
|
18
18
|
} from "./codexWeb.js";
|
|
@@ -60,8 +60,9 @@ export function main(argv = process.argv) {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
export function doctor() {
|
|
63
|
-
const codex = spawnSync("codex", ["--version"], { encoding: "utf8" });
|
|
64
63
|
const codexPath = resolveCodexPath();
|
|
64
|
+
const codexCommand = codexPath || "codex";
|
|
65
|
+
const codex = spawnSync(codexCommand, ["--version"], { encoding: "utf8" });
|
|
65
66
|
if (codex.error || codex.status !== 0) {
|
|
66
67
|
fail([
|
|
67
68
|
"Codex CLI was not found.",
|
|
@@ -89,7 +90,7 @@ export function doctor() {
|
|
|
89
90
|
]);
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
const helpResult = spawnSync(
|
|
93
|
+
const helpResult = spawnSync(codexCommand, ["remote-control", "--help"], {
|
|
93
94
|
encoding: "utf8",
|
|
94
95
|
});
|
|
95
96
|
if (helpResult.status !== 0) {
|
|
@@ -102,12 +103,25 @@ export function doctor() {
|
|
|
102
103
|
]);
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
const appServerResult = spawnSync(codexCommand, ["app-server", "--help"], {
|
|
107
|
+
encoding: "utf8",
|
|
108
|
+
});
|
|
109
|
+
if (appServerResult.status !== 0) {
|
|
110
|
+
fail([
|
|
111
|
+
"`codex app-server --help` did not run successfully.",
|
|
112
|
+
"",
|
|
113
|
+
"Codex is installed, but the local app-server substrate is unavailable.",
|
|
114
|
+
"Update Codex and try again:",
|
|
115
|
+
" npm install -g @openai/codex@latest",
|
|
116
|
+
]);
|
|
117
|
+
}
|
|
118
|
+
|
|
105
119
|
ok([
|
|
106
120
|
BANNER,
|
|
107
121
|
`Codex CLI is ready: ${versionText}`,
|
|
108
122
|
`Codex executable: ${codexPath || "unknown"}`,
|
|
109
123
|
"`codex remote-control` is available.",
|
|
110
|
-
`codex-
|
|
124
|
+
"`codex app-server` is available.",
|
|
111
125
|
"Next:",
|
|
112
126
|
" npx -y codex-webapp start",
|
|
113
127
|
"",
|
|
@@ -117,20 +131,16 @@ export function doctor() {
|
|
|
117
131
|
|
|
118
132
|
export async function start(args = []) {
|
|
119
133
|
const options = parseStartArgs(args);
|
|
120
|
-
const
|
|
134
|
+
const codexPath = resolveCodexPath();
|
|
135
|
+
const codexCommand = codexPath || process.env.CODEX_CLI_PATH || "codex";
|
|
136
|
+
const check = spawnSync(codexCommand, ["app-server", "--help"], {
|
|
121
137
|
encoding: "utf8",
|
|
122
138
|
});
|
|
123
|
-
|
|
124
|
-
fail([
|
|
125
|
-
"Cannot start because `codex remote-control` is unavailable.",
|
|
126
|
-
"Run:",
|
|
127
|
-
" npx codex-webapp doctor",
|
|
128
|
-
]);
|
|
129
|
-
}
|
|
139
|
+
const appServerReady = check.status === 0;
|
|
130
140
|
|
|
131
141
|
assertSafeHost(options.host, { allowNonLoopback: options.allowNonLoopback });
|
|
132
142
|
const plannedWebUrl = buildWebUrl({ host: options.host, port: options.port });
|
|
133
|
-
const
|
|
143
|
+
const localServer = buildLocalServerSummary({
|
|
134
144
|
host: options.host,
|
|
135
145
|
port: options.port,
|
|
136
146
|
});
|
|
@@ -139,12 +149,10 @@ export async function start(args = []) {
|
|
|
139
149
|
ok([
|
|
140
150
|
BANNER,
|
|
141
151
|
"Dry run passed.",
|
|
142
|
-
`Would start
|
|
152
|
+
`Would start Codex WebApp: ${plannedWebUrl}`,
|
|
153
|
+
`Runtime: ${localServer.runtime}`,
|
|
143
154
|
"",
|
|
144
155
|
...codexAppFlowLines(plannedWebUrl),
|
|
145
|
-
"",
|
|
146
|
-
"Would start:",
|
|
147
|
-
` npx ${npxArgs.join(" ")}`,
|
|
148
156
|
]);
|
|
149
157
|
return;
|
|
150
158
|
}
|
|
@@ -153,7 +161,7 @@ export async function start(args = []) {
|
|
|
153
161
|
|
|
154
162
|
if (!options.yes) {
|
|
155
163
|
console.log(BANNER);
|
|
156
|
-
console.log(`About to start
|
|
164
|
+
console.log(`About to start Codex WebApp: ${plannedWebUrl}`);
|
|
157
165
|
console.log("Default expectation: keep it on localhost or behind Tailscale, Cloudflare Access, or an equivalent trusted boundary.");
|
|
158
166
|
console.log("Anyone who can reach this URL can operate Codex on this host.");
|
|
159
167
|
const rl = createInterface({ input, output });
|
|
@@ -165,22 +173,21 @@ export async function start(args = []) {
|
|
|
165
173
|
}
|
|
166
174
|
}
|
|
167
175
|
|
|
168
|
-
console.log(
|
|
176
|
+
console.log("Starting Codex WebApp local server...");
|
|
177
|
+
if (!appServerReady) {
|
|
178
|
+
console.log("Warning: `codex app-server --help` did not pass. The UI will still open, but health will show the Codex substrate issue.");
|
|
179
|
+
console.log("Run `npx codex-webapp doctor` in another terminal for the exact fix.");
|
|
180
|
+
}
|
|
169
181
|
console.log(`Open: ${plannedWebUrl}`);
|
|
170
182
|
console.log("");
|
|
171
183
|
for (const line of codexAppFlowLines(plannedWebUrl)) console.log(line);
|
|
172
184
|
console.log("");
|
|
173
185
|
console.log("Keep this terminal open. Expose it only through a trusted local, Tailscale, Cloudflare Access, or equivalent boundary.");
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
CODEX_CLI_PATH: codexPath || process.env.CODEX_CLI_PATH || "codex",
|
|
180
|
-
},
|
|
181
|
-
});
|
|
182
|
-
child.on("exit", (code) => {
|
|
183
|
-
process.exit(code ?? 0);
|
|
186
|
+
await startLocalServer({
|
|
187
|
+
host: options.host,
|
|
188
|
+
port: options.port,
|
|
189
|
+
codexPath: codexCommand,
|
|
190
|
+
cwd: process.cwd(),
|
|
184
191
|
});
|
|
185
192
|
}
|
|
186
193
|
|
|
@@ -206,7 +213,7 @@ export function help() {
|
|
|
206
213
|
|
|
207
214
|
Commands:
|
|
208
215
|
codex-webapp doctor Check Codex CLI >= ${MIN_CODEX_VERSION}
|
|
209
|
-
codex-webapp start Confirm, then start
|
|
216
|
+
codex-webapp start Confirm, then start Codex WebApp
|
|
210
217
|
codex-webapp start --dry-run
|
|
211
218
|
codex-webapp start --yes Start without the confirmation prompt
|
|
212
219
|
codex-webapp start --port 8214
|
|
@@ -239,7 +246,7 @@ function fail(lines) {
|
|
|
239
246
|
}
|
|
240
247
|
|
|
241
248
|
export function resolveCodexPath() {
|
|
242
|
-
const result = spawnSync("bash", ["-
|
|
249
|
+
const result = spawnSync("bash", ["-l", "-c", "which codex"], {
|
|
243
250
|
encoding: "utf8",
|
|
244
251
|
});
|
|
245
252
|
if (result.status !== 0) return "";
|