fluxy-bot 0.12.0 → 0.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/supervisor/backend.ts +26 -10
- package/supervisor/index.ts +24 -13
package/package.json
CHANGED
package/supervisor/backend.ts
CHANGED
|
@@ -97,31 +97,47 @@ export function spawnBackend(port: number): ChildProcess {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
/** Stop the backend and wait for the process to fully exit before resolving.
|
|
100
|
-
* This prevents port collisions when restarting (old process must release the port first).
|
|
100
|
+
* This prevents port collisions when restarting (old process must release the port first).
|
|
101
|
+
* Concurrent calls return the same promise to avoid double-spawn races. */
|
|
102
|
+
let stopPromise: Promise<void> | null = null;
|
|
103
|
+
|
|
101
104
|
export function stopBackend(): Promise<void> {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
resolve();
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
intentionallyStopped = true;
|
|
109
|
-
const dying = child;
|
|
105
|
+
if (stopPromise) return stopPromise;
|
|
106
|
+
|
|
107
|
+
if (!child || child.exitCode !== null) {
|
|
110
108
|
child = null;
|
|
111
|
-
|
|
109
|
+
return Promise.resolve();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
intentionallyStopped = true;
|
|
113
|
+
const dying = child;
|
|
114
|
+
child = null;
|
|
115
|
+
|
|
116
|
+
stopPromise = new Promise<void>((resolve) => {
|
|
117
|
+
dying.once('exit', () => {
|
|
118
|
+
stopPromise = null;
|
|
119
|
+
resolve();
|
|
120
|
+
});
|
|
112
121
|
dying.kill();
|
|
113
122
|
// Safety: force kill after 3s if SIGTERM doesn't work
|
|
114
123
|
setTimeout(() => {
|
|
115
124
|
try { dying.kill('SIGKILL'); } catch {}
|
|
125
|
+
stopPromise = null;
|
|
116
126
|
resolve();
|
|
117
127
|
}, 3000);
|
|
118
128
|
});
|
|
129
|
+
|
|
130
|
+
return stopPromise;
|
|
119
131
|
}
|
|
120
132
|
|
|
121
133
|
export function isBackendAlive(): boolean {
|
|
122
134
|
return child !== null && child.exitCode === null;
|
|
123
135
|
}
|
|
124
136
|
|
|
137
|
+
export function isBackendStopping(): boolean {
|
|
138
|
+
return stopPromise !== null;
|
|
139
|
+
}
|
|
140
|
+
|
|
125
141
|
export function resetBackendRestarts(): void {
|
|
126
142
|
restarts = 0;
|
|
127
143
|
}
|
package/supervisor/index.ts
CHANGED
|
@@ -11,13 +11,14 @@ import { log } from '../shared/logger.js';
|
|
|
11
11
|
import { startTunnel, stopTunnel, isTunnelAlive, restartTunnel, startNamedTunnel, restartNamedTunnel } from './tunnel.js';
|
|
12
12
|
import { createWorkerApp } from '../worker/index.js';
|
|
13
13
|
import { closeDb, getSession, getSetting } from '../worker/db.js';
|
|
14
|
-
import { spawnBackend, stopBackend, getBackendPort, isBackendAlive, resetBackendRestarts } from './backend.js';
|
|
14
|
+
import { spawnBackend, stopBackend, getBackendPort, isBackendAlive, isBackendStopping, resetBackendRestarts } from './backend.js';
|
|
15
15
|
import { updateTunnelUrl, startHeartbeat, stopHeartbeat, disconnect } from '../shared/relay.js';
|
|
16
16
|
import { startFluxyAgentQuery, stopFluxyAgentQuery, type RecentMessage } from './fluxy-agent.js';
|
|
17
17
|
import { ensureFileDirs, saveAttachment, type SavedFile } from './file-saver.js';
|
|
18
18
|
import { startViteDevServers, stopViteDevServers } from './vite-dev.js';
|
|
19
19
|
import { startScheduler, stopScheduler } from './scheduler.js';
|
|
20
20
|
import { execSync, spawn as cpSpawn } from 'child_process';
|
|
21
|
+
import crypto from 'crypto';
|
|
21
22
|
|
|
22
23
|
const DIST_FLUXY = path.join(PKG_DIR, 'dist-fluxy');
|
|
23
24
|
|
|
@@ -197,6 +198,7 @@ const RECOVERING_HTML = `<!DOCTYPE html><html style="background:#222122"><head><
|
|
|
197
198
|
export async function startSupervisor() {
|
|
198
199
|
const config = loadConfig();
|
|
199
200
|
const backendPort = getBackendPort(config.port);
|
|
201
|
+
const internalSecret = crypto.randomBytes(16).toString('hex');
|
|
200
202
|
|
|
201
203
|
// Create HTTP server first (Vite needs it for HMR WebSocket)
|
|
202
204
|
// The request handler is set up later via server.on('request')
|
|
@@ -232,7 +234,7 @@ export async function startSupervisor() {
|
|
|
232
234
|
|
|
233
235
|
/** Call worker API endpoints (in-process via supervisor's own HTTP server) */
|
|
234
236
|
async function workerApi(apiPath: string, method = 'GET', body?: any) {
|
|
235
|
-
const opts: RequestInit = { method, headers: { 'Content-Type': 'application/json' } };
|
|
237
|
+
const opts: RequestInit = { method, headers: { 'Content-Type': 'application/json', 'x-internal': internalSecret } };
|
|
236
238
|
if (body) opts.body = JSON.stringify(body);
|
|
237
239
|
const res = await fetch(`http://127.0.0.1:${config.port}${apiPath}`, opts);
|
|
238
240
|
return res.json();
|
|
@@ -366,17 +368,22 @@ export async function startSupervisor() {
|
|
|
366
368
|
|
|
367
369
|
// API routes → handled in-process by worker Express app
|
|
368
370
|
if (req.url?.startsWith('/api')) {
|
|
369
|
-
//
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
371
|
+
// Internal supervisor calls (workerApi) bypass auth — they carry a per-process secret
|
|
372
|
+
const isInternal = req.headers['x-internal'] === internalSecret;
|
|
373
|
+
|
|
374
|
+
if (!isInternal) {
|
|
375
|
+
// Auth check for mutation routes (POST/PUT/DELETE) — GET/HEAD are read-only, skip auth
|
|
376
|
+
const method = req.method || 'GET';
|
|
377
|
+
if (method !== 'GET' && method !== 'HEAD' && !isExemptRoute(method, req.url || '')) {
|
|
378
|
+
const needsAuth = await isAuthRequired();
|
|
379
|
+
if (needsAuth) {
|
|
380
|
+
const authHeader = req.headers['authorization'];
|
|
381
|
+
const token = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null;
|
|
382
|
+
if (!token || !(await validateToken(token))) {
|
|
383
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
384
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
380
387
|
}
|
|
381
388
|
}
|
|
382
389
|
}
|
|
@@ -853,6 +860,7 @@ export async function startSupervisor() {
|
|
|
853
860
|
if (eventData.usedFileTools || pendingBackendRestart) {
|
|
854
861
|
console.log('[supervisor] Agent turn ended — restarting backend');
|
|
855
862
|
pendingBackendRestart = false;
|
|
863
|
+
if (backendRestartTimer) { clearTimeout(backendRestartTimer); backendRestartTimer = null; }
|
|
856
864
|
resetBackendRestarts();
|
|
857
865
|
stopBackend().then(() => spawnBackend(backendPort));
|
|
858
866
|
}
|
|
@@ -1051,8 +1059,11 @@ export async function startSupervisor() {
|
|
|
1051
1059
|
pendingBackendRestart = true;
|
|
1052
1060
|
return;
|
|
1053
1061
|
}
|
|
1062
|
+
// Skip if a stop/restart is already in progress (bot:done handler owns the restart)
|
|
1063
|
+
if (isBackendStopping()) return;
|
|
1054
1064
|
if (backendRestartTimer) clearTimeout(backendRestartTimer);
|
|
1055
1065
|
backendRestartTimer = setTimeout(async () => {
|
|
1066
|
+
if (isBackendStopping()) return; // re-check after delay
|
|
1056
1067
|
log.info(`[watcher] ${reason} — restarting backend...`);
|
|
1057
1068
|
resetBackendRestarts();
|
|
1058
1069
|
await stopBackend();
|