@agimon-ai/po-domain 0.0.0-alpha.0
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/LICENSE +21 -0
- package/README.md +71 -0
- package/dist/apiContracts.cjs +17 -0
- package/dist/apiContracts.cjs.map +1 -0
- package/dist/apiContracts.d.mts +53 -0
- package/dist/apiContracts.d.mts.map +1 -0
- package/dist/apiContracts.mjs +13 -0
- package/dist/apiContracts.mjs.map +1 -0
- package/dist/apply.cjs +76 -0
- package/dist/apply.cjs.map +1 -0
- package/dist/apply.d.mts +23 -0
- package/dist/apply.d.mts.map +1 -0
- package/dist/apply.mjs +72 -0
- package/dist/apply.mjs.map +1 -0
- package/dist/domainText-Dm8k7p4z.mjs +97 -0
- package/dist/domainText-Dm8k7p4z.mjs.map +1 -0
- package/dist/domainText-UIRIz6-_.cjs +204 -0
- package/dist/domainText-UIRIz6-_.cjs.map +1 -0
- package/dist/extensions/mcp.cjs +26 -0
- package/dist/extensions/mcp.cjs.map +1 -0
- package/dist/extensions/mcp.d.mts +5 -0
- package/dist/extensions/mcp.d.mts.map +1 -0
- package/dist/extensions/mcp.mjs +21 -0
- package/dist/extensions/mcp.mjs.map +1 -0
- package/dist/extensions/server.cjs +229 -0
- package/dist/extensions/server.cjs.map +1 -0
- package/dist/extensions/server.d.mts +5 -0
- package/dist/extensions/server.d.mts.map +1 -0
- package/dist/extensions/server.mjs +221 -0
- package/dist/extensions/server.mjs.map +1 -0
- package/dist/extensions/web.mjs +26 -0
- package/dist/extensions/web.mjs.map +1 -0
- package/dist/index.cjs +19 -0
- package/dist/index.d.mts +84 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +3 -0
- package/dist/mcp.cjs +32 -0
- package/dist/mcp.cjs.map +1 -0
- package/dist/mcp.d.mts +52 -0
- package/dist/mcp.d.mts.map +1 -0
- package/dist/mcp.mjs +19 -0
- package/dist/mcp.mjs.map +1 -0
- package/dist/mcpFilter-CZNmU_VF.cjs +189 -0
- package/dist/mcpFilter-CZNmU_VF.cjs.map +1 -0
- package/dist/mcpFilter-Cf-czy8O.mjs +151 -0
- package/dist/mcpFilter-Cf-czy8O.mjs.map +1 -0
- package/dist/pluginMaterializer-C8IZ1PSY.cjs +267 -0
- package/dist/pluginMaterializer-C8IZ1PSY.cjs.map +1 -0
- package/dist/pluginMaterializer-D9-oDF-b.mjs +259 -0
- package/dist/pluginMaterializer-D9-oDF-b.mjs.map +1 -0
- package/dist/plugins.cjs +3 -0
- package/dist/plugins.d.mts +14 -0
- package/dist/plugins.d.mts.map +1 -0
- package/dist/plugins.mjs +2 -0
- package/dist/resourceCollector-CRY5cmeY.mjs +773 -0
- package/dist/resourceCollector-CRY5cmeY.mjs.map +1 -0
- package/dist/resourceCollector-Cu-lp5hJ.cjs +837 -0
- package/dist/resourceCollector-Cu-lp5hJ.cjs.map +1 -0
- package/dist/resources-BEjxE3Zn.d.mts +66 -0
- package/dist/resources-BEjxE3Zn.d.mts.map +1 -0
- package/dist/resources.cjs +12 -0
- package/dist/resources.d.mts +43 -0
- package/dist/resources.d.mts.map +1 -0
- package/dist/resources.mjs +2 -0
- package/dist/rolldown-runtime-VH7oDXx4.cjs +28 -0
- package/dist/toolNames-Ckp5VLMD.mjs +43 -0
- package/dist/toolNames-Ckp5VLMD.mjs.map +1 -0
- package/dist/toolNames-Cuu-u8r1.cjs +48 -0
- package/dist/toolNames-Cuu-u8r1.cjs.map +1 -0
- package/llms.txt +12 -0
- package/package.json +158 -0
- package/src/prompts/po-author-domain/SKILL.md +18 -0
- package/src/prompts/po-author-domain/references/domains-contract.md +165 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
const require_rolldown_runtime = require("./rolldown-runtime-VH7oDXx4.cjs");
|
|
2
|
+
let node_fs = require("node:fs");
|
|
3
|
+
node_fs = require_rolldown_runtime.__toESM(node_fs, 1);
|
|
4
|
+
let node_path = require("node:path");
|
|
5
|
+
node_path = require_rolldown_runtime.__toESM(node_path, 1);
|
|
6
|
+
let _agimon_ai_po_config_domains = require("@agimon-ai/po-config/domains");
|
|
7
|
+
let yaml = require("yaml");
|
|
8
|
+
//#region src/services/mcpFilter/index.ts
|
|
9
|
+
/** Server name in .mcp.json whose upstreams are declared in mcp-config.yaml. */
|
|
10
|
+
const PROXY_SERVER_NAME = "agiflow-proxy";
|
|
11
|
+
const PROXY_CONFIG_FILE = "mcp-config.yaml";
|
|
12
|
+
const CONFIG_ARGUMENT = "--config";
|
|
13
|
+
/**
|
|
14
|
+
* Unions the mcp allowlists of the selected domains.
|
|
15
|
+
*
|
|
16
|
+
* Returns undefined when any selected domain omits `mcp`, which means "give me
|
|
17
|
+
* everything". Filtering only kicks in once every selected domain has opted in,
|
|
18
|
+
* so a domain that has not been migrated yet cannot silently lose its tools.
|
|
19
|
+
*/
|
|
20
|
+
function resolveMcpAllowlist(repoRoot, domainNames) {
|
|
21
|
+
const manifest = (0, _agimon_ai_po_config_domains.loadDomains)(repoRoot);
|
|
22
|
+
const expanded = (0, _agimon_ai_po_config_domains.expandDomainNames)(manifest, domainNames);
|
|
23
|
+
if (expanded.length === 0) return void 0;
|
|
24
|
+
const servers = /* @__PURE__ */ new Set();
|
|
25
|
+
const proxy = /* @__PURE__ */ new Set();
|
|
26
|
+
for (const name of expanded) {
|
|
27
|
+
const allowlist = manifest.domains[name]?.mcp;
|
|
28
|
+
if (!allowlist) return void 0;
|
|
29
|
+
for (const server of allowlist.servers ?? []) servers.add(server);
|
|
30
|
+
for (const upstream of allowlist.proxy ?? []) proxy.add(upstream);
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
servers: [...servers],
|
|
34
|
+
proxy: [...proxy]
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** Drops servers that are not on the allowlist. An empty list means keep all. */
|
|
38
|
+
function filterMcpServers(config, allowed) {
|
|
39
|
+
if (!allowed || allowed.length === 0) return config;
|
|
40
|
+
const servers = config.mcpServers ?? {};
|
|
41
|
+
const kept = {};
|
|
42
|
+
for (const [name, definition] of Object.entries(servers)) if (allowed.includes(name)) kept[name] = definition;
|
|
43
|
+
return {
|
|
44
|
+
...config,
|
|
45
|
+
mcpServers: kept
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Rewrites the proxy's upstream list, keeping only allowed servers.
|
|
50
|
+
*
|
|
51
|
+
* Entries are removed rather than marked disabled so a filtered upstream cannot
|
|
52
|
+
* be reached even if the proxy ignores the disabled flag. Comments in the
|
|
53
|
+
* source file are not preserved; the result is a per-run temporary file.
|
|
54
|
+
*/
|
|
55
|
+
function filterProxyConfig(yamlText, allowed) {
|
|
56
|
+
if (!allowed || allowed.length === 0) return yamlText;
|
|
57
|
+
const config = (0, yaml.parse)(yamlText) ?? {};
|
|
58
|
+
const servers = config.mcpServers ?? {};
|
|
59
|
+
const kept = {};
|
|
60
|
+
const dropped = [];
|
|
61
|
+
for (const [name, definition] of Object.entries(servers)) if (allowed.includes(name)) kept[name] = definition;
|
|
62
|
+
else dropped.push(name);
|
|
63
|
+
return `${[
|
|
64
|
+
"# Generated by po for this run. Do not edit.",
|
|
65
|
+
`# Upstreams kept: ${Object.keys(kept).join(", ") || "(none)"}`,
|
|
66
|
+
`# Upstreams removed by the domain allowlist: ${dropped.join(", ") || "(none)"}`,
|
|
67
|
+
""
|
|
68
|
+
].join("\n")}${(0, yaml.stringify)({
|
|
69
|
+
...config,
|
|
70
|
+
mcpServers: kept
|
|
71
|
+
})}`;
|
|
72
|
+
}
|
|
73
|
+
async function copyFileIfPresent(source, target) {
|
|
74
|
+
try {
|
|
75
|
+
await node_fs.default.promises.copyFile(source, target);
|
|
76
|
+
return true;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (error.code === "ENOENT") return false;
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Copies a generated MCP config out of the run directory so another frontend
|
|
84
|
+
* can use it, keeping the proxy's --config pointing at the copy.
|
|
85
|
+
*
|
|
86
|
+
* The run directory is deleted when the harness exits, so emitting for
|
|
87
|
+
* claude.sh or codex.sh has to relocate both files together.
|
|
88
|
+
*/
|
|
89
|
+
async function persistMcpConfig(sourceConfigPath, temporaryDirectory, targetDirectory) {
|
|
90
|
+
await node_fs.default.promises.mkdir(targetDirectory, { recursive: true });
|
|
91
|
+
const config = JSON.parse(await node_fs.default.promises.readFile(sourceConfigPath, "utf8"));
|
|
92
|
+
const servers = config.mcpServers ?? {};
|
|
93
|
+
const proxy = servers[PROXY_SERVER_NAME];
|
|
94
|
+
const generatedProxyConfig = node_path.default.join(temporaryDirectory, PROXY_CONFIG_FILE);
|
|
95
|
+
if (proxy) {
|
|
96
|
+
const args = [...proxy.args ?? []];
|
|
97
|
+
const configIndex = args.indexOf(CONFIG_ARGUMENT);
|
|
98
|
+
if (configIndex !== -1) {
|
|
99
|
+
const targetProxyConfig = node_path.default.join(targetDirectory, PROXY_CONFIG_FILE);
|
|
100
|
+
if (await copyFileIfPresent(generatedProxyConfig, targetProxyConfig)) {
|
|
101
|
+
args[configIndex + 1] = targetProxyConfig;
|
|
102
|
+
servers[PROXY_SERVER_NAME] = {
|
|
103
|
+
...proxy,
|
|
104
|
+
args
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const targetPath = node_path.default.join(targetDirectory, "mcp.json");
|
|
110
|
+
await node_fs.default.promises.writeFile(targetPath, `${JSON.stringify({
|
|
111
|
+
...config,
|
|
112
|
+
mcpServers: servers
|
|
113
|
+
}, null, 2)}\n`, { mode: 384 });
|
|
114
|
+
return targetPath;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Applies the allowlist to a merged MCP config, writing a filtered proxy config
|
|
118
|
+
* into the run directory and repointing the proxy at it when needed.
|
|
119
|
+
*/
|
|
120
|
+
async function applyMcpAllowlist(config, allowlist, repoRoot, temporaryDirectory) {
|
|
121
|
+
if (!allowlist) return config;
|
|
122
|
+
const filtered = filterMcpServers(config, allowlist.servers);
|
|
123
|
+
const servers = filtered.mcpServers ?? {};
|
|
124
|
+
const proxy = servers[PROXY_SERVER_NAME];
|
|
125
|
+
if (!proxy || !allowlist.proxy || allowlist.proxy.length === 0) return filtered;
|
|
126
|
+
const args = [...proxy.args ?? []];
|
|
127
|
+
const configIndex = args.indexOf(CONFIG_ARGUMENT);
|
|
128
|
+
if (configIndex === -1 || !args[configIndex + 1]) return filtered;
|
|
129
|
+
const sourcePath = node_path.default.resolve(repoRoot, args[configIndex + 1]);
|
|
130
|
+
let sourceConfig;
|
|
131
|
+
try {
|
|
132
|
+
sourceConfig = await node_fs.default.promises.readFile(sourcePath, "utf8");
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (error.code === "ENOENT") return filtered;
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
const targetPath = node_path.default.join(temporaryDirectory, PROXY_CONFIG_FILE);
|
|
138
|
+
await node_fs.default.promises.writeFile(targetPath, filterProxyConfig(sourceConfig, allowlist.proxy), { mode: 384 });
|
|
139
|
+
args[configIndex + 1] = targetPath;
|
|
140
|
+
return {
|
|
141
|
+
...filtered,
|
|
142
|
+
mcpServers: {
|
|
143
|
+
...servers,
|
|
144
|
+
[PROXY_SERVER_NAME]: {
|
|
145
|
+
...proxy,
|
|
146
|
+
args
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
//#endregion
|
|
152
|
+
Object.defineProperty(exports, "PROXY_SERVER_NAME", {
|
|
153
|
+
enumerable: true,
|
|
154
|
+
get: function() {
|
|
155
|
+
return PROXY_SERVER_NAME;
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
Object.defineProperty(exports, "applyMcpAllowlist", {
|
|
159
|
+
enumerable: true,
|
|
160
|
+
get: function() {
|
|
161
|
+
return applyMcpAllowlist;
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
Object.defineProperty(exports, "filterMcpServers", {
|
|
165
|
+
enumerable: true,
|
|
166
|
+
get: function() {
|
|
167
|
+
return filterMcpServers;
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
Object.defineProperty(exports, "filterProxyConfig", {
|
|
171
|
+
enumerable: true,
|
|
172
|
+
get: function() {
|
|
173
|
+
return filterProxyConfig;
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
Object.defineProperty(exports, "persistMcpConfig", {
|
|
177
|
+
enumerable: true,
|
|
178
|
+
get: function() {
|
|
179
|
+
return persistMcpConfig;
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
Object.defineProperty(exports, "resolveMcpAllowlist", {
|
|
183
|
+
enumerable: true,
|
|
184
|
+
get: function() {
|
|
185
|
+
return resolveMcpAllowlist;
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
//# sourceMappingURL=mcpFilter-CZNmU_VF.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcpFilter-CZNmU_VF.cjs","names":["loadDomains","expandDomainNames","parseYaml","stringifyYaml","fs","path"],"sources":["../src/services/mcpFilter/index.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\nimport { type DomainMcpAllowlist, expandDomainNames, loadDomains } from '@agimon-ai/po-config/domains';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\n\nimport type { JsonObject } from '../../types/resources';\n\n/** Server name in .mcp.json whose upstreams are declared in mcp-config.yaml. */\nexport const PROXY_SERVER_NAME = 'agiflow-proxy';\nconst PROXY_CONFIG_FILE = 'mcp-config.yaml';\nconst CONFIG_ARGUMENT = '--config';\n\n/**\n * Unions the mcp allowlists of the selected domains.\n *\n * Returns undefined when any selected domain omits `mcp`, which means \"give me\n * everything\". Filtering only kicks in once every selected domain has opted in,\n * so a domain that has not been migrated yet cannot silently lose its tools.\n */\nexport function resolveMcpAllowlist(repoRoot: string, domainNames: string[]): DomainMcpAllowlist | undefined {\n const manifest = loadDomains(repoRoot);\n const expanded = expandDomainNames(manifest, domainNames);\n if (expanded.length === 0) return undefined;\n\n const servers = new Set<string>();\n const proxy = new Set<string>();\n for (const name of expanded) {\n const allowlist = manifest.domains[name]?.mcp;\n if (!allowlist) return undefined;\n for (const server of allowlist.servers ?? []) servers.add(server);\n for (const upstream of allowlist.proxy ?? []) proxy.add(upstream);\n }\n return { servers: [...servers], proxy: [...proxy] };\n}\n\n/** Drops servers that are not on the allowlist. An empty list means keep all. */\nexport function filterMcpServers(config: JsonObject, allowed?: string[]): JsonObject {\n if (!allowed || allowed.length === 0) return config;\n const servers = (config.mcpServers ?? {}) as Record<string, unknown>;\n const kept: Record<string, unknown> = {};\n for (const [name, definition] of Object.entries(servers)) {\n if (allowed.includes(name)) kept[name] = definition;\n }\n return { ...config, mcpServers: kept };\n}\n\n/**\n * Rewrites the proxy's upstream list, keeping only allowed servers.\n *\n * Entries are removed rather than marked disabled so a filtered upstream cannot\n * be reached even if the proxy ignores the disabled flag. Comments in the\n * source file are not preserved; the result is a per-run temporary file.\n */\nexport function filterProxyConfig(yamlText: string, allowed?: string[]): string {\n if (!allowed || allowed.length === 0) return yamlText;\n const config = (parseYaml(yamlText) ?? {}) as JsonObject;\n const servers = (config.mcpServers ?? {}) as Record<string, unknown>;\n const kept: Record<string, unknown> = {};\n const dropped: string[] = [];\n for (const [name, definition] of Object.entries(servers)) {\n if (allowed.includes(name)) kept[name] = definition;\n else dropped.push(name);\n }\n const header = [\n '# Generated by po for this run. Do not edit.',\n `# Upstreams kept: ${Object.keys(kept).join(', ') || '(none)'}`,\n `# Upstreams removed by the domain allowlist: ${dropped.join(', ') || '(none)'}`,\n '',\n ].join('\\n');\n return `${header}${stringifyYaml({ ...config, mcpServers: kept })}`;\n}\n\nasync function copyFileIfPresent(source: string, target: string): Promise<boolean> {\n try {\n await fs.promises.copyFile(source, target);\n return true;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false;\n throw error;\n }\n}\n\n/**\n * Copies a generated MCP config out of the run directory so another frontend\n * can use it, keeping the proxy's --config pointing at the copy.\n *\n * The run directory is deleted when the harness exits, so emitting for\n * claude.sh or codex.sh has to relocate both files together.\n */\nexport async function persistMcpConfig(\n sourceConfigPath: string,\n temporaryDirectory: string,\n targetDirectory: string,\n): Promise<string> {\n await fs.promises.mkdir(targetDirectory, { recursive: true });\n const config = JSON.parse(await fs.promises.readFile(sourceConfigPath, 'utf8')) as JsonObject;\n const servers = (config.mcpServers ?? {}) as Record<string, JsonObject>;\n const proxy = servers[PROXY_SERVER_NAME];\n const generatedProxyConfig = path.join(temporaryDirectory, PROXY_CONFIG_FILE);\n\n if (proxy) {\n const args = [...((proxy.args ?? []) as string[])];\n const configIndex = args.indexOf(CONFIG_ARGUMENT);\n if (configIndex !== -1) {\n const targetProxyConfig = path.join(targetDirectory, PROXY_CONFIG_FILE);\n if (await copyFileIfPresent(generatedProxyConfig, targetProxyConfig)) {\n args[configIndex + 1] = targetProxyConfig;\n servers[PROXY_SERVER_NAME] = { ...proxy, args };\n }\n }\n }\n\n const targetPath = path.join(targetDirectory, 'mcp.json');\n await fs.promises.writeFile(targetPath, `${JSON.stringify({ ...config, mcpServers: servers }, null, 2)}\\n`, {\n mode: 0o600,\n });\n return targetPath;\n}\n\n/**\n * Applies the allowlist to a merged MCP config, writing a filtered proxy config\n * into the run directory and repointing the proxy at it when needed.\n */\nexport async function applyMcpAllowlist(\n config: JsonObject,\n allowlist: DomainMcpAllowlist | undefined,\n repoRoot: string,\n temporaryDirectory: string,\n): Promise<JsonObject> {\n if (!allowlist) return config;\n const filtered = filterMcpServers(config, allowlist.servers);\n const servers = (filtered.mcpServers ?? {}) as Record<string, JsonObject>;\n const proxy = servers[PROXY_SERVER_NAME];\n if (!proxy || !allowlist.proxy || allowlist.proxy.length === 0) return filtered;\n\n const args = [...((proxy.args ?? []) as string[])];\n const configIndex = args.indexOf(CONFIG_ARGUMENT);\n if (configIndex === -1 || !args[configIndex + 1]) return filtered;\n\n const sourcePath = path.resolve(repoRoot, args[configIndex + 1]);\n let sourceConfig: string;\n try {\n sourceConfig = await fs.promises.readFile(sourcePath, 'utf8');\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return filtered;\n throw error;\n }\n const targetPath = path.join(temporaryDirectory, PROXY_CONFIG_FILE);\n await fs.promises.writeFile(targetPath, filterProxyConfig(sourceConfig, allowlist.proxy), { mode: 0o600 });\n args[configIndex + 1] = targetPath;\n return { ...filtered, mcpServers: { ...servers, [PROXY_SERVER_NAME]: { ...proxy, args } } };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { expandDomainNames, loadDomains } from "@agimon-ai/po-config/domains";
|
|
4
|
+
import { parse, stringify } from "yaml";
|
|
5
|
+
//#region src/services/mcpFilter/index.ts
|
|
6
|
+
/** Server name in .mcp.json whose upstreams are declared in mcp-config.yaml. */
|
|
7
|
+
const PROXY_SERVER_NAME = "agiflow-proxy";
|
|
8
|
+
const PROXY_CONFIG_FILE = "mcp-config.yaml";
|
|
9
|
+
const CONFIG_ARGUMENT = "--config";
|
|
10
|
+
/**
|
|
11
|
+
* Unions the mcp allowlists of the selected domains.
|
|
12
|
+
*
|
|
13
|
+
* Returns undefined when any selected domain omits `mcp`, which means "give me
|
|
14
|
+
* everything". Filtering only kicks in once every selected domain has opted in,
|
|
15
|
+
* so a domain that has not been migrated yet cannot silently lose its tools.
|
|
16
|
+
*/
|
|
17
|
+
function resolveMcpAllowlist(repoRoot, domainNames) {
|
|
18
|
+
const manifest = loadDomains(repoRoot);
|
|
19
|
+
const expanded = expandDomainNames(manifest, domainNames);
|
|
20
|
+
if (expanded.length === 0) return void 0;
|
|
21
|
+
const servers = /* @__PURE__ */ new Set();
|
|
22
|
+
const proxy = /* @__PURE__ */ new Set();
|
|
23
|
+
for (const name of expanded) {
|
|
24
|
+
const allowlist = manifest.domains[name]?.mcp;
|
|
25
|
+
if (!allowlist) return void 0;
|
|
26
|
+
for (const server of allowlist.servers ?? []) servers.add(server);
|
|
27
|
+
for (const upstream of allowlist.proxy ?? []) proxy.add(upstream);
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
servers: [...servers],
|
|
31
|
+
proxy: [...proxy]
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** Drops servers that are not on the allowlist. An empty list means keep all. */
|
|
35
|
+
function filterMcpServers(config, allowed) {
|
|
36
|
+
if (!allowed || allowed.length === 0) return config;
|
|
37
|
+
const servers = config.mcpServers ?? {};
|
|
38
|
+
const kept = {};
|
|
39
|
+
for (const [name, definition] of Object.entries(servers)) if (allowed.includes(name)) kept[name] = definition;
|
|
40
|
+
return {
|
|
41
|
+
...config,
|
|
42
|
+
mcpServers: kept
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Rewrites the proxy's upstream list, keeping only allowed servers.
|
|
47
|
+
*
|
|
48
|
+
* Entries are removed rather than marked disabled so a filtered upstream cannot
|
|
49
|
+
* be reached even if the proxy ignores the disabled flag. Comments in the
|
|
50
|
+
* source file are not preserved; the result is a per-run temporary file.
|
|
51
|
+
*/
|
|
52
|
+
function filterProxyConfig(yamlText, allowed) {
|
|
53
|
+
if (!allowed || allowed.length === 0) return yamlText;
|
|
54
|
+
const config = parse(yamlText) ?? {};
|
|
55
|
+
const servers = config.mcpServers ?? {};
|
|
56
|
+
const kept = {};
|
|
57
|
+
const dropped = [];
|
|
58
|
+
for (const [name, definition] of Object.entries(servers)) if (allowed.includes(name)) kept[name] = definition;
|
|
59
|
+
else dropped.push(name);
|
|
60
|
+
return `${[
|
|
61
|
+
"# Generated by po for this run. Do not edit.",
|
|
62
|
+
`# Upstreams kept: ${Object.keys(kept).join(", ") || "(none)"}`,
|
|
63
|
+
`# Upstreams removed by the domain allowlist: ${dropped.join(", ") || "(none)"}`,
|
|
64
|
+
""
|
|
65
|
+
].join("\n")}${stringify({
|
|
66
|
+
...config,
|
|
67
|
+
mcpServers: kept
|
|
68
|
+
})}`;
|
|
69
|
+
}
|
|
70
|
+
async function copyFileIfPresent(source, target) {
|
|
71
|
+
try {
|
|
72
|
+
await fs.promises.copyFile(source, target);
|
|
73
|
+
return true;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error.code === "ENOENT") return false;
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Copies a generated MCP config out of the run directory so another frontend
|
|
81
|
+
* can use it, keeping the proxy's --config pointing at the copy.
|
|
82
|
+
*
|
|
83
|
+
* The run directory is deleted when the harness exits, so emitting for
|
|
84
|
+
* claude.sh or codex.sh has to relocate both files together.
|
|
85
|
+
*/
|
|
86
|
+
async function persistMcpConfig(sourceConfigPath, temporaryDirectory, targetDirectory) {
|
|
87
|
+
await fs.promises.mkdir(targetDirectory, { recursive: true });
|
|
88
|
+
const config = JSON.parse(await fs.promises.readFile(sourceConfigPath, "utf8"));
|
|
89
|
+
const servers = config.mcpServers ?? {};
|
|
90
|
+
const proxy = servers[PROXY_SERVER_NAME];
|
|
91
|
+
const generatedProxyConfig = path.join(temporaryDirectory, PROXY_CONFIG_FILE);
|
|
92
|
+
if (proxy) {
|
|
93
|
+
const args = [...proxy.args ?? []];
|
|
94
|
+
const configIndex = args.indexOf(CONFIG_ARGUMENT);
|
|
95
|
+
if (configIndex !== -1) {
|
|
96
|
+
const targetProxyConfig = path.join(targetDirectory, PROXY_CONFIG_FILE);
|
|
97
|
+
if (await copyFileIfPresent(generatedProxyConfig, targetProxyConfig)) {
|
|
98
|
+
args[configIndex + 1] = targetProxyConfig;
|
|
99
|
+
servers[PROXY_SERVER_NAME] = {
|
|
100
|
+
...proxy,
|
|
101
|
+
args
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const targetPath = path.join(targetDirectory, "mcp.json");
|
|
107
|
+
await fs.promises.writeFile(targetPath, `${JSON.stringify({
|
|
108
|
+
...config,
|
|
109
|
+
mcpServers: servers
|
|
110
|
+
}, null, 2)}\n`, { mode: 384 });
|
|
111
|
+
return targetPath;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Applies the allowlist to a merged MCP config, writing a filtered proxy config
|
|
115
|
+
* into the run directory and repointing the proxy at it when needed.
|
|
116
|
+
*/
|
|
117
|
+
async function applyMcpAllowlist(config, allowlist, repoRoot, temporaryDirectory) {
|
|
118
|
+
if (!allowlist) return config;
|
|
119
|
+
const filtered = filterMcpServers(config, allowlist.servers);
|
|
120
|
+
const servers = filtered.mcpServers ?? {};
|
|
121
|
+
const proxy = servers[PROXY_SERVER_NAME];
|
|
122
|
+
if (!proxy || !allowlist.proxy || allowlist.proxy.length === 0) return filtered;
|
|
123
|
+
const args = [...proxy.args ?? []];
|
|
124
|
+
const configIndex = args.indexOf(CONFIG_ARGUMENT);
|
|
125
|
+
if (configIndex === -1 || !args[configIndex + 1]) return filtered;
|
|
126
|
+
const sourcePath = path.resolve(repoRoot, args[configIndex + 1]);
|
|
127
|
+
let sourceConfig;
|
|
128
|
+
try {
|
|
129
|
+
sourceConfig = await fs.promises.readFile(sourcePath, "utf8");
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (error.code === "ENOENT") return filtered;
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
const targetPath = path.join(temporaryDirectory, PROXY_CONFIG_FILE);
|
|
135
|
+
await fs.promises.writeFile(targetPath, filterProxyConfig(sourceConfig, allowlist.proxy), { mode: 384 });
|
|
136
|
+
args[configIndex + 1] = targetPath;
|
|
137
|
+
return {
|
|
138
|
+
...filtered,
|
|
139
|
+
mcpServers: {
|
|
140
|
+
...servers,
|
|
141
|
+
[PROXY_SERVER_NAME]: {
|
|
142
|
+
...proxy,
|
|
143
|
+
args
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
//#endregion
|
|
149
|
+
export { persistMcpConfig as a, filterProxyConfig as i, applyMcpAllowlist as n, resolveMcpAllowlist as o, filterMcpServers as r, PROXY_SERVER_NAME as t };
|
|
150
|
+
|
|
151
|
+
//# sourceMappingURL=mcpFilter-Cf-czy8O.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcpFilter-Cf-czy8O.mjs","names":["parseYaml","stringifyYaml"],"sources":["../src/services/mcpFilter/index.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\nimport { type DomainMcpAllowlist, expandDomainNames, loadDomains } from '@agimon-ai/po-config/domains';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\n\nimport type { JsonObject } from '../../types/resources';\n\n/** Server name in .mcp.json whose upstreams are declared in mcp-config.yaml. */\nexport const PROXY_SERVER_NAME = 'agiflow-proxy';\nconst PROXY_CONFIG_FILE = 'mcp-config.yaml';\nconst CONFIG_ARGUMENT = '--config';\n\n/**\n * Unions the mcp allowlists of the selected domains.\n *\n * Returns undefined when any selected domain omits `mcp`, which means \"give me\n * everything\". Filtering only kicks in once every selected domain has opted in,\n * so a domain that has not been migrated yet cannot silently lose its tools.\n */\nexport function resolveMcpAllowlist(repoRoot: string, domainNames: string[]): DomainMcpAllowlist | undefined {\n const manifest = loadDomains(repoRoot);\n const expanded = expandDomainNames(manifest, domainNames);\n if (expanded.length === 0) return undefined;\n\n const servers = new Set<string>();\n const proxy = new Set<string>();\n for (const name of expanded) {\n const allowlist = manifest.domains[name]?.mcp;\n if (!allowlist) return undefined;\n for (const server of allowlist.servers ?? []) servers.add(server);\n for (const upstream of allowlist.proxy ?? []) proxy.add(upstream);\n }\n return { servers: [...servers], proxy: [...proxy] };\n}\n\n/** Drops servers that are not on the allowlist. An empty list means keep all. */\nexport function filterMcpServers(config: JsonObject, allowed?: string[]): JsonObject {\n if (!allowed || allowed.length === 0) return config;\n const servers = (config.mcpServers ?? {}) as Record<string, unknown>;\n const kept: Record<string, unknown> = {};\n for (const [name, definition] of Object.entries(servers)) {\n if (allowed.includes(name)) kept[name] = definition;\n }\n return { ...config, mcpServers: kept };\n}\n\n/**\n * Rewrites the proxy's upstream list, keeping only allowed servers.\n *\n * Entries are removed rather than marked disabled so a filtered upstream cannot\n * be reached even if the proxy ignores the disabled flag. Comments in the\n * source file are not preserved; the result is a per-run temporary file.\n */\nexport function filterProxyConfig(yamlText: string, allowed?: string[]): string {\n if (!allowed || allowed.length === 0) return yamlText;\n const config = (parseYaml(yamlText) ?? {}) as JsonObject;\n const servers = (config.mcpServers ?? {}) as Record<string, unknown>;\n const kept: Record<string, unknown> = {};\n const dropped: string[] = [];\n for (const [name, definition] of Object.entries(servers)) {\n if (allowed.includes(name)) kept[name] = definition;\n else dropped.push(name);\n }\n const header = [\n '# Generated by po for this run. Do not edit.',\n `# Upstreams kept: ${Object.keys(kept).join(', ') || '(none)'}`,\n `# Upstreams removed by the domain allowlist: ${dropped.join(', ') || '(none)'}`,\n '',\n ].join('\\n');\n return `${header}${stringifyYaml({ ...config, mcpServers: kept })}`;\n}\n\nasync function copyFileIfPresent(source: string, target: string): Promise<boolean> {\n try {\n await fs.promises.copyFile(source, target);\n return true;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false;\n throw error;\n }\n}\n\n/**\n * Copies a generated MCP config out of the run directory so another frontend\n * can use it, keeping the proxy's --config pointing at the copy.\n *\n * The run directory is deleted when the harness exits, so emitting for\n * claude.sh or codex.sh has to relocate both files together.\n */\nexport async function persistMcpConfig(\n sourceConfigPath: string,\n temporaryDirectory: string,\n targetDirectory: string,\n): Promise<string> {\n await fs.promises.mkdir(targetDirectory, { recursive: true });\n const config = JSON.parse(await fs.promises.readFile(sourceConfigPath, 'utf8')) as JsonObject;\n const servers = (config.mcpServers ?? {}) as Record<string, JsonObject>;\n const proxy = servers[PROXY_SERVER_NAME];\n const generatedProxyConfig = path.join(temporaryDirectory, PROXY_CONFIG_FILE);\n\n if (proxy) {\n const args = [...((proxy.args ?? []) as string[])];\n const configIndex = args.indexOf(CONFIG_ARGUMENT);\n if (configIndex !== -1) {\n const targetProxyConfig = path.join(targetDirectory, PROXY_CONFIG_FILE);\n if (await copyFileIfPresent(generatedProxyConfig, targetProxyConfig)) {\n args[configIndex + 1] = targetProxyConfig;\n servers[PROXY_SERVER_NAME] = { ...proxy, args };\n }\n }\n }\n\n const targetPath = path.join(targetDirectory, 'mcp.json');\n await fs.promises.writeFile(targetPath, `${JSON.stringify({ ...config, mcpServers: servers }, null, 2)}\\n`, {\n mode: 0o600,\n });\n return targetPath;\n}\n\n/**\n * Applies the allowlist to a merged MCP config, writing a filtered proxy config\n * into the run directory and repointing the proxy at it when needed.\n */\nexport async function applyMcpAllowlist(\n config: JsonObject,\n allowlist: DomainMcpAllowlist | undefined,\n repoRoot: string,\n temporaryDirectory: string,\n): Promise<JsonObject> {\n if (!allowlist) return config;\n const filtered = filterMcpServers(config, allowlist.servers);\n const servers = (filtered.mcpServers ?? {}) as Record<string, JsonObject>;\n const proxy = servers[PROXY_SERVER_NAME];\n if (!proxy || !allowlist.proxy || allowlist.proxy.length === 0) return filtered;\n\n const args = [...((proxy.args ?? []) as string[])];\n const configIndex = args.indexOf(CONFIG_ARGUMENT);\n if (configIndex === -1 || !args[configIndex + 1]) return filtered;\n\n const sourcePath = path.resolve(repoRoot, args[configIndex + 1]);\n let sourceConfig: string;\n try {\n sourceConfig = await fs.promises.readFile(sourcePath, 'utf8');\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return filtered;\n throw error;\n }\n const targetPath = path.join(temporaryDirectory, PROXY_CONFIG_FILE);\n await fs.promises.writeFile(targetPath, filterProxyConfig(sourceConfig, allowlist.proxy), { mode: 0o600 });\n args[configIndex + 1] = targetPath;\n return { ...filtered, mcpServers: { ...servers, [PROXY_SERVER_NAME]: { ...proxy, args } } };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
const require_rolldown_runtime = require("./rolldown-runtime-VH7oDXx4.cjs");
|
|
2
|
+
let node_fs = require("node:fs");
|
|
3
|
+
node_fs = require_rolldown_runtime.__toESM(node_fs, 1);
|
|
4
|
+
let node_path = require("node:path");
|
|
5
|
+
node_path = require_rolldown_runtime.__toESM(node_path, 1);
|
|
6
|
+
let _agimon_ai_po_config_domains = require("@agimon-ai/po-config/domains");
|
|
7
|
+
let node_child_process = require("node:child_process");
|
|
8
|
+
let _agimon_ai_po_core_piSettings = require("@agimon-ai/po-core/piSettings");
|
|
9
|
+
//#region src/services/pluginMaterializer/index.ts
|
|
10
|
+
const AGENT_PLUGIN_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json";
|
|
11
|
+
const INSTALL_MARKER = ".po-plugin.json";
|
|
12
|
+
const INSTALL_MARKER_VERSION = 1;
|
|
13
|
+
const OUTPUT_LIMIT = 65536;
|
|
14
|
+
/** The staging project that declares the one npm plugin being installed. */
|
|
15
|
+
const STAGING_PACKAGE_NAME = "po-plugin-staging";
|
|
16
|
+
const LATEST_TAG = "latest";
|
|
17
|
+
function appendOutput(current, chunk) {
|
|
18
|
+
const next = current + chunk.toString("utf8");
|
|
19
|
+
return next.length <= OUTPUT_LIMIT ? next : next.slice(-65536);
|
|
20
|
+
}
|
|
21
|
+
function runCommand(command, args, options = {}) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const child = (0, node_child_process.spawn)(command, args, {
|
|
24
|
+
cwd: options.cwd,
|
|
25
|
+
env: options.environment ?? process.env,
|
|
26
|
+
stdio: [
|
|
27
|
+
"ignore",
|
|
28
|
+
"pipe",
|
|
29
|
+
"pipe"
|
|
30
|
+
]
|
|
31
|
+
});
|
|
32
|
+
let stdout = "";
|
|
33
|
+
let stderr = "";
|
|
34
|
+
child.stdout.on("data", (chunk) => {
|
|
35
|
+
stdout = appendOutput(stdout, chunk);
|
|
36
|
+
});
|
|
37
|
+
child.stderr.on("data", (chunk) => {
|
|
38
|
+
stderr = appendOutput(stderr, chunk);
|
|
39
|
+
});
|
|
40
|
+
child.once("error", (error) => {
|
|
41
|
+
reject(new Error(`Failed to run ${command}: ${error.message}`, { cause: error }));
|
|
42
|
+
});
|
|
43
|
+
child.once("close", (code, signal) => {
|
|
44
|
+
if (code === 0) {
|
|
45
|
+
resolve(stdout.trim());
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const outcome = signal ? `signal ${signal}` : `exit code ${code ?? "unknown"}`;
|
|
49
|
+
const details = stderr.trim() || stdout.trim();
|
|
50
|
+
reject(/* @__PURE__ */ new Error(`${command} ${args.join(" ")} failed with ${outcome}${details ? `\n${details}` : ""}`));
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function assertContained(root, candidate, label) {
|
|
55
|
+
const relative = node_path.default.relative(root, candidate);
|
|
56
|
+
if (relative === "" || !relative.startsWith("..") && !node_path.default.isAbsolute(relative)) return;
|
|
57
|
+
throw new Error(`${label} resolves outside ${root}`);
|
|
58
|
+
}
|
|
59
|
+
async function materializeGitSource(source, workspace) {
|
|
60
|
+
const checkout = node_path.default.join(workspace, "checkout");
|
|
61
|
+
const pluginRoot = source.path ? node_path.default.resolve(checkout, source.path) : checkout;
|
|
62
|
+
if (source.path) assertContained(checkout, pluginRoot, "Git plugin path");
|
|
63
|
+
const cloneArgs = ["clone", "--filter=blob:none"];
|
|
64
|
+
if (source.path) cloneArgs.push("--sparse", "--no-checkout");
|
|
65
|
+
cloneArgs.push(source.url, checkout);
|
|
66
|
+
const environment = {
|
|
67
|
+
...process.env,
|
|
68
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
69
|
+
};
|
|
70
|
+
await runCommand("git", cloneArgs, { environment });
|
|
71
|
+
if (source.path) await runCommand("git", [
|
|
72
|
+
"sparse-checkout",
|
|
73
|
+
"set",
|
|
74
|
+
"--no-cone",
|
|
75
|
+
"--",
|
|
76
|
+
source.path
|
|
77
|
+
], {
|
|
78
|
+
cwd: checkout,
|
|
79
|
+
environment
|
|
80
|
+
});
|
|
81
|
+
if (source.sha) {
|
|
82
|
+
await runCommand("git", ["checkout", source.sha], {
|
|
83
|
+
cwd: checkout,
|
|
84
|
+
environment
|
|
85
|
+
});
|
|
86
|
+
const checkedOutSha = await runCommand("git", ["rev-parse", "HEAD"], {
|
|
87
|
+
cwd: checkout,
|
|
88
|
+
environment
|
|
89
|
+
});
|
|
90
|
+
if (checkedOutSha.toLowerCase() !== source.sha.toLowerCase()) throw new Error(`Checked out Git SHA ${checkedOutSha} does not match requested SHA ${source.sha}`);
|
|
91
|
+
} else if (source.ref) await runCommand("git", ["checkout", source.ref], {
|
|
92
|
+
cwd: checkout,
|
|
93
|
+
environment
|
|
94
|
+
});
|
|
95
|
+
else if (source.path) await runCommand("git", ["checkout"], {
|
|
96
|
+
cwd: checkout,
|
|
97
|
+
environment
|
|
98
|
+
});
|
|
99
|
+
return pluginRoot;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Installs an npm plugin into the staging workspace with upm: a one-dependency
|
|
103
|
+
* project, never a spawned package manager. upm runs no lifecycle scripts, and
|
|
104
|
+
* its store is Po's shared `<po home>/store`.
|
|
105
|
+
*/
|
|
106
|
+
async function materializeNpmSource(source, workspace) {
|
|
107
|
+
const prefix = node_path.default.join(workspace, "npm");
|
|
108
|
+
await node_fs.default.promises.mkdir(prefix, { recursive: true });
|
|
109
|
+
await node_fs.default.promises.writeFile(node_path.default.join(prefix, "package.json"), `${JSON.stringify({
|
|
110
|
+
name: STAGING_PACKAGE_NAME,
|
|
111
|
+
private: true,
|
|
112
|
+
dependencies: { [source.package]: source.version ?? LATEST_TAG }
|
|
113
|
+
}, null, 2)}\n`);
|
|
114
|
+
const { installRuntimePackages } = await import("@agimon-ai/po-core/runtimePackages");
|
|
115
|
+
await installRuntimePackages({
|
|
116
|
+
dir: prefix,
|
|
117
|
+
poHome: (0, _agimon_ai_po_core_piSettings.poHomeDirectory)(),
|
|
118
|
+
...source.registry ? { registry: source.registry } : {}
|
|
119
|
+
});
|
|
120
|
+
const pluginRoot = node_path.default.resolve(prefix, "node_modules", ...source.package.split("/"));
|
|
121
|
+
assertContained(node_path.default.join(prefix, "node_modules"), pluginRoot, "npm plugin package");
|
|
122
|
+
return node_fs.default.promises.realpath(pluginRoot);
|
|
123
|
+
}
|
|
124
|
+
const DEFAULT_MATERIALIZERS = {
|
|
125
|
+
git: materializeGitSource,
|
|
126
|
+
npm: materializeNpmSource
|
|
127
|
+
};
|
|
128
|
+
async function pathType(target) {
|
|
129
|
+
try {
|
|
130
|
+
return (await node_fs.default.promises.lstat(target)).isDirectory() ? "directory" : "other";
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (error.code === "ENOENT") return "missing";
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function optionalManifestString(manifest, key) {
|
|
137
|
+
const value = manifest[key];
|
|
138
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
139
|
+
}
|
|
140
|
+
function validatePluginManifest(pluginRoot, manifestPath) {
|
|
141
|
+
try {
|
|
142
|
+
const manifest = JSON.parse(node_fs.default.readFileSync(manifestPath, "utf8"));
|
|
143
|
+
if (typeof manifest !== "object" || manifest === null || Array.isArray(manifest)) throw new Error("manifest must contain a JSON object");
|
|
144
|
+
const record = manifest;
|
|
145
|
+
const agentPluginSchema = manifestPath === node_path.default.join(pluginRoot, "plugin.json") && record.$schema === AGENT_PLUGIN_SCHEMA ? AGENT_PLUGIN_SCHEMA : void 0;
|
|
146
|
+
const name = optionalManifestString(record, "name");
|
|
147
|
+
const version = optionalManifestString(record, "version");
|
|
148
|
+
const description = optionalManifestString(record, "description");
|
|
149
|
+
return {
|
|
150
|
+
skillDiscovery: agentPluginSchema ? "direct-children" : "recursive",
|
|
151
|
+
manifest: {
|
|
152
|
+
path: manifestPath,
|
|
153
|
+
...name ? { name } : {},
|
|
154
|
+
...version ? { version } : {},
|
|
155
|
+
...description ? { description } : {},
|
|
156
|
+
...agentPluginSchema ? { agentPluginSchema } : {}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
} catch (error) {
|
|
160
|
+
throw new Error(`Invalid plugin manifest ${manifestPath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async function inspectInstallation(entry) {
|
|
164
|
+
const type = await pathType(entry.directory);
|
|
165
|
+
if (type === "missing") return false;
|
|
166
|
+
if (type !== "directory") throw new Error(`Remote plugin cache path is not a directory: ${entry.directory}`);
|
|
167
|
+
if (!entry.source || !(0, _agimon_ai_po_config_domains.isRemotePluginSource)(entry.source)) throw new Error(`Remote plugin entry is missing its source: ${entry.name ?? entry.directory}`);
|
|
168
|
+
let marker;
|
|
169
|
+
try {
|
|
170
|
+
marker = JSON.parse(await node_fs.default.promises.readFile(node_path.default.join(entry.directory, INSTALL_MARKER), "utf8"));
|
|
171
|
+
} catch (error) {
|
|
172
|
+
throw new Error(`Remote plugin cache is not managed by Po: ${entry.directory}`, { cause: error });
|
|
173
|
+
}
|
|
174
|
+
if (marker.version !== INSTALL_MARKER_VERSION || JSON.stringify(marker.source) !== JSON.stringify(entry.source)) throw new Error(`Remote plugin cache metadata does not match ${entry.name ?? entry.directory}: ${entry.directory}`);
|
|
175
|
+
const manifestPath = (0, _agimon_ai_po_config_domains.findPluginManifestPath)(entry.directory);
|
|
176
|
+
if (!manifestPath) throw new Error(`Remote plugin cache is missing a supported plugin manifest: ${entry.directory}`);
|
|
177
|
+
return validatePluginManifest(entry.directory, manifestPath);
|
|
178
|
+
}
|
|
179
|
+
async function copyPluginSource(sourceRoot, destination) {
|
|
180
|
+
if (await pathType(sourceRoot) !== "directory") throw new Error(`Materialized plugin source is not a directory: ${sourceRoot}`);
|
|
181
|
+
await node_fs.default.promises.cp(sourceRoot, destination, {
|
|
182
|
+
recursive: true,
|
|
183
|
+
filter: (candidate) => node_path.default.basename(candidate) !== ".git"
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
function isRenameRace(error) {
|
|
187
|
+
const code = error.code;
|
|
188
|
+
return code === "EEXIST" || code === "ENOTEMPTY";
|
|
189
|
+
}
|
|
190
|
+
async function installRemotePlugin(entry, materializers) {
|
|
191
|
+
if (!entry.source || !(0, _agimon_ai_po_config_domains.isRemotePluginSource)(entry.source)) return entry;
|
|
192
|
+
const existingManifest = await inspectInstallation(entry);
|
|
193
|
+
if (existingManifest) return {
|
|
194
|
+
...entry,
|
|
195
|
+
...existingManifest
|
|
196
|
+
};
|
|
197
|
+
const parent = node_path.default.dirname(entry.directory);
|
|
198
|
+
await node_fs.default.promises.mkdir(parent, {
|
|
199
|
+
recursive: true,
|
|
200
|
+
mode: 448
|
|
201
|
+
});
|
|
202
|
+
if (await pathType(parent) !== "directory") throw new Error(`Remote plugin cache root is not a directory: ${parent}`);
|
|
203
|
+
await node_fs.default.promises.chmod(parent, 448);
|
|
204
|
+
const workspace = await node_fs.default.promises.mkdtemp(node_path.default.join(parent, ".plugin-staging-"));
|
|
205
|
+
const installation = node_path.default.join(workspace, "installation");
|
|
206
|
+
try {
|
|
207
|
+
await copyPluginSource(entry.source.type === "git" ? await materializers.git(entry.source, workspace) : await materializers.npm(entry.source, workspace), installation);
|
|
208
|
+
const manifestPath = (0, _agimon_ai_po_config_domains.findPluginManifestPath)(installation);
|
|
209
|
+
if (!manifestPath) throw new Error(`Remote plugin ${entry.name ?? entry.directory} does not contain a supported plugin manifest`);
|
|
210
|
+
validatePluginManifest(installation, manifestPath);
|
|
211
|
+
const markerPath = node_path.default.join(installation, INSTALL_MARKER);
|
|
212
|
+
if (await pathType(markerPath) !== "missing") throw new Error(`Remote plugin contains reserved cache metadata path: ${INSTALL_MARKER}`);
|
|
213
|
+
const marker = {
|
|
214
|
+
version: INSTALL_MARKER_VERSION,
|
|
215
|
+
source: entry.source
|
|
216
|
+
};
|
|
217
|
+
await node_fs.default.promises.writeFile(markerPath, `${JSON.stringify(marker, null, 2)}\n`, {
|
|
218
|
+
flag: "wx",
|
|
219
|
+
mode: 384
|
|
220
|
+
});
|
|
221
|
+
try {
|
|
222
|
+
await node_fs.default.promises.rename(installation, entry.directory);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
if (!isRenameRace(error)) throw error;
|
|
225
|
+
await node_fs.default.promises.rm(installation, {
|
|
226
|
+
recursive: true,
|
|
227
|
+
force: true
|
|
228
|
+
});
|
|
229
|
+
const racedManifest = await inspectInstallation(entry);
|
|
230
|
+
if (!racedManifest) throw new Error(`Remote plugin installation race left no cache: ${entry.directory}`);
|
|
231
|
+
return {
|
|
232
|
+
...entry,
|
|
233
|
+
...racedManifest
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
const installedManifest = await inspectInstallation(entry);
|
|
237
|
+
if (!installedManifest) throw new Error(`Remote plugin installation left no cache: ${entry.directory}`);
|
|
238
|
+
return {
|
|
239
|
+
...entry,
|
|
240
|
+
...installedManifest
|
|
241
|
+
};
|
|
242
|
+
} catch (error) {
|
|
243
|
+
throw new Error(`Failed to install remote plugin ${entry.name ?? entry.directory}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
244
|
+
} finally {
|
|
245
|
+
await node_fs.default.promises.rm(workspace, {
|
|
246
|
+
recursive: true,
|
|
247
|
+
force: true
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/** Downloads selected remote plugins once into their content-addressed cache directories. */
|
|
252
|
+
async function materializePluginEntries(entries, options = {}) {
|
|
253
|
+
const materializers = {
|
|
254
|
+
git: options.materializers?.git ?? DEFAULT_MATERIALIZERS.git,
|
|
255
|
+
npm: options.materializers?.npm ?? DEFAULT_MATERIALIZERS.npm
|
|
256
|
+
};
|
|
257
|
+
return Promise.all(entries.map((entry) => installRemotePlugin(entry, materializers)));
|
|
258
|
+
}
|
|
259
|
+
//#endregion
|
|
260
|
+
Object.defineProperty(exports, "materializePluginEntries", {
|
|
261
|
+
enumerable: true,
|
|
262
|
+
get: function() {
|
|
263
|
+
return materializePluginEntries;
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
//# sourceMappingURL=pluginMaterializer-C8IZ1PSY.cjs.map
|