relmio 0.5.0 → 0.6.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/CHANGELOG.md +24 -0
- package/README.md +29 -14
- package/docs/architecture.md +21 -9
- package/docs/local-endpoints-spec.md +101 -13
- package/docs/local-endpoints.md +83 -21
- package/docs/security.md +40 -15
- package/package.json +1 -1
- package/src/domain/local-endpoints.js +183 -13
- package/src/gateway/codex-chat.js +754 -0
- package/src/services/codex-login.js +11 -3
- package/src/services/local-installer.js +94 -14
- package/src/ui/local.html +20 -8
- package/src/ui/local.js +99 -37
- package/src/web/server.js +11 -3
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
|
|
10
10
|
const COMPOSE_FILE_NAME = "docker-compose.yml";
|
|
11
11
|
const PROJECT_NAME_PATTERN =
|
|
12
|
-
/^relmio-codex-chatgpt-[a-f0-9]{32}$/u;
|
|
12
|
+
/^relmio-codex-(chatgpt|chat)-[a-f0-9]{32}$/u;
|
|
13
13
|
const DEFAULT_RESPONSE_TIMEOUT_MS = 15_000;
|
|
14
14
|
const DEFAULT_COMPLETION_TIMEOUT_MS = 300_000;
|
|
15
15
|
const DEFAULT_TERMINATION_GRACE_MS = 2_000;
|
|
@@ -88,7 +88,12 @@ function validateOptions({
|
|
|
88
88
|
if (maxLineBytes > maxStdoutBytes) {
|
|
89
89
|
throw new TypeError("maxLineBytes cannot exceed maxStdoutBytes.");
|
|
90
90
|
}
|
|
91
|
-
|
|
91
|
+
const projectKind = PROJECT_NAME_PATTERN.exec(projectName)?.[1];
|
|
92
|
+
return {
|
|
93
|
+
dockerHost: validatedDockerHost,
|
|
94
|
+
projectName,
|
|
95
|
+
serviceName: projectKind === "chat" ? "codex-chat" : "codex",
|
|
96
|
+
};
|
|
92
97
|
}
|
|
93
98
|
|
|
94
99
|
function validateVerificationUrl(value) {
|
|
@@ -205,7 +210,10 @@ export async function startCodexDeviceLogin({
|
|
|
205
210
|
"run",
|
|
206
211
|
"--rm",
|
|
207
212
|
"--no-deps",
|
|
208
|
-
"codex"
|
|
213
|
+
...(validated.serviceName === "codex-chat"
|
|
214
|
+
? ["--entrypoint", "codex"]
|
|
215
|
+
: []),
|
|
216
|
+
validated.serviceName,
|
|
209
217
|
"app-server",
|
|
210
218
|
"--strict-config",
|
|
211
219
|
"--stdio",
|
|
@@ -6,6 +6,10 @@ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
createCodexComposeFile,
|
|
9
|
+
createCodexChatComposeFile,
|
|
10
|
+
createCodexChatConfig,
|
|
11
|
+
createCodexChatDockerfile,
|
|
12
|
+
createCodexChatRequirements,
|
|
9
13
|
createCodexConfig,
|
|
10
14
|
createCodexDockerfile,
|
|
11
15
|
createCodexRequirements,
|
|
@@ -39,6 +43,11 @@ const PROJECTS = Object.freeze({
|
|
|
39
43
|
serviceName: "codex",
|
|
40
44
|
containerPort: 4_500,
|
|
41
45
|
}),
|
|
46
|
+
"codex-chat": Object.freeze({
|
|
47
|
+
projectPrefix: "relmio-codex-chat",
|
|
48
|
+
serviceName: "codex-chat",
|
|
49
|
+
containerPort: 14_501,
|
|
50
|
+
}),
|
|
42
51
|
});
|
|
43
52
|
const DOCKER_SELECTION_VARIABLES = Object.freeze([
|
|
44
53
|
"DOCKER_HOST",
|
|
@@ -613,10 +622,9 @@ function replaceClientCredentialVerifier({ target, composeFile, tokenSha256 }) {
|
|
|
613
622
|
throw new Error("The managed local endpoint configuration is invalid.");
|
|
614
623
|
}
|
|
615
624
|
|
|
616
|
-
const pattern =
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
: /^([ \t]*-[ \t]+--ws-token-sha256[ \t]*\n[ \t]*-[ \t]+)[a-f0-9]{64}([ \t]*)$/gmu;
|
|
625
|
+
const pattern = target === "openai-api" || target === "codex-chat"
|
|
626
|
+
? /^([ \t]*RELMIO_GATEWAY_TOKEN_SHA256:[ \t]*)[a-f0-9]{64}([ \t]*)$/gmu
|
|
627
|
+
: /^([ \t]*-[ \t]+--ws-token-sha256[ \t]*\n[ \t]*-[ \t]+)[a-f0-9]{64}([ \t]*)$/gmu;
|
|
620
628
|
const matches = [...composeFile.matchAll(pattern)];
|
|
621
629
|
if (matches.length !== 1) {
|
|
622
630
|
throw new Error("The managed local endpoint configuration is invalid.");
|
|
@@ -745,30 +753,34 @@ export async function getLocalDockerStatus({
|
|
|
745
753
|
}
|
|
746
754
|
|
|
747
755
|
export async function restartLocalCodex(
|
|
748
|
-
{ installDirectory },
|
|
756
|
+
{ installDirectory, target = "codex-chatgpt" },
|
|
749
757
|
dependencies = {},
|
|
750
758
|
) {
|
|
759
|
+
const safeTarget = validateLocalTarget(target);
|
|
760
|
+
if (safeTarget !== "codex-chatgpt" && safeTarget !== "codex-chat") {
|
|
761
|
+
throw new TypeError("The Codex login target is invalid.");
|
|
762
|
+
}
|
|
751
763
|
const runProcess = dependencies.runProcess ?? runLocalProcess;
|
|
752
764
|
const releaseLock = dependencies.changeLockHeld === true
|
|
753
765
|
? async () => {}
|
|
754
766
|
: await acquireLocalProjectLock(
|
|
755
|
-
{ installRoot: installDirectory, target:
|
|
767
|
+
{ installRoot: installDirectory, target: safeTarget },
|
|
756
768
|
dependencies,
|
|
757
769
|
);
|
|
758
770
|
try {
|
|
759
771
|
const attested = await attestLocalCodexInstallation(
|
|
760
|
-
{ installDirectory },
|
|
772
|
+
{ installDirectory, target: safeTarget },
|
|
761
773
|
dependencies,
|
|
762
774
|
);
|
|
763
775
|
|
|
764
776
|
await runOrThrow(runProcess, {
|
|
765
777
|
label: "Codex credential reload",
|
|
766
778
|
file: "docker",
|
|
767
|
-
args: createComposeArgs(
|
|
779
|
+
args: createComposeArgs(safeTarget, attested.projectName, [
|
|
768
780
|
"restart",
|
|
769
781
|
"--timeout",
|
|
770
782
|
"10",
|
|
771
|
-
|
|
783
|
+
PROJECTS[safeTarget].serviceName,
|
|
772
784
|
]),
|
|
773
785
|
cwd: installDirectory,
|
|
774
786
|
dockerHost: attested.dockerHost,
|
|
@@ -776,14 +788,14 @@ export async function restartLocalCodex(
|
|
|
776
788
|
await runOrThrow(runProcess, {
|
|
777
789
|
label: "Codex readiness wait",
|
|
778
790
|
file: "docker",
|
|
779
|
-
args: createComposeArgs(
|
|
791
|
+
args: createComposeArgs(safeTarget, attested.projectName, [
|
|
780
792
|
"up",
|
|
781
793
|
"-d",
|
|
782
794
|
"--wait",
|
|
783
795
|
"--wait-timeout",
|
|
784
796
|
"90",
|
|
785
797
|
"--no-deps",
|
|
786
|
-
|
|
798
|
+
PROJECTS[safeTarget].serviceName,
|
|
787
799
|
]),
|
|
788
800
|
cwd: installDirectory,
|
|
789
801
|
dockerHost: attested.dockerHost,
|
|
@@ -1111,7 +1123,7 @@ function parseModelIds(value) {
|
|
|
1111
1123
|
}
|
|
1112
1124
|
|
|
1113
1125
|
async function verifyHttpEndpoint({ plan, clientCredential, fetchImpl }) {
|
|
1114
|
-
const healthPath = plan.target === "
|
|
1126
|
+
const healthPath = plan.target === "codex-chatgpt" ? "/readyz" : "/health";
|
|
1115
1127
|
const httpEndpoint = `http://127.0.0.1:${plan.port}${healthPath}`;
|
|
1116
1128
|
let health;
|
|
1117
1129
|
try {
|
|
@@ -1126,6 +1138,29 @@ async function verifyHttpEndpoint({ plan, clientCredential, fetchImpl }) {
|
|
|
1126
1138
|
throw new Error("The local endpoint did not pass its readiness check.");
|
|
1127
1139
|
}
|
|
1128
1140
|
|
|
1141
|
+
if (plan.target === "codex-chat" && clientCredential !== undefined) {
|
|
1142
|
+
let verification;
|
|
1143
|
+
try {
|
|
1144
|
+
verification = await fetchImpl(
|
|
1145
|
+
`http://127.0.0.1:${plan.port}/auth/verify`,
|
|
1146
|
+
{
|
|
1147
|
+
method: "GET",
|
|
1148
|
+
headers: { Authorization: `Bearer ${clientCredential}` },
|
|
1149
|
+
signal: AbortSignal.timeout(10_000),
|
|
1150
|
+
},
|
|
1151
|
+
);
|
|
1152
|
+
} catch {
|
|
1153
|
+
throw new Error(
|
|
1154
|
+
"The Codex Chat client credential could not be verified.",
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
if (!verification.ok) {
|
|
1158
|
+
throw new Error(
|
|
1159
|
+
"The Codex Chat client credential could not be verified.",
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1129
1164
|
if (plan.target !== "openai-api" || clientCredential === undefined) {
|
|
1130
1165
|
return [];
|
|
1131
1166
|
}
|
|
@@ -1275,6 +1310,13 @@ async function defaultReadGatewaySource() {
|
|
|
1275
1310
|
);
|
|
1276
1311
|
}
|
|
1277
1312
|
|
|
1313
|
+
async function defaultReadCodexChatSource() {
|
|
1314
|
+
return defaultFileSystem.readFile(
|
|
1315
|
+
new URL("../gateway/codex-chat.js", import.meta.url),
|
|
1316
|
+
"utf8",
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1278
1320
|
async function attestManagedLocalEndpoint(
|
|
1279
1321
|
{ target, installDirectory, missingMessage, notRunningMessage },
|
|
1280
1322
|
{
|
|
@@ -1334,12 +1376,16 @@ async function attestManagedLocalEndpoint(
|
|
|
1334
1376
|
}
|
|
1335
1377
|
|
|
1336
1378
|
export async function attestLocalCodexInstallation(
|
|
1337
|
-
{ installDirectory },
|
|
1379
|
+
{ installDirectory, target = "codex-chatgpt" },
|
|
1338
1380
|
dependencies = {},
|
|
1339
1381
|
) {
|
|
1382
|
+
const safeTarget = validateLocalTarget(target);
|
|
1383
|
+
if (safeTarget !== "codex-chatgpt" && safeTarget !== "codex-chat") {
|
|
1384
|
+
throw new TypeError("The Codex login target is invalid.");
|
|
1385
|
+
}
|
|
1340
1386
|
const attested = await attestManagedLocalEndpoint(
|
|
1341
1387
|
{
|
|
1342
|
-
target:
|
|
1388
|
+
target: safeTarget,
|
|
1343
1389
|
installDirectory,
|
|
1344
1390
|
missingMessage: "Install the local Codex endpoint before signing in.",
|
|
1345
1391
|
notRunningMessage: "The managed local Codex endpoint is not running.",
|
|
@@ -1611,6 +1657,7 @@ export async function installLocalEndpoint(
|
|
|
1611
1657
|
randomBytes = createRandomBytes,
|
|
1612
1658
|
isPortAvailable = isLoopbackPortAvailable,
|
|
1613
1659
|
readGatewaySource = defaultReadGatewaySource,
|
|
1660
|
+
readCodexChatSource = defaultReadCodexChatSource,
|
|
1614
1661
|
fetchImpl = fetch,
|
|
1615
1662
|
verifyCodexCapability = verifyCodexWebSocketCapability,
|
|
1616
1663
|
platform = process.platform,
|
|
@@ -1727,6 +1774,39 @@ export async function installLocalEndpoint(
|
|
|
1727
1774
|
gatewaySource,
|
|
1728
1775
|
0o600,
|
|
1729
1776
|
);
|
|
1777
|
+
} else if (normalizedPlan.target === "codex-chat") {
|
|
1778
|
+
const gatewaySource = await readCodexChatSource();
|
|
1779
|
+
if (
|
|
1780
|
+
typeof gatewaySource !== "string" ||
|
|
1781
|
+
gatewaySource.length === 0 ||
|
|
1782
|
+
gatewaySource.length > 512 * 1024
|
|
1783
|
+
) {
|
|
1784
|
+
throw new Error("The packaged Codex Chat runtime is invalid.");
|
|
1785
|
+
}
|
|
1786
|
+
dockerfile = createCodexChatDockerfile();
|
|
1787
|
+
composeFile = createCodexChatComposeFile({
|
|
1788
|
+
port: normalizedPlan.port,
|
|
1789
|
+
tokenSha256,
|
|
1790
|
+
installId,
|
|
1791
|
+
});
|
|
1792
|
+
await writeManagedFile(
|
|
1793
|
+
fileSystem,
|
|
1794
|
+
join(installRoot, "gateway.mjs"),
|
|
1795
|
+
gatewaySource,
|
|
1796
|
+
0o600,
|
|
1797
|
+
);
|
|
1798
|
+
await writeManagedFile(
|
|
1799
|
+
fileSystem,
|
|
1800
|
+
join(installRoot, "config.toml"),
|
|
1801
|
+
createCodexChatConfig(),
|
|
1802
|
+
0o600,
|
|
1803
|
+
);
|
|
1804
|
+
await writeManagedFile(
|
|
1805
|
+
fileSystem,
|
|
1806
|
+
join(installRoot, "requirements.toml"),
|
|
1807
|
+
createCodexChatRequirements(),
|
|
1808
|
+
0o600,
|
|
1809
|
+
);
|
|
1730
1810
|
} else {
|
|
1731
1811
|
dockerfile = createCodexDockerfile();
|
|
1732
1812
|
composeFile = createCodexComposeFile({
|
package/src/ui/local.html
CHANGED
|
@@ -92,8 +92,8 @@
|
|
|
92
92
|
<section class="intro" aria-labelledby="page-title">
|
|
93
93
|
<h1 id="page-title">Run Relmio on localhost</h1>
|
|
94
94
|
<p class="lede">
|
|
95
|
-
Choose the official credential path that matches your client.
|
|
96
|
-
options run in
|
|
95
|
+
Choose the official credential path that matches your client. All
|
|
96
|
+
three options run in hardened Docker containers and bind only to
|
|
97
97
|
<code>127.0.0.1</code>.
|
|
98
98
|
</p>
|
|
99
99
|
</section>
|
|
@@ -200,6 +200,20 @@
|
|
|
200
200
|
</span>
|
|
201
201
|
</span>
|
|
202
202
|
</label>
|
|
203
|
+
|
|
204
|
+
<label class="target-card warning-card">
|
|
205
|
+
<input type="radio" name="target" value="codex-chat" />
|
|
206
|
+
<span class="target-card-copy">
|
|
207
|
+
<strong>Codex Chat Adapter</strong>
|
|
208
|
+
<span class="target-meta">Experimental · trusted local backends or development servers only</span>
|
|
209
|
+
<span>
|
|
210
|
+
Relmio-specific authenticated <code>POST /chat</code>, backed by
|
|
211
|
+
official Codex App Server and ChatGPT sign-in. This is not an
|
|
212
|
+
OpenAI-compatible <code>/v1</code> endpoint and browser bundles
|
|
213
|
+
must never connect to it.
|
|
214
|
+
</span>
|
|
215
|
+
</span>
|
|
216
|
+
</label>
|
|
203
217
|
</fieldset>
|
|
204
218
|
|
|
205
219
|
<div class="field-grid local-fields">
|
|
@@ -358,8 +372,8 @@
|
|
|
358
372
|
</div>
|
|
359
373
|
|
|
360
374
|
<aside id="codex-install-warning" class="high-trust-warning" hidden>
|
|
361
|
-
<strong>High-trust capability</strong>
|
|
362
|
-
<p>
|
|
375
|
+
<strong id="codex-install-warning-title">High-trust capability</strong>
|
|
376
|
+
<p id="codex-install-warning-detail">
|
|
363
377
|
Anyone holding the generated client credential can control Codex
|
|
364
378
|
inside its isolated container, act through your signed-in ChatGPT
|
|
365
379
|
session, and may be able to recover that container's ChatGPT session
|
|
@@ -445,10 +459,8 @@
|
|
|
445
459
|
</dl>
|
|
446
460
|
|
|
447
461
|
<aside id="codex-production-warning" class="high-trust-warning" hidden>
|
|
448
|
-
<strong>Experimental
|
|
449
|
-
<p>
|
|
450
|
-
Codex App Server WebSocket is experimental and unsupported for production workloads.
|
|
451
|
-
</p>
|
|
462
|
+
<strong id="codex-production-warning-title">Experimental Codex integration</strong>
|
|
463
|
+
<p id="codex-production-warning-detail"></p>
|
|
452
464
|
</aside>
|
|
453
465
|
|
|
454
466
|
<section id="codex-login" class="codex-login" aria-labelledby="codex-login-title" hidden>
|
package/src/ui/local.js
CHANGED
|
@@ -129,6 +129,14 @@ function readAllowedOrigins() {
|
|
|
129
129
|
.filter((value) => value !== "");
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
function isCodexChat(target) {
|
|
133
|
+
return target === "codex-chat";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isOpenAiApi(target) {
|
|
137
|
+
return target === "openai-api";
|
|
138
|
+
}
|
|
139
|
+
|
|
132
140
|
function invalidatePlan() {
|
|
133
141
|
state.planId = null;
|
|
134
142
|
state.plan = null;
|
|
@@ -140,15 +148,24 @@ function renderTarget() {
|
|
|
140
148
|
state.target = selectedTarget();
|
|
141
149
|
invalidatePlan();
|
|
142
150
|
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
element("
|
|
146
|
-
|
|
151
|
+
const openAiApi = isOpenAiApi(state.target);
|
|
152
|
+
const codexChat = isCodexChat(state.target);
|
|
153
|
+
element("local-port").value = openAiApi
|
|
154
|
+
? "12435"
|
|
155
|
+
: codexChat
|
|
156
|
+
? "14501"
|
|
157
|
+
: "14500";
|
|
158
|
+
element("origins-field").hidden = !openAiApi;
|
|
159
|
+
element("target-guidance-title").textContent = openAiApi
|
|
147
160
|
? "Uses an OpenAI Platform API key"
|
|
148
|
-
:
|
|
149
|
-
|
|
161
|
+
: codexChat
|
|
162
|
+
? "Uses official Codex ChatGPT sign-in through the experimental Relmio-specific Chat Adapter"
|
|
163
|
+
: "Uses the official Codex ChatGPT sign-in";
|
|
164
|
+
element("target-guidance-detail").textContent = openAiApi
|
|
150
165
|
? "Your Platform key is seeded over stdin into a private Docker volume and is never returned to the browser. A separate local client credential is generated for your apps."
|
|
151
|
-
:
|
|
166
|
+
: codexChat
|
|
167
|
+
? "This exposes Relmio-specific HTTP POST /chat for a trusted local backend or development server. It is not OpenAI /v1, has no CORS, and browser bundles must never connect directly."
|
|
168
|
+
: "This runs Codex App Server as its own experimental protocol. It does not translate the ChatGPT session into a generic OpenAI /v1 API and it does not accept direct browser connections.";
|
|
152
169
|
}
|
|
153
170
|
|
|
154
171
|
function appendPolicyNotice(container, heading, detail) {
|
|
@@ -160,33 +177,44 @@ function appendPolicyNotice(container, heading, detail) {
|
|
|
160
177
|
}
|
|
161
178
|
|
|
162
179
|
function renderPlan(plan) {
|
|
163
|
-
const
|
|
180
|
+
const openAiApi = isOpenAiApi(plan.target);
|
|
181
|
+
const codexChat = isCodexChat(plan.target);
|
|
164
182
|
element("review-endpoint").textContent = plan.endpoint;
|
|
165
|
-
element("review-protocol").textContent =
|
|
183
|
+
element("review-protocol").textContent = openAiApi
|
|
166
184
|
? "OpenAI-compatible HTTP /v1"
|
|
167
|
-
:
|
|
168
|
-
|
|
185
|
+
: codexChat
|
|
186
|
+
? "Relmio Codex Chat HTTP: POST /chat"
|
|
187
|
+
: "Codex App Server JSON-RPC over WebSocket";
|
|
188
|
+
element("review-auth").textContent = openAiApi
|
|
169
189
|
? "OpenAI Platform API key"
|
|
170
190
|
: "ChatGPT sign-in through official Codex";
|
|
171
|
-
element("review-browser").textContent =
|
|
191
|
+
element("review-browser").textContent = openAiApi
|
|
172
192
|
? plan.allowedOrigins.length > 0
|
|
173
193
|
? `Only ${plan.allowedOrigins.length} exact allowed origin(s)`
|
|
174
194
|
: "Native clients only until exact origins are added"
|
|
175
|
-
:
|
|
176
|
-
|
|
177
|
-
|
|
195
|
+
: codexChat
|
|
196
|
+
? "No — trusted local backends and development servers only"
|
|
197
|
+
: "No — trusted native local clients only";
|
|
198
|
+
element("review-origins-row").hidden = !openAiApi;
|
|
199
|
+
element("review-origins").textContent = openAiApi
|
|
178
200
|
? plan.allowedOrigins.length > 0
|
|
179
201
|
? plan.allowedOrigins.join(", ")
|
|
180
202
|
: "None"
|
|
181
203
|
: "";
|
|
182
204
|
element("review-path").textContent = plan.managedPath;
|
|
183
205
|
|
|
184
|
-
if (
|
|
206
|
+
if (openAiApi) {
|
|
185
207
|
appendPolicyNotice(
|
|
186
208
|
element("review-policy"),
|
|
187
209
|
"OpenAI Platform terms and billing apply",
|
|
188
210
|
"This option sends requests to the OpenAI API with your developer Platform key. ChatGPT subscriptions and open-source program benefits do not turn a ChatGPT credential into an API key.",
|
|
189
211
|
);
|
|
212
|
+
} else if (codexChat) {
|
|
213
|
+
appendPolicyNotice(
|
|
214
|
+
element("review-policy"),
|
|
215
|
+
"Experimental Codex Chat Adapter — trusted local backends or development servers only",
|
|
216
|
+
"This option runs a small authenticated Relmio HTTP adapter on loopback. It accepts only POST /chat from a trusted local backend or development server. It has no CORS and is not OpenAI /v1. ChatGPT credentials stay in the isolated Codex Docker volume and never become Platform API keys.",
|
|
217
|
+
);
|
|
190
218
|
} else {
|
|
191
219
|
appendPolicyNotice(
|
|
192
220
|
element("review-policy"),
|
|
@@ -197,18 +225,31 @@ function renderPlan(plan) {
|
|
|
197
225
|
}
|
|
198
226
|
|
|
199
227
|
function prepareInstallPanel() {
|
|
200
|
-
const
|
|
228
|
+
const openAiApi = isOpenAiApi(state.plan.target);
|
|
229
|
+
const codexChat = isCodexChat(state.plan.target);
|
|
201
230
|
const apiKey = element("platform-api-key");
|
|
202
|
-
element("api-key-field").hidden = !
|
|
203
|
-
element("codex-install-warning").hidden =
|
|
204
|
-
|
|
231
|
+
element("api-key-field").hidden = !openAiApi;
|
|
232
|
+
element("codex-install-warning").hidden = openAiApi;
|
|
233
|
+
element("codex-install-warning-title").textContent = codexChat
|
|
234
|
+
? "Credential for trusted local backends or development servers only"
|
|
235
|
+
: "High-trust capability";
|
|
236
|
+
element("codex-install-warning-detail").textContent = codexChat
|
|
237
|
+
? "The generated bearer authorizes chat turns through your signed-in Codex container. Keep it only in a trusted local backend or development server; never put it in browser code."
|
|
238
|
+
: "Anyone holding the generated client credential can control Codex inside its isolated container, act through your signed-in ChatGPT session, and may be able to recover that container's ChatGPT session credential. Treat this capability like your ChatGPT password and give it only to a trusted native local app.";
|
|
239
|
+
apiKey.required = openAiApi;
|
|
205
240
|
apiKey.value = "";
|
|
206
|
-
element("install-intro").textContent =
|
|
241
|
+
element("install-intro").textContent = openAiApi
|
|
207
242
|
? "Enter the OpenAI Platform API key this endpoint will use upstream. It is sent only to this local Relmio process."
|
|
208
|
-
:
|
|
243
|
+
: codexChat
|
|
244
|
+
? "Relmio will install the experimental Codex Chat Adapter and official Codex App Server first. You will complete ChatGPT device sign-in after the container is ready."
|
|
245
|
+
: "Relmio will install the official Codex App Server first. You will complete ChatGPT device sign-in after the container is ready.";
|
|
209
246
|
setButtonLabel(
|
|
210
247
|
element("install-button"),
|
|
211
|
-
|
|
248
|
+
openAiApi
|
|
249
|
+
? "Install OpenAI API endpoint"
|
|
250
|
+
: codexChat
|
|
251
|
+
? "Install Codex Chat Adapter"
|
|
252
|
+
: "Install Codex App Server",
|
|
212
253
|
);
|
|
213
254
|
}
|
|
214
255
|
|
|
@@ -216,29 +257,46 @@ function renderInstallResult(result) {
|
|
|
216
257
|
if (
|
|
217
258
|
typeof result.endpoint !== "string" ||
|
|
218
259
|
typeof result.clientCredential !== "string" ||
|
|
219
|
-
!["openai-api", "codex-chatgpt"].includes(result.target)
|
|
260
|
+
!["openai-api", "codex-chatgpt", "codex-chat"].includes(result.target)
|
|
220
261
|
) {
|
|
221
262
|
throw new Error("The local installer returned an unexpected response.");
|
|
222
263
|
}
|
|
223
264
|
|
|
224
|
-
const
|
|
265
|
+
const openAiApi = isOpenAiApi(result.target);
|
|
266
|
+
const codexChat = isCodexChat(result.target);
|
|
225
267
|
state.installedTarget = result.target;
|
|
226
268
|
element("result-endpoint").textContent = result.endpoint;
|
|
227
269
|
element("result-credential").textContent = result.clientCredential;
|
|
228
|
-
element("codex-production-warning").hidden =
|
|
229
|
-
element("codex-login").hidden =
|
|
230
|
-
element("
|
|
270
|
+
element("codex-production-warning").hidden = openAiApi;
|
|
271
|
+
element("codex-login").hidden = openAiApi;
|
|
272
|
+
element("codex-production-warning-title").textContent = codexChat
|
|
273
|
+
? "Experimental Chat Adapter — trusted local backends or development servers only"
|
|
274
|
+
: "Experimental WebSocket transport";
|
|
275
|
+
element("codex-production-warning-detail").textContent = codexChat
|
|
276
|
+
? "The adapter is for trusted local backends or development servers only. It has a Relmio-specific POST /chat contract, no CORS, and is not OpenAI /v1."
|
|
277
|
+
: "Codex App Server WebSocket is experimental and unsupported for production workloads.";
|
|
278
|
+
element("done-title").textContent = openAiApi
|
|
231
279
|
? "OpenAI API endpoint is ready"
|
|
232
|
-
:
|
|
233
|
-
|
|
280
|
+
: codexChat
|
|
281
|
+
? "Codex Chat Adapter is installed"
|
|
282
|
+
: "Codex App Server is installed";
|
|
283
|
+
element("done-detail").textContent = openAiApi
|
|
234
284
|
? "Copy the endpoint and generated bearer credential into your local app."
|
|
235
|
-
:
|
|
285
|
+
: codexChat
|
|
286
|
+
? "Copy the endpoint and generated bearer credential into your trusted local backend or development server, then sign the isolated Codex container in to ChatGPT."
|
|
287
|
+
: "Copy the endpoint and capability, then sign the isolated Codex container in to ChatGPT.";
|
|
236
288
|
appendPolicyNotice(
|
|
237
289
|
element("client-warning"),
|
|
238
|
-
|
|
239
|
-
|
|
290
|
+
openAiApi
|
|
291
|
+
? "For local OpenAI-compatible clients"
|
|
292
|
+
: codexChat
|
|
293
|
+
? "For trusted local backends or development servers only"
|
|
294
|
+
: "For trusted native Codex clients only",
|
|
295
|
+
openAiApi
|
|
240
296
|
? "Use the generated client credential as the bearer API key. Your upstream Platform key remains private in the managed Docker volume."
|
|
241
|
-
:
|
|
297
|
+
: codexChat
|
|
298
|
+
? "Use this one-time credential only as a Bearer token for the Relmio-specific POST /chat endpoint. It is not an OpenAI API key, has no browser CORS support, and must remain in a trusted local backend or development server."
|
|
299
|
+
: "This capability is not an OpenAI API key. Treat it like your ChatGPT password: the client is trusted to control the isolated container and may recover its ChatGPT session credential. It must speak official Codex App Server JSON-RPC over WebSocket.",
|
|
242
300
|
);
|
|
243
301
|
}
|
|
244
302
|
|
|
@@ -432,7 +490,9 @@ element("install-button").addEventListener("click", async (event) => {
|
|
|
432
490
|
setMessage(
|
|
433
491
|
result.target === "openai-api"
|
|
434
492
|
? "Local OpenAI API endpoint verified. Copy its one-time client credential now."
|
|
435
|
-
:
|
|
493
|
+
: result.target === "codex-chat"
|
|
494
|
+
? "Codex Chat Adapter for trusted local backends or development servers verified. Copy its one-time bearer and complete ChatGPT sign-in."
|
|
495
|
+
: "Codex App Server verified. Copy its one-time capability and complete ChatGPT sign-in.",
|
|
436
496
|
);
|
|
437
497
|
} catch (error) {
|
|
438
498
|
invalidatePlan();
|
|
@@ -494,7 +554,7 @@ element("codex-login-button").addEventListener("click", async (event) => {
|
|
|
494
554
|
try {
|
|
495
555
|
const result = await api("/api/local/codex/login", {
|
|
496
556
|
method: "POST",
|
|
497
|
-
body: {},
|
|
557
|
+
body: { target: state.installedTarget },
|
|
498
558
|
});
|
|
499
559
|
const verificationUrl = validateVerificationUrl(result.verificationUrl);
|
|
500
560
|
const userCode = validateDeviceCode(result.userCode);
|
|
@@ -504,7 +564,9 @@ element("codex-login-button").addEventListener("click", async (event) => {
|
|
|
504
564
|
resultBox.hidden = false;
|
|
505
565
|
setMessage("Open the official OpenAI page and enter the displayed device code.");
|
|
506
566
|
await waitForCodexLogin();
|
|
507
|
-
status.textContent =
|
|
567
|
+
status.textContent = isCodexChat(state.installedTarget)
|
|
568
|
+
? "ChatGPT sign-in completed. Codex Chat Adapter is ready for your trusted local backend or development server."
|
|
569
|
+
: "ChatGPT sign-in completed. Codex is ready for your trusted native client.";
|
|
508
570
|
setMessage("Codex ChatGPT sign-in completed successfully.");
|
|
509
571
|
} catch (error) {
|
|
510
572
|
status.textContent = "ChatGPT sign-in did not complete.";
|
package/src/web/server.js
CHANGED
|
@@ -776,6 +776,10 @@ async function handleApi(request, response, path, state) {
|
|
|
776
776
|
);
|
|
777
777
|
}
|
|
778
778
|
|
|
779
|
+
const target = validateLocalTarget(body?.target ?? "codex-chatgpt");
|
|
780
|
+
if (target !== "codex-chatgpt" && target !== "codex-chat") {
|
|
781
|
+
throw new Error("Choose a Codex local endpoint before signing in.");
|
|
782
|
+
}
|
|
779
783
|
state.codexLoginStartInFlight = true;
|
|
780
784
|
let finishStart;
|
|
781
785
|
const startPromise = new Promise((resolvePromise) => {
|
|
@@ -802,7 +806,7 @@ async function handleApi(request, response, path, state) {
|
|
|
802
806
|
}
|
|
803
807
|
|
|
804
808
|
releaseChangeLock = await state.services.acquireLocalEndpointChangeLock({
|
|
805
|
-
target
|
|
809
|
+
target,
|
|
806
810
|
});
|
|
807
811
|
if (state.closing) {
|
|
808
812
|
throw Object.assign(new Error("The local wizard is closing."), {
|
|
@@ -810,11 +814,12 @@ async function handleApi(request, response, path, state) {
|
|
|
810
814
|
});
|
|
811
815
|
}
|
|
812
816
|
const installDirectory = await state.services.resolveLocalInstallRoot({
|
|
813
|
-
target
|
|
817
|
+
target,
|
|
814
818
|
});
|
|
815
819
|
const { dockerHost, projectName } =
|
|
816
820
|
await state.services.attestLocalCodexInstallation({
|
|
817
821
|
installDirectory,
|
|
822
|
+
...(target === "codex-chat" ? { target } : {}),
|
|
818
823
|
});
|
|
819
824
|
|
|
820
825
|
const attempt = await state.services.startCodexDeviceLogin({
|
|
@@ -847,7 +852,10 @@ async function handleApi(request, response, path, state) {
|
|
|
847
852
|
return;
|
|
848
853
|
}
|
|
849
854
|
await state.services.restartLocalCodex(
|
|
850
|
-
{
|
|
855
|
+
{
|
|
856
|
+
installDirectory,
|
|
857
|
+
...(target === "codex-chat" ? { target } : {}),
|
|
858
|
+
},
|
|
851
859
|
{ changeLockHeld: true },
|
|
852
860
|
);
|
|
853
861
|
if (state.codexLogin === login && !state.closing) {
|