diff --git a/apps/pi-extension/server/network.test.ts b/apps/pi-extension/server/network.test.ts index c261b32b..174e7157 100644 --- a/apps/pi-extension/server/network.test.ts +++ b/apps/pi-extension/server/network.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, test } from "bun:test"; -import { getServerPort, isRemoteSession } from "./network"; +import { getServerHostname, getServerPort, isRemoteSession } from "./network"; const savedEnv: Record = {}; const envKeys = ["PLANNOTATOR_REMOTE", "PLANNOTATOR_PORT", "SSH_TTY", "SSH_CONNECTION"]; @@ -94,3 +94,16 @@ describe("pi port selection", () => { expect(getServerPort()).toEqual({ port: 9999, portSource: "env" }); }); }); + +describe("pi server hostname", () => { + test("binds local sessions to loopback", () => { + clearEnv(); + expect(getServerHostname()).toBe("127.0.0.1"); + }); + + test("binds remote sessions to all interfaces", () => { + clearEnv(); + process.env.PLANNOTATOR_REMOTE = "1"; + expect(getServerHostname()).toBe("0.0.0.0"); + }); +}); diff --git a/apps/pi-extension/server/network.ts b/apps/pi-extension/server/network.ts index 1399aa05..522f4ef5 100644 --- a/apps/pi-extension/server/network.ts +++ b/apps/pi-extension/server/network.ts @@ -8,6 +8,7 @@ import type { Server } from "node:http"; import { release } from "node:os"; const DEFAULT_REMOTE_PORT = 19432; +const LOOPBACK_HOST = "127.0.0.1"; /** * Check if running in a remote session (SSH, devcontainer, etc.) @@ -67,6 +68,10 @@ export function getServerPort(): { return { port: 0, portSource: "random" }; } +export function getServerHostname(): string { + return isRemoteSession() ? "0.0.0.0" : LOOPBACK_HOST; +} + const MAX_RETRIES = 5; const RETRY_DELAY_MS = 500; @@ -81,7 +86,7 @@ export async function listenOnPort( server.once("error", reject); server.listen( result.port, - isRemoteSession() ? "0.0.0.0" : "127.0.0.1", + getServerHostname(), () => { server.removeListener("error", reject); resolve(); diff --git a/packages/server/annotate.ts b/packages/server/annotate.ts index 7d913c55..13865f2a 100644 --- a/packages/server/annotate.ts +++ b/packages/server/annotate.ts @@ -11,7 +11,7 @@ * PLANNOTATOR_PORT - Fixed port to use (default: random locally, 19432 for remote) */ -import { isRemoteSession, getServerPort } from "./remote"; +import { isRemoteSession, getServerHostname, getServerPort } from "./remote"; import { getRepoInfo } from "./repo"; import type { Origin } from "@plannotator/shared/agents"; import { handleImage, handleUpload, handleServerReady, handleDraftSave, handleDraftLoad, handleDraftDelete, handleFavicon } from "./shared-handlers"; @@ -131,6 +131,7 @@ export async function startAnnotateServer( for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { try { server = Bun.serve({ + hostname: getServerHostname(), port: configuredPort, async fetch(req, server) { diff --git a/packages/server/index.ts b/packages/server/index.ts index 91f01f5d..d2c416aa 100644 --- a/packages/server/index.ts +++ b/packages/server/index.ts @@ -11,7 +11,7 @@ import type { Origin } from "@plannotator/shared/agents"; import { resolve } from "path"; -import { isRemoteSession, getServerPort } from "./remote"; +import { isRemoteSession, getServerHostname, getServerPort } from "./remote"; import { openEditorDiff } from "./ide"; import { saveToObsidian, @@ -200,6 +200,7 @@ export async function startPlannotatorServer( for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { try { server = Bun.serve({ + hostname: getServerHostname(), port: configuredPort, async fetch(req, server) { diff --git a/packages/server/remote.test.ts b/packages/server/remote.test.ts index 6cc4eba9..bdf6840d 100644 --- a/packages/server/remote.test.ts +++ b/packages/server/remote.test.ts @@ -5,7 +5,7 @@ */ import { afterEach, describe, expect, test } from "bun:test"; -import { isRemoteSession, getServerPort } from "./remote"; +import { isRemoteSession, getServerHostname, getServerPort } from "./remote"; // Save and restore env between tests const savedEnv: Record = {}; @@ -135,3 +135,16 @@ describe("getServerPort", () => { expect(getServerPort()).toBe(0); }); }); + +describe("getServerHostname", () => { + test("returns loopback for local sessions", () => { + clearEnv(); + expect(getServerHostname()).toBe("127.0.0.1"); + }); + + test("returns all interfaces for remote sessions", () => { + clearEnv(); + process.env.PLANNOTATOR_REMOTE = "1"; + expect(getServerHostname()).toBe("0.0.0.0"); + }); +}); diff --git a/packages/server/remote.ts b/packages/server/remote.ts index 379afca0..64d84f6e 100644 --- a/packages/server/remote.ts +++ b/packages/server/remote.ts @@ -9,6 +9,7 @@ */ const DEFAULT_REMOTE_PORT = 19432; +const LOOPBACK_HOST = "127.0.0.1"; function getRemoteOverride(): boolean | null { const remote = process.env.PLANNOTATOR_REMOTE; @@ -63,3 +64,11 @@ export function getServerPort(): number { // Remote sessions use fixed port for port forwarding; local uses random return isRemoteSession() ? DEFAULT_REMOTE_PORT : 0; } + +/** + * Bind local sessions to loopback, but keep remote sessions reachable via the + * container or host network interface for SSH/devcontainer/Docker forwarding. + */ +export function getServerHostname(): string { + return isRemoteSession() ? "0.0.0.0" : LOOPBACK_HOST; +} diff --git a/packages/server/review.ts b/packages/server/review.ts index 4c785c8f..c6b98ac0 100644 --- a/packages/server/review.ts +++ b/packages/server/review.ts @@ -9,7 +9,7 @@ * PLANNOTATOR_PORT - Fixed port to use (default: random locally, 19432 for remote) */ -import { isRemoteSession, getServerPort } from "./remote"; +import { isRemoteSession, getServerHostname, getServerPort } from "./remote"; import type { Origin } from "@plannotator/shared/agents"; import { type DiffType, type GitContext, runVcsDiff, getVcsFileContentsForDiff, canStageFiles, stageFile, unstageFile, resolveVcsCwd, validateFilePath } from "./vcs"; import { getRepoInfo } from "./repo"; @@ -348,6 +348,7 @@ export async function startReviewServer( for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { try { server = Bun.serve({ + hostname: getServerHostname(), port: configuredPort, async fetch(req, server) {