feat(server): validate OPENCHAMBER_OPENCODE_HOSTNAME bind hostname

The env var was already read and passed to the managed OpenCode server
spawn, but any non-empty string was accepted. Reject values that are
not a valid IP (IPv4/IPv6, brackets allowed) or DNS-style hostname with
a clear [config] error and fall back to the secure loopback default so
a typo can never silently bind a non-loopback address.

Refs OPE-231
This commit is contained in:
Serhii Dziupin
2026-08-05 11:24:14 +03:00
parent 34c221b07f
commit ddae6f2545
4 changed files with 152 additions and 2 deletions
@@ -50,7 +50,7 @@ const createMockChild = () => {
return child;
};
const createRuntime = (overrides = {}, stateOverrides = {}) => {
const createRuntime = (overrides = {}, stateOverrides = {}, envOverrides = {}) => {
const state = {
openCodeWorkingDirectory: '/tmp/project',
openCodeProcess: null,
@@ -83,6 +83,7 @@ const createRuntime = (overrides = {}, stateOverrides = {}) => {
ENV_EFFECTIVE_PORT: 3001,
ENV_CONFIGURED_OPENCODE_HOSTNAME: '127.0.0.1',
ENV_SKIP_OPENCODE_START: false,
...envOverrides,
},
syncToHmrState: vi.fn(),
syncFromHmrState: vi.fn(),
@@ -312,6 +313,27 @@ describe('OpenCode lifecycle', () => {
expect(server.signalCode).toBe('SIGTERM');
});
it('launches managed OpenCode on the configured bind hostname', async () => {
delete process.env.OPENCODE_BINARY;
const child = createMockChild();
spawnMock.mockImplementationOnce(() => {
queueMicrotask(() => {
child.stdout.emit('data', 'opencode server listening on http://0.0.0.0:45678\n');
});
return child;
});
const runtime = createRuntime({}, {}, { ENV_CONFIGURED_OPENCODE_HOSTNAME: '0.0.0.0' });
const server = await runtime.startOpenCode();
const [binary, args] = spawnMock.mock.calls[0];
expect(binary).toBe('opencode');
expect(args).toEqual(['serve', '--hostname', '0.0.0.0', '--port', '45678']);
await server.close();
expect(server.signalCode).toBe('SIGTERM');
});
it('strips AppImage ARGV0 from managed OpenCode launch env', async () => {
delete process.env.OPENCODE_BINARY;
const previousArgv0 = process.env.ARGV0;