chore: portless setup

This commit is contained in:
Nicolas Meienberger 2026-05-27 17:01:21 +02:00
parent e4898b97ea
commit 4ea9f34154
No known key found for this signature in database
8 changed files with 42 additions and 13 deletions

View file

@ -4,6 +4,6 @@ RESTIC_CACHE_DIR=./data/restic/cache
ZEROBYTE_REPOSITORIES_DIR=./data/repositories
ZEROBYTE_VOLUMES_DIR=./data/volumes
APP_SECRET=<openssl rand -hex 32>
BASE_URL=http://localhost:300
BASE_URL=http://*.localhost
TRUST_PROXY=false
PORT=3000

5
.gitignore vendored
View file

@ -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

View file

@ -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 () => {

View file

@ -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<Parameters<NonNullable<typeof fetch>>[1]>({ upgrade });

View file

@ -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");
}

View file

@ -46,6 +46,7 @@ class StopAgentManagerServerError extends Data.TaggedError("StopAgentManagerServ
export function createAgentManagerRuntime(onEvent: (event: AgentManagerEvent) => void) {
let sessions = new Map<string, ControllerAgentSessionHandle>();
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<AgentConnectionData>({
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;

View file

@ -11,7 +11,7 @@ type LocalAgentState = {
localAgentRestartTimeout: ReturnType<typeof setTimeout> | 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);
});

View file

@ -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",