relmio 0.5.0 → 0.7.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
@@ -6,6 +6,16 @@ key, a Codex/ChatGPT session, and generated local capabilities. Treat every one
6
6
  of these values as password-equivalent. Read this page before offering the
7
7
  wizard to another person.
8
8
 
9
+ ## ChatGPT/Codex sign-in lifetime
10
+
11
+ ChatGPT/Codex sign-in tokens expire, but the official Codex client refreshes
12
+ them automatically during active use before they expire, so active sessions
13
+ usually continue without another browser login. The official [OpenAI
14
+ authentication documentation](https://learn.chatgpt.com/docs/auth) does not
15
+ publish a fixed 10-day lifetime; do not plan around one. This provider
16
+ credential is separate from Relmio's local capability, which remains valid
17
+ until you rotate it.
18
+
9
19
  ## Trust model
10
20
 
11
21
  The design assumes:
@@ -26,12 +36,19 @@ For a local Docker endpoint, the design additionally assumes:
26
36
  by the same person;
27
37
  - a browser origin allowlist is not being used as a substitute for secret
28
38
  storage; and
29
- - a Codex client is trusted with App Server's broad agent and account surface.
39
+ - a raw Codex client is trusted with App Server's broad agent and account
40
+ surface, while the Chat Adapter bearer is held only by a trusted local
41
+ backend or development server.
30
42
 
31
43
  The raw Codex App Server is not a multi-user boundary. It is for a trusted
32
44
  native client owned by the same account holder, not a browser, shared service,
33
45
  public app, or untrusted plugin.
34
46
 
47
+ The Codex Chat Adapter is a separate, narrower contract. It is for a trusted
48
+ local backend or development server owned by the same account holder. Browser
49
+ JavaScript must not call it directly, and it is not for a remote, hosted,
50
+ shared, or production service.
51
+
35
52
  ## Controls implemented by the wizard
36
53
 
37
54
  - The web server binds only to `127.0.0.1`.
@@ -62,11 +79,12 @@ public app, or untrusted plugin.
62
79
 
63
80
  - Generated Compose files publish only literal
64
81
  `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.
82
+ - Every OpenAI `/v1` operation that can reach OpenAI, every raw Codex WebSocket
83
+ upgrade, and every Codex Chat Adapter route except `GET /health` requires a
84
+ random Relmio capability displayed once by the wizard; only its SHA-256
85
+ verifier is persisted. Exact-origin CORS `OPTIONS` is a non-forwarding
86
+ exception only for the Platform-backed `/v1` gateway. The bearer remains
87
+ valid until an endpoint update rotates it.
70
88
  - The Platform API key is accepted only for the OpenAI gateway. A transient,
71
89
  network-disabled helper receives it over stdin and atomically seeds a private,
72
90
  labeled Docker volume that the gateway mounts read-only. No host key file or
@@ -77,9 +95,16 @@ public app, or untrusted plugin.
77
95
  - Browser requests to the OpenAI gateway require an exact configured `http`
78
96
  or `https` origin. Wildcards and `null` are rejected; requests without an
79
97
  `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.
98
+ - The Chat Adapter rejects every request carrying an `Origin` header, emits no
99
+ CORS permission, and exposes only its authenticated Relmio-specific
100
+ `POST /chat` contract plus readiness and credential-verification probes.
101
+ - Each Codex target receives its own private named credential and workspace
102
+ volumes. No long-running local endpoint service mounts a host directory,
103
+ Docker socket, SSH key, browser profile, or host home directory.
104
+ - Chat Adapter turns use a named read-only permission profile with network
105
+ disabled. Its model-visible filesystem policy denies root by default, allows
106
+ only Codex's minimal runtime paths and the empty private workspace, and
107
+ explicitly denies the persisted Codex credential store.
83
108
  - Local managed paths use mode `0700`, generated files use owner-only modes,
84
109
  symlinks are rejected, and existing unmanaged directories are not
85
110
  overwritten.
@@ -89,7 +114,7 @@ public app, or untrusted plugin.
89
114
  - Each install uses a random Compose project identity. Containers, networks,
90
115
  and volumes must carry matching Relmio ownership labels before update,
91
116
  restart, recovery, or sign-in actions are allowed.
92
- - Both long-running endpoint containers run as a non-root user, drop Linux
117
+ - All three long-running endpoint containers run as a non-root user, drop Linux
93
118
  capabilities, set `no-new-privileges`, use a read-only root filesystem, and
94
119
  have bounded temporary storage and resource limits.
95
120
  - The one-shot OpenAI credential seed helper is the narrow exception: it has no
@@ -97,6 +122,30 @@ public app, or untrusted plugin.
97
122
  resource limits; and uses root plus only `CHOWN` long enough to atomically
98
123
  make the stdin-seeded volume entry readable by the non-root gateway.
99
124
 
125
+ ### In-wizard Chat Adapter tester
126
+
127
+ The Ready screen's Chat Adapter tester is a deliberately narrow convenience
128
+ path, not a browser CORS exception. Its browser calls stay same-origin to the
129
+ setup-token-protected wizard. Only the local wizard server calls the adapter,
130
+ using a server-side `POST /chat` request without an `Origin` header.
131
+
132
+ The tester accepts only a literal `http://127.0.0.1:PORT` base URL and appends
133
+ `/chat` itself. It refuses `localhost`, IPv6, LAN/private/public addresses,
134
+ credentials, query strings, fragments, redirects, malformed JSON, oversized
135
+ payloads, excessive IDs/ciphertext, concurrent key use, and preview mode. The
136
+ server bounds timeout and response size, validates the upstream shape, and
137
+ returns only a conversation ID plus output with generic redacted errors.
138
+
139
+ Before a test, the browser obtains an ephemeral RSA-OAEP SHA-256 public key
140
+ from the local wizard, clears the credential input, and retains only ciphertext
141
+ and key ID in page memory. The matching private key remains only in the local
142
+ server's in-memory, time-limited, bounded session map and can be invalidated
143
+ explicitly. Prompts and transcript are not persisted server-side.
144
+
145
+ This is not encryption at rest or end-to-end encryption. It reduces accidental
146
+ credential transit and storage exposure, but cannot protect a compromised
147
+ browser, extension, or local machine.
148
+
100
149
  ## What “private” means here
101
150
 
102
151
  Port `10531` is not reachable from the public internet or VPS host through a
@@ -126,11 +175,16 @@ The local capabilities have separate consequences:
126
175
 
127
176
  - The OpenAI gateway capability can spend through the protected Platform API
128
177
  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.
178
+ - The raw Codex App Server capability can invoke broad App Server methods
179
+ inside its isolated container and use its signed-in ChatGPT/Codex session.
180
+ - The separate Chat Adapter bearer can submit chat turns and resume its bounded
181
+ conversation threads through the signed-in Codex container. Its narrower
182
+ HTTP surface and model permission profile reduce access, but do not make the
183
+ bearer safe to expose or share.
131
184
  - 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
185
+ private. The Chat Adapter rejects browser origins entirely; keep its bearer
186
+ in a trusted local backend or development server.
187
+ - Do not expose any local endpoint on a LAN, public IP, domain, reverse proxy, or
134
188
  hosted service. Loopback binding and the bearer capability are both required.
135
189
  - If a capability is disclosed, update the endpoint to rotate it. If an
136
190
  upstream credential may be exposed, revoke or sign out through the provider
@@ -152,12 +206,17 @@ The local capabilities have separate consequences:
152
206
  is billed or credited to the associated Platform project; a ChatGPT
153
207
  subscription or Codex for Open Source benefit is not substituted for API
154
208
  billing.
155
- - The local Codex option preserves the official App Server JSON-RPC protocol.
209
+ - The raw local Codex option preserves the official App Server JSON-RPC protocol.
156
210
  It does not provide `/v1/chat/completions`, `/v1/responses`, or any other
157
211
  OpenAI API compatibility route.
158
212
  - OpenAI documents App Server WebSocket transport as experimental and
159
213
  unsupported for production. It rejects browser-origin requests and is
160
214
  limited here to trusted native same-owner clients.
215
+ - The Codex Chat Adapter uses the official App Server lifecycle internally but
216
+ exposes only Relmio's experimental `POST /chat` contract. It is not
217
+ `/v1/chat/completions`, `/v1/responses`, or an OpenAI SDK replacement; it
218
+ rejects browser origins and is limited to trusted local backends or
219
+ development servers.
161
220
  - Acceptance into Codex for Open Source is not treated by Relmio as permission
162
221
  to repurpose credentials, share an account, bypass controls, or broaden the
163
222
  scope of another agreement. Review the current
@@ -9,6 +9,39 @@ wizard or any manual VPS command. The documented commands are sidecar-only and
9
9
  do not delete, restart, or rebuild n8n, but they still access your VPS and write
10
10
  files there.
11
11
 
12
+ ## Docker is not running
13
+
14
+ Start Docker Desktop or Docker Engine and wait until `docker info` and
15
+ `docker compose version` both succeed. Close any stale Relmio wizard tab, start
16
+ one fresh wizard session, and review the local plan again. Do not restart or
17
+ rebuild unrelated containers while checking the local endpoint.
18
+
19
+ ## Authentication fails
20
+
21
+ Close stale wizard and device-code tabs, keep the newest Relmio terminal open,
22
+ and use only the complete wizard URL printed by that active process. Start one
23
+ fresh ChatGPT device-code attempt and complete the newest code. A ChatGPT
24
+ subscription credential is valid only for the Codex targets; the generic
25
+ OpenAI-compatible `/v1` target requires a separately billed Platform API key.
26
+ ChatGPT/Codex sign-in tokens expire, but the official Codex client refreshes
27
+ them automatically during active use before they expire, so active sessions
28
+ usually continue without another browser login. The official [OpenAI
29
+ authentication documentation](https://learn.chatgpt.com/docs/auth) does not
30
+ publish a fixed 10-day lifetime; do not plan around one. This provider
31
+ credential is separate from Relmio's local capability, which remains valid
32
+ until you rotate it. If Relmio reports the credential is invalid or refresh no
33
+ longer succeeds, select **Start ChatGPT sign-in** again in the active local
34
+ wizard. The VPS sidecar flow labels that action **Refresh ChatGPT sign-in**.
35
+
36
+ ## Local image build failed
37
+
38
+ The local wizard intentionally does not show Docker build output, filesystem
39
+ paths, or stderr in the browser. Confirm Docker Desktop or Docker Engine is
40
+ running, check that the local disk has room for the image, and confirm your
41
+ network can reach the image registry. Then close the old wizard, start one new
42
+ wizard session, review a fresh plan, and retry. Do not delete an existing
43
+ managed endpoint or rebuild unrelated containers as a workaround.
44
+
12
45
  ## Hosted chat browser extension
13
46
 
14
47
  The hosted demo at [relmio.vercel.app](https://relmio.vercel.app/) needs the
@@ -0,0 +1,30 @@
1
+ # VPS and n8n
2
+
3
+ Relmio installs a separate sidecar project at
4
+ `/docker/n8n-openai-oauth`. It does not edit, rebuild, recreate, stop, or
5
+ restart your existing n8n Compose project or image. The sidecar has no host
6
+ port: n8n reaches it over the shared Docker network at
7
+ `http://n8n-openai-oauth:10531/v1`.
8
+
9
+ ## Wizard route
10
+
11
+ 1. Run the local wizard and complete the fresh ChatGPT sign-in on your own
12
+ computer.
13
+ 2. Enter your VPS address and compare the presented SSH host fingerprint with
14
+ your provider before authorizing password authentication.
15
+ 3. Select an already-running n8n container and one of its existing shared
16
+ networks.
17
+ 4. Review the exact plan. Remote writes begin only after final confirmation.
18
+ 5. In n8n, use the private sidecar hostname rather than `127.0.0.1`.
19
+
20
+ The n8n credential's required API-key field uses `local-only` only as a UI
21
+ placeholder; it is not an OpenAI Platform API key.
22
+
23
+ ## Follow-on guides
24
+
25
+ - [Configure n8n nodes](./n8n-configuration.md) has copy-ready AI Agent and
26
+ HTTP Request recipes.
27
+ - [Beginner manual installation](./manual-install.md) is the auditable fallback
28
+ when the wizard cannot be used.
29
+ - [Troubleshooting](./troubleshooting.md) includes connection, Docker-network,
30
+ and browser sign-in recovery steps.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relmio",
3
- "version": "0.5.0",
3
+ "version": "0.7.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,6 +405,20 @@ 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
 
@@ -378,6 +441,113 @@ ENTRYPOINT ["codex"]
378
441
  `;
379
442
  }
380
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
+
381
551
  export function createCodexComposeFile({ port, tokenSha256, installId }) {
382
552
  const safePort = validateLocalPort(port);
383
553
  const safeVerifier = validateSha256Verifier(tokenSha256);