NODEDC_DESIGN_GUIDELINE/server/assets/foundry-agent-npm/bin/nodedc-foundry-codex-agent.mjs

200 lines
8.7 KiB
JavaScript

#!/usr/bin/env node
import { mkdir, readFile, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
const FOUNDRY_SERVER_NAME = "nodedc_module_foundry";
const ONTOLOGY_SERVER_NAME = "nodedc_ontology";
const SKILL_NAME = "foundry-context";
const MCP_PROTOCOL_VERSION = "2025-06-18";
function usage(message = "") {
if (message) console.error(message);
console.log(`Usage:
nodedc-foundry-codex-agent --server <Foundry URL> --code <one-time code>
nodedc-foundry-codex-agent doctor [--codex-home <path>]
The setup command is issued by Foundry Settings → Codex Agent API.`);
process.exitCode = message ? 1 : 0;
}
function parseArgs(raw) {
const args = raw[0] === "--" ? raw.slice(1) : raw;
const out = { command: "setup", server: "", code: "", codexHome: process.env.CODEX_HOME || "" };
if (args[0] === "doctor") out.command = "doctor";
const values = out.command === "doctor" ? args.slice(1) : args;
for (let index = 0; index < values.length; index += 1) {
const item = values[index];
if (item === "--server") out.server = String(values[++index] || "");
else if (item.startsWith("--server=")) out.server = item.slice("--server=".length);
else if (item === "--code") out.code = String(values[++index] || "");
else if (item.startsWith("--code=")) out.code = item.slice("--code=".length);
else if (item === "--codex-home") out.codexHome = String(values[++index] || "");
else if (item.startsWith("--codex-home=")) out.codexHome = item.slice("--codex-home=".length);
else if (item === "--help" || item === "-h") out.help = true;
else throw new Error(`unknown_argument:${item}`);
}
out.server = String(out.server || "").replace(/\/+$/, "");
return out;
}
function codexHome(value) {
if (!value) return path.join(os.homedir(), ".codex");
if (value === "~") return os.homedir();
if (value.startsWith("~/") || value.startsWith("~\\")) return path.join(os.homedir(), value.slice(2));
return path.resolve(value);
}
async function readIfExists(file) {
try {
return await readFile(file, "utf8");
} catch (error) {
if (error?.code === "ENOENT") return null;
throw error;
}
}
function stripMcpBlocks(content, serverNames) {
const targets = serverNames.map((name) => `mcp_servers.${name}`);
let skip = false;
const kept = [];
for (const line of String(content || "").split(/\r?\n/)) {
const section = line.match(/^\s*\[([^\]]+)\]\s*(?:#.*)?$/)?.[1]?.trim() || "";
if (section) skip = targets.some((target) => section === target || section.startsWith(`${target}.`));
if (!skip) kept.push(line);
}
return kept.join("\n").trimEnd();
}
function mcpBlock(serverName, endpoint, token) {
return [
`[mcp_servers.${serverName}]`,
`url = ${JSON.stringify(endpoint)}`,
"enabled = true",
"required = false",
"startup_timeout_sec = 30",
"tool_timeout_sec = 120",
"",
`[mcp_servers.${serverName}.http_headers]`,
`Authorization = ${JSON.stringify(`Bearer ${token}`)}`,
`Accept = ${JSON.stringify("application/json, text/event-stream")}`,
`${JSON.stringify("MCP-Protocol-Version")} = ${JSON.stringify(MCP_PROTOCOL_VERSION)}`,
].join("\n");
}
async function writeConfig(home, setup) {
const configPath = path.join(home, "config.toml");
await mkdir(path.dirname(configPath), { recursive: true });
const original = await readIfExists(configPath);
if (original !== null) await writeFile(`${configPath}.nodedc-foundry-agent.bak`, original, "utf8");
const prefix = stripMcpBlocks(original || "", [FOUNDRY_SERVER_NAME, ONTOLOGY_SERVER_NAME]);
const blocks = [
mcpBlock(FOUNDRY_SERVER_NAME, setup.foundry.mcpUrl, setup.foundry.token),
mcpBlock(ONTOLOGY_SERVER_NAME, setup.ontology.mcpUrl, setup.ontology.token),
].join("\n\n");
await writeFile(configPath, `${prefix}${prefix ? "\n\n" : ""}${blocks}\n`, "utf8");
return configPath;
}
function skillBody() {
return `---
name: foundry-context
description: Use for NODE.DC Module Foundry application work. Use Foundry and the separate read-only Ontology MCP; never depend on Foundry source files.
---
# NODE.DC Module Foundry
- Treat \`nodedc_module_foundry\` as the only authority for Foundry application instances and Page Library bindings. Do not read or edit Foundry source, runtime files or manifests directly.
- Treat \`nodedc_ontology\` as a separate read-only semantic authority. Use it before inventing entity ids, semantic types, aliases, relations or routing assumptions.
- Work only inside the currently granted Foundry user contour. Sharing, cross-user visibility, roles and public release are outside this connector until explicit tools expose them.
- Canonical Page Library templates are immutable. Create and modify application instances through Foundry tools only.
- Use a fresh idempotency key for every write. Reuse a key only when intentionally replaying the exact same operation.
- Provider URLs, credentials, polling and normalization stay in NDC Engine L2. Foundry consumes provider-neutral Data Products and semantic bindings only.
- Never request or place provider tokens, internal workload grants or raw secret values in an application, page binding, prompt or MCP argument.
- For the first Map slice, resolve semantics through Ontology, create the application/page in Foundry, bind the approved Data Product, and verify the resulting application manifest through Foundry MCP reads.
`;
}
async function writeSkill(home) {
const file = path.join(home, "skills", SKILL_NAME, "SKILL.md");
await mkdir(path.dirname(file), { recursive: true });
await writeFile(file, skillBody(), "utf8");
return file;
}
async function redeem(server, code) {
const response = await fetch(`${server}/api/foundry-agent/v1/setup/redeem`, {
method: "POST",
headers: { accept: "application/json", "content-type": "application/json" },
body: JSON.stringify({ code, deviceName: `${os.hostname()} Codex Desktop` }),
});
const payload = await response.json().catch(() => ({}));
if (!response.ok || !payload?.ok || !payload?.foundry?.token || !payload?.ontology?.token) {
throw new Error(payload?.error || `setup_redeem_failed_${response.status}`);
}
return payload;
}
async function toolsSmoke(serverName, endpoint, token) {
const headers = {
accept: "application/json, text/event-stream",
authorization: `Bearer ${token}`,
"content-type": "application/json",
"mcp-protocol-version": MCP_PROTOCOL_VERSION,
};
const initialize = await fetch(endpoint, {
method: "POST",
headers,
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "initialize",
params: { protocolVersion: MCP_PROTOCOL_VERSION, capabilities: {}, clientInfo: { name: "nodedc-foundry-setup", version: "0.1.0" } },
}),
});
if (!initialize.ok) throw new Error(`${serverName}_initialize_failed_${initialize.status}`);
const list = await fetch(endpoint, {
method: "POST",
headers,
body: JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} }),
});
const payload = await list.json().catch(() => ({}));
if (!list.ok || !Array.isArray(payload?.result?.tools)) throw new Error(`${serverName}_tools_list_failed_${list.status}`);
return payload.result.tools.length;
}
async function main() {
const args = parseArgs(process.argv.slice(2));
if (args.help) return usage();
const home = codexHome(args.codexHome);
if (args.command === "doctor") {
const config = await readIfExists(path.join(home, "config.toml"));
const skill = await readIfExists(path.join(home, "skills", SKILL_NAME, "SKILL.md"));
const ok = Boolean(
config?.includes(`[mcp_servers.${FOUNDRY_SERVER_NAME}]`)
&& config?.includes(`[mcp_servers.${ONTOLOGY_SERVER_NAME}]`)
&& skill,
);
console.log(ok ? "NODE.DC Foundry + Ontology Codex install is present." : "NODE.DC Foundry + Ontology Codex install is incomplete.");
process.exitCode = ok ? 0 : 1;
return;
}
if (!args.server || !args.code) return usage("server_and_code_required");
const setup = await redeem(args.server, args.code);
const configPath = await writeConfig(home, setup);
const skillPath = await writeSkill(home);
const foundryToolCount = await toolsSmoke(FOUNDRY_SERVER_NAME, setup.foundry.mcpUrl, setup.foundry.token);
const ontologyToolCount = await toolsSmoke(ONTOLOGY_SERVER_NAME, setup.ontology.mcpUrl, setup.ontology.token);
console.log("NODE.DC Foundry + Ontology Codex setup complete.");
console.log("Config:", configPath);
console.log("Skill:", skillPath);
console.log("Foundry MCP smoke tools:", foundryToolCount);
console.log("Ontology MCP smoke tools:", ontologyToolCount);
console.log("Restart Codex Desktop completely before using the new MCP tools.");
}
main().catch((error) => {
console.error(`NODE.DC Foundry Codex setup failed: ${error?.message || error}`);
process.exitCode = 1;
});