feat(foundry): add managed data consumers and agent settings
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
#!/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;
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@nodedc/foundry-codex-agent",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"nodedc-foundry-codex-agent": "bin/nodedc-foundry-codex-agent.mjs"
|
||||
},
|
||||
"files": [
|
||||
"bin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Reference in New Issue
Block a user