fix(core,tooling): keep the harness's own ports out of the Agent's environment and off the dev server (#100)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: J.Li <jl2388@cam.ac.uk>
This commit is contained in:
Yaowei Zheng
2026-07-29 16:15:23 +08:00
committed by GitHub
parent 5530b7ffad
commit 6b3574f5c9
14 changed files with 237 additions and 15 deletions
+2 -2
View File
@@ -23,11 +23,11 @@ DTO types are imported type-only from `@prismshadow/penguin-server/api`; no serv
Prereqs: Node >= 24, pnpm; run `pnpm install` at the repo root first (core must be built — the root `dev:*` scripts handle that).
```bash
pnpm dev:server # backend at 127.0.0.1:7364
pnpm dev:server # backend at 127.0.0.1:7368 (dev port, not the installed server's 7364)
pnpm dev:web # Vite dev server at 127.0.0.1:7365; /api proxied (SSE passes through)
```
The proxy target defaults to `http://127.0.0.1:7364` (`PENGUIN_API_PROXY` overrides). Auth is a same-origin HttpOnly cookie, so the proxy keeps everything same-origin.
The proxy target defaults to `http://127.0.0.1:7368` — the development backend, kept off the installed server's 7364 so the two can run at once (`PORT` moves both, `PENGUIN_API_PROXY` overrides the target outright). Auth is a same-origin HttpOnly cookie, so the proxy keeps everything same-origin.
```bash
pnpm --filter @prismshadow/penguin-web typecheck
+32
View File
@@ -0,0 +1,32 @@
/**
* Dev-server `/api` proxy target resolution (vite.config.ts). The interesting case is an
* empty `PORT=`: shells export it, `.env` files carry it, and resolving it to a bare
* `http://127.0.0.1:` sends every API call to port 80 without ever failing — a wrong backend
* is far harder to notice than a broken one, so the empty case is pinned here.
*/
import { describe, expect, it } from "vitest";
import { apiProxyTarget } from "../vite.config.js";
describe("apiProxyTarget", () => {
it("defaults to the development backend, not the installed server's 7364", () => {
expect(apiProxyTarget({})).toBe("http://127.0.0.1:7368");
});
it("treats an empty PORT as unset rather than as port 80", () => {
expect(apiProxyTarget({ PORT: "" })).toBe("http://127.0.0.1:7368");
});
it("follows PORT so moving the backend moves the proxy with it", () => {
expect(apiProxyTarget({ PORT: "9999" })).toBe("http://127.0.0.1:9999");
});
it("PENGUIN_API_PROXY replaces the whole target, PORT and all", () => {
expect(apiProxyTarget({ PORT: "9999", PENGUIN_API_PROXY: "http://10.0.0.2:8080" })).toBe(
"http://10.0.0.2:8080",
);
});
it("an empty PENGUIN_API_PROXY falls back instead of proxying to nowhere", () => {
expect(apiProxyTarget({ PENGUIN_API_PROXY: "" })).toBe("http://127.0.0.1:7368");
});
});
+25 -6
View File
@@ -1,9 +1,11 @@
/**
* Vite config: React SPA + Tailwind CSS 4.
*
* Dev server listens on 7365; `/api` is proxied to the local Web server (defaults to 127.0.0.1:7364,
* overridable via PENGUIN_API_PROXY). SSE (text/event-stream) passes through http-proxy transparently, no
* special config needed.
* Dev server listens on 7365; `/api` is proxied to the **development** backend (127.0.0.1:7368 --
* `pnpm dev:server`, deliberately not the installed server's 7364, which is routinely running at the
* same time). Honors PORT so overriding the backend port moves the proxy with it, and
* PENGUIN_API_PROXY overrides the whole target. SSE (text/event-stream) passes through http-proxy
* transparently, no special config needed.
* The vitest config is kept separate in vitest.config.ts (its embedded vite 5 types conflict with this
* package's vite 7 plugin types, hence the separate file to avoid the clash).
*/
@@ -11,15 +13,32 @@ import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";
/**
* Resolves the `/api` proxy target: PENGUIN_API_PROXY replaces it outright, otherwise the
* development backend on PORT — the same variable `pnpm dev:server` binds — defaulting to 7368.
*
* Empty counts as unset, as everywhere else PORT is read in this repo (server/src/config.ts,
* cli/src/commands/serve.ts, scripts/run-with-env.mjs). Here `??` would be actively harmful: an
* exported-but-empty `PORT=` yields `http://127.0.0.1:` — port 80 — and every /api call would be
* answered by whatever happens to listen there, silently and without an error, which is the exact
* wrong-backend failure this default exists to prevent.
*
* Takes the environment as an argument so the resolution can be unit-tested (a vite config's
* `server.proxy` cannot be exercised without starting a dev server).
*/
export function apiProxyTarget(env: Record<string, string | undefined> = process.env): string {
return env.PENGUIN_API_PROXY || `http://127.0.0.1:${env.PORT || "7368"}`;
}
export default defineConfig({
plugins: [react(), tailwindcss()],
server: {
// Fixed PenguinHarness dev port (stands alone — only the main server default is
// shared, as DEFAULT_SERVER_PORT in core; vite configs cannot import core TS).
// Fixed PenguinHarness dev port (stands alone — vite configs cannot import core TS,
// so the numbers are literals here; the allocation table lives in core's internal/ports.ts).
port: 7365,
proxy: {
"/api": {
target: process.env.PENGUIN_API_PROXY ?? "http://127.0.0.1:7364",
target: apiProxyTarget(),
changeOrigin: false,
},
},