relmio 0.3.0 → 0.4.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.
@@ -0,0 +1,370 @@
1
+ # Spec: Policy-safe local endpoints
2
+
3
+ ## Status
4
+
5
+ Approved for implementation on `codex/local-openai-endpoint` on 2026-08-13.
6
+
7
+ This spec is product and engineering guidance based on the current official
8
+ OpenAI documentation. It is not a legal opinion. Relmio must not claim that
9
+ OpenAI has endorsed, certified, or pre-approved the project.
10
+
11
+ ## Objective
12
+
13
+ Add a local Docker installation path to the Relmio browser wizard without
14
+ weakening the existing VPS/n8n safety boundary.
15
+
16
+ Relmio offers two intentionally different local providers:
17
+
18
+ 1. `openai-api` is an OpenAI-compatible HTTP gateway backed by the user's
19
+ OpenAI Platform API key.
20
+ 2. `codex-chatgpt` is the official Codex App Server protocol backed by the
21
+ user's ChatGPT/Codex sign-in.
22
+
23
+ Relmio must never exchange, translate, or present a ChatGPT/Codex credential as
24
+ a general OpenAI API bearer credential. The Codex provider must not expose an
25
+ OpenAI-shaped `/v1` compatibility surface.
26
+
27
+ ## Official-source boundary
28
+
29
+ - General OpenAI API requests use a Platform API key (or an officially
30
+ supported workload identity credential).
31
+ - ChatGPT subscription access stays inside the official Codex CLI/App Server
32
+ workflow.
33
+ - Codex App Server uses its native JSON-RPC thread, turn, approval, and event
34
+ protocol.
35
+ - Codex App Server WebSocket transport is experimental and unsupported for
36
+ production workloads. Relmio must display that limitation before install and
37
+ in the result screen.
38
+ - Codex for Open Source benefits remain personal, limited, and governed by the
39
+ program terms. Program acceptance does not become a generic credential-scope
40
+ waiver.
41
+
42
+ Primary references:
43
+
44
+ - <https://learn.chatgpt.com/docs/app-server>
45
+ - <https://learn.chatgpt.com/docs/auth>
46
+ - <https://learn.chatgpt.com/docs/enterprise/access-tokens>
47
+ - <https://developers.openai.com/api/reference/overview#authentication>
48
+ - <https://learn.chatgpt.com/docs/codex-for-oss-terms>
49
+
50
+ ## User experience
51
+
52
+ The existing VPS/n8n wizard remains a separate legacy setup path. The wizard
53
+ landing experience adds a prominent **Local endpoints** option which opens a
54
+ dedicated local installer.
55
+
56
+ The local installer starts with two provider cards:
57
+
58
+ ### OpenAI API
59
+
60
+ - Label: **OpenAI API — compatible clients**
61
+ - Default HTTP port: `12435`
62
+ - Requires an OpenAI Platform API key beginning with `sk-`.
63
+ - Accepts zero or more exact browser origins. No wildcard origin is allowed.
64
+ - Result:
65
+ - Base URL: `http://127.0.0.1:<port>/v1`
66
+ - A newly generated Relmio bearer key, displayed once
67
+ - A warning that the upstream Platform API key is seeded over stdin into a
68
+ private labeled Docker volume, never written to a host file, and never
69
+ returned by the wizard
70
+
71
+ ### Codex with ChatGPT
72
+
73
+ - Label: **Codex with ChatGPT — agent clients**
74
+ - Default WebSocket port: `14500`
75
+ - Uses pinned official `@openai/codex@0.147.0`.
76
+ - Result:
77
+ - Endpoint: `ws://127.0.0.1:<port>`
78
+ - A newly generated capability token, displayed once
79
+ - A device-code sign-in action using `account/login/start` with
80
+ `{ "type": "chatgptDeviceCode" }`
81
+ - An explicit statement that this is Codex JSON-RPC, not OpenAI `/v1`
82
+ - An explicit experimental/non-production notice
83
+
84
+ Both flows show a review screen and require a final confirmation before any
85
+ filesystem or Docker write.
86
+
87
+ This release supports macOS, Linux, and Linux under WSL2. Native Windows is
88
+ unsupported because its filesystem permission model does not provide the POSIX
89
+ owner-only protection this installer requires. The UI and documentation must
90
+ say so, and the installer must reject native Windows before any write.
91
+
92
+ ## Wizard API contract
93
+
94
+ Every wizard API route continues to require the existing `X-Setup-Token` and
95
+ same-origin protections.
96
+
97
+ ### `GET /api/local/docker/status`
98
+
99
+ Returns local Docker and Compose availability. It never returns filesystem
100
+ paths containing the user's home directory.
101
+
102
+ ```json
103
+ {
104
+ "dockerAvailable": true,
105
+ "dockerVersion": "27.0.0",
106
+ "composeVersion": "2.29.0"
107
+ }
108
+ ```
109
+
110
+ ### `POST /api/local/plan`
111
+
112
+ Request:
113
+
114
+ ```json
115
+ {
116
+ "target": "openai-api",
117
+ "port": 12435,
118
+ "allowedOrigins": ["http://localhost:3000"]
119
+ }
120
+ ```
121
+
122
+ The request never contains an upstream credential. The response contains an
123
+ opaque, single-use `planId` and the validated binding, managed path alias,
124
+ compatibility type, authentication type, and caveats.
125
+
126
+ ### `POST /api/local/install`
127
+
128
+ Request fields:
129
+
130
+ - `planId`: the opaque identifier returned by the most recent reviewed plan
131
+ - `apiKey`: required only for `openai-api`; accepted only in request memory
132
+ - `confirmed`: must be exactly `true`
133
+
134
+ The server consumes the plan before attempting installation, so callers cannot
135
+ change the reviewed target, port, or origins or replay a failed attempt.
136
+ The response never includes the upstream Platform API key or ChatGPT
137
+ credential. It includes the new local capability once.
138
+
139
+ Only one installation may execute in a wizard process at a time. A concurrent
140
+ attempt receives `409` without consuming its reviewed plan. The in-flight lock
141
+ is released in a `finally` path after both success and failure.
142
+
143
+ ### `POST /api/local/codex/login`
144
+
145
+ Starts an official Codex App Server device-code login through a one-shot stdio
146
+ App Server process attached to the same persistent Codex home volume.
147
+
148
+ Every login attempt resolves the managed Codex directory and attests its
149
+ schema-2 marker and matching Docker resources, even in a fresh wizard process.
150
+ The server passes only the attested Docker host and unique project name to the
151
+ stdio login service. An in-memory "installed" flag is not sufficient. Preview
152
+ mode and rate-limit guards run before attestation.
153
+
154
+ Response:
155
+
156
+ ```json
157
+ {
158
+ "verificationUrl": "https://auth.openai.com/...",
159
+ "userCode": "ABCD-EFGH"
160
+ }
161
+ ```
162
+
163
+ ### `GET /api/local/codex/login/status`
164
+
165
+ Returns `idle`, `pending`, `success`, or `error`. Errors are sanitized; raw
166
+ App Server output is never returned.
167
+
168
+ ## Local OpenAI gateway contract
169
+
170
+ ### Listener
171
+
172
+ - Container listener: `0.0.0.0:10531`
173
+ - Host publication: `127.0.0.1:<selected-port>:10531`
174
+ - A generated Compose file containing `0.0.0.0:<port>` or an unqualified
175
+ `<port>:<port>` mapping is invalid.
176
+
177
+ ### Authentication
178
+
179
+ - Relmio generates 32 random bytes and returns the base64url capability once.
180
+ - Only the SHA-256 verifier is persisted.
181
+ - Every operation that can reach `/v1` upstream requires
182
+ `Authorization: Bearer <Relmio capability>`. An exact-origin `OPTIONS`
183
+ preflight is the sole unauthenticated, non-forwarding metadata exception.
184
+ - Comparison uses a constant-time operation.
185
+ - The upstream OpenAI key replaces, and is never combined with, the client's
186
+ Authorization header.
187
+
188
+ ### Proxy behavior
189
+
190
+ - The upstream origin is fixed to `https://api.openai.com`.
191
+ - Only `GET /v1/models`, `POST /v1/responses`, and
192
+ `POST /v1/chat/completions` are forwarded.
193
+ - `CONNECT`, `TRACE`, absolute-form URLs, protocol-relative URLs, invalid Host
194
+ headers, and oversized headers are rejected.
195
+ - Hop-by-hop, cookie, forwarding, proxy-authorization, origin, and referrer
196
+ headers are not forwarded upstream.
197
+ - Response status, supported end-to-end headers, streaming bodies, client
198
+ cancellation, and backpressure are preserved.
199
+ - Upstream `429` responses and `Retry-After` are passed through unchanged.
200
+ - Local overload responses use `429` and never retry upstream automatically.
201
+
202
+ ### Browser origin policy
203
+
204
+ - Requests without `Origin` are accepted after bearer authentication.
205
+ - Browser requests require an exact configured `http` or `https` origin.
206
+ - Wildcards, `null`, credentials, paths, queries, and fragments are rejected.
207
+ - Preflight allows only the configured origin and a small documented header
208
+ list.
209
+ - Browser credentials are still caller secrets; Relmio must not encourage
210
+ embedding the local key in a public frontend bundle.
211
+
212
+ ### Health
213
+
214
+ - `GET /health` is the only unauthenticated gateway route.
215
+ - It returns only local process readiness and no provider/account details.
216
+
217
+ ## Codex App Server contract
218
+
219
+ The container command is equivalent to:
220
+
221
+ ```text
222
+ codex app-server
223
+ --strict-config
224
+ --listen ws://0.0.0.0:4500
225
+ --ws-auth capability-token
226
+ --ws-token-sha256 <sha256-verifier>
227
+ ```
228
+
229
+ The host mapping is exactly `127.0.0.1:<selected-port>:4500`.
230
+
231
+ - `CODEX_HOME` is a private named Docker volume.
232
+ - Credential storage is forced to file mode inside the container so refreshed
233
+ credentials remain in the private volume.
234
+ - Login mode is forced to ChatGPT.
235
+ - Root-owned managed requirements allow only Relmio's network-disabled
236
+ permission profile (which extends Codex's built-in workspace profile),
237
+ on-request/user-reviewed approvals, disabled web search, no login shell, and
238
+ a closed set of optional features. Clients cannot request
239
+ `danger-full-access` or approval policy `never`.
240
+ - An empty named workspace volume is mounted; no host source directory, Docker
241
+ socket, SSH key, browser profile, or home directory is mounted.
242
+ - `GET /readyz` is the Docker readiness probe.
243
+ - The client sends `Authorization: Bearer <capability>` during WebSocket
244
+ upgrade, then `initialize`, `initialized`, `account/read`, and the normal
245
+ thread/turn protocol.
246
+ - Relmio does not inject or return raw ChatGPT OAuth tokens through the wizard.
247
+ Raw App Server is a high-trust surface: possession of its capability can
248
+ control the isolated container and may expose the signed-in ChatGPT session.
249
+ The capability is therefore password-equivalent and limited to a trusted,
250
+ same-owner native client.
251
+
252
+ ## Local filesystem and process boundary
253
+
254
+ Managed roots:
255
+
256
+ - `~/.relmio/local/openai-api`
257
+ - `~/.relmio/local/codex-chatgpt`
258
+
259
+ `RELMIO_HOME` may replace `~/.relmio` for testing or advanced use, but it must
260
+ be an absolute path whose final component is `.relmio`.
261
+
262
+ Controls:
263
+
264
+ - Managed directories use mode `0700`; generated files use owner-only modes.
265
+ - The Platform API key is seeded over stdin by a transient, network-disabled
266
+ helper into a private labeled named volume and is never written to a host
267
+ file or Compose environment value.
268
+ - Existing unmanaged directories are never overwritten.
269
+ - Symlinks in a managed path are rejected.
270
+ - A schema-2 JSON marker identifies the target, configured port, validated
271
+ Docker host, 32-hex-character install ID, and collision-resistant Compose
272
+ project name; it contains no secrets.
273
+ - Docker is invoked with argument arrays and `shell: false`.
274
+ - The command allowlist is scoped to the selected Relmio Compose project and
275
+ service.
276
+ - No command targets n8n or `/docker/n8n-openai-oauth`.
277
+ - Local port availability is checked before a new install or a port change.
278
+ - Upstream credentials are cleared from request objects after installation
279
+ completes or fails.
280
+ - Native Windows is rejected before filesystem or Docker writes because the
281
+ required POSIX owner-only modes cannot be enforced there.
282
+
283
+ ## Container hardening
284
+
285
+ Both long-running endpoint services:
286
+
287
+ - run as a non-root user;
288
+ - set `no-new-privileges`;
289
+ - drop all Linux capabilities;
290
+ - use a read-only root filesystem;
291
+ - use bounded tmpfs, PID, memory, and CPU resources;
292
+ - have no Docker socket or host filesystem mount;
293
+ - publish one explicit loopback port only;
294
+ - use pinned application dependencies.
295
+
296
+ The OpenAI install also invokes a one-shot credential seed helper. It has no
297
+ network or published port, disables logging, uses the same read-only image,
298
+ sets `no-new-privileges`, and has tight CPU, memory, and PID limits. It runs as
299
+ root only long enough to replace the volume entry atomically and retains only
300
+ the `CHOWN` capability needed to make that entry readable by the non-root
301
+ gateway; it is removed immediately after seeding.
302
+
303
+ ## Threat model
304
+
305
+ ### Assets
306
+
307
+ - OpenAI Platform API key
308
+ - Codex/ChatGPT refresh and access credentials in the Codex volume
309
+ - generated local capability tokens
310
+ - user prompts, outputs, and Codex thread history
311
+ - local applications that trust the endpoint
312
+
313
+ ### Trust boundaries
314
+
315
+ - browser wizard to loopback wizard server
316
+ - wizard process to local filesystem
317
+ - wizard process to Docker Engine
318
+ - local client to published loopback endpoint
319
+ - gateway to `api.openai.com`
320
+ - Codex App Server to OpenAI's Codex services
321
+
322
+ ### Principal threats and controls
323
+
324
+ | Threat | Required control |
325
+ |---|---|
326
+ | LAN/public exposure | literal `127.0.0.1` Compose binding plus template and runtime inspection tests |
327
+ | Local cross-site request | bearer capability, exact Origin allowlist, strict preflight, Host validation |
328
+ | Upstream key disclosure | separate local/upstream credentials, stdin-seeded private named volume, redacted errors, no body logging |
329
+ | ChatGPT token repurposing | official App Server only; no `/v1` adapter for Codex |
330
+ | Command injection | validated scalar values, spawn argument arrays, no shell |
331
+ | Managed-path takeover | refuse unmanaged roots and every symlinked component |
332
+ | Streaming resource exhaustion | header/body/concurrency/time bounds and backpressure |
333
+ | Docker privilege compromise | document Docker control as a privileged local boundary; mount no Docker socket into services |
334
+ | Secret recovery from UI | show capabilities once; never use browser storage; rotate on reinstall |
335
+ | Codex capability compromise | explicit credential-equivalent warning; trusted same-owner native clients only; private container volumes and no host mounts |
336
+
337
+ ## Acceptance criteria
338
+
339
+ - The browser wizard visibly offers both local providers and the legacy VPS
340
+ path remains separate.
341
+ - Platform keys are accepted only by `openai-api`; ChatGPT auth is accepted only
342
+ by official App Server.
343
+ - Generated Compose files publish only literal loopback bindings.
344
+ - Every non-health gateway operation that can reach OpenAI and every App Server
345
+ WebSocket handshake is capability-authenticated; exact-origin CORS preflight
346
+ is a non-forwarding metadata exception.
347
+ - Gateway unit/integration tests cover auth, origins, Host validation,
348
+ streaming, cancellation, upstream errors, and secret redaction.
349
+ - Local installer tests prove confirmation, unmanaged-root refusal, symlink
350
+ refusal, port collision behavior, exact Docker arguments, file modes, and
351
+ absence of n8n commands.
352
+ - Server tests prove install serialization, lock release, and fresh-process
353
+ Codex login only after persisted installation attestation.
354
+ - Codex login tests use a fake stdio App Server process and cover initialization,
355
+ device-code response validation, completion, cancellation, malformed output,
356
+ and bounded output.
357
+ - Existing remote tests remain green.
358
+ - Opera GX runtime QA verifies keyboard flow, responsive layout, clean console,
359
+ no credential persistence, and correct mode-specific copy.
360
+ - Full `npm run check`, `npm audit --audit-level=high`, and
361
+ `npm pack --dry-run` succeed before handoff.
362
+
363
+ ## Out of scope
364
+
365
+ - Public, LAN, hosted, reverse-proxied, or multi-user endpoints
366
+ - Translating Codex turns into `/v1/chat/completions` or `/v1/responses`
367
+ - Sharing, pooling, reselling, or redistributing any ChatGPT account or benefit
368
+ - TLS termination (loopback-only transport is the boundary for this release)
369
+ - Automatic migration or modification of the existing VPS/n8n deployment
370
+ - A production-support promise for experimental Codex WebSocket transport