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/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 surface.
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 and every Codex WebSocket
66
- upgrade requires a random Relmio capability displayed once by the wizard;
67
- only its SHA-256 verifier is persisted. Exact-origin CORS `OPTIONS` is a
68
- non-forwarding metadata exception. The bearer remains valid until an
69
- endpoint update rotates it.
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
- - Codex receives private named credential and workspace volumes. No host
81
- directory, Docker socket, SSH key, browser profile, or home directory is
82
- mounted into either local service.
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
- - Both long-running endpoint containers run as a non-root user, drop Linux
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 inside the isolated
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. Use browser access only for private same-owner local development.
133
- - Do not expose either endpoint on a LAN, public IP, domain, reverse proxy, or
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,6 @@
1
1
  {
2
2
  "name": "relmio",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "description": "Install private local OpenAI API and Codex endpoints with explicit provider credential boundaries, plus the existing isolated n8n sidecar.",
5
5
  "keywords": [
6
6
  "relmio",
@@ -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
- : `ws://127.0.0.1:${safePort}`;
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
- return safeTarget === "openai-api"
149
- ? "**\n!Dockerfile\n!gateway.mjs\n"
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
- export function createCodexConfig() {
269
- return `approval_policy = "on-request"
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 = "relmio-workspace"
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.relmio-workspace]
285
- extends = ":workspace"
315
+ [permissions.${permissionProfile}]
316
+ extends = "${permissionProfileBase}"
317
+ ${filesystemPermissions}
286
318
 
287
- [permissions.relmio-workspace.network]
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 createCodexRequirements() {
319
- return `allowed_approval_policies = ["on-request"]
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 = "relmio-workspace"
376
+ default_permissions = "${permissionProfile}"
328
377
 
329
378
  [allowed_permission_profiles]
330
- "relmio-workspace" = true
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 npm install --global --ignore-scripts @openai/codex@${CODEX_CLI_VERSION} \\
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);