relmio 0.4.1 → 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 +51 -0
- package/README.md +40 -16
- package/docs/architecture.md +21 -9
- package/docs/local-endpoints-spec.md +101 -13
- package/docs/local-endpoints.md +101 -30
- package/docs/security.md +40 -15
- package/package.json +1 -1
- package/src/domain/local-endpoints.js +187 -14
- package/src/gateway/codex-chat.js +754 -0
- package/src/services/codex-login.js +11 -3
- package/src/services/local-installer.js +859 -20
- package/src/ui/local.css +140 -2
- package/src/ui/local.html +89 -9
- package/src/ui/local.js +157 -37
- package/src/web/server.js +247 -18
package/docs/security.md
CHANGED
|
@@ -26,12 +26,19 @@ For a local Docker endpoint, the design additionally assumes:
|
|
|
26
26
|
by the same person;
|
|
27
27
|
- a browser origin allowlist is not being used as a substitute for secret
|
|
28
28
|
storage; and
|
|
29
|
-
- a Codex client is trusted with App Server's broad agent and account
|
|
29
|
+
- a raw Codex client is trusted with App Server's broad agent and account
|
|
30
|
+
surface, while the Chat Adapter bearer is held only by a trusted local
|
|
31
|
+
backend or development server.
|
|
30
32
|
|
|
31
33
|
The raw Codex App Server is not a multi-user boundary. It is for a trusted
|
|
32
34
|
native client owned by the same account holder, not a browser, shared service,
|
|
33
35
|
public app, or untrusted plugin.
|
|
34
36
|
|
|
37
|
+
The Codex Chat Adapter is a separate, narrower contract. It is for a trusted
|
|
38
|
+
local backend or development server owned by the same account holder. Browser
|
|
39
|
+
JavaScript must not call it directly, and it is not for a remote, hosted,
|
|
40
|
+
shared, or production service.
|
|
41
|
+
|
|
35
42
|
## Controls implemented by the wizard
|
|
36
43
|
|
|
37
44
|
- The web server binds only to `127.0.0.1`.
|
|
@@ -62,11 +69,12 @@ public app, or untrusted plugin.
|
|
|
62
69
|
|
|
63
70
|
- Generated Compose files publish only literal
|
|
64
71
|
`127.0.0.1:<selected-port>:<container-port>` mappings.
|
|
65
|
-
- Every OpenAI `/v1` operation that can reach OpenAI
|
|
66
|
-
upgrade
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
- Every OpenAI `/v1` operation that can reach OpenAI, every raw Codex WebSocket
|
|
73
|
+
upgrade, and every Codex Chat Adapter route except `GET /health` requires a
|
|
74
|
+
random Relmio capability displayed once by the wizard; only its SHA-256
|
|
75
|
+
verifier is persisted. Exact-origin CORS `OPTIONS` is a non-forwarding
|
|
76
|
+
exception only for the Platform-backed `/v1` gateway. The bearer remains
|
|
77
|
+
valid until an endpoint update rotates it.
|
|
70
78
|
- The Platform API key is accepted only for the OpenAI gateway. A transient,
|
|
71
79
|
network-disabled helper receives it over stdin and atomically seeds a private,
|
|
72
80
|
labeled Docker volume that the gateway mounts read-only. No host key file or
|
|
@@ -77,9 +85,16 @@ public app, or untrusted plugin.
|
|
|
77
85
|
- Browser requests to the OpenAI gateway require an exact configured `http`
|
|
78
86
|
or `https` origin. Wildcards and `null` are rejected; requests without an
|
|
79
87
|
`Origin` remain available to authenticated native clients and backends.
|
|
80
|
-
-
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
- The Chat Adapter rejects every request carrying an `Origin` header, emits no
|
|
89
|
+
CORS permission, and exposes only its authenticated Relmio-specific
|
|
90
|
+
`POST /chat` contract plus readiness and credential-verification probes.
|
|
91
|
+
- Each Codex target receives its own private named credential and workspace
|
|
92
|
+
volumes. No long-running local endpoint service mounts a host directory,
|
|
93
|
+
Docker socket, SSH key, browser profile, or host home directory.
|
|
94
|
+
- Chat Adapter turns use a named read-only permission profile with network
|
|
95
|
+
disabled. Its model-visible filesystem policy denies root by default, allows
|
|
96
|
+
only Codex's minimal runtime paths and the empty private workspace, and
|
|
97
|
+
explicitly denies the persisted Codex credential store.
|
|
83
98
|
- Local managed paths use mode `0700`, generated files use owner-only modes,
|
|
84
99
|
symlinks are rejected, and existing unmanaged directories are not
|
|
85
100
|
overwritten.
|
|
@@ -89,7 +104,7 @@ public app, or untrusted plugin.
|
|
|
89
104
|
- Each install uses a random Compose project identity. Containers, networks,
|
|
90
105
|
and volumes must carry matching Relmio ownership labels before update,
|
|
91
106
|
restart, recovery, or sign-in actions are allowed.
|
|
92
|
-
-
|
|
107
|
+
- All three long-running endpoint containers run as a non-root user, drop Linux
|
|
93
108
|
capabilities, set `no-new-privileges`, use a read-only root filesystem, and
|
|
94
109
|
have bounded temporary storage and resource limits.
|
|
95
110
|
- The one-shot OpenAI credential seed helper is the narrow exception: it has no
|
|
@@ -126,11 +141,16 @@ The local capabilities have separate consequences:
|
|
|
126
141
|
|
|
127
142
|
- The OpenAI gateway capability can spend through the protected Platform API
|
|
128
143
|
key, subject to that Platform project's permissions and limits.
|
|
129
|
-
- The Codex capability can invoke broad App Server methods
|
|
130
|
-
container and use its signed-in ChatGPT/Codex session.
|
|
144
|
+
- The raw Codex App Server capability can invoke broad App Server methods
|
|
145
|
+
inside its isolated container and use its signed-in ChatGPT/Codex session.
|
|
146
|
+
- The separate Chat Adapter bearer can submit chat turns and resume its bounded
|
|
147
|
+
conversation threads through the signed-in Codex container. Its narrower
|
|
148
|
+
HTTP surface and model permission profile reduce access, but do not make the
|
|
149
|
+
bearer safe to expose or share.
|
|
131
150
|
- An origin allowlist does not make a bearer embedded in browser JavaScript
|
|
132
|
-
private.
|
|
133
|
-
|
|
151
|
+
private. The Chat Adapter rejects browser origins entirely; keep its bearer
|
|
152
|
+
in a trusted local backend or development server.
|
|
153
|
+
- Do not expose any local endpoint on a LAN, public IP, domain, reverse proxy, or
|
|
134
154
|
hosted service. Loopback binding and the bearer capability are both required.
|
|
135
155
|
- If a capability is disclosed, update the endpoint to rotate it. If an
|
|
136
156
|
upstream credential may be exposed, revoke or sign out through the provider
|
|
@@ -152,12 +172,17 @@ The local capabilities have separate consequences:
|
|
|
152
172
|
is billed or credited to the associated Platform project; a ChatGPT
|
|
153
173
|
subscription or Codex for Open Source benefit is not substituted for API
|
|
154
174
|
billing.
|
|
155
|
-
- The local Codex option preserves the official App Server JSON-RPC protocol.
|
|
175
|
+
- The raw local Codex option preserves the official App Server JSON-RPC protocol.
|
|
156
176
|
It does not provide `/v1/chat/completions`, `/v1/responses`, or any other
|
|
157
177
|
OpenAI API compatibility route.
|
|
158
178
|
- OpenAI documents App Server WebSocket transport as experimental and
|
|
159
179
|
unsupported for production. It rejects browser-origin requests and is
|
|
160
180
|
limited here to trusted native same-owner clients.
|
|
181
|
+
- The Codex Chat Adapter uses the official App Server lifecycle internally but
|
|
182
|
+
exposes only Relmio's experimental `POST /chat` contract. It is not
|
|
183
|
+
`/v1/chat/completions`, `/v1/responses`, or an OpenAI SDK replacement; it
|
|
184
|
+
rejects browser origins and is limited to trusted local backends or
|
|
185
|
+
development servers.
|
|
161
186
|
- Acceptance into Codex for Open Source is not treated by Relmio as permission
|
|
162
187
|
to repurpose credentials, share an account, bypass controls, or broaden the
|
|
163
188
|
scope of another agreement. Review the current
|
package/package.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { validatePort } from "./validation.js";
|
|
2
|
+
import packageManifest from "../../package.json" with { type: "json" };
|
|
2
3
|
|
|
3
4
|
export const CODEX_CLI_VERSION = "0.147.0";
|
|
5
|
+
const PACKAGE_VERSION = packageManifest.version;
|
|
4
6
|
|
|
5
7
|
export const LOCAL_TARGETS = Object.freeze({
|
|
6
8
|
"openai-api": Object.freeze({
|
|
@@ -19,6 +21,14 @@ export const LOCAL_TARGETS = Object.freeze({
|
|
|
19
21
|
experimental: true,
|
|
20
22
|
containerPort: 4_500,
|
|
21
23
|
}),
|
|
24
|
+
"codex-chat": Object.freeze({
|
|
25
|
+
label: "Codex Chat Adapter",
|
|
26
|
+
protocol: "relmio-codex-chat-http",
|
|
27
|
+
upstreamAuth: "chatgpt-via-codex",
|
|
28
|
+
browserClients: false,
|
|
29
|
+
experimental: true,
|
|
30
|
+
containerPort: 14_501,
|
|
31
|
+
}),
|
|
22
32
|
});
|
|
23
33
|
|
|
24
34
|
export function validateLocalTarget(value) {
|
|
@@ -114,7 +124,9 @@ export function createLocalDeploymentPlan({
|
|
|
114
124
|
const endpoint =
|
|
115
125
|
safeTarget === "openai-api"
|
|
116
126
|
? `http://127.0.0.1:${safePort}/v1`
|
|
117
|
-
:
|
|
127
|
+
: safeTarget === "codex-chatgpt"
|
|
128
|
+
? `ws://127.0.0.1:${safePort}`
|
|
129
|
+
: `http://127.0.0.1:${safePort}`;
|
|
118
130
|
|
|
119
131
|
return {
|
|
120
132
|
target: safeTarget,
|
|
@@ -145,8 +157,11 @@ ENTRYPOINT ["node", "/app/gateway.mjs"]
|
|
|
145
157
|
|
|
146
158
|
export function createLocalDockerignore(target) {
|
|
147
159
|
const safeTarget = validateLocalTarget(target);
|
|
148
|
-
|
|
149
|
-
|
|
160
|
+
if (safeTarget === "openai-api") {
|
|
161
|
+
return "**\n!Dockerfile\n!gateway.mjs\n";
|
|
162
|
+
}
|
|
163
|
+
return safeTarget === "codex-chat"
|
|
164
|
+
? "**\n!Dockerfile\n!gateway.mjs\n!config.toml\n!requirements.toml\n"
|
|
150
165
|
: "**\n!Dockerfile\n!config.toml\n!requirements.toml\n";
|
|
151
166
|
}
|
|
152
167
|
|
|
@@ -265,13 +280,29 @@ volumes:
|
|
|
265
280
|
`;
|
|
266
281
|
}
|
|
267
282
|
|
|
268
|
-
|
|
269
|
-
|
|
283
|
+
function renderCodexConfig({
|
|
284
|
+
approvalPolicy,
|
|
285
|
+
permissionProfile,
|
|
286
|
+
permissionProfileBase,
|
|
287
|
+
protectCredentialStore = false,
|
|
288
|
+
}) {
|
|
289
|
+
const filesystemPermissions = protectCredentialStore
|
|
290
|
+
? `
|
|
291
|
+
[permissions.${permissionProfile}.filesystem]
|
|
292
|
+
":root" = "deny"
|
|
293
|
+
":minimal" = "read"
|
|
294
|
+
":tmpdir" = "deny"
|
|
295
|
+
":slash_tmp" = "deny"
|
|
296
|
+
"/workspace" = "read"
|
|
297
|
+
"/home/node/.codex" = "deny"
|
|
298
|
+
`
|
|
299
|
+
: "";
|
|
300
|
+
return `approval_policy = "${approvalPolicy}"
|
|
270
301
|
approvals_reviewer = "user"
|
|
271
302
|
allow_login_shell = false
|
|
272
303
|
check_for_update_on_startup = false
|
|
273
304
|
cli_auth_credentials_store = "file"
|
|
274
|
-
default_permissions = "
|
|
305
|
+
default_permissions = "${permissionProfile}"
|
|
275
306
|
forced_login_method = "chatgpt"
|
|
276
307
|
web_search = "disabled"
|
|
277
308
|
|
|
@@ -281,10 +312,11 @@ enabled = false
|
|
|
281
312
|
[feedback]
|
|
282
313
|
enabled = false
|
|
283
314
|
|
|
284
|
-
[permissions
|
|
285
|
-
extends = "
|
|
315
|
+
[permissions.${permissionProfile}]
|
|
316
|
+
extends = "${permissionProfileBase}"
|
|
317
|
+
${filesystemPermissions}
|
|
286
318
|
|
|
287
|
-
[permissions.
|
|
319
|
+
[permissions.${permissionProfile}.network]
|
|
288
320
|
enabled = false
|
|
289
321
|
|
|
290
322
|
[shell_environment_policy]
|
|
@@ -315,8 +347,25 @@ tool_suggest = false
|
|
|
315
347
|
`;
|
|
316
348
|
}
|
|
317
349
|
|
|
318
|
-
export function
|
|
319
|
-
return
|
|
350
|
+
export function createCodexConfig() {
|
|
351
|
+
return renderCodexConfig({
|
|
352
|
+
approvalPolicy: "on-request",
|
|
353
|
+
permissionProfile: "relmio-workspace",
|
|
354
|
+
permissionProfileBase: ":workspace",
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function createCodexChatConfig() {
|
|
359
|
+
return renderCodexConfig({
|
|
360
|
+
approvalPolicy: "never",
|
|
361
|
+
permissionProfile: "relmio-chat-readonly",
|
|
362
|
+
permissionProfileBase: ":read-only",
|
|
363
|
+
protectCredentialStore: true,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function renderCodexRequirements({ approvalPolicy, permissionProfile }) {
|
|
368
|
+
return `allowed_approval_policies = ["${approvalPolicy}"]
|
|
320
369
|
allowed_approvals_reviewers = ["user"]
|
|
321
370
|
allowed_login_methods = ["chatgpt"]
|
|
322
371
|
allowed_web_search_modes = ["disabled"]
|
|
@@ -324,10 +373,10 @@ allow_managed_hooks_only = true
|
|
|
324
373
|
allow_remote_control = false
|
|
325
374
|
check_for_update_on_startup = false
|
|
326
375
|
allow_login_shell = false
|
|
327
|
-
default_permissions = "
|
|
376
|
+
default_permissions = "${permissionProfile}"
|
|
328
377
|
|
|
329
378
|
[allowed_permission_profiles]
|
|
330
|
-
"
|
|
379
|
+
"${permissionProfile}" = true
|
|
331
380
|
|
|
332
381
|
[feedback]
|
|
333
382
|
enabled = false
|
|
@@ -356,10 +405,27 @@ tool_suggest = false
|
|
|
356
405
|
`;
|
|
357
406
|
}
|
|
358
407
|
|
|
408
|
+
export function createCodexRequirements() {
|
|
409
|
+
return renderCodexRequirements({
|
|
410
|
+
approvalPolicy: "on-request",
|
|
411
|
+
permissionProfile: "relmio-workspace",
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export function createCodexChatRequirements() {
|
|
416
|
+
return renderCodexRequirements({
|
|
417
|
+
approvalPolicy: "never",
|
|
418
|
+
permissionProfile: "relmio-chat-readonly",
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
359
422
|
export function createCodexDockerfile() {
|
|
360
423
|
return `FROM node:22-bookworm-slim
|
|
361
424
|
|
|
362
|
-
RUN
|
|
425
|
+
RUN apt-get update \\
|
|
426
|
+
&& apt-get install --no-install-recommends -y ca-certificates \\
|
|
427
|
+
&& rm -rf /var/lib/apt/lists/* \\
|
|
428
|
+
&& npm install --global --ignore-scripts @openai/codex@${CODEX_CLI_VERSION} \\
|
|
363
429
|
&& npm cache clean --force \\
|
|
364
430
|
&& mkdir -p /etc/codex /home/node/.codex /workspace \\
|
|
365
431
|
&& chown -R node:node /home/node/.codex /workspace
|
|
@@ -375,6 +441,113 @@ ENTRYPOINT ["codex"]
|
|
|
375
441
|
`;
|
|
376
442
|
}
|
|
377
443
|
|
|
444
|
+
export function createCodexChatDockerfile() {
|
|
445
|
+
return `FROM node:22-bookworm-slim
|
|
446
|
+
|
|
447
|
+
WORKDIR /app
|
|
448
|
+
|
|
449
|
+
RUN apt-get update \\
|
|
450
|
+
&& apt-get install --no-install-recommends -y ca-certificates \\
|
|
451
|
+
&& rm -rf /var/lib/apt/lists/* \\
|
|
452
|
+
&& npm install --global --ignore-scripts @openai/codex@${CODEX_CLI_VERSION} \\
|
|
453
|
+
&& npm cache clean --force \\
|
|
454
|
+
&& mkdir -p /etc/codex /home/node/.codex /workspace \\
|
|
455
|
+
&& chown -R node:node /home/node/.codex /workspace
|
|
456
|
+
|
|
457
|
+
COPY --chmod=0444 requirements.toml /etc/codex/requirements.toml
|
|
458
|
+
COPY --chown=node:node config.toml /home/node/.codex/config.toml
|
|
459
|
+
COPY --chown=node:node gateway.mjs /app/gateway.mjs
|
|
460
|
+
|
|
461
|
+
ENV CODEX_HOME=/home/node/.codex
|
|
462
|
+
WORKDIR /workspace
|
|
463
|
+
USER node
|
|
464
|
+
|
|
465
|
+
ENTRYPOINT ["node", "/app/gateway.mjs"]
|
|
466
|
+
`;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export function createCodexChatComposeFile({ port, tokenSha256, installId }) {
|
|
470
|
+
const safePort = validateLocalPort(port);
|
|
471
|
+
const safeVerifier = validateSha256Verifier(tokenSha256);
|
|
472
|
+
const safeInstallId = validateInstallId(installId);
|
|
473
|
+
const gatewayImage = `relmio-codex-chat-${safeInstallId}:local`;
|
|
474
|
+
|
|
475
|
+
return `services:
|
|
476
|
+
codex-chat:
|
|
477
|
+
image: ${gatewayImage}
|
|
478
|
+
build:
|
|
479
|
+
context: .
|
|
480
|
+
dockerfile: Dockerfile
|
|
481
|
+
restart: unless-stopped
|
|
482
|
+
init: true
|
|
483
|
+
environment:
|
|
484
|
+
RELMIO_GATEWAY_TOKEN_SHA256: ${safeVerifier}
|
|
485
|
+
RELMIO_GATEWAY_HOST: 0.0.0.0
|
|
486
|
+
RELMIO_GATEWAY_PORT: "14501"
|
|
487
|
+
RELMIO_PACKAGE_VERSION: "${PACKAGE_VERSION}"
|
|
488
|
+
ports:
|
|
489
|
+
- "127.0.0.1:${safePort}:14501"
|
|
490
|
+
volumes:
|
|
491
|
+
- codex-home:/home/node/.codex
|
|
492
|
+
- codex-workspace:/workspace
|
|
493
|
+
security_opt:
|
|
494
|
+
- no-new-privileges:true
|
|
495
|
+
cap_drop:
|
|
496
|
+
- ALL
|
|
497
|
+
read_only: true
|
|
498
|
+
tmpfs:
|
|
499
|
+
- /tmp:size=64m,mode=1777,nodev,nosuid
|
|
500
|
+
- /run:size=16m,mode=0755,nodev,nosuid
|
|
501
|
+
- /home/node/.cache:uid=1000,gid=1000,mode=0700,nodev,nosuid
|
|
502
|
+
pids_limit: 128
|
|
503
|
+
mem_limit: 2g
|
|
504
|
+
cpus: 2.0
|
|
505
|
+
ulimits:
|
|
506
|
+
nofile:
|
|
507
|
+
soft: 1024
|
|
508
|
+
hard: 1024
|
|
509
|
+
core: 0
|
|
510
|
+
logging:
|
|
511
|
+
driver: json-file
|
|
512
|
+
options:
|
|
513
|
+
max-size: 10m
|
|
514
|
+
max-file: "3"
|
|
515
|
+
healthcheck:
|
|
516
|
+
test:
|
|
517
|
+
- CMD
|
|
518
|
+
- node
|
|
519
|
+
- -e
|
|
520
|
+
- 'fetch("http://127.0.0.1:14501/health").then((response) => process.exit(response.ok ? 0 : 1)).catch(() => process.exit(1))'
|
|
521
|
+
interval: 10s
|
|
522
|
+
timeout: 5s
|
|
523
|
+
retries: 9
|
|
524
|
+
start_period: 20s
|
|
525
|
+
labels:
|
|
526
|
+
io.relmio.managed: "true"
|
|
527
|
+
io.relmio.target: "codex-chat"
|
|
528
|
+
io.relmio.install: "${safeInstallId}"
|
|
529
|
+
|
|
530
|
+
networks:
|
|
531
|
+
default:
|
|
532
|
+
labels:
|
|
533
|
+
io.relmio.managed: "true"
|
|
534
|
+
io.relmio.target: "codex-chat"
|
|
535
|
+
io.relmio.install: "${safeInstallId}"
|
|
536
|
+
|
|
537
|
+
volumes:
|
|
538
|
+
codex-home:
|
|
539
|
+
labels:
|
|
540
|
+
io.relmio.managed: "true"
|
|
541
|
+
io.relmio.target: "codex-chat"
|
|
542
|
+
io.relmio.install: "${safeInstallId}"
|
|
543
|
+
codex-workspace:
|
|
544
|
+
labels:
|
|
545
|
+
io.relmio.managed: "true"
|
|
546
|
+
io.relmio.target: "codex-chat"
|
|
547
|
+
io.relmio.install: "${safeInstallId}"
|
|
548
|
+
`;
|
|
549
|
+
}
|
|
550
|
+
|
|
378
551
|
export function createCodexComposeFile({ port, tokenSha256, installId }) {
|
|
379
552
|
const safePort = validateLocalPort(port);
|
|
380
553
|
const safeVerifier = validateSha256Verifier(tokenSha256);
|