Skip to content

Commit 1053a34

Browse files
Bartok9davidmckayv
andauthored
Refuse a bad COMPUTER_MEMORY_BYTES instead of capping a computer at 512 bytes (#374)
* Refuse a bad COMPUTER_MEMORY_BYTES instead of parseInt-prefixing it Empty COMPUTER_MEMORY_BYTES= is still unset (no cap). Suffix units such as 512m used to become 512 bytes via parseInt, which Docker accepts and Chromium cannot live in. * format --------- Co-authored-by: David McKay <david@copilotkit.ai>
1 parent 54f937f commit 1053a34

4 files changed

Lines changed: 68 additions & 3 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged.
88

99
## Unreleased
1010

11+
### A bad `COMPUTER_MEMORY_BYTES` refuses to start the supervisor, instead of capping a computer at 512 bytes
12+
13+
`COMPUTER_MEMORY_BYTES=512m` used to parse as `512` via `parseInt`, which Docker accepts as a memory
14+
cap Chromium cannot live in. Empty `COMPUTER_MEMORY_BYTES=` is still unset (no cap). A value that is
15+
not a whole number of bytes now exits before any computer is created.
1116
### An IPv6 address in `AGENT_ENDPOINT_ALLOWED_HOSTS` now matches however it is written
1217

1318
The endpoint check compares the list against the address as the URL parser spells it, compressed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Optional Docker memory cap for each computer, from `COMPUTER_MEMORY_BYTES`.
3+
*
4+
* Unset or empty (compose blank, leftover `.env` line) means no cap. The same empty-string and
5+
* prefix traps as `listenPort`: `Number.parseInt("", 10)` is `NaN` (silently dropped) and
6+
* `Number.parseInt("512m", 10)` is `512` bytes, which Docker accepts and Chromium cannot live in.
7+
*/
8+
export function computerMemoryBytes(
9+
raw: string | undefined,
10+
): { ok: true; bytes: number | undefined } | { ok: false; reason: string } {
11+
const trimmed = raw?.trim();
12+
if (!trimmed) return { ok: true, bytes: undefined };
13+
if (!/^\d+$/.test(trimmed)) {
14+
return {
15+
ok: false,
16+
reason: `COMPUTER_MEMORY_BYTES must be a whole number of bytes (got ${JSON.stringify(raw)}).`,
17+
};
18+
}
19+
const value = Number.parseInt(trimmed, 10);
20+
if (value < 1) {
21+
return {
22+
ok: false,
23+
reason: `COMPUTER_MEMORY_BYTES must be a whole number of bytes (got ${JSON.stringify(raw)}).`,
24+
};
25+
}
26+
return { ok: true, bytes: value };
27+
}

‎supervisor/src/index.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
} from "./docker";
1313
import { registerEntry } from "./identity";
1414
import { namesFor } from "./names";
15+
import { computerMemoryBytes } from "./computer-memory-bytes";
1516
import { listenPort } from "./listen-port";
1617

1718
/**
@@ -60,9 +61,12 @@ if (!token) {
6061
const image = process.env.COMPUTER_IMAGE ?? "openbot-agent-computer:latest";
6162
const network = process.env.COMPUTER_NETWORK;
6263
const runtime = process.env.COMPUTER_RUNTIME;
63-
const memoryBytes = process.env.COMPUTER_MEMORY_BYTES
64-
? Number.parseInt(process.env.COMPUTER_MEMORY_BYTES, 10)
65-
: undefined;
64+
const resolvedMemory = computerMemoryBytes(process.env.COMPUTER_MEMORY_BYTES);
65+
if (!resolvedMemory.ok) {
66+
console.error(resolvedMemory.reason);
67+
process.exit(1);
68+
}
69+
const memoryBytes = resolvedMemory.bytes;
6670
const spireSocketVolume = process.env.SPIRE_AGENT_SOCKET_VOLUME;
6771

6872
/**
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { computerMemoryBytes } from "../src/computer-memory-bytes";
3+
4+
describe("supervisor computer memory bytes", () => {
5+
test("unset and empty string mean no cap", () => {
6+
expect(computerMemoryBytes(undefined)).toEqual({
7+
ok: true,
8+
bytes: undefined,
9+
});
10+
expect(computerMemoryBytes("")).toEqual({ ok: true, bytes: undefined });
11+
expect(computerMemoryBytes(" ")).toEqual({ ok: true, bytes: undefined });
12+
});
13+
14+
test("a whole number of bytes is accepted", () => {
15+
expect(computerMemoryBytes("1073741824")).toEqual({
16+
ok: true,
17+
bytes: 1_073_741_824,
18+
});
19+
expect(computerMemoryBytes("1")).toEqual({ ok: true, bytes: 1 });
20+
});
21+
22+
test("suffix units and non-digits are refused instead of parseInt prefixes", () => {
23+
expect(computerMemoryBytes("512m").ok).toBe(false);
24+
expect(computerMemoryBytes("1e9").ok).toBe(false);
25+
expect(computerMemoryBytes("0").ok).toBe(false);
26+
expect(computerMemoryBytes("-1").ok).toBe(false);
27+
expect(computerMemoryBytes("1.5").ok).toBe(false);
28+
});
29+
});

0 commit comments

Comments
 (0)