Skip to content

Commit 66bb621

Browse files
fix: harden Open Design Docker entrypoint (#6)
Merge reviewed fix for issue #6.
1 parent 6d0e3e0 commit 66bb621

4 files changed

Lines changed: 123 additions & 1 deletion

File tree

docker/open-design/Dockerfile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ NODE
3737
RUN pnpm --filter @open-design/daemon build \
3838
&& (pnpm build || pnpm -r build)
3939

40+
COPY entrypoint.mjs /usr/local/bin/open-design-entrypoint.mjs
41+
4042
ENV OD_HOST=0.0.0.0
4143
ENV OD_PORT=7456
4244
ENV OPENCODE_CONFIG_DIR=/home/opencode/.config/opencode
@@ -47,4 +49,4 @@ RUN mkdir -p /home/opencode/.config/opencode /home/opencode/.local/share/opencod
4749
USER opencode
4850
EXPOSE 7456
4951

50-
CMD ["sh", "-lc", "node apps/daemon/dist/cli.js --port ${OD_PORT:-7456} --no-open"]
52+
ENTRYPOINT ["node", "/usr/local/bin/open-design-entrypoint.mjs"]

docker/open-design/entrypoint.mjs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#!/usr/bin/env node
2+
3+
import { spawn } from "node:child_process";
4+
import path from "node:path";
5+
import { fileURLToPath } from "node:url";
6+
7+
const DEFAULT_PORT = "7456";
8+
const DAEMON_ENTRYPOINT = "apps/daemon/dist/cli.js";
9+
10+
export function parsePort(rawPort) {
11+
if (typeof rawPort !== "string" || !/^[0-9]+$/.test(rawPort)) {
12+
throw new Error("OD_PORT must be a decimal TCP port between 1 and 65535");
13+
}
14+
15+
const port = Number(rawPort);
16+
if (!Number.isSafeInteger(port) || port < 1 || port > 65535) {
17+
throw new Error("OD_PORT must be a decimal TCP port between 1 and 65535");
18+
}
19+
20+
return port;
21+
}
22+
23+
export function daemonArguments(port) {
24+
return [DAEMON_ENTRYPOINT, "--port", String(port), "--no-open"];
25+
}
26+
27+
function main() {
28+
let port;
29+
try {
30+
port = parsePort(process.env.OD_PORT ?? DEFAULT_PORT);
31+
} catch (error) {
32+
console.error(`Open Design startup failed: ${error.message}`);
33+
process.exitCode = 1;
34+
return;
35+
}
36+
37+
const daemon = spawn(process.execPath, daemonArguments(port), { stdio: "inherit" });
38+
const forwardSignal = (signal) => daemon.kill(signal);
39+
const cleanup = () => {
40+
process.removeListener("SIGINT", onSigint);
41+
process.removeListener("SIGTERM", onSigterm);
42+
};
43+
const onSigint = () => forwardSignal("SIGINT");
44+
const onSigterm = () => forwardSignal("SIGTERM");
45+
46+
process.once("SIGINT", onSigint);
47+
process.once("SIGTERM", onSigterm);
48+
daemon.once("error", (error) => {
49+
cleanup();
50+
console.error(`Open Design daemon failed to start: ${error.message}`);
51+
process.exitCode = 1;
52+
});
53+
daemon.once("exit", (code, signal) => {
54+
cleanup();
55+
process.exitCode = signal ? 1 : (code ?? 1);
56+
});
57+
}
58+
59+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
60+
main();
61+
}

docs/docker-open-design.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ The compose file mounts:
4545

4646
Do not commit `data` or `opencode-auth`.
4747

48+
`OD_PORT` must be a decimal TCP port from `1` to `65535`; it defaults to
49+
`7456`. The image validates this value before starting the daemon and passes it
50+
as a separate argument without a shell. Invalid values stop the container with
51+
a deterministic startup error.
52+
4853
## HTTP LAN and crypto.randomUUID
4954

5055
Some browser APIs require a secure context. If Open Design frontend code calls `crypto.randomUUID` and your browser blocks it over HTTP LAN, prefer HTTPS. Only patch upstream frontend code as a local operational workaround.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import assert from "node:assert/strict";
2+
import fs from "node:fs";
3+
import os from "node:os";
4+
import path from "node:path";
5+
import { spawnSync } from "node:child_process";
6+
import test from "node:test";
7+
import { fileURLToPath } from "node:url";
8+
9+
import { daemonArguments, parsePort } from "../docker/open-design/entrypoint.mjs";
10+
11+
const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
12+
const ENTRYPOINT = path.join(ROOT, "docker/open-design/entrypoint.mjs");
13+
const DOCKERFILE = path.join(ROOT, "docker/open-design/Dockerfile");
14+
15+
test("[OD001] parsePort accepts decimal ports in the valid range", () => {
16+
for (const [raw, expected] of [["1", 1], ["7456", 7456], ["65535", 65535], ["07456", 7456]]) {
17+
assert.equal(parsePort(raw), expected, raw);
18+
}
19+
});
20+
21+
for (const raw of ["", "0", "65536", "7456 ", " 7456", "+7456", "7456; touch marker", "7456\n--help"]) {
22+
test(`[OD002] parsePort rejects unsafe value ${JSON.stringify(raw)}`, () => {
23+
assert.throws(() => parsePort(raw), /OD_PORT.*1 and 65535/);
24+
});
25+
}
26+
27+
test("[OD003] daemon arguments keep the port as one argument", () => {
28+
assert.deepEqual(daemonArguments(parsePort("7456")), [
29+
"apps/daemon/dist/cli.js",
30+
"--port",
31+
"7456",
32+
"--no-open",
33+
]);
34+
});
35+
36+
test("[OD004] a shell metacharacter fails before any daemon can start", () => {
37+
const marker = path.join(os.tmpdir(), `open-design-entrypoint-${process.pid}-marker`);
38+
const result = spawnSync(process.execPath, [ENTRYPOINT], {
39+
cwd: ROOT,
40+
encoding: "utf8",
41+
env: { ...process.env, OD_PORT: `7456; touch ${marker}` },
42+
});
43+
44+
assert.equal(result.status, 1, result.stderr);
45+
assert.match(result.stderr, /OD_PORT/);
46+
assert.equal(fs.existsSync(marker), false);
47+
});
48+
49+
test("[OD005] Docker uses an exec-form Node entrypoint without shell interpolation", () => {
50+
const dockerfile = fs.readFileSync(DOCKERFILE, "utf8");
51+
assert.match(dockerfile, /COPY entrypoint\.mjs \/usr\/local\/bin\/open-design-entrypoint\.mjs/);
52+
assert.match(dockerfile, /ENTRYPOINT \["node", "\/usr\/local\/bin\/open-design-entrypoint\.mjs"\]/);
53+
assert.doesNotMatch(dockerfile, /sh\s+-lc|\$\{OD_PORT/);
54+
});

0 commit comments

Comments
 (0)