@auroraflow/code 0.0.10 → 0.0.12
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/packages/cli/src/index.js +33 -1
- package/packages/clients/src/index.js +4 -85
- package/packages/service/package.json +9 -0
- package/packages/service/src/detached.js +64 -0
- package/packages/service/src/index.js +146 -0
- package/packages/service/src/launchd.js +99 -0
- package/packages/service/src/schtasks.js +92 -0
- package/packages/service/src/systemd.js +72 -0
- package/packages/sidecar/src/index.js +161 -17
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ensureLayout, readAccount, readState, writeAccount, writeState } from "../../state/src/index.js";
|
|
2
2
|
import { officialClientStatuses, runClient, updateOfficialClients } from "../../clients/src/index.js";
|
|
3
|
+
import { ensureSidecarRunning, installSidecarService, sidecarServiceStatus, uninstallSidecarService } from "../../service/src/index.js";
|
|
3
4
|
import { chooseClient, promptFields } from "../../../lib/prompt.js";
|
|
4
5
|
|
|
5
6
|
export async function runAuroraCLI(argv = process.argv.slice(2)) {
|
|
@@ -34,11 +35,41 @@ export async function runAuroraCLI(argv = process.argv.slice(2)) {
|
|
|
34
35
|
await updateOfficialClients();
|
|
35
36
|
console.log(JSON.stringify({ officialClients: officialClientStatuses() }, null, 2));
|
|
36
37
|
return;
|
|
38
|
+
case "sidecar":
|
|
39
|
+
await sidecarCommand(rest);
|
|
40
|
+
return;
|
|
37
41
|
default:
|
|
38
42
|
usage(1);
|
|
39
43
|
}
|
|
40
44
|
}
|
|
41
45
|
|
|
46
|
+
async function sidecarCommand(args) {
|
|
47
|
+
const action = args[0] ?? "status";
|
|
48
|
+
switch (action) {
|
|
49
|
+
case "install":
|
|
50
|
+
await installSidecarService();
|
|
51
|
+
await ensureSidecarRunning();
|
|
52
|
+
console.log(JSON.stringify(await sidecarServiceStatus(), null, 2));
|
|
53
|
+
return;
|
|
54
|
+
case "uninstall":
|
|
55
|
+
await uninstallSidecarService();
|
|
56
|
+
console.log("Aurora sidecar service uninstalled.");
|
|
57
|
+
return;
|
|
58
|
+
case "restart":
|
|
59
|
+
await uninstallSidecarService();
|
|
60
|
+
await installSidecarService();
|
|
61
|
+
await ensureSidecarRunning();
|
|
62
|
+
console.log(JSON.stringify(await sidecarServiceStatus(), null, 2));
|
|
63
|
+
return;
|
|
64
|
+
case "status":
|
|
65
|
+
console.log(JSON.stringify(await sidecarServiceStatus(), null, 2));
|
|
66
|
+
return;
|
|
67
|
+
default:
|
|
68
|
+
console.error("Usage: aurora sidecar [install|uninstall|restart|status]");
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
42
73
|
export async function runAuroraClaude(argv = process.argv.slice(2)) {
|
|
43
74
|
await ensureLayout();
|
|
44
75
|
await runClient("claude", argv);
|
|
@@ -135,6 +166,7 @@ function usage(exitCode) {
|
|
|
135
166
|
aurora codex [args...]
|
|
136
167
|
aurora install-clients
|
|
137
168
|
aurora update-clients
|
|
138
|
-
aurora status
|
|
169
|
+
aurora status
|
|
170
|
+
aurora sidecar [install|uninstall|restart|status]`);
|
|
139
171
|
process.exit(exitCode);
|
|
140
172
|
}
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
-
import {
|
|
3
|
-
import { closeSync, existsSync, openSync, readFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
3
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
5
4
|
import { delimiter, dirname, join, resolve } from "node:path";
|
|
6
5
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
6
|
+
import { CLAUDE_HOME, CODEX_HOME, readState } from "../../state/src/index.js";
|
|
7
|
+
import { ensureSidecarRunning } from "../../service/src/index.js";
|
|
9
8
|
|
|
10
9
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
11
10
|
const packageRoot = resolve(here, "..", "..", "..");
|
|
12
|
-
const rootSidecarBin = join(packageRoot, "bin", "aurora-sidecar.js");
|
|
13
|
-
const sidecarLogPath = join(AURORA_HOME, "log", "sidecar.log");
|
|
14
|
-
let sidecarSupervisor = null;
|
|
15
11
|
|
|
16
12
|
const OFFICIAL_CLIENTS = {
|
|
17
13
|
claude: {
|
|
@@ -27,7 +23,7 @@ const OFFICIAL_CLIENTS = {
|
|
|
27
23
|
};
|
|
28
24
|
|
|
29
25
|
export async function createClientLaunchSpec(client, args = []) {
|
|
30
|
-
const sidecar = await
|
|
26
|
+
const sidecar = await ensureSidecarRunning();
|
|
31
27
|
const state = await readState();
|
|
32
28
|
const model = selectedModelAlias(state);
|
|
33
29
|
const key = selectedKey(state);
|
|
@@ -144,53 +140,6 @@ function readOfficialClientVersion(bin) {
|
|
|
144
140
|
}
|
|
145
141
|
}
|
|
146
142
|
|
|
147
|
-
async function ensureSidecar() {
|
|
148
|
-
try {
|
|
149
|
-
const info = await readSidecarInfo();
|
|
150
|
-
if (info?.port && info?.token && await pingSidecar(info)) return normalizedSidecarInfo(info);
|
|
151
|
-
} catch {
|
|
152
|
-
// Start below.
|
|
153
|
-
}
|
|
154
|
-
const token = `aurora-local-${randomBytes(24).toString("hex")}`;
|
|
155
|
-
const info = { port: AURORA_SIDECAR_PORT, token, baseURL: `http://127.0.0.1:${AURORA_SIDECAR_PORT}` };
|
|
156
|
-
await startManagedSidecar(info);
|
|
157
|
-
await waitForSidecar(info);
|
|
158
|
-
await writeSidecarInfo(info);
|
|
159
|
-
return info;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
async function startManagedSidecar(info) {
|
|
163
|
-
if (sidecarSupervisor) return;
|
|
164
|
-
await mkdir(dirname(sidecarLogPath), { recursive: true });
|
|
165
|
-
const launch = () => {
|
|
166
|
-
const logFd = openSync(sidecarLogPath, "a");
|
|
167
|
-
const child = spawn(process.execPath, [
|
|
168
|
-
process.env.AURORA_SIDECAR_BIN || rootSidecarBin,
|
|
169
|
-
"--token",
|
|
170
|
-
info.token,
|
|
171
|
-
"--port",
|
|
172
|
-
String(info.port)
|
|
173
|
-
], {
|
|
174
|
-
detached: true,
|
|
175
|
-
stdio: ["ignore", logFd, logFd],
|
|
176
|
-
env: process.env
|
|
177
|
-
});
|
|
178
|
-
closeSync(logFd);
|
|
179
|
-
sidecarSupervisor.child = child;
|
|
180
|
-
child.on("error", error => {
|
|
181
|
-
console.error(`Aurora sidecar failed to start: ${error.message}`);
|
|
182
|
-
});
|
|
183
|
-
child.on("exit", () => {
|
|
184
|
-
sidecarSupervisor.child = null;
|
|
185
|
-
setTimeout(launch, 1000);
|
|
186
|
-
});
|
|
187
|
-
};
|
|
188
|
-
sidecarSupervisor = {
|
|
189
|
-
child: null
|
|
190
|
-
};
|
|
191
|
-
launch();
|
|
192
|
-
}
|
|
193
|
-
|
|
194
143
|
async function writeCodexRuntimeFiles({ sidecar, model }) {
|
|
195
144
|
await mkdir(CODEX_HOME, { recursive: true });
|
|
196
145
|
const modelCatalogPath = join(CODEX_HOME, "model_catalog.json");
|
|
@@ -245,36 +194,6 @@ async function writeCodexModelCatalog(sidecar, modelCatalogPath) {
|
|
|
245
194
|
await writeFile(join(CODEX_HOME, "models_cache.json"), `${JSON.stringify(cache, null, 2)}\n`, { mode: 0o600 });
|
|
246
195
|
}
|
|
247
196
|
|
|
248
|
-
async function waitForSidecar(info) {
|
|
249
|
-
const deadline = Date.now() + 2500;
|
|
250
|
-
while (Date.now() < deadline) {
|
|
251
|
-
if (await pingSidecar(info)) return;
|
|
252
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
253
|
-
}
|
|
254
|
-
throw new Error("Aurora sidecar did not become ready on 127.0.0.1:17878");
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
async function pingSidecar(info) {
|
|
258
|
-
try {
|
|
259
|
-
const normalized = normalizedSidecarInfo(info);
|
|
260
|
-
const response = await fetch(`${normalized.baseURL}/aurora/status`, {
|
|
261
|
-
headers: { authorization: `Bearer ${normalized.token}` },
|
|
262
|
-
signal: AbortSignal.timeout(500)
|
|
263
|
-
});
|
|
264
|
-
return response.ok;
|
|
265
|
-
} catch {
|
|
266
|
-
return false;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function normalizedSidecarInfo(info) {
|
|
271
|
-
return {
|
|
272
|
-
port: Number(info.port),
|
|
273
|
-
token: String(info.token),
|
|
274
|
-
baseURL: info.baseURL || `http://127.0.0.1:${Number(info.port)}`
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
|
|
278
197
|
function selectedModelAlias(state) {
|
|
279
198
|
const model = state.selectedModel?.alias;
|
|
280
199
|
if (!model) throw new Error("Aurora runtime model is not selected. Select a model in Aurora Desktop > 本机调用.");
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Fallback backend: a detached, unref'd process.
|
|
2
|
+
//
|
|
3
|
+
// Used when no OS service manager is available (unknown platform, or Linux
|
|
4
|
+
// without systemd --user). It cannot survive a reboot, but combined with the
|
|
5
|
+
// sidecar's crash-only design (no process.exit on per-request errors) it stays
|
|
6
|
+
// up for the whole login session, which is the best a no-service host allows.
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
9
|
+
import { openSync, closeSync } from "node:fs";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { AURORA_HOME } from "../../state/src/index.js";
|
|
12
|
+
|
|
13
|
+
function pidPath() {
|
|
14
|
+
return join(AURORA_HOME, "sidecar.pid");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function alive(pid) {
|
|
18
|
+
try {
|
|
19
|
+
process.kill(pid, 0);
|
|
20
|
+
return true;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function install(spec) {
|
|
27
|
+
if (await isInstalled()) return;
|
|
28
|
+
await mkdir(dirname(spec.logPath), { recursive: true });
|
|
29
|
+
const logFd = openSync(spec.logPath, "a");
|
|
30
|
+
const child = spawn(spec.nodePath, [spec.scriptPath], {
|
|
31
|
+
detached: true,
|
|
32
|
+
stdio: ["ignore", logFd, logFd],
|
|
33
|
+
env: { ...process.env, AURORA_HOME: spec.auroraHome }
|
|
34
|
+
});
|
|
35
|
+
closeSync(logFd);
|
|
36
|
+
child.unref();
|
|
37
|
+
await writeFile(pidPath(), String(child.pid), { mode: 0o600 });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function uninstall() {
|
|
41
|
+
const pid = await readPid();
|
|
42
|
+
if (pid && alive(pid)) {
|
|
43
|
+
try {
|
|
44
|
+
process.kill(pid);
|
|
45
|
+
} catch {
|
|
46
|
+
// Already gone.
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
await rm(pidPath(), { force: true }).catch(() => {});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function isInstalled() {
|
|
53
|
+
const pid = await readPid();
|
|
54
|
+
return Boolean(pid && alive(pid));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function readPid() {
|
|
58
|
+
try {
|
|
59
|
+
const pid = Number(await readFile(pidPath(), "utf8"));
|
|
60
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Cross-platform supervision for the Aurora sidecar.
|
|
2
|
+
//
|
|
3
|
+
// "Always connectable" is delegated to the OS service manager rather than a
|
|
4
|
+
// parent process: a launcher that spawns the sidecar dies with its terminal,
|
|
5
|
+
// orphaning the daemon with no supervisor. Each platform uses its best
|
|
6
|
+
// no-admin (npm-friendly) mechanism:
|
|
7
|
+
//
|
|
8
|
+
// - macOS → launchd LaunchAgent (~/Library/LaunchAgents)
|
|
9
|
+
// - Linux → systemd --user unit (~/.config/systemd/user)
|
|
10
|
+
// - Windows → Scheduled Task at logon (schtasks, no UAC)
|
|
11
|
+
// - other / unavailable → detached supervised process (best effort)
|
|
12
|
+
//
|
|
13
|
+
// All backends run the SAME `bin/aurora-sidecar.js` with no secret in the
|
|
14
|
+
// service definition — the sidecar reads its stable token from sidecar.json.
|
|
15
|
+
import { dirname, join, resolve } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { AURORA_LOCALHOST } from "../../protocol/src/index.js";
|
|
18
|
+
import { AURORA_HOME, readSidecarInfo } from "../../state/src/index.js";
|
|
19
|
+
import { ensureSidecarIdentity } from "../../sidecar/src/index.js";
|
|
20
|
+
import * as launchd from "./launchd.js";
|
|
21
|
+
import * as systemd from "./systemd.js";
|
|
22
|
+
import * as schtasks from "./schtasks.js";
|
|
23
|
+
import * as detached from "./detached.js";
|
|
24
|
+
|
|
25
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const packageRoot = resolve(here, "..", "..", "..");
|
|
27
|
+
|
|
28
|
+
export const SERVICE_LABEL = "com.auroraflow.sidecar";
|
|
29
|
+
|
|
30
|
+
// serviceSpec is the platform-agnostic description every backend renders into
|
|
31
|
+
// its own format. Absolute paths are resolved fresh each call so a node
|
|
32
|
+
// upgrade (nvm/brew) self-heals on the next launch.
|
|
33
|
+
export function serviceSpec() {
|
|
34
|
+
return {
|
|
35
|
+
label: SERVICE_LABEL,
|
|
36
|
+
nodePath: process.execPath,
|
|
37
|
+
scriptPath: join(packageRoot, "bin", "aurora-sidecar.js"),
|
|
38
|
+
logPath: join(AURORA_HOME, "log", "sidecar.log"),
|
|
39
|
+
auroraHome: AURORA_HOME
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function backend() {
|
|
44
|
+
switch (process.platform) {
|
|
45
|
+
case "darwin":
|
|
46
|
+
return launchd;
|
|
47
|
+
case "linux":
|
|
48
|
+
return systemd;
|
|
49
|
+
case "win32":
|
|
50
|
+
return schtasks;
|
|
51
|
+
default:
|
|
52
|
+
return detached;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ensureSidecarRunning is the single entry the launcher calls. It seeds the
|
|
57
|
+
// stable identity, installs/repairs the OS service, then waits until the
|
|
58
|
+
// sidecar answers. If the platform's service manager is unavailable, it falls
|
|
59
|
+
// back to a detached process so the client still gets a working sidecar.
|
|
60
|
+
export async function ensureSidecarRunning() {
|
|
61
|
+
const identity = await ensureSidecarIdentity();
|
|
62
|
+
if (await pingSidecar(identity)) return identity;
|
|
63
|
+
const spec = serviceSpec();
|
|
64
|
+
try {
|
|
65
|
+
await backend().install(spec);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (backend() !== detached) {
|
|
68
|
+
console.error(`[aurora] ${process.platform} service manager unavailable (${error?.message || error}); using detached fallback.`);
|
|
69
|
+
await detached.install(spec);
|
|
70
|
+
} else {
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
await waitForSidecar(identity);
|
|
75
|
+
return identity;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function installSidecarService() {
|
|
79
|
+
await ensureSidecarIdentity();
|
|
80
|
+
await backend().install(serviceSpec());
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function uninstallSidecarService() {
|
|
84
|
+
await backend().uninstall(serviceSpec());
|
|
85
|
+
// Detached instances are not managed by the OS; stop any stray one too.
|
|
86
|
+
await detached.uninstall(serviceSpec()).catch(() => {});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function sidecarServiceStatus() {
|
|
90
|
+
const spec = serviceSpec();
|
|
91
|
+
let identity = null;
|
|
92
|
+
try {
|
|
93
|
+
identity = await readSidecarInfo();
|
|
94
|
+
} catch {
|
|
95
|
+
// Not seeded yet.
|
|
96
|
+
}
|
|
97
|
+
const installed = await backend().isInstalled(spec).catch(() => false);
|
|
98
|
+
const running = identity ? await pingSidecar(identity) : false;
|
|
99
|
+
return {
|
|
100
|
+
platform: process.platform,
|
|
101
|
+
backend: backendName(),
|
|
102
|
+
installed,
|
|
103
|
+
running,
|
|
104
|
+
port: identity?.port ?? null,
|
|
105
|
+
scriptPath: spec.scriptPath,
|
|
106
|
+
nodePath: spec.nodePath,
|
|
107
|
+
logPath: spec.logPath
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function backendName() {
|
|
112
|
+
switch (process.platform) {
|
|
113
|
+
case "darwin":
|
|
114
|
+
return "launchd";
|
|
115
|
+
case "linux":
|
|
116
|
+
return "systemd";
|
|
117
|
+
case "win32":
|
|
118
|
+
return "schtasks";
|
|
119
|
+
default:
|
|
120
|
+
return "detached";
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function pingSidecar(identity) {
|
|
125
|
+
try {
|
|
126
|
+
const port = Number(identity?.port);
|
|
127
|
+
const token = String(identity?.token ?? "");
|
|
128
|
+
if (!port || !token) return false;
|
|
129
|
+
const response = await fetch(`http://${AURORA_LOCALHOST}:${port}/aurora/status`, {
|
|
130
|
+
headers: { authorization: `Bearer ${token}` },
|
|
131
|
+
signal: AbortSignal.timeout(500)
|
|
132
|
+
});
|
|
133
|
+
return response.ok;
|
|
134
|
+
} catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function waitForSidecar(identity, timeoutMs = 5000) {
|
|
140
|
+
const deadline = Date.now() + timeoutMs;
|
|
141
|
+
while (Date.now() < deadline) {
|
|
142
|
+
if (await pingSidecar(identity)) return;
|
|
143
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
144
|
+
}
|
|
145
|
+
throw new Error(`Aurora sidecar did not become ready on ${AURORA_LOCALHOST}:${identity?.port}`);
|
|
146
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// macOS backend: launchd LaunchAgent.
|
|
2
|
+
//
|
|
3
|
+
// KeepAlive is the unconditional boolean: launchd's { Crashed: true } variant
|
|
4
|
+
// does not reliably restart after a SIGKILL, which defeats the whole purpose.
|
|
5
|
+
// Unconditional restart guarantees "always connectable"; the only cost is that
|
|
6
|
+
// the rare single-instance "yield" exit (when another sidecar already owns the
|
|
7
|
+
// port) gets relaunched every ~10s (launchd throttle) until the peer goes away,
|
|
8
|
+
// which self-corrects. RunAtLoad + the LaunchAgents domain give login
|
|
9
|
+
// auto-start without admin rights.
|
|
10
|
+
import { execFile } from "node:child_process";
|
|
11
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
import { promisify } from "node:util";
|
|
16
|
+
|
|
17
|
+
const run = promisify(execFile);
|
|
18
|
+
|
|
19
|
+
function plistPath(spec) {
|
|
20
|
+
return join(homedir(), "Library", "LaunchAgents", `${spec.label}.plist`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function domainTarget() {
|
|
24
|
+
return `gui/${process.getuid()}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function serviceTarget(spec) {
|
|
28
|
+
return `${domainTarget()}/${spec.label}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function renderPlist(spec) {
|
|
32
|
+
const escape = value => String(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
33
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
34
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
35
|
+
<plist version="1.0">
|
|
36
|
+
<dict>
|
|
37
|
+
<key>Label</key><string>${escape(spec.label)}</string>
|
|
38
|
+
<key>ProgramArguments</key>
|
|
39
|
+
<array>
|
|
40
|
+
<string>${escape(spec.nodePath)}</string>
|
|
41
|
+
<string>${escape(spec.scriptPath)}</string>
|
|
42
|
+
</array>
|
|
43
|
+
<key>EnvironmentVariables</key>
|
|
44
|
+
<dict>
|
|
45
|
+
<key>AURORA_HOME</key><string>${escape(spec.auroraHome)}</string>
|
|
46
|
+
</dict>
|
|
47
|
+
<key>RunAtLoad</key><true/>
|
|
48
|
+
<key>KeepAlive</key><true/>
|
|
49
|
+
<key>ThrottleInterval</key><integer>1</integer>
|
|
50
|
+
<key>ProcessType</key><string>Background</string>
|
|
51
|
+
<key>StandardOutPath</key><string>${escape(spec.logPath)}</string>
|
|
52
|
+
<key>StandardErrorPath</key><string>${escape(spec.logPath)}</string>
|
|
53
|
+
</dict>
|
|
54
|
+
</plist>
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function install(spec) {
|
|
59
|
+
const path = plistPath(spec);
|
|
60
|
+
const desired = renderPlist(spec);
|
|
61
|
+
await mkdir(dirname(path), { recursive: true });
|
|
62
|
+
await mkdir(dirname(spec.logPath), { recursive: true });
|
|
63
|
+
const current = existsSync(path) ? await readFile(path, "utf8") : null;
|
|
64
|
+
const loaded = await isLoaded(spec);
|
|
65
|
+
if (current === desired && loaded) {
|
|
66
|
+
// Already correct — make sure it is actually started, no-op if running.
|
|
67
|
+
await run("launchctl", ["kickstart", serviceTarget(spec)]).catch(() => {});
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
await writeFile(path, desired, { mode: 0o644 });
|
|
71
|
+
// Reload cleanly: bootout the old definition (ignore if absent), bootstrap
|
|
72
|
+
// the new one, then kickstart to start immediately.
|
|
73
|
+
await run("launchctl", ["bootout", serviceTarget(spec)]).catch(() => {});
|
|
74
|
+
await run("launchctl", ["bootstrap", domainTarget(), path]);
|
|
75
|
+
await run("launchctl", ["enable", serviceTarget(spec)]).catch(() => {});
|
|
76
|
+
await run("launchctl", ["kickstart", "-k", serviceTarget(spec)]).catch(() => {});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function uninstall(spec) {
|
|
80
|
+
await run("launchctl", ["bootout", serviceTarget(spec)]).catch(() => {});
|
|
81
|
+
const path = plistPath(spec);
|
|
82
|
+
if (existsSync(path)) {
|
|
83
|
+
const { rm } = await import("node:fs/promises");
|
|
84
|
+
await rm(path, { force: true });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function isInstalled(spec) {
|
|
89
|
+
return existsSync(plistPath(spec));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function isLoaded(spec) {
|
|
93
|
+
try {
|
|
94
|
+
await run("launchctl", ["print", serviceTarget(spec)]);
|
|
95
|
+
return true;
|
|
96
|
+
} catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Windows backend: Scheduled Task at logon (no admin / no UAC).
|
|
2
|
+
//
|
|
3
|
+
// A real Windows Service requires elevation, which an npm global install does
|
|
4
|
+
// not have. A per-user Scheduled Task triggered at logon, with RestartOnFailure
|
|
5
|
+
// and no execution time limit, is the standard no-admin always-on mechanism.
|
|
6
|
+
//
|
|
7
|
+
// Note: node.exe started by Task Scheduler may flash a console window. That is
|
|
8
|
+
// cosmetic and does not affect connectivity; a windowless vbs shim can be added
|
|
9
|
+
// later if needed.
|
|
10
|
+
import { execFile } from "node:child_process";
|
|
11
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { promisify } from "node:util";
|
|
14
|
+
import { AURORA_HOME } from "../../state/src/index.js";
|
|
15
|
+
|
|
16
|
+
const run = promisify(execFile);
|
|
17
|
+
const TASK_NAME = "AuroraSidecar";
|
|
18
|
+
|
|
19
|
+
function taskXmlPath() {
|
|
20
|
+
return join(AURORA_HOME, "aurora-sidecar-task.xml");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function renderTaskXml(spec) {
|
|
24
|
+
const escape = value => String(value)
|
|
25
|
+
.replace(/&/g, "&")
|
|
26
|
+
.replace(/</g, "<")
|
|
27
|
+
.replace(/>/g, ">")
|
|
28
|
+
.replace(/"/g, """);
|
|
29
|
+
return `<?xml version="1.0" encoding="UTF-16"?>
|
|
30
|
+
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
31
|
+
<RegistrationInfo>
|
|
32
|
+
<Description>Aurora local sidecar</Description>
|
|
33
|
+
</RegistrationInfo>
|
|
34
|
+
<Triggers>
|
|
35
|
+
<LogonTrigger>
|
|
36
|
+
<Enabled>true</Enabled>
|
|
37
|
+
</LogonTrigger>
|
|
38
|
+
</Triggers>
|
|
39
|
+
<Principals>
|
|
40
|
+
<Principal id="Author">
|
|
41
|
+
<LogonType>InteractiveToken</LogonType>
|
|
42
|
+
<RunLevel>LeastPrivilege</RunLevel>
|
|
43
|
+
</Principal>
|
|
44
|
+
</Principals>
|
|
45
|
+
<Settings>
|
|
46
|
+
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
|
47
|
+
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
|
48
|
+
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
|
49
|
+
<AllowHardTerminate>true</AllowHardTerminate>
|
|
50
|
+
<StartWhenAvailable>true</StartWhenAvailable>
|
|
51
|
+
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
|
|
52
|
+
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
|
|
53
|
+
<Enabled>true</Enabled>
|
|
54
|
+
<Hidden>false</Hidden>
|
|
55
|
+
<RestartOnFailure>
|
|
56
|
+
<Interval>PT1M</Interval>
|
|
57
|
+
<Count>999</Count>
|
|
58
|
+
</RestartOnFailure>
|
|
59
|
+
</Settings>
|
|
60
|
+
<Actions Context="Author">
|
|
61
|
+
<Exec>
|
|
62
|
+
<Command>"${escape(spec.nodePath)}"</Command>
|
|
63
|
+
<Arguments>"${escape(spec.scriptPath)}"</Arguments>
|
|
64
|
+
</Exec>
|
|
65
|
+
</Actions>
|
|
66
|
+
</Task>
|
|
67
|
+
`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function install(spec) {
|
|
71
|
+
const path = taskXmlPath();
|
|
72
|
+
await mkdir(AURORA_HOME, { recursive: true });
|
|
73
|
+
// Task Scheduler XML must be UTF-16 with a BOM.
|
|
74
|
+
await writeFile(path, "" + renderTaskXml(spec), { encoding: "utf16le" });
|
|
75
|
+
// /f overwrites any existing definition, self-healing a drifted node path.
|
|
76
|
+
await run("schtasks", ["/create", "/tn", TASK_NAME, "/xml", path, "/f"]);
|
|
77
|
+
await run("schtasks", ["/run", "/tn", TASK_NAME]).catch(() => {});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function uninstall() {
|
|
81
|
+
await run("schtasks", ["/end", "/tn", TASK_NAME]).catch(() => {});
|
|
82
|
+
await run("schtasks", ["/delete", "/tn", TASK_NAME, "/f"]).catch(() => {});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function isInstalled() {
|
|
86
|
+
try {
|
|
87
|
+
await run("schtasks", ["/query", "/tn", TASK_NAME]);
|
|
88
|
+
return true;
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Linux backend: systemd --user unit.
|
|
2
|
+
//
|
|
3
|
+
// Restart=on-failure (not always) so the single-instance clean "yield" exit
|
|
4
|
+
// does not loop, while crashes restart after 1s. WantedBy=default.target gives
|
|
5
|
+
// login auto-start without root. If systemctl --user is unavailable (no
|
|
6
|
+
// systemd, containers, some WSL setups) install() throws and the dispatcher
|
|
7
|
+
// falls back to a detached process.
|
|
8
|
+
import { execFile } from "node:child_process";
|
|
9
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
import { promisify } from "node:util";
|
|
14
|
+
|
|
15
|
+
const run = promisify(execFile);
|
|
16
|
+
|
|
17
|
+
function unitName(spec) {
|
|
18
|
+
// systemd unit names cannot contain dots beyond the type suffix.
|
|
19
|
+
return `${spec.label.replace(/\./g, "-")}.service`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function unitPath(spec) {
|
|
23
|
+
return join(homedir(), ".config", "systemd", "user", unitName(spec));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function renderUnit(spec) {
|
|
27
|
+
return `[Unit]
|
|
28
|
+
Description=Aurora local sidecar
|
|
29
|
+
After=network.target
|
|
30
|
+
|
|
31
|
+
[Service]
|
|
32
|
+
Type=simple
|
|
33
|
+
ExecStart=${spec.nodePath} ${spec.scriptPath}
|
|
34
|
+
Environment=AURORA_HOME=${spec.auroraHome}
|
|
35
|
+
Restart=on-failure
|
|
36
|
+
RestartSec=1
|
|
37
|
+
|
|
38
|
+
[Install]
|
|
39
|
+
WantedBy=default.target
|
|
40
|
+
`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function install(spec) {
|
|
44
|
+
// Probe availability first so an unsupported host triggers the fallback.
|
|
45
|
+
await run("systemctl", ["--user", "--version"]);
|
|
46
|
+
const path = unitPath(spec);
|
|
47
|
+
const desired = renderUnit(spec);
|
|
48
|
+
await mkdir(dirname(path), { recursive: true });
|
|
49
|
+
await mkdir(dirname(spec.logPath), { recursive: true });
|
|
50
|
+
const current = existsSync(path) ? await readFile(path, "utf8") : null;
|
|
51
|
+
if (current !== desired) {
|
|
52
|
+
await writeFile(path, desired, { mode: 0o644 });
|
|
53
|
+
}
|
|
54
|
+
await run("systemctl", ["--user", "daemon-reload"]);
|
|
55
|
+
await run("systemctl", ["--user", "enable", "--now", unitName(spec)]);
|
|
56
|
+
// Apply any unit change and ensure it is up.
|
|
57
|
+
await run("systemctl", ["--user", "restart", unitName(spec)]).catch(() => {});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function uninstall(spec) {
|
|
61
|
+
await run("systemctl", ["--user", "disable", "--now", unitName(spec)]).catch(() => {});
|
|
62
|
+
const path = unitPath(spec);
|
|
63
|
+
if (existsSync(path)) {
|
|
64
|
+
const { rm } = await import("node:fs/promises");
|
|
65
|
+
await rm(path, { force: true });
|
|
66
|
+
}
|
|
67
|
+
await run("systemctl", ["--user", "daemon-reload"]).catch(() => {});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function isInstalled(spec) {
|
|
71
|
+
return existsSync(unitPath(spec));
|
|
72
|
+
}
|
|
@@ -1,38 +1,150 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
2
3
|
import { readFile, writeFile } from "node:fs/promises";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { AURORA_LOCALHOST, AURORA_SIDECAR_PORT, isRuntimePath, rewriteRuntimeModel, selectedKeyHeaders, toClientModelItem, toCodexModelInfo, trimSlash } from "../../protocol/src/index.js";
|
|
5
|
-
import { CODEX_HOME, readState, writeSidecarInfo } from "../../state/src/index.js";
|
|
6
|
+
import { CODEX_HOME, readSidecarInfo, readState, writeSidecarInfo } from "../../state/src/index.js";
|
|
6
7
|
|
|
7
8
|
const CODEX_MODEL_CAPABILITIES_PATH = join(CODEX_HOME, "model_capabilities.json");
|
|
8
9
|
|
|
10
|
+
// Header / idle timeouts for the upstream gateway connection. The stream body
|
|
11
|
+
// has NO total timeout (long answers must not be cut), but a stalled gateway
|
|
12
|
+
// (half-dead connection that stops sending bytes) is detected by the idle
|
|
13
|
+
// timer and turned into a clean error so the client retries that one request
|
|
14
|
+
// instead of spinning forever.
|
|
15
|
+
const GATEWAY_HEADER_TIMEOUT_MS = 60000;
|
|
16
|
+
const GATEWAY_IDLE_TIMEOUT_MS = 120000;
|
|
17
|
+
|
|
18
|
+
// Keep the sidecar->gateway TLS connection warm. The expensive part of a cold
|
|
19
|
+
// request is the ~1s TLS handshake to the gateway edge; undici pools the
|
|
20
|
+
// connection but drops it after ~4s idle, so every think-pause re-handshakes
|
|
21
|
+
// ("时快时慢"). A lightweight heartbeat under that idle window keeps one pooled
|
|
22
|
+
// connection alive so real requests reuse a warm socket. It only runs within a
|
|
23
|
+
// window after real activity, so an idle/abandoned sidecar stops chattering.
|
|
24
|
+
const GATEWAY_KEEPALIVE_INTERVAL_MS = 3000;
|
|
25
|
+
const GATEWAY_KEEPWARM_WINDOW_MS = 10 * 60 * 1000;
|
|
26
|
+
let lastActivityAt = 0;
|
|
27
|
+
|
|
28
|
+
function markActivity() {
|
|
29
|
+
lastActivityAt = Date.now();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ensureSidecarIdentity generates the local token once and persists it so the
|
|
33
|
+
// OS service, the launcher, and the running client all agree on a stable
|
|
34
|
+
// token/port across restarts and node upgrades. Random-per-run tokens broke
|
|
35
|
+
// reconnection when a long-lived service outlived the launcher that seeded it.
|
|
36
|
+
export async function ensureSidecarIdentity() {
|
|
37
|
+
let existing = null;
|
|
38
|
+
try {
|
|
39
|
+
existing = await readSidecarInfo();
|
|
40
|
+
} catch {
|
|
41
|
+
// First run — generate below.
|
|
42
|
+
}
|
|
43
|
+
const port = Number(existing?.port) || AURORA_SIDECAR_PORT;
|
|
44
|
+
const token = existing?.token || `aurora-local-${randomBytes(24).toString("hex")}`;
|
|
45
|
+
const baseURL = `http://${AURORA_LOCALHOST}:${port}`;
|
|
46
|
+
await writeSidecarInfo({ port, token, baseURL });
|
|
47
|
+
return { port, token, baseURL };
|
|
48
|
+
}
|
|
49
|
+
|
|
9
50
|
export async function startSidecar(options = {}) {
|
|
10
|
-
const
|
|
11
|
-
const
|
|
51
|
+
const tokenOverride = options.token || readArg("--token") || process.env.AURORA_SIDECAR_TOKEN;
|
|
52
|
+
const portOverride = options.port || readArg("--port") || process.env.AURORA_SIDECAR_PORT;
|
|
53
|
+
const identity = await ensureSidecarIdentity();
|
|
54
|
+
const token = tokenOverride || identity.token;
|
|
55
|
+
const port = Number(portOverride || identity.port);
|
|
12
56
|
const server = createServer((request, response) => {
|
|
13
57
|
handle(request, response, token).catch(error => {
|
|
58
|
+
// A streaming response has already flushed its headers, so a 500 here
|
|
59
|
+
// would throw ERR_HTTP_HEADERS_SENT and (via the global rejection
|
|
60
|
+
// handler) take the whole daemon down — that is exactly what produced
|
|
61
|
+
// the Claude Code "ConnectionRefused / retrying" spin. Only synthesize
|
|
62
|
+
// an error body when nothing has been sent yet; otherwise just close.
|
|
63
|
+
if (response.headersSent) {
|
|
64
|
+
response.end();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
14
67
|
response.writeHead(500, { "content-type": "application/json" });
|
|
15
68
|
response.end(JSON.stringify({ error: { type: "sidecar_error", message: error.message } }));
|
|
16
69
|
});
|
|
17
70
|
});
|
|
18
|
-
|
|
71
|
+
// Streaming requests can run for many minutes; the default 5-minute
|
|
72
|
+
// requestTimeout would cut long turns. Disable the whole-request timeout and
|
|
73
|
+
// keep only the header timeout that protects against a stuck client.
|
|
74
|
+
server.requestTimeout = 0;
|
|
75
|
+
server.headersTimeout = 60000;
|
|
76
|
+
server.keepAliveTimeout = 75000;
|
|
77
|
+
server.on("error", async error => {
|
|
78
|
+
if (error.code === "EADDRINUSE") {
|
|
79
|
+
// Single-instance: another sidecar already owns the port. If it is
|
|
80
|
+
// healthy, yield with a clean exit (service managers keep-alive only on
|
|
81
|
+
// crash, so exit 0 does not loop). If it is a dead bind, exit non-zero
|
|
82
|
+
// so the service manager retries shortly.
|
|
83
|
+
if (await pingLocalSidecar(port, token)) {
|
|
84
|
+
console.error(`Aurora sidecar: port ${port} already served by a healthy instance; yielding.`);
|
|
85
|
+
process.exit(0);
|
|
86
|
+
}
|
|
87
|
+
console.error(`Aurora sidecar: port ${port} busy but unhealthy; will retry.`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
19
90
|
console.error(`Aurora sidecar listen error on ${AURORA_LOCALHOST}:${port}: ${error.message}`);
|
|
20
91
|
process.exit(1);
|
|
21
92
|
});
|
|
22
93
|
server.listen(port, AURORA_LOCALHOST, async () => {
|
|
23
94
|
await writeSidecarInfo({ port, token, baseURL: `http://${AURORA_LOCALHOST}:${port}` });
|
|
24
95
|
console.error(`Aurora sidecar listening on http://${AURORA_LOCALHOST}:${port}`);
|
|
96
|
+
// Open the warm window and pre-warm the gateway connection so the very
|
|
97
|
+
// first client request reuses an established TLS socket.
|
|
98
|
+
markActivity();
|
|
99
|
+
startGatewayKeepAlive();
|
|
25
100
|
});
|
|
26
101
|
}
|
|
27
102
|
|
|
103
|
+
function startGatewayKeepAlive() {
|
|
104
|
+
const beat = async () => {
|
|
105
|
+
if (Date.now() - lastActivityAt > GATEWAY_KEEPWARM_WINDOW_MS) return;
|
|
106
|
+
try {
|
|
107
|
+
const state = await readState();
|
|
108
|
+
const gatewayURL = trimSlash(state.gatewayURL ?? "https://auroramos.com");
|
|
109
|
+
// Same origin as proxyRuntime → same undici pool, so this keeps the edge
|
|
110
|
+
// TLS handshake amortized for real requests. The body MUST be drained:
|
|
111
|
+
// undici destroys (does not pool) a connection whose response body was
|
|
112
|
+
// left unconsumed, which silently defeats the warm-up. Result ignored.
|
|
113
|
+
const response = await fetch(gatewayURL, { signal: AbortSignal.timeout(GATEWAY_HEADER_TIMEOUT_MS) });
|
|
114
|
+
await response.text();
|
|
115
|
+
} catch {
|
|
116
|
+
// Best effort; a failed heartbeat just means the next real request pays
|
|
117
|
+
// a cold handshake, which is the pre-fix behaviour.
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const timer = setInterval(beat, GATEWAY_KEEPALIVE_INTERVAL_MS);
|
|
121
|
+
timer.unref();
|
|
122
|
+
beat();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function pingLocalSidecar(port, token) {
|
|
126
|
+
try {
|
|
127
|
+
const response = await fetch(`http://${AURORA_LOCALHOST}:${port}/aurora/status`, {
|
|
128
|
+
headers: { authorization: `Bearer ${token}` },
|
|
129
|
+
signal: AbortSignal.timeout(500)
|
|
130
|
+
});
|
|
131
|
+
return response.ok;
|
|
132
|
+
} catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// The sidecar is a long-lived local proxy that Claude Code / Codex depend on
|
|
138
|
+
// for every request. A single bad request (e.g. a mid-stream upstream drop)
|
|
139
|
+
// must NOT kill the process: an orphaned sidecar with no live supervisor never
|
|
140
|
+
// comes back, leaving the client stuck on ConnectionRefused. Log and keep
|
|
141
|
+
// serving; the HTTP server stays healthy for the next request.
|
|
28
142
|
process.on("uncaughtException", error => {
|
|
29
143
|
console.error(`Aurora sidecar uncaught exception: ${error?.stack || error?.message || error}`);
|
|
30
|
-
process.exit(1);
|
|
31
144
|
});
|
|
32
145
|
|
|
33
146
|
process.on("unhandledRejection", reason => {
|
|
34
147
|
console.error(`Aurora sidecar unhandled rejection: ${reason?.stack || reason?.message || reason}`);
|
|
35
|
-
process.exit(1);
|
|
36
148
|
});
|
|
37
149
|
|
|
38
150
|
async function handle(request, response, token) {
|
|
@@ -62,6 +174,7 @@ async function handle(request, response, token) {
|
|
|
62
174
|
}
|
|
63
175
|
|
|
64
176
|
async function proxyModels(response, incomingURL) {
|
|
177
|
+
markActivity();
|
|
65
178
|
const state = await readState();
|
|
66
179
|
const gatewayURL = trimSlash(state.gatewayURL ?? "https://auroramos.com");
|
|
67
180
|
const clientVersion = incomingURL.searchParams.get("client_version");
|
|
@@ -87,24 +200,55 @@ async function proxyModels(response, incomingURL) {
|
|
|
87
200
|
}
|
|
88
201
|
|
|
89
202
|
async function proxyRuntime(request, response, path) {
|
|
203
|
+
markActivity();
|
|
90
204
|
const state = await readState();
|
|
91
205
|
const body = await readBody(request);
|
|
92
206
|
const capabilitiesByAlias = await readCodexModelCapabilities();
|
|
93
207
|
const gatewayURL = trimSlash(state.gatewayURL ?? "https://auroramos.com");
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
208
|
+
// One controller drives both phases: a header timeout until the gateway
|
|
209
|
+
// responds, then an idle timer that aborts only after a gap with no bytes.
|
|
210
|
+
const controller = new AbortController();
|
|
211
|
+
let watchdog = setTimeout(() => controller.abort(new Error("gateway header timeout")), GATEWAY_HEADER_TIMEOUT_MS);
|
|
212
|
+
let upstream;
|
|
213
|
+
try {
|
|
214
|
+
upstream = await fetch(`${gatewayURL}${path}`, {
|
|
215
|
+
method: request.method,
|
|
216
|
+
headers: {
|
|
217
|
+
...copyForwardHeaders(request.headers),
|
|
218
|
+
...selectedKeyHeaders(state),
|
|
219
|
+
"content-type": request.headers["content-type"] ?? "application/json",
|
|
220
|
+
"accept-encoding": "identity"
|
|
221
|
+
},
|
|
222
|
+
body: rewriteRuntimeModel(body, state.selectedModel?.alias, capabilitiesByAlias),
|
|
223
|
+
signal: controller.signal
|
|
224
|
+
});
|
|
225
|
+
} catch (error) {
|
|
226
|
+
clearTimeout(watchdog);
|
|
227
|
+
// Gateway never produced response headers (refused / DNS / header timeout).
|
|
228
|
+
// Nothing has been sent to the client yet, so a clean 502 lets it retry.
|
|
229
|
+
writeJSON(response, 502, { error: { type: "gateway_unreachable", message: error?.message || String(error) } });
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
104
232
|
response.writeHead(upstream.status, responseHeaders(upstream.headers));
|
|
105
233
|
if (upstream.body) {
|
|
106
|
-
|
|
234
|
+
// Switch from header timeout to an idle timeout that resets on every chunk.
|
|
235
|
+
clearTimeout(watchdog);
|
|
236
|
+
watchdog = setTimeout(() => controller.abort(new Error("gateway idle timeout")), GATEWAY_IDLE_TIMEOUT_MS);
|
|
237
|
+
try {
|
|
238
|
+
for await (const chunk of upstream.body) {
|
|
239
|
+
watchdog.refresh();
|
|
240
|
+
response.write(chunk);
|
|
241
|
+
}
|
|
242
|
+
} catch (error) {
|
|
243
|
+
// Mid-stream gateway/upstream drop or idle-timeout abort. Headers are
|
|
244
|
+
// already flushed so we can't promote this to an error status — end the
|
|
245
|
+
// partial response and let the client retry that single request.
|
|
246
|
+
// Re-throwing here used to crash the daemon and trigger the
|
|
247
|
+
// ConnectionRefused spin.
|
|
248
|
+
console.error(`Aurora sidecar stream relay aborted: ${error?.message || error}`);
|
|
249
|
+
}
|
|
107
250
|
}
|
|
251
|
+
clearTimeout(watchdog);
|
|
108
252
|
response.end();
|
|
109
253
|
}
|
|
110
254
|
|