Initial Hermes Agent stack: Claude-Max proxy + gateway, docker-compose, README
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* hermes-gateway — minimaler Bearer-Token Reverse-Proxy vor hermes-proxy.
|
||||
*
|
||||
* WARUM: hermes-proxy selbst hat KEINE Authentifizierung — genau wie ARIAs
|
||||
* Proxy (siehe /root/ARIA-AGENT/docker-compose.yml, Service "proxy"). Bei
|
||||
* ARIA ist das ok, weil der Proxy NIE einen Netzwerk-Port bekommt, nur
|
||||
* Docker-intern von aria-brain erreichbar ist.
|
||||
*
|
||||
* Hermes Agent laeuft aber auf einer ANDEREN Maschine (Stefans Wunsch) und
|
||||
* muss den Proxy ueber Netzwerk erreichen -> der Port muss also offen sein.
|
||||
* Ohne Auth koennte dann JEDER der den Port erreicht (falsches Netz-Segment,
|
||||
* offenes WLAN, Portscan) Stefans Claude-Max-Subscription fremdnutzen —
|
||||
* die eigentliche "Auth" ist ja nur die eingeloggte Claude-CLI-Session im
|
||||
* Volume, kein Provider-seitiger API-Key-Check.
|
||||
*
|
||||
* Dieses Gateway sitzt davor: prueft den Authorization-Header gegen ein
|
||||
* Shared Secret (ENV TOKEN), leitet nur bei Match an hermes-proxy weiter.
|
||||
* Alles andere -> 401, kein Request geht durch.
|
||||
*
|
||||
* Praktischer Nebeneffekt: OpenAI-kompatible Clients (Hermes Agent
|
||||
* eingeschlossen) schicken ihren konfigurierten `api_key` sowieso schon als
|
||||
* "Authorization: Bearer <api_key>"-Header. In Hermes' config.yaml also
|
||||
* einfach `api_key: "<TOKEN>"` (= derselbe Wert wie hier in TOKEN) setzen —
|
||||
* kein Extra-Code auf Hermes-Seite noetig.
|
||||
*/
|
||||
|
||||
const http = require("http");
|
||||
const crypto = require("crypto");
|
||||
|
||||
const TOKEN = process.env.TOKEN;
|
||||
const UPSTREAM_HOST = process.env.UPSTREAM_HOST || "hermes-proxy";
|
||||
const UPSTREAM_PORT = parseInt(process.env.UPSTREAM_PORT || "3456", 10);
|
||||
const LISTEN_PORT = parseInt(process.env.LISTEN_PORT || "8080", 10);
|
||||
|
||||
if (!TOKEN || TOKEN.length < 16) {
|
||||
console.error(
|
||||
"FATAL: TOKEN env fehlt oder ist zu kurz (< 16 Zeichen). " +
|
||||
"Ohne ordentliches Shared Secret startet das Gateway bewusst nicht " +
|
||||
"— sonst haengt hier ein offener Claude-Proxy im Netz."
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function safeEqual(a, b) {
|
||||
const ab = Buffer.from(a, "utf8");
|
||||
const bb = Buffer.from(b, "utf8");
|
||||
if (ab.length !== bb.length) return false;
|
||||
return crypto.timingSafeEqual(ab, bb);
|
||||
}
|
||||
|
||||
function unauthorized(res) {
|
||||
res.writeHead(401, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ error: { message: "unauthorized", type: "invalid_api_key" } }));
|
||||
}
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
const auth = req.headers["authorization"] || "";
|
||||
const expected = `Bearer ${TOKEN}`;
|
||||
|
||||
if (!safeEqual(auth, expected)) {
|
||||
console.log(`[hermes-gateway] 401 ${req.method} ${req.url} von ${req.socket.remoteAddress}`);
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const proxyReq = http.request(
|
||||
{
|
||||
hostname: UPSTREAM_HOST,
|
||||
port: UPSTREAM_PORT,
|
||||
path: req.url,
|
||||
method: req.method,
|
||||
headers: req.headers,
|
||||
},
|
||||
(proxyRes) => {
|
||||
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
||||
proxyRes.pipe(res);
|
||||
}
|
||||
);
|
||||
|
||||
proxyReq.on("error", (err) => {
|
||||
console.error(`[hermes-gateway] upstream error: ${err.message}`);
|
||||
if (!res.headersSent) {
|
||||
res.writeHead(502, { "Content-Type": "application/json" });
|
||||
}
|
||||
res.end(JSON.stringify({ error: { message: "bad_gateway", detail: String(err.message) } }));
|
||||
});
|
||||
|
||||
req.pipe(proxyReq);
|
||||
});
|
||||
|
||||
server.listen(LISTEN_PORT, "0.0.0.0", () => {
|
||||
console.log(`[hermes-gateway] listening on 0.0.0.0:${LISTEN_PORT} -> http://${UPSTREAM_HOST}:${UPSTREAM_PORT}`);
|
||||
});
|
||||
Reference in New Issue
Block a user