Skip to content

Commit c2dadc9

Browse files
authored
Bind local Plannotator servers to loopback by default (#533)
* Harden review agent job launch endpoints * Stabilize agent job auth tests * Drop review agent job token gating * Align useAgentJobs with upstream formatting * Allow remote sessions to bind all interfaces
1 parent e410ac2 commit c2dadc9

7 files changed

Lines changed: 49 additions & 6 deletions

File tree

apps/pi-extension/server/network.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { afterEach, describe, expect, test } from "bun:test";
2-
import { getServerPort, isRemoteSession } from "./network";
2+
import { getServerHostname, getServerPort, isRemoteSession } from "./network";
33

44
const savedEnv: Record<string, string | undefined> = {};
55
const envKeys = ["PLANNOTATOR_REMOTE", "PLANNOTATOR_PORT", "SSH_TTY", "SSH_CONNECTION"];
@@ -94,3 +94,16 @@ describe("pi port selection", () => {
9494
expect(getServerPort()).toEqual({ port: 9999, portSource: "env" });
9595
});
9696
});
97+
98+
describe("pi server hostname", () => {
99+
test("binds local sessions to loopback", () => {
100+
clearEnv();
101+
expect(getServerHostname()).toBe("127.0.0.1");
102+
});
103+
104+
test("binds remote sessions to all interfaces", () => {
105+
clearEnv();
106+
process.env.PLANNOTATOR_REMOTE = "1";
107+
expect(getServerHostname()).toBe("0.0.0.0");
108+
});
109+
});

apps/pi-extension/server/network.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { Server } from "node:http";
88
import { release } from "node:os";
99

1010
const DEFAULT_REMOTE_PORT = 19432;
11+
const LOOPBACK_HOST = "127.0.0.1";
1112

1213
/**
1314
* Check if running in a remote session (SSH, devcontainer, etc.)
@@ -67,6 +68,10 @@ export function getServerPort(): {
6768
return { port: 0, portSource: "random" };
6869
}
6970

71+
export function getServerHostname(): string {
72+
return isRemoteSession() ? "0.0.0.0" : LOOPBACK_HOST;
73+
}
74+
7075
const MAX_RETRIES = 5;
7176
const RETRY_DELAY_MS = 500;
7277

@@ -81,7 +86,7 @@ export async function listenOnPort(
8186
server.once("error", reject);
8287
server.listen(
8388
result.port,
84-
isRemoteSession() ? "0.0.0.0" : "127.0.0.1",
89+
getServerHostname(),
8590
() => {
8691
server.removeListener("error", reject);
8792
resolve();

packages/server/annotate.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
* PLANNOTATOR_PORT - Fixed port to use (default: random locally, 19432 for remote)
1212
*/
1313

14-
import { isRemoteSession, getServerPort } from "./remote";
14+
import { isRemoteSession, getServerHostname, getServerPort } from "./remote";
1515
import { getRepoInfo } from "./repo";
1616
import type { Origin } from "@plannotator/shared/agents";
1717
import { handleImage, handleUpload, handleServerReady, handleDraftSave, handleDraftLoad, handleDraftDelete, handleFavicon } from "./shared-handlers";
@@ -131,6 +131,7 @@ export async function startAnnotateServer(
131131
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
132132
try {
133133
server = Bun.serve({
134+
hostname: getServerHostname(),
134135
port: configuredPort,
135136

136137
async fetch(req, server) {

packages/server/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
import type { Origin } from "@plannotator/shared/agents";
1313
import { resolve } from "path";
14-
import { isRemoteSession, getServerPort } from "./remote";
14+
import { isRemoteSession, getServerHostname, getServerPort } from "./remote";
1515
import { openEditorDiff } from "./ide";
1616
import {
1717
saveToObsidian,
@@ -200,6 +200,7 @@ export async function startPlannotatorServer(
200200
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
201201
try {
202202
server = Bun.serve({
203+
hostname: getServerHostname(),
203204
port: configuredPort,
204205

205206
async fetch(req, server) {

packages/server/remote.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77
import { afterEach, describe, expect, test } from "bun:test";
8-
import { isRemoteSession, getServerPort } from "./remote";
8+
import { isRemoteSession, getServerHostname, getServerPort } from "./remote";
99

1010
// Save and restore env between tests
1111
const savedEnv: Record<string, string | undefined> = {};
@@ -135,3 +135,16 @@ describe("getServerPort", () => {
135135
expect(getServerPort()).toBe(0);
136136
});
137137
});
138+
139+
describe("getServerHostname", () => {
140+
test("returns loopback for local sessions", () => {
141+
clearEnv();
142+
expect(getServerHostname()).toBe("127.0.0.1");
143+
});
144+
145+
test("returns all interfaces for remote sessions", () => {
146+
clearEnv();
147+
process.env.PLANNOTATOR_REMOTE = "1";
148+
expect(getServerHostname()).toBe("0.0.0.0");
149+
});
150+
});

packages/server/remote.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
*/
1010

1111
const DEFAULT_REMOTE_PORT = 19432;
12+
const LOOPBACK_HOST = "127.0.0.1";
1213

1314
function getRemoteOverride(): boolean | null {
1415
const remote = process.env.PLANNOTATOR_REMOTE;
@@ -63,3 +64,11 @@ export function getServerPort(): number {
6364
// Remote sessions use fixed port for port forwarding; local uses random
6465
return isRemoteSession() ? DEFAULT_REMOTE_PORT : 0;
6566
}
67+
68+
/**
69+
* Bind local sessions to loopback, but keep remote sessions reachable via the
70+
* container or host network interface for SSH/devcontainer/Docker forwarding.
71+
*/
72+
export function getServerHostname(): string {
73+
return isRemoteSession() ? "0.0.0.0" : LOOPBACK_HOST;
74+
}

packages/server/review.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* PLANNOTATOR_PORT - Fixed port to use (default: random locally, 19432 for remote)
1010
*/
1111

12-
import { isRemoteSession, getServerPort } from "./remote";
12+
import { isRemoteSession, getServerHostname, getServerPort } from "./remote";
1313
import type { Origin } from "@plannotator/shared/agents";
1414
import { type DiffType, type GitContext, runVcsDiff, getVcsFileContentsForDiff, canStageFiles, stageFile, unstageFile, resolveVcsCwd, validateFilePath } from "./vcs";
1515
import { getRepoInfo } from "./repo";
@@ -348,6 +348,7 @@ export async function startReviewServer(
348348
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
349349
try {
350350
server = Bun.serve({
351+
hostname: getServerHostname(),
351352
port: configuredPort,
352353

353354
async fetch(req, server) {

0 commit comments

Comments
 (0)