NODEDC_DESIGN_GUIDELINE/server/foundry-agent-gateway.mjs

272 lines
10 KiB
JavaScript

import { createReadStream } from "node:fs";
import { stat } from "node:fs/promises";
const MCP_PROTOCOL_VERSION = "2025-06-18";
const MAX_BODY_BYTES = 1024 * 1024;
function sendJson(response, statusCode, payload, headers = {}) {
response.writeHead(statusCode, {
"cache-control": "no-store",
"content-type": "application/json; charset=utf-8",
...headers,
});
response.end(JSON.stringify(payload));
}
async function readJsonBody(request, maxBytes = MAX_BODY_BYTES) {
const chunks = [];
let size = 0;
for await (const chunk of request) {
size += chunk.length;
if (size > maxBytes) throw new Error("payload_too_large");
chunks.push(chunk);
}
if (!chunks.length) return {};
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
}
function bearerToken(request) {
const match = String(request.headers.authorization || "").match(/^Bearer\s+(.+)$/i);
return match?.[1]?.trim() || "";
}
function publicOrigin(request, configuredOrigin) {
if (configuredOrigin) return String(configuredOrigin).replace(/\/+$/, "");
const forwardedProto = String(request.headers["x-forwarded-proto"] || "").split(",")[0].trim();
const protocol = forwardedProto === "https" ? "https" : "http";
const forwardedHost = String(request.headers["x-forwarded-host"] || "").split(",")[0].trim();
const host = forwardedHost || String(request.headers.host || "127.0.0.1");
return `${protocol}://${host}`.replace(/\/+$/, "");
}
function actorFromProfile(profile) {
const ownerId = String(profile?.id || "").trim();
const ownerKey = String(profile?.email || ownerId).trim().toLowerCase();
if (!ownerId || !ownerKey) throw new Error("foundry_agent_owner_required");
return { ownerId, ownerKey };
}
function errorCode(error) {
return String(error?.message || error || "foundry_agent_operation_failed").slice(0, 160);
}
function managementMatch(pathname) {
const match = pathname.match(/^\/api\/foundry-agent-api\/agents(?:\/([^/]+)(?:\/(revoke|setup-code))?)?$/);
if (!match) return null;
return { agentId: match[1] ? decodeURIComponent(match[1]) : null, action: match[2] || null };
}
export function createFoundryAgentGateway({
store,
configuredPublicOrigin = "",
installerPackagePath,
ontologyCoreUrl = "",
ontologyCoreAccessToken = "",
}) {
if (!store) throw new Error("foundry_agent_store_required");
const ontologyUrl = String(ontologyCoreUrl || "").trim().replace(/\/+$/, "");
const ontologyToken = String(ontologyCoreAccessToken || "").trim();
async function handleInstaller(request, response, url) {
if (url.pathname !== "/api/foundry-agent/install/nodedc-foundry-codex-agent-setup.tgz") return false;
if (request.method !== "GET") {
response.writeHead(405, { allow: "GET" });
response.end();
return true;
}
try {
const info = await stat(installerPackagePath);
response.writeHead(200, {
"cache-control": "no-store",
"content-disposition": 'attachment; filename="nodedc-foundry-codex-agent-setup.tgz"',
"content-length": info.size,
"content-type": "application/octet-stream",
});
createReadStream(installerPackagePath).pipe(response);
} catch {
sendJson(response, 404, { ok: false, error: "foundry_agent_installer_not_found" });
}
return true;
}
async function handleSetupRedeem(request, response, url) {
if (url.pathname !== "/api/foundry-agent/v1/setup/redeem") return false;
if (request.method !== "POST") {
response.writeHead(405, { allow: "POST" });
response.end();
return true;
}
try {
const input = await readJsonBody(request, 16 * 1024);
const redeemed = await store.redeemSetupCode(input.code, input.deviceName);
const origin = publicOrigin(request, configuredPublicOrigin);
sendJson(response, 200, {
ok: true,
agent: redeemed.agent,
device: redeemed.device,
foundry: {
serverName: "nodedc_module_foundry",
mcpUrl: `${origin}/api/mcp`,
token: redeemed.foundryToken,
},
ontology: {
serverName: "nodedc_ontology",
mcpUrl: `${origin}/api/ontology-mcp`,
token: redeemed.ontologyToken,
readOnly: true,
},
});
} catch (error) {
const code = errorCode(error);
sendJson(response, code === "payload_too_large" ? 413 : 400, { ok: false, error: code });
}
return true;
}
async function handleDoctor(request, response, url) {
if (url.pathname !== "/api/foundry-agent/v1/doctor") return false;
if (request.method !== "GET") {
response.writeHead(405, { allow: "GET" });
response.end();
return true;
}
const session = await store.authenticateFoundryToken(bearerToken(request), { check: true });
if (!session) {
sendJson(response, 401, { ok: false, error: "foundry_agent_unauthorized" });
return true;
}
sendJson(response, 200, {
ok: true,
agent: session.agent,
device: session.device,
foundryMcpUrl: `${publicOrigin(request, configuredPublicOrigin)}/api/mcp`,
ontologyMcpUrl: `${publicOrigin(request, configuredPublicOrigin)}/api/ontology-mcp`,
ontologyReadOnly: true,
});
return true;
}
async function handlePublicRequest(request, response, url) {
return await handleInstaller(request, response, url)
|| await handleSetupRedeem(request, response, url)
|| await handleDoctor(request, response, url);
}
async function handleManagementRequest(request, response, url, profile) {
const match = managementMatch(url.pathname);
if (!match) return false;
const owner = actorFromProfile(profile);
try {
if (!match.agentId && request.method === "GET") {
sendJson(response, 200, { ok: true, agents: await store.listAgents(owner.ownerKey) });
return true;
}
if (!match.agentId && request.method === "POST") {
const input = await readJsonBody(request, 640 * 1024);
const agent = await store.createAgent({ ...owner, name: input.name, avatarUrl: input.avatarUrl });
sendJson(response, 201, { ok: true, agent });
return true;
}
if (match.agentId && !match.action && request.method === "PATCH") {
const input = await readJsonBody(request, 640 * 1024);
const agent = await store.updateAgent(owner.ownerKey, match.agentId, input);
sendJson(response, 200, { ok: true, agent });
return true;
}
if (match.agentId && match.action === "revoke" && request.method === "POST") {
const agent = await store.revokeAgent(owner.ownerKey, match.agentId);
sendJson(response, 200, { ok: true, agent });
return true;
}
if (match.agentId && match.action === "setup-code" && request.method === "POST") {
const setup = await store.issueSetupCode(owner.ownerKey, match.agentId);
const origin = publicOrigin(request, configuredPublicOrigin);
const packageUrl = `${origin}/api/foundry-agent/install/nodedc-foundry-codex-agent-setup.tgz`;
const command = `npm exec --yes --package=${packageUrl} nodedc-foundry-codex-agent -- --server ${origin} --code ${setup.code}`;
sendJson(response, 201, { ok: true, install: { command, expiresAt: setup.expiresAt } });
return true;
}
response.writeHead(405, { allow: "GET, POST, PATCH" });
response.end();
} catch (error) {
const code = errorCode(error);
const status = code === "foundry_agent_not_found" ? 404 : code === "payload_too_large" ? 413 : 400;
sendJson(response, status, { ok: false, error: code });
}
return true;
}
async function authenticateFoundryToken(token, options) {
const session = await store.authenticateFoundryToken(token, options);
return session?.actor || null;
}
async function handleOntologyMcp(request, response, url) {
if (url.pathname !== "/api/ontology-mcp") return false;
if (request.method !== "POST") {
response.writeHead(405, { allow: "POST" });
response.end();
return true;
}
const session = await store.authenticateOntologyToken(bearerToken(request), { check: true });
if (!session) {
sendJson(response, 401, { ok: false, error: "ontology_agent_unauthorized" });
return true;
}
if (!ontologyUrl || !ontologyToken) {
sendJson(response, 503, { ok: false, error: "ontology_agent_proxy_not_configured" });
return true;
}
let input;
try {
input = await readJsonBody(request);
} catch (error) {
sendJson(response, error?.message === "payload_too_large" ? 413 : 400, { ok: false, error: errorCode(error) });
return true;
}
let upstream;
try {
upstream = await fetch(`${ontologyUrl}/mcp`, {
method: "POST",
redirect: "manual",
headers: {
accept: String(request.headers.accept || "application/json, text/event-stream"),
authorization: `Bearer ${ontologyToken}`,
"content-type": "application/json",
"mcp-protocol-version": String(request.headers["mcp-protocol-version"] || MCP_PROTOCOL_VERSION),
},
body: JSON.stringify(input),
signal: AbortSignal.timeout(60_000),
});
} catch {
sendJson(response, 503, { ok: false, error: "ontology_agent_proxy_unavailable" });
return true;
}
const payload = await upstream.text();
response.statusCode = upstream.status;
response.setHeader("cache-control", "no-store");
response.setHeader("content-type", upstream.headers.get("content-type") || "application/json; charset=utf-8");
for (const header of ["mcp-protocol-version", "mcp-session-id", "vary"]) {
const value = upstream.headers.get(header);
if (value) response.setHeader(header, value);
}
response.end(payload);
return true;
}
return {
handlePublicRequest,
handleManagementRequest,
handleOntologyMcp,
authenticateFoundryToken,
health() {
return {
configured: Boolean(installerPackagePath),
ontologyProxyConfigured: Boolean(ontologyUrl && ontologyToken),
tokenLifecycle: "durable-until-revoke",
};
},
};
}