Repository navigation
feat(container): add loopback-first Compose with compatibility identity (carry of #3421) #3604
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
168b792
2c37259
24f4cb2
9416028
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| ** | ||
|
|
||
| !package.json | ||
| !bun.lock | ||
| !tsconfig.json | ||
|
|
||
| !src/ | ||
| !src/** | ||
| # Prepared on the host with the canonical Git-tracked-source generator. | ||
| !src/generated/compatibility-version.json | ||
|
|
||
| !docker/ | ||
| !docker/** | ||
|
|
||
| !gui/ | ||
| !gui/** | ||
| gui/node_modules/ | ||
| gui/dist/ | ||
| gui/.vite/ | ||
|
|
||
| **/*.log | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| # syntax=docker/dockerfile:1 | ||
|
|
||
| # Keep the runtime aligned with package.json and pin the multi-platform image index. | ||
| ARG BUN_IMAGE=oven/bun:1.4.0@sha256:5ff609364c049b54eb0ff560ec96319729a972078ef2c755d758f0c6ef89c2d6 | ||
|
|
||
| FROM ${BUN_IMAGE} AS build | ||
| WORKDIR /home/bun/app | ||
|
|
||
| COPY --chown=bun:bun package.json bun.lock tsconfig.json ./ | ||
| RUN bun install --frozen-lockfile | ||
|
|
||
| COPY --chown=bun:bun gui/package.json gui/bun.lock ./gui/ | ||
| RUN cd gui && bun install --frozen-lockfile | ||
|
|
||
| COPY --chown=bun:bun src ./src | ||
| COPY --chown=bun:bun docker ./docker | ||
| COPY --chown=bun:bun gui ./gui | ||
| RUN cd gui && bun run build | ||
|
|
||
| FROM ${BUN_IMAGE} AS runtime | ||
| WORKDIR /home/bun/app | ||
|
|
||
| ENV NODE_ENV=production \ | ||
| OPENCODEX_HOME=/home/bun/.opencodex \ | ||
| OCX_API_TOKEN_FILE=/home/bun/.opencodex/service-api-token | ||
|
|
||
| RUN install -d -m 0700 -o bun -g bun /home/bun/.opencodex | ||
| COPY --chown=bun:bun --chmod=0600 docker/config.json /home/bun/.opencodex/config.json | ||
|
|
||
| COPY --from=build --chown=bun:bun /home/bun/app/package.json ./package.json | ||
| COPY --from=build --chown=bun:bun /home/bun/app/bun.lock ./bun.lock | ||
| COPY --from=build --chown=bun:bun /home/bun/app/node_modules ./node_modules | ||
| COPY --from=build --chown=bun:bun /home/bun/app/src ./src | ||
| # Run `bun scripts/generate-compatibility-version.ts` on the host before building. | ||
| # Explicit COPY makes a missing artifact a build failure; .git stays outside the context. | ||
| COPY --chown=bun:bun src/generated/compatibility-version.json ./src/generated/compatibility-version.json | ||
| COPY --from=build --chown=bun:bun /home/bun/app/docker ./docker | ||
| COPY --from=build --chown=bun:bun /home/bun/app/gui/dist ./gui/dist | ||
|
|
||
| USER bun | ||
| RUN ["bun", "-e", "import { readOpenCodexCompatibilityVersion } from './src/routing/compatibility/version.ts'; if (!/^[0-9a-f]{64}$/.test(readOpenCodexCompatibilityVersion() ?? '')) throw new Error('Missing or invalid generated compatibility manifest');"] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
| VOLUME ["/home/bun/.opencodex"] | ||
| EXPOSE 10100 | ||
|
|
||
| HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ | ||
| CMD ["bun", "-e", "const r=await fetch('http://127.0.0.1:10100/healthz');if(!r.ok)process.exit(1)"] | ||
|
|
||
| CMD ["bun", "run", "src/cli/index.ts", "start", "--port", "10100"] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| name: opencodex | ||
|
|
||
| services: | ||
| hub: | ||
| image: opencodex:local | ||
| build: | ||
| context: . | ||
| dockerfile: Dockerfile | ||
| target: runtime | ||
| init: true | ||
| read_only: true | ||
| ports: | ||
| - "${OPENCODEX_BIND_ADDRESS:-127.0.0.1}:${OPENCODEX_PORT:-10100}:10100" | ||
| volumes: | ||
| - ocx-state:/home/bun/.opencodex | ||
| tmpfs: | ||
| - /tmp:size=64m,mode=1777 | ||
| security_opt: | ||
| - no-new-privileges:true | ||
| cap_drop: | ||
| - ALL | ||
| restart: unless-stopped | ||
| stop_grace_period: 30s | ||
|
|
||
| volumes: | ||
| ocx-state: |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| import { writeServiceApiTokenFile } from "../src/lib/service-secrets"; | ||
|
|
||
| const MAX_TOKEN_BYTES = 4096; | ||
| const MAX_INPUT_BYTES = MAX_TOKEN_BYTES + 2; | ||
|
|
||
| export async function readBoundedToken(stream: ReadableStream<Uint8Array>): Promise<string> { | ||
| const reader = stream.getReader(); | ||
| const decoder = new TextDecoder(); | ||
| let bytes = 0; | ||
| let raw = ""; | ||
|
|
||
| try { | ||
| while (true) { | ||
| const { done, value } = await reader.read(); | ||
| if (done) break; | ||
| bytes += value.byteLength; | ||
| if (bytes > MAX_INPUT_BYTES) throw new Error("token input exceeds 4096 bytes"); | ||
| raw += decoder.decode(value, { stream: true }); | ||
| } | ||
| raw += decoder.decode(); | ||
| } finally { | ||
| reader.releaseLock(); | ||
| } | ||
|
|
||
| const line = raw.endsWith("\r\n") ? raw.slice(0, -2) : raw.endsWith("\n") ? raw.slice(0, -1) : raw; | ||
| if (/[\r\n\0]/.test(line)) throw new Error("token input must contain exactly one line"); | ||
|
|
||
| const token = line.trim(); | ||
| if (!token) throw new Error("token input is empty"); | ||
| if (Buffer.byteLength(token) > MAX_TOKEN_BYTES) throw new Error("token input exceeds 4096 bytes"); | ||
| return token; | ||
| } | ||
|
|
||
| export async function bootstrapToken(stream: ReadableStream<Uint8Array>): Promise<void> { | ||
| const token = await readBoundedToken(stream); | ||
| writeServiceApiTokenFile(token); | ||
| } | ||
|
|
||
| if (import.meta.main) { | ||
| try { | ||
| await bootstrapToken(Bun.stdin.stream()); | ||
| console.log("Initialized the owner-only data-plane token in the container state volume."); | ||
| } catch (error) { | ||
| console.error(`Token initialization failed: ${error instanceof Error ? error.message : "unknown error"}`); | ||
| process.exitCode = 1; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| { | ||
| "port": 10100, | ||
| "runtimeRole": "hub", | ||
| "hostname": "0.0.0.0", | ||
| "providers": { | ||
| "openai": { | ||
| "adapter": "openai-responses", | ||
| "baseUrl": "https://chatgpt.com/backend-api/codex", | ||
| "authMode": "forward", | ||
| "codexAccountMode": "pool" | ||
| } | ||
| }, | ||
| "defaultProvider": "openai", | ||
| "codexAutoStart": false, | ||
| "codexShimAutoRestore": false | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even immediately after generating the manifest,
!src/**sends every untracked file undersrcinto the build, whilegenerate-compatibility-version.tsintentionally enumerates onlygit ls-files. An untracked module that shadows a tracked directory import (for example,foo.tsalongside trackedfoo/index.ts) can consequently execute in the container without contributing to its compatibility identity. Build the source context from the tracked file list, or reject unexpected source files before accepting the image.Useful? React with 👍 / 👎.