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.
@@ -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
- return { dockerHost: validatedDockerHost, projectName };
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
- target === "openai-api"
618
- ? /^([ \t]*RELMIO_GATEWAY_TOKEN_SHA256:[ \t]*)[a-f0-9]{64}([ \t]*)$/gmu
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: "codex-chatgpt" },
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("codex-chatgpt", attested.projectName, [
779
+ args: createComposeArgs(safeTarget, attested.projectName, [
768
780
  "restart",
769
781
  "--timeout",
770
782
  "10",
771
- "codex",
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("codex-chatgpt", attested.projectName, [
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
- "codex",
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 === "openai-api" ? "/health" : "/readyz";
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: "codex-chatgpt",
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. Both
96
- options run in a hardened Docker container and bind only to
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 WebSocket transport</strong>
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 isOpenAiApi = state.target === "openai-api";
144
- element("local-port").value = isOpenAiApi ? "12435" : "14500";
145
- element("origins-field").hidden = !isOpenAiApi;
146
- element("target-guidance-title").textContent = isOpenAiApi
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
- : "Uses the official Codex ChatGPT sign-in";
149
- element("target-guidance-detail").textContent = isOpenAiApi
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
- : "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.";
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 isOpenAiApi = plan.target === "openai-api";
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 = isOpenAiApi
183
+ element("review-protocol").textContent = openAiApi
166
184
  ? "OpenAI-compatible HTTP /v1"
167
- : "Codex App Server JSON-RPC over WebSocket";
168
- element("review-auth").textContent = isOpenAiApi
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 = isOpenAiApi
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
- : "No — trusted native local clients only";
176
- element("review-origins-row").hidden = !isOpenAiApi;
177
- element("review-origins").textContent = isOpenAiApi
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 (isOpenAiApi) {
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 isOpenAiApi = state.plan.target === "openai-api";
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 = !isOpenAiApi;
203
- element("codex-install-warning").hidden = isOpenAiApi;
204
- apiKey.required = isOpenAiApi;
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 = isOpenAiApi
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
- : "Relmio will install the official Codex App Server first. You will complete ChatGPT device sign-in after the container is ready.";
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
- isOpenAiApi ? "Install OpenAI API endpoint" : "Install Codex App Server",
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 isOpenAiApi = result.target === "openai-api";
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 = isOpenAiApi;
229
- element("codex-login").hidden = isOpenAiApi;
230
- element("done-title").textContent = isOpenAiApi
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
- : "Codex App Server is installed";
233
- element("done-detail").textContent = isOpenAiApi
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
- : "Copy the endpoint and capability, then sign the isolated Codex container in to ChatGPT.";
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
- isOpenAiApi ? "For local OpenAI-compatible clients" : "For trusted native Codex clients only",
239
- isOpenAiApi
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
- : "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.",
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
- : "Codex App Server verified. Copy its one-time capability and complete ChatGPT sign-in.",
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 = "ChatGPT sign-in completed. Codex is ready for your trusted native client.";
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: "codex-chatgpt",
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: "codex-chatgpt",
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
- { installDirectory },
855
+ {
856
+ installDirectory,
857
+ ...(target === "codex-chat" ? { target } : {}),
858
+ },
851
859
  { changeLockHeld: true },
852
860
  );
853
861
  if (state.codexLogin === login && !state.closing) {