Bonapara/twenty codex plugin (#20857)
@martmull v2.0 ;) --------- Co-authored-by: martmull <martmull@hotmail.fr> Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
This commit is contained in:
co-authored by
martmull
bosiraphael
parent
cb744b2eeb
commit
1642be86f5
@@ -0,0 +1,256 @@
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const {
|
||||
PLUGIN_ROOT,
|
||||
REPO_ROOT,
|
||||
PUBLIC_DOCS_MCP_SERVER_NAME,
|
||||
PUBLIC_DOCS_MCP_URL,
|
||||
VALID_CAPABILITIES,
|
||||
VALID_CATEGORIES,
|
||||
SHORT_DESCRIPTION_MAX,
|
||||
readText,
|
||||
listFiles,
|
||||
isAllowedDocumentationHost,
|
||||
createJsonReaders,
|
||||
} = require('./lib');
|
||||
|
||||
const assertJsonMetadata = (fail) => {
|
||||
const { readJson, readOptionalJson } = createJsonReaders(fail);
|
||||
const packageJson = readJson('packages/twenty-codex-plugin/package.json');
|
||||
const pluginJson = readJson('packages/twenty-codex-plugin/.codex-plugin/plugin.json');
|
||||
const mcpJson = readJson('packages/twenty-codex-plugin/.mcp.json');
|
||||
const marketplaceJson = readOptionalJson('.agents/plugins/marketplace.json');
|
||||
|
||||
if (packageJson?.version !== pluginJson?.version) {
|
||||
fail('package.json version must match .codex-plugin/plugin.json version');
|
||||
}
|
||||
|
||||
if (!packageJson?.files?.includes('.mcp.json')) {
|
||||
fail('package.json files must include .mcp.json for the public docs MCP server');
|
||||
}
|
||||
|
||||
if (!packageJson?.files?.includes('references')) {
|
||||
fail('package.json files must include references for shared plugin guidance');
|
||||
}
|
||||
|
||||
if (pluginJson?.mcpServers !== './.mcp.json') {
|
||||
fail('.codex-plugin/plugin.json must declare mcpServers as ./.mcp.json');
|
||||
}
|
||||
|
||||
const servers = mcpJson?.mcpServers;
|
||||
|
||||
if (!servers || typeof servers !== 'object' || Array.isArray(servers)) {
|
||||
fail('.mcp.json must declare an mcpServers object');
|
||||
} else {
|
||||
const serverNames = Object.keys(servers);
|
||||
|
||||
if (serverNames.length !== 1 || serverNames[0] !== PUBLIC_DOCS_MCP_SERVER_NAME) {
|
||||
fail(`.mcp.json must only declare ${PUBLIC_DOCS_MCP_SERVER_NAME}`);
|
||||
}
|
||||
|
||||
const docsServer = servers[PUBLIC_DOCS_MCP_SERVER_NAME];
|
||||
|
||||
if (!docsServer || typeof docsServer !== 'object' || Array.isArray(docsServer)) {
|
||||
fail(`${PUBLIC_DOCS_MCP_SERVER_NAME} must be an object`);
|
||||
} else {
|
||||
const docsServerKeys = Object.keys(docsServer);
|
||||
|
||||
if (docsServerKeys.length !== 1 || docsServerKeys[0] !== 'url') {
|
||||
fail(`${PUBLIC_DOCS_MCP_SERVER_NAME} must only declare a url`);
|
||||
}
|
||||
|
||||
if (docsServer.url !== PUBLIC_DOCS_MCP_URL) {
|
||||
fail(`${PUBLIC_DOCS_MCP_SERVER_NAME} url must be ${PUBLIC_DOCS_MCP_URL}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const marketplaceEntry = marketplaceJson?.plugins?.find((entry) => entry.name === 'twenty');
|
||||
|
||||
if (marketplaceJson && !marketplaceEntry) {
|
||||
fail('.agents/plugins/marketplace.json includes plugins but not the twenty plugin entry');
|
||||
} else if (marketplaceEntry && marketplaceEntry.source?.path !== './packages/twenty-codex-plugin') {
|
||||
fail('marketplace twenty source path must be ./packages/twenty-codex-plugin');
|
||||
}
|
||||
|
||||
if (fs.existsSync(path.join(REPO_ROOT, 'plugins', 'twenty'))) {
|
||||
fail('legacy plugins/twenty path must not exist; use packages/twenty-codex-plugin directly');
|
||||
}
|
||||
};
|
||||
|
||||
const assertNoBundledMcpConfig = (fail) => {
|
||||
const gitignorePath = path.join(PLUGIN_ROOT, '.gitignore');
|
||||
|
||||
if (fs.existsSync(gitignorePath) && readText(gitignorePath).split(/\r?\n/).includes('.mcp.json')) {
|
||||
fail('packages/twenty-codex-plugin/.gitignore must not ignore the public .mcp.json');
|
||||
}
|
||||
|
||||
for (const filePath of listFiles(PLUGIN_ROOT)) {
|
||||
const relativePath = path.relative(PLUGIN_ROOT, filePath);
|
||||
|
||||
if (relativePath.split(path.sep).includes('__tests__')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (path.basename(filePath) === '.mcp.json' && relativePath !== '.mcp.json') {
|
||||
fail(`workspace-specific MCP config must not be shipped: ${relativePath}`);
|
||||
}
|
||||
|
||||
if (path.basename(filePath) === '.app.json') {
|
||||
fail(`app declarations must not be shipped unless intentionally allowed in validation: ${relativePath}`);
|
||||
}
|
||||
|
||||
const contents = readText(filePath);
|
||||
|
||||
const urls = contents.matchAll(/https?:\/\/[^\s"`'<>)]*/g);
|
||||
|
||||
for (const [rawUrl] of urls) {
|
||||
let parsedUrl;
|
||||
|
||||
if (/[${}*]/.test(rawUrl)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
parsedUrl = new URL(rawUrl);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isAllowedDocumentationHost(parsedUrl.hostname)) {
|
||||
fail(`non-placeholder URL found in ${relativePath}: ${parsedUrl.origin}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (/Bearer\s+(?!YOUR_API_KEY\b)[A-Za-z0-9._-]{20,}/.test(contents)) {
|
||||
fail(`possible bearer token found in ${relativePath}`);
|
||||
}
|
||||
|
||||
if (/sk-[A-Za-z0-9_-]{20,}/.test(contents)) {
|
||||
fail(`possible API key found in ${relativePath}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const assertInterfaceFields = (fail) => {
|
||||
const { readJson } = createJsonReaders(fail);
|
||||
const pluginJson = readJson('packages/twenty-codex-plugin/.codex-plugin/plugin.json');
|
||||
const interfaceMetadata = pluginJson?.interface;
|
||||
|
||||
if (!interfaceMetadata || typeof interfaceMetadata !== 'object' || Array.isArray(interfaceMetadata)) {
|
||||
fail('.codex-plugin/plugin.json must declare an interface object');
|
||||
return;
|
||||
}
|
||||
|
||||
const requiredStringFields = [
|
||||
'displayName',
|
||||
'shortDescription',
|
||||
'longDescription',
|
||||
'developerName',
|
||||
'category',
|
||||
'websiteURL',
|
||||
'privacyPolicyURL',
|
||||
'termsOfServiceURL',
|
||||
'brandColor',
|
||||
'logo',
|
||||
'composerIcon',
|
||||
];
|
||||
|
||||
for (const field of requiredStringFields) {
|
||||
const value = interfaceMetadata[field];
|
||||
|
||||
if (typeof value !== 'string' || value.trim().length === 0) {
|
||||
fail(`.codex-plugin/plugin.json interface.${field} must be a non-empty string`);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof interfaceMetadata.shortDescription === 'string' && interfaceMetadata.shortDescription.length > SHORT_DESCRIPTION_MAX) {
|
||||
fail(`.codex-plugin/plugin.json interface.shortDescription must be ${SHORT_DESCRIPTION_MAX} characters or fewer`);
|
||||
}
|
||||
|
||||
if (typeof interfaceMetadata.brandColor === 'string' && !/^#[0-9a-fA-F]{6}$/.test(interfaceMetadata.brandColor)) {
|
||||
fail('.codex-plugin/plugin.json interface.brandColor must match #RRGGBB hex format');
|
||||
}
|
||||
|
||||
if (typeof interfaceMetadata.category === 'string' && !VALID_CATEGORIES.has(interfaceMetadata.category)) {
|
||||
fail(`.codex-plugin/plugin.json interface.category must be one of: ${[...VALID_CATEGORIES].join(', ')}`);
|
||||
}
|
||||
|
||||
if (!Array.isArray(interfaceMetadata.capabilities) || interfaceMetadata.capabilities.length === 0) {
|
||||
fail('.codex-plugin/plugin.json interface.capabilities must be a non-empty array');
|
||||
} else {
|
||||
for (const capability of interfaceMetadata.capabilities) {
|
||||
if (!VALID_CAPABILITIES.has(capability)) {
|
||||
fail(`.codex-plugin/plugin.json interface.capabilities contains invalid value: ${capability}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Array.isArray(interfaceMetadata.defaultPrompt) || interfaceMetadata.defaultPrompt.length === 0) {
|
||||
fail('.codex-plugin/plugin.json interface.defaultPrompt must be a non-empty array of strings');
|
||||
} else {
|
||||
for (const prompt of interfaceMetadata.defaultPrompt) {
|
||||
if (typeof prompt !== 'string' || prompt.trim().length === 0) {
|
||||
fail('.codex-plugin/plugin.json interface.defaultPrompt entries must be non-empty strings');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Array.isArray(interfaceMetadata.screenshots)) {
|
||||
fail('.codex-plugin/plugin.json interface.screenshots must be an array (use [] if no screenshots yet)');
|
||||
}
|
||||
};
|
||||
|
||||
const assertMarketplaceTemplate = (fail) => {
|
||||
const { readJson, readOptionalJson } = createJsonReaders(fail);
|
||||
const templatePath = 'packages/twenty-codex-plugin/templates/marketplace.example.json';
|
||||
const template = readOptionalJson(templatePath);
|
||||
const pluginJson = readJson('packages/twenty-codex-plugin/.codex-plugin/plugin.json');
|
||||
|
||||
if (!template) {
|
||||
fail(`marketplace template is missing at ${templatePath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = template.plugins;
|
||||
|
||||
if (!Array.isArray(entries) || entries.length === 0) {
|
||||
fail(`${templatePath} must declare a non-empty plugins array`);
|
||||
return;
|
||||
}
|
||||
|
||||
const twentyEntry = entries.find((entry) => entry?.name === 'twenty');
|
||||
|
||||
if (!twentyEntry) {
|
||||
fail(`${templatePath} must include a plugin entry named "twenty"`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (twentyEntry.version !== pluginJson?.version) {
|
||||
fail(`${templatePath} twenty.version must match plugin.json version`);
|
||||
}
|
||||
|
||||
if (twentyEntry.source?.path !== './packages/twenty-codex-plugin') {
|
||||
fail(`${templatePath} twenty.source.path must be ./packages/twenty-codex-plugin`);
|
||||
}
|
||||
|
||||
if (!twentyEntry.policy?.installation) {
|
||||
fail(`${templatePath} twenty.policy.installation is required`);
|
||||
}
|
||||
|
||||
if (!twentyEntry.policy?.authentication) {
|
||||
fail(`${templatePath} twenty.policy.authentication is required`);
|
||||
}
|
||||
|
||||
if (twentyEntry.category !== pluginJson?.interface?.category) {
|
||||
fail(`${templatePath} twenty.category must match plugin.json interface.category`);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
assertJsonMetadata,
|
||||
assertNoBundledMcpConfig,
|
||||
assertInterfaceFields,
|
||||
assertMarketplaceTemplate,
|
||||
};
|
||||
Reference in New Issue
Block a user