Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s
265 lines
9.1 KiB
JavaScript
265 lines
9.1 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
import {
|
|
PLUGIN_ROOT,
|
|
REPO_ROOT,
|
|
PUBLIC_DOCS_MCP_SERVER_NAME,
|
|
PUBLIC_DOCS_MCP_URL,
|
|
VALID_CAPABILITIES,
|
|
VALID_CATEGORIES,
|
|
SHORT_DESCRIPTION_MAX,
|
|
readText,
|
|
listFiles,
|
|
isAllowedDocumentationHost,
|
|
createJsonReaders,
|
|
} from './lib.js';
|
|
|
|
const assertJsonMetadata = (fail) => {
|
|
const { readJson, readOptionalJson } = createJsonReaders(fail);
|
|
const packageJson = readJson('packages/twenty-agent-skills/package.json');
|
|
const pluginJson = readJson('packages/twenty-agent-skills/.codex-plugin/plugin.json');
|
|
const mcpJson = readJson('packages/twenty-agent-skills/.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-agent-skills/dist') {
|
|
fail('marketplace twenty source path must be ./packages/twenty-agent-skills/dist');
|
|
}
|
|
|
|
if (fs.existsSync(path.join(REPO_ROOT, 'plugins', 'twenty'))) {
|
|
fail('legacy plugins/twenty path must not exist; use packages/twenty-agent-skills/dist');
|
|
}
|
|
};
|
|
|
|
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-agent-skills/.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-agent-skills/.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-agent-skills/templates/marketplace.example.json';
|
|
const template = readOptionalJson(templatePath);
|
|
const pluginJson = readJson('packages/twenty-agent-skills/.codex-plugin/plugin.json');
|
|
|
|
if (!template) {
|
|
fail(`marketplace template is missing at ${templatePath}`);
|
|
return;
|
|
}
|
|
|
|
if (typeof template.name !== 'string' || template.name.trim().length === 0) {
|
|
fail(`${templatePath} name must be a non-empty string`);
|
|
}
|
|
|
|
if (typeof template.interface?.displayName !== 'string' || template.interface.displayName.trim().length === 0) {
|
|
fail(`${templatePath} interface.displayName must be a non-empty string`);
|
|
}
|
|
|
|
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.source?.source !== 'local') {
|
|
fail(`${templatePath} twenty.source.source must be local`);
|
|
}
|
|
|
|
if (twentyEntry.source?.path !== './packages/twenty-agent-skills/dist') {
|
|
fail(`${templatePath} twenty.source.path must be ./packages/twenty-agent-skills/dist`);
|
|
}
|
|
|
|
if (!['NOT_AVAILABLE', 'AVAILABLE', 'INSTALLED_BY_DEFAULT'].includes(twentyEntry.policy?.installation)) {
|
|
fail(`${templatePath} twenty.policy.installation must be NOT_AVAILABLE, AVAILABLE, or INSTALLED_BY_DEFAULT`);
|
|
}
|
|
|
|
if (!['ON_INSTALL', 'ON_USE'].includes(twentyEntry.policy?.authentication)) {
|
|
fail(`${templatePath} twenty.policy.authentication must be ON_INSTALL or ON_USE`);
|
|
}
|
|
|
|
if (twentyEntry.category !== pluginJson?.interface?.category) {
|
|
fail(`${templatePath} twenty.category must match plugin.json interface.category`);
|
|
}
|
|
};
|
|
|
|
export {
|
|
assertJsonMetadata,
|
|
assertNoBundledMcpConfig,
|
|
assertInterfaceFields,
|
|
assertMarketplaceTemplate,
|
|
};
|