daemon: fixa TDZ-dispatch + hook-port-per-instans (flera klienter/host)
Två buggar som hittades vid test mot riktiga servern: 1. Dispatchen (`void runConnect()`) låg överst i filen, ovanför module-level const CONFIG_DIR/CRED_FILE. runConnect:s synkrona del (fram till första await) kördes då under modul-init medan de const:erna var i TDZ → loadCreds() fick undefined path → returnerade null → klienten körde alltid nytt device-flöde istället för att återuppta. Flyttad sist i filen. 2. Hook-porten var hårdkodad (8788) → flera daemon-instanser på samma host krockade (EADDRINUSE) och kraschade. Nu: bind hook-porten FÖRST på en ephemeral port (om AGENT_HELM_HOOK_PORT ej satt) och skicka faktiska porten till agenten; bind-fel är icke-fatalt (kör vidare utan hook). Verifierat mot rcai.brasse-pc.eu: resume återupptar rätt session, --new ger en parallell session, två instanser kör samtidigt med var sin hook-port. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U68YyHsxU91ecsen84WQV4
This commit is contained in:
@@ -29,12 +29,6 @@ type Decision = { decision: "allow" | "deny"; reason?: string };
|
||||
const HOOK_PORT = Number(process.env.AGENT_HELM_HOOK_PORT ?? 8788);
|
||||
const AGENT_CMD = process.env.AGENT_CMD ?? "gemini";
|
||||
|
||||
const cmd = process.argv[2];
|
||||
if (cmd === "hook") void runHookMode();
|
||||
else if (cmd === "logout") logout();
|
||||
else if (cmd === "whoami") whoami();
|
||||
else void runConnect();
|
||||
|
||||
/* ------------------------------ creds-hantering ---------------------------- */
|
||||
|
||||
interface Creds {
|
||||
@@ -184,21 +178,17 @@ function runAgent(wsUrl: string, token: string, sessionId: string, clientName: s
|
||||
const AGENT_CWD = process.env.AGENT_CWD ?? process.cwd();
|
||||
const HOOK_DEFAULT: "allow" | "deny" = process.env.AGENT_HELM_HOOK_DEFAULT === "deny" ? "deny" : "allow";
|
||||
const HOOK_TIMEOUT = Number(process.env.AGENT_HELM_HOOK_TIMEOUT ?? 120000);
|
||||
// Explicit hook-port om satt; annars en ledig (ephemeral) port så flera
|
||||
// instanser på samma host inte krockar. Faktiska porten skickas till agenten.
|
||||
const explicitHookPort = process.env.AGENT_HELM_HOOK_PORT ? Number(process.env.AGENT_HELM_HOOK_PORT) : 0;
|
||||
const COLS = 100;
|
||||
const ROWS = 30;
|
||||
|
||||
const pty: IPty = spawn(AGENT_CMD, AGENT_ARGS, {
|
||||
name: "xterm-256color",
|
||||
cols: COLS,
|
||||
rows: ROWS,
|
||||
cwd: AGENT_CWD,
|
||||
env: { ...process.env, AGENT_HELM_HOOK_PORT: String(HOOK_PORT) } as Record<string, string>,
|
||||
});
|
||||
console.log(`[daemon] startade '${AGENT_CMD}' (pid ${pty.pid})`);
|
||||
|
||||
let ws: WebSocket | null = null;
|
||||
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let authFailed = false;
|
||||
let pty: IPty | null = null;
|
||||
let started = false;
|
||||
const pending = new Map<string, (d: Decision) => void>();
|
||||
|
||||
function send(msg: DaemonMessage): void {
|
||||
@@ -215,7 +205,7 @@ function runAgent(wsUrl: string, token: string, sessionId: string, clientName: s
|
||||
const msg = parseMessage<{ type: string; [k: string]: any }>(raw.toString());
|
||||
if (!msg) return;
|
||||
if (msg.type === "server:input") {
|
||||
pty.write(msg.data);
|
||||
pty?.write(msg.data);
|
||||
} else if (msg.type === "server:approval-decision") {
|
||||
const resolve = pending.get(msg.approvalId);
|
||||
if (resolve) {
|
||||
@@ -232,7 +222,7 @@ function runAgent(wsUrl: string, token: string, sessionId: string, clientName: s
|
||||
});
|
||||
ws.on("close", () => {
|
||||
if (authFailed) {
|
||||
pty.kill();
|
||||
pty?.kill();
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("[daemon] anslutning stängd — återansluter om 2s");
|
||||
@@ -249,14 +239,34 @@ function runAgent(wsUrl: string, token: string, sessionId: string, clientName: s
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
pty.onData((data) => send({ type: "daemon:output", data }));
|
||||
pty.onExit(({ exitCode }) => {
|
||||
console.log(`[daemon] agenten avslutades (kod ${exitCode})`);
|
||||
send({ type: "daemon:exit", code: exitCode });
|
||||
setTimeout(() => process.exit(exitCode), 200);
|
||||
});
|
||||
function startPty(hookPort: number): void {
|
||||
if (started) return;
|
||||
started = true;
|
||||
pty = spawn(AGENT_CMD, AGENT_ARGS, {
|
||||
name: "xterm-256color",
|
||||
cols: COLS,
|
||||
rows: ROWS,
|
||||
cwd: AGENT_CWD,
|
||||
env: { ...process.env, AGENT_HELM_HOOK_PORT: String(hookPort) } as Record<string, string>,
|
||||
});
|
||||
console.log(`[daemon] startade '${AGENT_CMD}' (pid ${pty.pid})`);
|
||||
pty.onData((data) => send({ type: "daemon:output", data }));
|
||||
pty.onExit(({ exitCode }) => {
|
||||
console.log(`[daemon] agenten avslutades (kod ${exitCode})`);
|
||||
send({ type: "daemon:exit", code: exitCode });
|
||||
setTimeout(() => process.exit(exitCode), 200);
|
||||
});
|
||||
process.on("SIGINT", shutdown);
|
||||
process.on("SIGTERM", shutdown);
|
||||
connect();
|
||||
}
|
||||
|
||||
// Lokal hook-endpoint (endast 127.0.0.1).
|
||||
function shutdown(): void {
|
||||
pty?.kill();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Lokal hook-endpoint (endast 127.0.0.1). Binds FÖRST så agenten får porten.
|
||||
const hookServer = http.createServer((req, res) => {
|
||||
if (req.method !== "POST" || req.url !== "/approve") {
|
||||
res.writeHead(404);
|
||||
@@ -293,18 +303,16 @@ function runAgent(wsUrl: string, token: string, sessionId: string, clientName: s
|
||||
send({ type: "daemon:approval", approvalId, toolName, toolInput: parsed.toolInput });
|
||||
});
|
||||
});
|
||||
hookServer.listen(HOOK_PORT, "127.0.0.1", () => {
|
||||
console.log(`[daemon] hook-endpoint på 127.0.0.1:${HOOK_PORT} (default vid ingen UI: ${HOOK_DEFAULT})`);
|
||||
hookServer.on("error", (e: NodeJS.ErrnoException) => {
|
||||
console.error(`[daemon] kunde inte binda hook-porten (${e.code ?? e.message}) — kör utan hook (default: ${HOOK_DEFAULT}).`);
|
||||
startPty(explicitHookPort || HOOK_PORT);
|
||||
});
|
||||
hookServer.listen(explicitHookPort, "127.0.0.1", () => {
|
||||
const addr = hookServer.address();
|
||||
const port = typeof addr === "object" && addr ? addr.port : explicitHookPort;
|
||||
console.log(`[daemon] hook-endpoint på 127.0.0.1:${port} (default vid ingen UI: ${HOOK_DEFAULT})`);
|
||||
startPty(port);
|
||||
});
|
||||
|
||||
function shutdown(): void {
|
||||
pty.kill();
|
||||
process.exit(0);
|
||||
}
|
||||
process.on("SIGINT", shutdown);
|
||||
process.on("SIGTERM", shutdown);
|
||||
|
||||
connect();
|
||||
}
|
||||
|
||||
/* --------------------------------- hook-läge ------------------------------- */
|
||||
@@ -372,3 +380,14 @@ function askDaemon(toolName: string, toolInput: unknown): Promise<Decision> {
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/* --------------------------------- dispatch -------------------------------- */
|
||||
// VIKTIGT: körs sist, efter att alla module-level const (CONFIG_DIR/CRED_FILE …)
|
||||
// och funktioner initierats. Annars skulle runConnect:s synkrona del (fram till
|
||||
// första await) köras under modul-init och läsa dessa const i TDZ → loadCreds()
|
||||
// får en undefined path → returnerar null → onödigt nytt device-flöde.
|
||||
const cmd = process.argv[2];
|
||||
if (cmd === "hook") void runHookMode();
|
||||
else if (cmd === "logout") logout();
|
||||
else if (cmd === "whoami") whoami();
|
||||
else void runConnect();
|
||||
|
||||
Reference in New Issue
Block a user