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.
- package/CHANGELOG.md +51 -0
- package/README.md +126 -36
- package/docs/architecture.md +57 -0
- package/docs/images/examples/gpt-56-model-selector.png +0 -0
- package/docs/images/examples/n8n-openai-credential-connected.png +0 -0
- package/docs/images/examples/telegram-model-results.png +0 -0
- package/docs/images/examples/telegram-n8n-workflow-execution.png +0 -0
- package/docs/images/setup/00-install-methods.png +0 -0
- package/docs/images/setup/01-local-sign-in-ready.png +0 -0
- package/docs/images/setup/02-vps-identity-confirmed.png +0 -0
- package/docs/images/setup/03-n8n-detected.png +0 -0
- package/docs/images/setup/04-install-plan.png +0 -0
- package/docs/images/setup/05-bridge-ready.png +0 -0
- package/docs/local-endpoints-spec.md +370 -0
- package/docs/local-endpoints.md +374 -0
- package/docs/npm-publish.md +121 -201
- package/docs/security.md +92 -3
- package/package.json +2 -2
- package/src/domain/local-endpoints.js +460 -0
- package/src/gateway/openai.js +834 -0
- package/src/infrastructure/local-process.js +375 -0
- package/src/services/codex-login.js +711 -0
- package/src/services/local-installer.js +1120 -0
- package/src/ui/app.js +4 -0
- package/src/ui/index.html +119 -108
- package/src/ui/local.css +262 -0
- package/src/ui/local.html +442 -0
- package/src/ui/local.js +550 -0
- package/src/ui/styles.css +577 -278
- package/src/web/server.js +247 -12
|
@@ -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
|
+
}
|