From 4ea9f341544a0221bfcb0985b81abab8c26f1693 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Wed, 27 May 2026 17:01:21 +0200 Subject: [PATCH] chore: portless setup --- .env.example | 2 +- .gitignore | 5 ++++- .../modules/agents/__tests__/agents-manager.test.ts | 13 ++++++++++++- .../agents/__tests__/controller-runtime.test.ts | 6 ++++-- app/server/modules/agents/agents-manager.ts | 10 ++++++++-- app/server/modules/agents/controller/server.ts | 7 ++++++- app/server/modules/agents/local/process.ts | 10 ++++++---- package.json | 2 +- 8 files changed, 42 insertions(+), 13 deletions(-) diff --git a/.env.example b/.env.example index 321cc1e8..8d2c6210 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,6 @@ RESTIC_CACHE_DIR=./data/restic/cache ZEROBYTE_REPOSITORIES_DIR=./data/repositories ZEROBYTE_VOLUMES_DIR=./data/volumes APP_SECRET= -BASE_URL=http://localhost:300 +BASE_URL=http://*.localhost TRUST_PROXY=false PORT=3000 diff --git a/.gitignore b/.gitignore index d3b47d61..856ba616 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,7 @@ notes.md smb-password.txt cache.db -data/ +/data .env* !.env.example @@ -34,8 +34,11 @@ node_modules/ playwright/.auth playwright/temp +playwright/data .idea/ +.deepsec/ +.codex/ # OpenAPI error logs openapi-ts-error-*.log diff --git a/app/server/modules/agents/__tests__/agents-manager.test.ts b/app/server/modules/agents/__tests__/agents-manager.test.ts index 325c2dc3..116e9152 100644 --- a/app/server/modules/agents/__tests__/agents-manager.test.ts +++ b/app/server/modules/agents/__tests__/agents-manager.test.ts @@ -22,7 +22,11 @@ const processWithAgentRuntime = process as ProcessWithAgentRuntime; const setAgentRuntime = () => { processWithAgentRuntime.__zerobyteAgentRuntime = { - agentManager: fromAny({ stop: Effect.void, waitForAgentReady: vi.fn(async () => true) }), + agentManager: fromAny({ + stop: Effect.void, + getControllerUrl: vi.fn(() => "ws://127.0.0.1:4567"), + waitForAgentReady: vi.fn(async () => true), + }), localAgent: null, isStoppingLocalAgent: false, localAgentRestartTimeout: null, @@ -88,6 +92,13 @@ test("respawns the local agent after an unexpected exit", async () => { await vi.advanceTimersByTimeAsync(1_000); expect(spawnMock).toHaveBeenCalledTimes(2); + expect(spawnMock).toHaveBeenLastCalledWith( + "bun", + expect.any(Array), + expect.objectContaining({ + env: expect.objectContaining({ ZEROBYTE_CONTROLLER_URL: "ws://127.0.0.1:4567" }), + }), + ); }); test("does not respawn the local agent after an intentional stop", async () => { diff --git a/app/server/modules/agents/__tests__/controller-runtime.test.ts b/app/server/modules/agents/__tests__/controller-runtime.test.ts index 9ded4e20..7c3ef8b3 100644 --- a/app/server/modules/agents/__tests__/controller-runtime.test.ts +++ b/app/server/modules/agents/__tests__/controller-runtime.test.ts @@ -111,7 +111,7 @@ afterEach(() => { test("websocket fetch rejects requests without a bearer token", async () => { const serve = vi .spyOn(Bun, "serve") - .mockReturnValue(fromPartial({ port: 3001, stop: vi.fn(() => Promise.resolve()) })); + .mockReturnValue(fromPartial({ port: 4567, stop: vi.fn(() => Promise.resolve()) })); const { runtime } = await startRuntime(); const fetch = serve.mock.calls[0]?.[0].fetch; const upgrade = vi.fn(); @@ -120,6 +120,7 @@ test("websocket fetch rejects requests without a bearer token", async () => { const response = await invokeFetch(fetch, new Request("http://localhost:3001/agent"), srv); await Effect.runPromise(runtime.stop); + expect(runtime.getControllerUrl()).toBeNull(); expect(response?.status).toBe(401); expect(await response?.text()).toBe("Missing token"); expect(upgrade).not.toHaveBeenCalled(); @@ -129,8 +130,9 @@ test("websocket fetch rejects invalid bearer tokens", async () => { tokenMocks.validateAgentToken.mockResolvedValue(undefined); const serve = vi .spyOn(Bun, "serve") - .mockReturnValue(fromPartial({ port: 3001, stop: vi.fn(() => Promise.resolve()) })); + .mockReturnValue(fromPartial({ port: 4567, stop: vi.fn(() => Promise.resolve()) })); const { runtime } = await startRuntime(); + expect(runtime.getControllerUrl()).toBe("ws://127.0.0.1:4567"); const fetch = serve.mock.calls[0]?.[0].fetch; const upgrade = vi.fn(); const srv = fromPartial>[1]>({ upgrade }); diff --git a/app/server/modules/agents/agents-manager.ts b/app/server/modules/agents/agents-manager.ts index 09cdd2ce..7a5b9c90 100644 --- a/app/server/modules/agents/agents-manager.ts +++ b/app/server/modules/agents/agents-manager.ts @@ -311,14 +311,20 @@ export const agentManager = { export const startLocalAgent = async () => { const runtime = getAgentRuntimeState(); - await spawnLocalAgentProcess(runtime); if (!runtime.agentManager) { throw new Error( - `startLocalAgent spawned ${LOCAL_AGENT_ID} via spawnLocalAgentProcess, but runtime.agentManager is missing; waitForAgentReady cannot check readiness`, + `startLocalAgent cannot spawn ${LOCAL_AGENT_ID} because runtime.agentManager is missing; waitForAgentReady cannot check readiness`, ); } + const controllerUrl = runtime.agentManager.getControllerUrl(); + if (!controllerUrl) { + throw new Error(`startLocalAgent cannot spawn ${LOCAL_AGENT_ID} because the controller URL is not available`); + } + + await spawnLocalAgentProcess(runtime, controllerUrl); + if (!(await runtime.agentManager.waitForAgentReady(LOCAL_AGENT_ID))) { throw new Error("Local agent did not become ready before startup"); } diff --git a/app/server/modules/agents/controller/server.ts b/app/server/modules/agents/controller/server.ts index 43320cb7..b3de99f5 100644 --- a/app/server/modules/agents/controller/server.ts +++ b/app/server/modules/agents/controller/server.ts @@ -46,6 +46,7 @@ class StopAgentManagerServerError extends Data.TaggedError("StopAgentManagerServ export function createAgentManagerRuntime(onEvent: (event: AgentManagerEvent) => void) { let sessions = new Map(); let runtimeScope: Scope.CloseableScope | null = null; + let controllerUrl: string | null = null; const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const closeSession = (sessionHandle: ControllerAgentSessionHandle) => @@ -215,7 +216,8 @@ export function createAgentManagerRuntime(onEvent: (event: AgentManagerEvent) => const acquireServer = Effect.acquireRelease( Effect.sync(() => Bun.serve({ - port: 3001, + hostname: "127.0.0.1", + port: 0, async fetch(req, srv) { const authorizationHeader = req.headers.get("authorization"); const token = authorizationHeader?.slice("Bearer ".length); @@ -279,6 +281,7 @@ export function createAgentManagerRuntime(onEvent: (event: AgentManagerEvent) => logger.info("Stopping Agent Manager..."); const scope = runtimeScope; runtimeScope = null; + controllerUrl = null; yield* Scope.close(scope, Exit.succeed(undefined)); }); @@ -296,11 +299,13 @@ export function createAgentManagerRuntime(onEvent: (event: AgentManagerEvent) => ), ); runtimeScope = scope; + controllerUrl = `ws://127.0.0.1:${server.port}`; logger.info(`Agent Manager listening on port ${server.port}`); }); return { start, + getControllerUrl: () => controllerUrl, waitForAgentReady: async (agentId: string, timeoutMs = 10_000) => { const deadline = Date.now() + timeoutMs; diff --git a/app/server/modules/agents/local/process.ts b/app/server/modules/agents/local/process.ts index 3852b36a..c70044f7 100644 --- a/app/server/modules/agents/local/process.ts +++ b/app/server/modules/agents/local/process.ts @@ -11,7 +11,7 @@ type LocalAgentState = { localAgentRestartTimeout: ReturnType | null; }; -export async function spawnLocalAgentProcess(runtime: LocalAgentState) { +export async function spawnLocalAgentProcess(runtime: LocalAgentState, controllerUrl: string) { await stopLocalAgentProcess(runtime); if (!config.flags.enableLocalAgent) { @@ -31,7 +31,7 @@ export async function spawnLocalAgentProcess(runtime: LocalAgentState) { const agentProcess = spawn("bun", args, { env: { ...process.env, - ZEROBYTE_CONTROLLER_URL: "ws://localhost:3001", + ZEROBYTE_CONTROLLER_URL: controllerUrl, ZEROBYTE_AGENT_TOKEN: agentToken, }, stdio: ["ignore", "pipe", "pipe"], @@ -62,8 +62,10 @@ export async function spawnLocalAgentProcess(runtime: LocalAgentState) { runtime.localAgentRestartTimeout = setTimeout(() => { runtime.localAgentRestartTimeout = null; - void spawnLocalAgentProcess(runtime).catch((error) => { - logger.error(`Failed to restart local agent: ${error instanceof Error ? error.message : String(error)}`); + void spawnLocalAgentProcess(runtime, controllerUrl).catch((error) => { + logger.error( + `Failed to restart local agent: ${error instanceof Error ? error.message : String(error)}`, + ); }); }, 1_000); }); diff --git a/package.json b/package.json index 27797723..7bf01153 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "fmt": "vp fmt", "lint": "vp lint --type-aware --type-check", "check": "vp check", - "dev": "NODE_ENV=development bunx --bun vite", + "dev": "NODE_ENV=development portless run bunx --bun vite", "build": "bunx --bun vite build", "start": "bun run .output/server/index.mjs", "build:backend-integration": "bun build app/test/backend-integration/index.ts --outdir .output/backend-integration --target bun",