import { validatePort } from "./validation.js"; import { getProviderTargetBinding } from "./provider-lifecycle.js"; import { validateSiwcAuthBinding, validateSiwcRegistrationId } from "./safety.js"; import packageManifest from "../../package.json" with { type: "json" }; // The constant lives in model-discovery.mjs. Importing it from the gateway would load the whole sidecar server and jose. import { CODEX_CLI_VERSION } from "../services/model-discovery.mjs"; export const GROK_BUILD_CLI_VERSION = "1.0.13"; export const GROK_BUILD_REQUIREMENTS_TOML = [ "[ui]", "disable_bypass_permissions_mode = true", "", "[permission]", "rules = [", ' { action = "deny", tool = "Bash" },', ' { action = "deny", tool = "Edit" },', ' { action = "deny", tool = "Read" },', ' { action = "deny", tool = "Grep" },', ' { action = "deny", tool = "MCPTool" },', ' { action = "deny", tool = "WebFetch" },', ' { action = "deny", tool = "WebSearch" },', "]", "", ].join("\n"); const PACKAGE_VERSION = packageManifest.version; function createLocalTargetDefinition(targetId, runtimeMetadata) { const binding = getProviderTargetBinding(targetId); if ( !runtimeMetadata || typeof runtimeMetadata !== "object" || Object.keys(runtimeMetadata).length !== 3 || !Object.hasOwn(runtimeMetadata, "browserClients") || !Object.hasOwn(runtimeMetadata, "experimental") || !Object.hasOwn(runtimeMetadata, "containerPort") ) { throw new TypeError("Local endpoint runtime metadata is invalid."); } return Object.freeze({ label: binding.label, protocol: binding.protocol, upstreamAuth: binding.upstreamAuth, browserClients: runtimeMetadata.browserClients, experimental: runtimeMetadata.experimental, containerPort: runtimeMetadata.containerPort, }); } export const LOCAL_TARGETS = Object.freeze({ "codex-chatgpt": createLocalTargetDefinition("codex-chatgpt", { browserClients: false, experimental: true, containerPort: 4_500, }), "codex-chat": createLocalTargetDefinition("codex-chat", { browserClients: false, experimental: true, containerPort: 14_501, }), "xai-grok-build": createLocalTargetDefinition("xai-grok-build", { browserClients: false, experimental: true, containerPort: 14_502, }), }); export function validateLocalTarget(value) { if ( typeof value !== "string" || !Object.prototype.hasOwnProperty.call(LOCAL_TARGETS, value) ) { throw new TypeError("Local endpoint target is invalid."); } return value; } export function validateLocalPort(value) { const port = validatePort(value); if (port < 1_024) { throw new TypeError("Local endpoint port must be between 1024 and 65535."); } return port; } export function validateSha256Verifier(value) { if (typeof value !== "string" || !/^[a-f0-9]{64}$/u.test(value)) { throw new TypeError("Local capability verifier is invalid."); } return value; } export function validateInstallId(value) { if (typeof value !== "string" || !/^[a-f0-9]{32}$/u.test(value)) { throw new TypeError("Local endpoint installation ID is invalid."); } return value; } export function createLocalDeploymentPlan({ target, port, authBinding, }) { const safeTarget = validateLocalTarget(target); const safePort = validateLocalPort( safeTarget === "xai-grok-build" ? port ?? 14_502 : port, ); const targetDefinition = LOCAL_TARGETS[safeTarget]; const endpoint = safeTarget === "codex-chatgpt" ? `ws://127.0.0.1:${safePort}` : `http://127.0.0.1:${safePort}`; return { target: safeTarget, label: targetDefinition.label, bindHost: "127.0.0.1", port: safePort, endpoint, protocol: targetDefinition.protocol, upstreamAuth: targetDefinition.upstreamAuth, browserClients: targetDefinition.browserClients, experimental: targetDefinition.experimental, ...(safeTarget === "xai-grok-build" ? {} : { authBinding: validateSiwcAuthBinding(authBinding) }), managedPath: `~/.relmio/local/${safeTarget}`, }; } export function createLocalDockerignore(target) { const safeTarget = validateLocalTarget(target); if (safeTarget === "xai-grok-build") { return "**\n!Dockerfile\n!gateway.js\n!chat.js\n!session.js\n"; } return safeTarget === "codex-chat" ? "**\n!Dockerfile\n!package.json\n!package-lock.json\n!gateway/\n!gateway/codex-chat.js\n!gateway/openai-oauth-sidecar.mjs\n!services/\n!services/siwc-session.mjs\n!services/siwc-handoff.mjs\n!services/local-integration-lifecycle-lock.js\n!services/codex-images.mjs\n!services/model-discovery.mjs\n!infrastructure/\n!infrastructure/local-process.js\n!infrastructure/process-identity.js\n!config.toml\n!requirements.toml\n" : "**\n!Dockerfile\n!package.json\n!package-lock.json\n!gateway/\n!gateway/codex-app-server.mjs\n!gateway/openai-oauth-sidecar.mjs\n!services/\n!services/siwc-session.mjs\n!services/siwc-handoff.mjs\n!services/local-integration-lifecycle-lock.js\n!services/codex-images.mjs\n!services/model-discovery.mjs\n!infrastructure/\n!infrastructure/local-process.js\n!infrastructure/process-identity.js\n!config.toml\n!requirements.toml\n"; } export function createGrokBuildDockerfile() { // The npm launcher prefers GROK_HOME/bin/grok, which is a writable credential // volume. Extract the pinned platform payload into the image instead. const bootstrap = [ 'const fs = require("node:fs");', 'const path = require("node:path");', 'const zlib = require("node:zlib");', 'if (process.platform !== "linux" || !["x64", "arm64"].includes(process.arch)) throw new Error("Unsupported Grok image platform");', 'const metadata = require.resolve("@xai-official/grok-linux-" + process.arch + "/package.json", { paths: ["/usr/local/lib/node_modules/@xai-official/grok"] });', `if (JSON.parse(fs.readFileSync(metadata, "utf8")).version !== "${GROK_BUILD_CLI_VERSION}") throw new Error("Grok platform version mismatch");`, 'const binary = zlib.brotliDecompressSync(fs.readFileSync(path.join(path.dirname(metadata), "bin/grok.br")));', 'fs.mkdirSync("/opt/relmio-grok", { mode: 0o755 });', 'fs.writeFileSync("/opt/relmio-grok/grok", binary, { mode: 0o755, flag: "wx" });', ].join(" "); const requirements = GROK_BUILD_REQUIREMENTS_TOML.trimEnd() .split("\n") .map((line) => `'${line}'`) .join(" "); return `FROM node:22-bookworm-slim WORKDIR /app RUN npm install --global --include=optional --ignore-scripts @xai-official/grok@${GROK_BUILD_CLI_VERSION} \\ && node -e '${bootstrap}' \\ && ln -sf /opt/relmio-grok/grok /usr/local/bin/grok \\ && GROK_HOME=/tmp/relmio-grok-bootstrap GROK_MANAGED_BY_NPM=1 grok --no-auto-update --version \\ && npm cache clean --force \\ && mkdir -p /etc/grok \\ && printf '%s\\n' ${requirements} > /etc/grok/requirements.toml \\ && chmod 0444 /etc/grok/requirements.toml \\ && mkdir -p /home/node/.grok /workspace \\ && chown -R node:node /home/node/.grok /workspace COPY --chown=node:node gateway.js chat.js session.js /app/ ENV GROK_HOME=/home/node/.grok ENV GROK_MANAGED_BY_NPM=1 WORKDIR /workspace USER node ENTRYPOINT ["node", "/app/gateway.js"] `; } export function createGrokBuildComposeFile({ port, tokenSha256, installId }) { const safePort = validateLocalPort(port); const safeVerifier = validateSha256Verifier(tokenSha256); const safeInstallId = validateInstallId(installId); return `services: grok-build: image: relmio-grok-build-${safeInstallId}:local build: context: . dockerfile: Dockerfile restart: unless-stopped init: true environment: RELMIO_GATEWAY_TOKEN_SHA256: ${safeVerifier} RELMIO_GATEWAY_VERSION: ${PACKAGE_VERSION} RELMIO_GATEWAY_HOST: 0.0.0.0 RELMIO_GATEWAY_PORT: "14502" RELMIO_GATEWAY_PUBLIC_PORT: "${safePort}" RELMIO_GATEWAY_INSTALL_ID: "${safeInstallId}" ports: - "127.0.0.1:${safePort}:14502" volumes: - grok-home:/home/node/.grok security_opt: - no-new-privileges:true cap_drop: - ALL read_only: true tmpfs: - /tmp:size=64m,mode=1777,nodev,nosuid - /run:size=16m,mode=0755,nodev,nosuid pids_limit: 128 mem_limit: 2g cpus: 2.0 healthcheck: test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:14502/health').then((r) => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"] interval: 10s timeout: 5s retries: 9 start_period: 20s labels: io.relmio.managed: "true" io.relmio.target: "xai-grok-build" io.relmio.install: "${safeInstallId}" networks: default: labels: io.relmio.managed: "true" io.relmio.target: "xai-grok-build" io.relmio.install: "${safeInstallId}" volumes: grok-home: labels: io.relmio.managed: "true" io.relmio.target: "xai-grok-build" io.relmio.install: "${safeInstallId}" `; } function renderCodexConfig({ approvalPolicy, permissionProfile, permissionProfileBase, }) { const filesystemPermissions = permissionProfile === "relmio-chat-readonly" ? ` [permissions.${permissionProfile}.filesystem] ":root" = "deny" ":minimal" = "read" ":tmpdir" = "deny" ":slash_tmp" = "deny" "/workspace" = "read" "/home/node/.relmio-siwc" = "deny" ` : ` [permissions.${permissionProfile}.filesystem] "/home/node/.relmio-siwc" = "deny" `; return `approval_policy = "${approvalPolicy}" approvals_reviewer = "user" allow_login_shell = false check_for_update_on_startup = false default_permissions = "${permissionProfile}" model_provider = "openai_chatgpt_plan" web_search = "disabled" [model_providers.openai_chatgpt_plan] name = "ChatGPT plan" base_url = "https://api.openai.com/v1" env_key = "ACCESS_TOKEN" wire_api = "responses" requires_openai_auth = false supports_websockets = false [analytics] enabled = false [feedback] enabled = false [permissions.${permissionProfile}] extends = "${permissionProfileBase}" ${filesystemPermissions} [permissions.${permissionProfile}.network] enabled = false [shell_environment_policy] inherit = "none" ignore_default_excludes = false [agents] enabled = false [features] apps = false auth_elicitation = false browser_use = false browser_use_external = false browser_use_full_cdp_access = false code_mode_host = false computer_use = false fast_mode = false goals = false hooks = false image_generation = false in_app_browser = false multi_agent = false plugins = false plugin_sharing = false remote_plugin = false shell_snapshot = false skill_mcp_dependency_install = false tool_call_mcp_elicitation = false tool_suggest = false `; } export function createCodexConfig() { return renderCodexConfig({ approvalPolicy: "on-request", permissionProfile: "relmio-workspace", permissionProfileBase: ":workspace", }); } export function createCodexChatConfig() { return renderCodexConfig({ approvalPolicy: "never", permissionProfile: "relmio-chat-readonly", permissionProfileBase: ":read-only", }); } function renderCodexRequirements({ approvalPolicy, permissionProfile }) { return `allowed_approval_policies = ["${approvalPolicy}"] allowed_approvals_reviewers = ["user"] allowed_web_search_modes = ["disabled"] allow_managed_hooks_only = true allow_remote_control = false check_for_update_on_startup = false allow_login_shell = false default_permissions = "${permissionProfile}" [allowed_permission_profiles] "${permissionProfile}" = true [feedback] enabled = false [features] apps = false auth_elicitation = false browser_use = false browser_use_external = false browser_use_full_cdp_access = false code_mode_host = false computer_use = false fast_mode = false goals = false hooks = false image_generation = false in_app_browser = false multi_agent = false plugins = false plugin_sharing = false remote_plugin = false shell_snapshot = false skill_mcp_dependency_install = false tool_call_mcp_elicitation = false tool_suggest = false `; } export function createCodexRequirements() { return renderCodexRequirements({ approvalPolicy: "on-request", permissionProfile: "relmio-workspace", }); } export function createCodexChatRequirements() { return renderCodexRequirements({ approvalPolicy: "never", permissionProfile: "relmio-chat-readonly", }); } export function createCodexDockerfile() { return `FROM node:24-bookworm-slim WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci --omit=dev --ignore-scripts \\ && npm install --global --ignore-scripts @openai/codex@${CODEX_CLI_VERSION} \\ && npm cache clean --force \\ && mkdir -p /etc/codex /home/node/.codex /home/node/.relmio-siwc /workspace \\ && chown -R node:node /home/node/.codex /home/node/.relmio-siwc /workspace COPY --chmod=0444 requirements.toml /etc/codex/requirements.toml COPY --chown=node:node config.toml /home/node/.codex/config.toml COPY --chown=node:node services/ ./services/ COPY --chown=node:node gateway/ ./gateway/ COPY --chown=node:node infrastructure/ ./infrastructure/ ENV CODEX_HOME=/home/node/.codex WORKDIR /workspace USER node ENTRYPOINT ["node", "/app/gateway/codex-app-server.mjs"] `; } export function createCodexChatDockerfile() { return `FROM node:24-bookworm-slim WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci --omit=dev --ignore-scripts \\ && npm install --global --ignore-scripts @openai/codex@${CODEX_CLI_VERSION} \\ && npm cache clean --force \\ && mkdir -p /etc/codex /home/node/.codex /home/node/.relmio-siwc /workspace \\ && chown -R node:node /home/node/.codex /home/node/.relmio-siwc /workspace COPY --chmod=0444 requirements.toml /etc/codex/requirements.toml COPY --chown=node:node config.toml /home/node/.codex/config.toml COPY --chown=node:node services/ ./services/ COPY --chown=node:node gateway/ ./gateway/ COPY --chown=node:node infrastructure/ ./infrastructure/ ENV CODEX_HOME=/home/node/.codex WORKDIR /workspace USER node ENTRYPOINT ["node", "/app/gateway/codex-chat.js"] `; } export function createCodexChatComposeFile({ port, tokenSha256, installId, registrationId }) { const safePort = validateLocalPort(port); const safeVerifier = validateSha256Verifier(tokenSha256); const safeInstallId = validateInstallId(installId); const safeRegistrationId = validateSiwcRegistrationId(registrationId); const gatewayImage = `relmio-codex-chat-${safeInstallId}:local`; return `services: codex-chat: image: ${gatewayImage} build: context: . dockerfile: Dockerfile restart: unless-stopped init: true environment: RELMIO_GATEWAY_TOKEN_SHA256: ${safeVerifier} RELMIO_GATEWAY_HOST: 0.0.0.0 RELMIO_GATEWAY_PORT: "14501" RELMIO_PACKAGE_VERSION: "${PACKAGE_VERSION}" N8N_OPENAI_OAUTH_HOME: /home/node/.relmio-siwc RELMIO_REGISTRATION_ID: "${safeRegistrationId}" RELMIO_RUNTIME_ID: "${safeInstallId}" ports: - "127.0.0.1:${safePort}:14501" volumes: - codex-state:/home/node/.codex - siwc-store:/home/node/.relmio-siwc - codex-workspace:/workspace security_opt: - no-new-privileges:true cap_drop: - ALL read_only: true tmpfs: - /tmp:size=64m,mode=1777,nodev,nosuid - /run:size=16m,mode=0755,nodev,nosuid - /home/node/.cache:uid=1000,gid=1000,mode=0700,nodev,nosuid pids_limit: 128 mem_limit: 2g cpus: 2.0 ulimits: nofile: soft: 1024 hard: 1024 core: 0 logging: driver: json-file options: max-size: 10m max-file: "3" healthcheck: test: - CMD - node - -e - 'fetch("http://127.0.0.1:14501/health").then((response) => process.exit(response.ok ? 0 : 1)).catch(() => process.exit(1))' interval: 10s timeout: 5s retries: 9 start_period: 20s labels: io.relmio.managed: "true" io.relmio.target: "codex-chat" io.relmio.install: "${safeInstallId}" networks: default: labels: io.relmio.managed: "true" io.relmio.target: "codex-chat" io.relmio.install: "${safeInstallId}" volumes: codex-state: labels: io.relmio.managed: "true" io.relmio.target: "codex-chat" io.relmio.install: "${safeInstallId}" siwc-store: labels: io.relmio.managed: "true" io.relmio.target: "codex-chat" io.relmio.install: "${safeInstallId}" codex-workspace: labels: io.relmio.managed: "true" io.relmio.target: "codex-chat" io.relmio.install: "${safeInstallId}" `; } export function createCodexComposeFile({ port, tokenSha256, installId, registrationId }) { const safePort = validateLocalPort(port); const safeVerifier = validateSha256Verifier(tokenSha256); const safeInstallId = validateInstallId(installId); const safeRegistrationId = validateSiwcRegistrationId(registrationId); return `services: codex: build: context: . dockerfile: Dockerfile restart: unless-stopped init: true environment: RELMIO_GATEWAY_TOKEN_SHA256: ${safeVerifier} RELMIO_GATEWAY_HOST: 0.0.0.0 RELMIO_GATEWAY_PORT: "4500" RELMIO_PACKAGE_VERSION: "${PACKAGE_VERSION}" N8N_OPENAI_OAUTH_HOME: /home/node/.relmio-siwc RELMIO_REGISTRATION_ID: "${safeRegistrationId}" RELMIO_RUNTIME_ID: "${safeInstallId}" ports: - "127.0.0.1:${safePort}:4500" volumes: - codex-state:/home/node/.codex - siwc-store:/home/node/.relmio-siwc - codex-workspace:/workspace security_opt: - no-new-privileges:true cap_drop: - ALL read_only: true tmpfs: - /tmp:size=64m,mode=1777,nodev,nosuid - /run:size=16m,mode=0755,nodev,nosuid - /home/node/.cache:uid=1000,gid=1000,mode=0700,nodev,nosuid pids_limit: 128 mem_limit: 2g cpus: 2.0 ulimits: nofile: soft: 1024 hard: 1024 core: 0 logging: driver: json-file options: max-size: 10m max-file: "3" healthcheck: test: - CMD - node - -e - 'fetch("http://127.0.0.1:4500/readyz").then((response) => process.exit(response.ok ? 0 : 1)).catch(() => process.exit(1))' interval: 10s timeout: 5s retries: 9 start_period: 20s labels: io.relmio.managed: "true" io.relmio.target: "codex-chatgpt" io.relmio.install: "${safeInstallId}" networks: default: labels: io.relmio.managed: "true" io.relmio.target: "codex-chatgpt" io.relmio.install: "${safeInstallId}" volumes: codex-state: labels: io.relmio.managed: "true" io.relmio.target: "codex-chatgpt" io.relmio.install: "${safeInstallId}" siwc-store: labels: io.relmio.managed: "true" io.relmio.target: "codex-chatgpt" io.relmio.install: "${safeInstallId}" codex-workspace: labels: io.relmio.managed: "true" io.relmio.target: "codex-chatgpt" io.relmio.install: "${safeInstallId}" `; }