diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c6169fda..65d99fecb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged. ## Unreleased +### A bad `COMPUTER_MEMORY_BYTES` refuses to start the supervisor, instead of capping a computer at 512 bytes + +`COMPUTER_MEMORY_BYTES=512m` used to parse as `512` via `parseInt`, which Docker accepts as a memory +cap Chromium cannot live in. Empty `COMPUTER_MEMORY_BYTES=` is still unset (no cap). A value that is +not a whole number of bytes now exits before any computer is created. ### An IPv6 address in `AGENT_ENDPOINT_ALLOWED_HOSTS` now matches however it is written The endpoint check compares the list against the address as the URL parser spells it, compressed diff --git a/supervisor/src/computer-memory-bytes.ts b/supervisor/src/computer-memory-bytes.ts new file mode 100644 index 000000000..8a1c60574 --- /dev/null +++ b/supervisor/src/computer-memory-bytes.ts @@ -0,0 +1,27 @@ +/** + * Optional Docker memory cap for each computer, from `COMPUTER_MEMORY_BYTES`. + * + * Unset or empty (compose blank, leftover `.env` line) means no cap. The same empty-string and + * prefix traps as `listenPort`: `Number.parseInt("", 10)` is `NaN` (silently dropped) and + * `Number.parseInt("512m", 10)` is `512` bytes, which Docker accepts and Chromium cannot live in. + */ +export function computerMemoryBytes( + raw: string | undefined, +): { ok: true; bytes: number | undefined } | { ok: false; reason: string } { + const trimmed = raw?.trim(); + if (!trimmed) return { ok: true, bytes: undefined }; + if (!/^\d+$/.test(trimmed)) { + return { + ok: false, + reason: `COMPUTER_MEMORY_BYTES must be a whole number of bytes (got ${JSON.stringify(raw)}).`, + }; + } + const value = Number.parseInt(trimmed, 10); + if (value < 1) { + return { + ok: false, + reason: `COMPUTER_MEMORY_BYTES must be a whole number of bytes (got ${JSON.stringify(raw)}).`, + }; + } + return { ok: true, bytes: value }; +} diff --git a/supervisor/src/index.ts b/supervisor/src/index.ts index 89357ccbc..a875679a0 100644 --- a/supervisor/src/index.ts +++ b/supervisor/src/index.ts @@ -12,6 +12,7 @@ import { } from "./docker"; import { registerEntry } from "./identity"; import { namesFor } from "./names"; +import { computerMemoryBytes } from "./computer-memory-bytes"; import { listenPort } from "./listen-port"; /** @@ -60,9 +61,12 @@ if (!token) { const image = process.env.COMPUTER_IMAGE ?? "openbot-agent-computer:latest"; const network = process.env.COMPUTER_NETWORK; const runtime = process.env.COMPUTER_RUNTIME; -const memoryBytes = process.env.COMPUTER_MEMORY_BYTES - ? Number.parseInt(process.env.COMPUTER_MEMORY_BYTES, 10) - : undefined; +const resolvedMemory = computerMemoryBytes(process.env.COMPUTER_MEMORY_BYTES); +if (!resolvedMemory.ok) { + console.error(resolvedMemory.reason); + process.exit(1); +} +const memoryBytes = resolvedMemory.bytes; const spireSocketVolume = process.env.SPIRE_AGENT_SOCKET_VOLUME; /** diff --git a/supervisor/tests/computer-memory-bytes.test.ts b/supervisor/tests/computer-memory-bytes.test.ts new file mode 100644 index 000000000..d5f696b55 --- /dev/null +++ b/supervisor/tests/computer-memory-bytes.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, test } from "bun:test"; +import { computerMemoryBytes } from "../src/computer-memory-bytes"; + +describe("supervisor computer memory bytes", () => { + test("unset and empty string mean no cap", () => { + expect(computerMemoryBytes(undefined)).toEqual({ + ok: true, + bytes: undefined, + }); + expect(computerMemoryBytes("")).toEqual({ ok: true, bytes: undefined }); + expect(computerMemoryBytes(" ")).toEqual({ ok: true, bytes: undefined }); + }); + + test("a whole number of bytes is accepted", () => { + expect(computerMemoryBytes("1073741824")).toEqual({ + ok: true, + bytes: 1_073_741_824, + }); + expect(computerMemoryBytes("1")).toEqual({ ok: true, bytes: 1 }); + }); + + test("suffix units and non-digits are refused instead of parseInt prefixes", () => { + expect(computerMemoryBytes("512m").ok).toBe(false); + expect(computerMemoryBytes("1e9").ok).toBe(false); + expect(computerMemoryBytes("0").ok).toBe(false); + expect(computerMemoryBytes("-1").ok).toBe(false); + expect(computerMemoryBytes("1.5").ok).toBe(false); + }); +});