relmio 0.3.1 → 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,460 @@
1
+ import { validatePort } from "./validation.js";
2
+
3
+ export const CODEX_CLI_VERSION = "0.147.0";
4
+
5
+ export const LOCAL_TARGETS = Object.freeze({
6
+ "openai-api": Object.freeze({
7
+ label: "OpenAI API",
8
+ protocol: "openai-v1",
9
+ upstreamAuth: "platform-api-key",
10
+ browserClients: true,
11
+ experimental: false,
12
+ containerPort: 10_531,
13
+ }),
14
+ "codex-chatgpt": Object.freeze({
15
+ label: "Codex with ChatGPT",
16
+ protocol: "codex-app-server-json-rpc",
17
+ upstreamAuth: "chatgpt-via-codex",
18
+ browserClients: false,
19
+ experimental: true,
20
+ containerPort: 4_500,
21
+ }),
22
+ });
23
+
24
+ export function validateLocalTarget(value) {
25
+ if (
26
+ typeof value !== "string" ||
27
+ !Object.prototype.hasOwnProperty.call(LOCAL_TARGETS, value)
28
+ ) {
29
+ throw new TypeError("Local endpoint target is invalid.");
30
+ }
31
+ return value;
32
+ }
33
+
34
+ export function validateLocalPort(value) {
35
+ const port = validatePort(value);
36
+ if (port < 1_024) {
37
+ throw new TypeError("Local endpoint port must be between 1024 and 65535.");
38
+ }
39
+ return port;
40
+ }
41
+
42
+ export function validatePlatformApiKey(value) {
43
+ if (
44
+ typeof value !== "string" ||
45
+ value.length > 512 ||
46
+ !/^sk-[A-Za-z0-9_-]{32,509}$/u.test(value)
47
+ ) {
48
+ throw new TypeError("OpenAI Platform API key is invalid.");
49
+ }
50
+ return value;
51
+ }
52
+
53
+ export function validateSha256Verifier(value) {
54
+ if (typeof value !== "string" || !/^[a-f0-9]{64}$/u.test(value)) {
55
+ throw new TypeError("Local capability verifier is invalid.");
56
+ }
57
+ return value;
58
+ }
59
+
60
+ export function validateInstallId(value) {
61
+ if (typeof value !== "string" || !/^[a-f0-9]{32}$/u.test(value)) {
62
+ throw new TypeError("Local endpoint installation ID is invalid.");
63
+ }
64
+ return value;
65
+ }
66
+
67
+ function validateBrowserOrigin(value) {
68
+ if (typeof value !== "string" || value.length > 2_048) {
69
+ throw new TypeError("Browser origin is invalid.");
70
+ }
71
+
72
+ let url;
73
+ try {
74
+ url = new URL(value);
75
+ } catch {
76
+ throw new TypeError("Browser origin is invalid.");
77
+ }
78
+
79
+ if (
80
+ !["http:", "https:"].includes(url.protocol) ||
81
+ url.username !== "" ||
82
+ url.password !== "" ||
83
+ url.pathname !== "/" ||
84
+ url.search !== "" ||
85
+ url.hash !== "" ||
86
+ value !== url.origin
87
+ ) {
88
+ throw new TypeError("Browser origin is invalid.");
89
+ }
90
+
91
+ return url.origin;
92
+ }
93
+
94
+ export function validateAllowedOrigins(value = []) {
95
+ if (!Array.isArray(value) || value.length > 10) {
96
+ throw new TypeError("Browser origin list is invalid.");
97
+ }
98
+
99
+ return [...new Set(value.map(validateBrowserOrigin))];
100
+ }
101
+
102
+ export function createLocalDeploymentPlan({
103
+ target,
104
+ port,
105
+ allowedOrigins,
106
+ }) {
107
+ const safeTarget = validateLocalTarget(target);
108
+ const safePort = validateLocalPort(port);
109
+ const targetDefinition = LOCAL_TARGETS[safeTarget];
110
+ const safeOrigins =
111
+ safeTarget === "openai-api"
112
+ ? validateAllowedOrigins(allowedOrigins)
113
+ : validateAllowedOrigins([]);
114
+ const endpoint =
115
+ safeTarget === "openai-api"
116
+ ? `http://127.0.0.1:${safePort}/v1`
117
+ : `ws://127.0.0.1:${safePort}`;
118
+
119
+ return {
120
+ target: safeTarget,
121
+ label: targetDefinition.label,
122
+ bindHost: "127.0.0.1",
123
+ port: safePort,
124
+ endpoint,
125
+ protocol: targetDefinition.protocol,
126
+ upstreamAuth: targetDefinition.upstreamAuth,
127
+ allowedOrigins: safeOrigins,
128
+ browserClients: targetDefinition.browserClients,
129
+ experimental: targetDefinition.experimental,
130
+ managedPath: `~/.relmio/local/${safeTarget}`,
131
+ };
132
+ }
133
+
134
+ export function createOpenAiGatewayDockerfile() {
135
+ return `FROM node:22-bookworm-slim
136
+
137
+ WORKDIR /app
138
+ COPY --chown=node:node gateway.mjs /app/gateway.mjs
139
+
140
+ USER node
141
+
142
+ ENTRYPOINT ["node", "/app/gateway.mjs"]
143
+ `;
144
+ }
145
+
146
+ export function createLocalDockerignore(target) {
147
+ const safeTarget = validateLocalTarget(target);
148
+ return safeTarget === "openai-api"
149
+ ? "**\n!Dockerfile\n!gateway.mjs\n"
150
+ : "**\n!Dockerfile\n!config.toml\n!requirements.toml\n";
151
+ }
152
+
153
+ export function createOpenAiGatewayComposeFile({
154
+ port,
155
+ tokenSha256,
156
+ allowedOrigins,
157
+ installId,
158
+ }) {
159
+ const safePort = validateLocalPort(port);
160
+ const safeVerifier = validateSha256Verifier(tokenSha256);
161
+ const safeOrigins = validateAllowedOrigins(allowedOrigins);
162
+ const safeInstallId = validateInstallId(installId);
163
+ const originsBase64 = Buffer.from(JSON.stringify(safeOrigins), "utf8").toString(
164
+ "base64",
165
+ );
166
+ const gatewayImage = `relmio-openai-api-${safeInstallId}-gateway:local`;
167
+
168
+ return `services:
169
+ gateway:
170
+ image: ${gatewayImage}
171
+ build:
172
+ context: .
173
+ dockerfile: Dockerfile
174
+ restart: unless-stopped
175
+ init: true
176
+ environment:
177
+ OPENAI_API_KEY_FILE: /run/relmio-secret/openai-api-key
178
+ RELMIO_GATEWAY_TOKEN_SHA256: ${safeVerifier}
179
+ RELMIO_ALLOWED_ORIGINS_BASE64: ${originsBase64}
180
+ RELMIO_GATEWAY_HOST: 0.0.0.0
181
+ RELMIO_GATEWAY_PORT: "10531"
182
+ volumes:
183
+ - openai-api-key:/run/relmio-secret:ro
184
+ ports:
185
+ - "127.0.0.1:${safePort}:10531"
186
+ security_opt:
187
+ - no-new-privileges:true
188
+ cap_drop:
189
+ - ALL
190
+ read_only: true
191
+ tmpfs:
192
+ - /tmp:size=16m,mode=1777
193
+ pids_limit: 128
194
+ mem_limit: 512m
195
+ cpus: 1.0
196
+ healthcheck:
197
+ test:
198
+ - CMD
199
+ - node
200
+ - -e
201
+ - 'fetch("http://127.0.0.1:10531/health").then((response) => process.exit(response.ok ? 0 : 1)).catch(() => process.exit(1))'
202
+ interval: 10s
203
+ timeout: 5s
204
+ retries: 6
205
+ start_period: 10s
206
+ labels:
207
+ io.relmio.managed: "true"
208
+ io.relmio.target: "openai-api"
209
+ io.relmio.install: "${safeInstallId}"
210
+
211
+ credential-seed:
212
+ image: ${gatewayImage}
213
+ pull_policy: never
214
+ profiles:
215
+ - relmio-credential-seed
216
+ restart: "no"
217
+ network_mode: none
218
+ user: "0:0"
219
+ entrypoint:
220
+ - /bin/sh
221
+ - -c
222
+ command:
223
+ - |
224
+ set -eu
225
+ umask 077
226
+ trap 'rm -f -- /run/relmio-secret/.openai-api-key.next' EXIT HUP INT TERM
227
+ rm -f -- /run/relmio-secret/.openai-api-key.next
228
+ cat > /run/relmio-secret/.openai-api-key.next
229
+ chmod 0400 /run/relmio-secret/.openai-api-key.next
230
+ chown 1000:1000 /run/relmio-secret/.openai-api-key.next
231
+ mv -f -- /run/relmio-secret/.openai-api-key.next /run/relmio-secret/openai-api-key
232
+ trap - EXIT HUP INT TERM
233
+ volumes:
234
+ - openai-api-key:/run/relmio-secret
235
+ security_opt:
236
+ - no-new-privileges:true
237
+ cap_drop:
238
+ - ALL
239
+ cap_add:
240
+ - CHOWN
241
+ read_only: true
242
+ pids_limit: 32
243
+ mem_limit: 64m
244
+ cpus: 0.25
245
+ logging:
246
+ driver: "none"
247
+ labels:
248
+ io.relmio.managed: "true"
249
+ io.relmio.target: "openai-api"
250
+ io.relmio.install: "${safeInstallId}"
251
+
252
+ networks:
253
+ default:
254
+ labels:
255
+ io.relmio.managed: "true"
256
+ io.relmio.target: "openai-api"
257
+ io.relmio.install: "${safeInstallId}"
258
+
259
+ volumes:
260
+ openai-api-key:
261
+ labels:
262
+ io.relmio.managed: "true"
263
+ io.relmio.target: "openai-api"
264
+ io.relmio.install: "${safeInstallId}"
265
+ `;
266
+ }
267
+
268
+ export function createCodexConfig() {
269
+ return `approval_policy = "on-request"
270
+ approvals_reviewer = "user"
271
+ allow_login_shell = false
272
+ check_for_update_on_startup = false
273
+ cli_auth_credentials_store = "file"
274
+ default_permissions = "relmio-workspace"
275
+ forced_login_method = "chatgpt"
276
+ web_search = "disabled"
277
+
278
+ [analytics]
279
+ enabled = false
280
+
281
+ [feedback]
282
+ enabled = false
283
+
284
+ [permissions.relmio-workspace]
285
+ extends = ":workspace"
286
+
287
+ [permissions.relmio-workspace.network]
288
+ enabled = false
289
+
290
+ [shell_environment_policy]
291
+ inherit = "none"
292
+ ignore_default_excludes = false
293
+
294
+ [features]
295
+ apps = false
296
+ auth_elicitation = false
297
+ browser_use = false
298
+ browser_use_external = false
299
+ browser_use_full_cdp_access = false
300
+ code_mode_host = false
301
+ computer_use = false
302
+ fast_mode = false
303
+ goals = false
304
+ hooks = false
305
+ image_generation = false
306
+ in_app_browser = false
307
+ multi_agent = false
308
+ plugins = false
309
+ plugin_sharing = false
310
+ remote_plugin = false
311
+ shell_snapshot = false
312
+ skill_mcp_dependency_install = false
313
+ tool_call_mcp_elicitation = false
314
+ tool_suggest = false
315
+ `;
316
+ }
317
+
318
+ export function createCodexRequirements() {
319
+ return `allowed_approval_policies = ["on-request"]
320
+ allowed_approvals_reviewers = ["user"]
321
+ allowed_login_methods = ["chatgpt"]
322
+ allowed_web_search_modes = ["disabled"]
323
+ allow_managed_hooks_only = true
324
+ allow_remote_control = false
325
+ check_for_update_on_startup = false
326
+ allow_login_shell = false
327
+ default_permissions = "relmio-workspace"
328
+
329
+ [allowed_permission_profiles]
330
+ "relmio-workspace" = true
331
+
332
+ [feedback]
333
+ enabled = false
334
+
335
+ [features]
336
+ apps = false
337
+ auth_elicitation = false
338
+ browser_use = false
339
+ browser_use_external = false
340
+ browser_use_full_cdp_access = false
341
+ code_mode_host = false
342
+ computer_use = false
343
+ fast_mode = false
344
+ goals = false
345
+ hooks = false
346
+ image_generation = false
347
+ in_app_browser = false
348
+ multi_agent = false
349
+ plugins = false
350
+ plugin_sharing = false
351
+ remote_plugin = false
352
+ shell_snapshot = false
353
+ skill_mcp_dependency_install = false
354
+ tool_call_mcp_elicitation = false
355
+ tool_suggest = false
356
+ `;
357
+ }
358
+
359
+ export function createCodexDockerfile() {
360
+ return `FROM node:22-bookworm-slim
361
+
362
+ RUN npm install --global --ignore-scripts @openai/codex@${CODEX_CLI_VERSION} \\
363
+ && npm cache clean --force \\
364
+ && mkdir -p /etc/codex /home/node/.codex /workspace \\
365
+ && chown -R node:node /home/node/.codex /workspace
366
+
367
+ COPY --chmod=0444 requirements.toml /etc/codex/requirements.toml
368
+ COPY --chown=node:node config.toml /home/node/.codex/config.toml
369
+
370
+ ENV CODEX_HOME=/home/node/.codex
371
+ WORKDIR /workspace
372
+ USER node
373
+
374
+ ENTRYPOINT ["codex"]
375
+ `;
376
+ }
377
+
378
+ export function createCodexComposeFile({ port, tokenSha256, installId }) {
379
+ const safePort = validateLocalPort(port);
380
+ const safeVerifier = validateSha256Verifier(tokenSha256);
381
+ const safeInstallId = validateInstallId(installId);
382
+
383
+ return `services:
384
+ codex:
385
+ build:
386
+ context: .
387
+ dockerfile: Dockerfile
388
+ restart: unless-stopped
389
+ init: true
390
+ command:
391
+ - app-server
392
+ - --strict-config
393
+ - --listen
394
+ - ws://0.0.0.0:4500
395
+ - --ws-auth
396
+ - capability-token
397
+ - --ws-token-sha256
398
+ - ${safeVerifier}
399
+ ports:
400
+ - "127.0.0.1:${safePort}:4500"
401
+ volumes:
402
+ - codex-home:/home/node/.codex
403
+ - codex-workspace:/workspace
404
+ security_opt:
405
+ - no-new-privileges:true
406
+ cap_drop:
407
+ - ALL
408
+ read_only: true
409
+ tmpfs:
410
+ - /tmp:size=64m,mode=1777,nodev,nosuid
411
+ - /run:size=16m,mode=0755,nodev,nosuid
412
+ - /home/node/.cache:uid=1000,gid=1000,mode=0700,nodev,nosuid
413
+ pids_limit: 128
414
+ mem_limit: 2g
415
+ cpus: 2.0
416
+ ulimits:
417
+ nofile:
418
+ soft: 1024
419
+ hard: 1024
420
+ core: 0
421
+ logging:
422
+ driver: json-file
423
+ options:
424
+ max-size: 10m
425
+ max-file: "3"
426
+ healthcheck:
427
+ test:
428
+ - CMD
429
+ - node
430
+ - -e
431
+ - 'fetch("http://127.0.0.1:4500/readyz").then((response) => process.exit(response.ok ? 0 : 1)).catch(() => process.exit(1))'
432
+ interval: 10s
433
+ timeout: 5s
434
+ retries: 9
435
+ start_period: 20s
436
+ labels:
437
+ io.relmio.managed: "true"
438
+ io.relmio.target: "codex-chatgpt"
439
+ io.relmio.install: "${safeInstallId}"
440
+
441
+ networks:
442
+ default:
443
+ labels:
444
+ io.relmio.managed: "true"
445
+ io.relmio.target: "codex-chatgpt"
446
+ io.relmio.install: "${safeInstallId}"
447
+
448
+ volumes:
449
+ codex-home:
450
+ labels:
451
+ io.relmio.managed: "true"
452
+ io.relmio.target: "codex-chatgpt"
453
+ io.relmio.install: "${safeInstallId}"
454
+ codex-workspace:
455
+ labels:
456
+ io.relmio.managed: "true"
457
+ io.relmio.target: "codex-chatgpt"
458
+ io.relmio.install: "${safeInstallId}"
459
+ `;
460
+ }