1201 lines
47 KiB
TypeScript
1201 lines
47 KiB
TypeScript
import { readFileSync } from 'node:fs';
|
|
import { readFile } from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import {
|
|
isValidSemVer,
|
|
} from './release-descriptor';
|
|
import {
|
|
AGENT_PLUGINS_SCHEMA_URL,
|
|
BUNDLED_CODING_PLUGIN_ADAPTER_IDS,
|
|
BUNDLED_CODING_PLUGIN_PREVIEW_SURFACES,
|
|
BUNDLED_CODING_PLUGIN_SETTINGS_SURFACES,
|
|
CODE_OWNED_PLUGIN_PERMISSION_IDS,
|
|
DATA_SERVICE_CAPABILITY_IDS,
|
|
DATA_SERVICE_OPERATION_DEFINITIONS,
|
|
DATA_SERVICE_PLUGIN_ID,
|
|
DATA_SERVICE_TOOL_NAMES,
|
|
type AgentPluginsRootManifest,
|
|
type CodingPluginDefinition,
|
|
type CodingPluginAcquisitionMode,
|
|
type CodingPluginExecutionMode,
|
|
type CodingPluginPackageProvenance,
|
|
type CodingPluginRuntimeKind,
|
|
type CodingPluginSkillDefinition,
|
|
type CodingPluginToolDefinition,
|
|
type PluginToolMutation,
|
|
} from '../../shared/coding-plugins';
|
|
|
|
/**
|
|
* P0 deliberately has one statically enumerated package root. Keeping this
|
|
* list relative makes it impossible for an environment variable or a project
|
|
* file to add a package to the trusted catalog.
|
|
*/
|
|
export const BUNDLED_CODING_PLUGIN_ROOTS = Object.freeze([
|
|
'data-service',
|
|
] as const);
|
|
|
|
const CAPABILITY_MANIFEST_RELATIVE_PATH = 'com.makelore/capability.json';
|
|
const PACKAGE_MANIFEST_FILE = 'plugin.json';
|
|
const SKILL_ID_PATTERN = /^[a-z][a-z0-9._-]{0,63}$/u;
|
|
const CAPABILITY_ID_PATTERN = /^[a-z][a-z0-9.-]{0,63}$/u;
|
|
const OPERATION_ID_PATTERN = /^[a-z][a-z0-9._-]{0,63}$/u;
|
|
const TOOL_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9._:-]{0,63}$/u;
|
|
const MAX_DISPLAY_TEXT = 256;
|
|
|
|
const UNSUPPORTED_COMPONENT_KEYS = new Set([
|
|
'commands',
|
|
'executables',
|
|
'hooks',
|
|
'mcp',
|
|
'mcpServers',
|
|
'scripts',
|
|
]);
|
|
|
|
const FORBIDDEN_FIELD_PATTERN = /^(?:price|prices|points|balance|owner|owners|token|tokens|url|urls|entrypoint|entryPoint|react)$/iu;
|
|
|
|
const DATA_SERVICE_OPERATION_CAPABILITIES: Readonly<Record<string, string>> = Object.freeze({
|
|
configure: 'data-service.control',
|
|
inspect: 'data-service.control',
|
|
list_projects: 'data-service.control',
|
|
remove_collection: 'data-service.control',
|
|
reset: 'data-service.control',
|
|
remove_project: 'data-service.control',
|
|
get_document: 'data-service.documents',
|
|
list_documents: 'data-service.documents',
|
|
put_document: 'data-service.documents',
|
|
delete_document: 'data-service.documents',
|
|
});
|
|
|
|
const ROOT_KEYS = new Set([
|
|
'$schema',
|
|
'name',
|
|
'version',
|
|
'description',
|
|
'author',
|
|
'extensions',
|
|
...UNSUPPORTED_COMPONENT_KEYS,
|
|
]);
|
|
|
|
const CAPABILITY_KEYS = new Set([
|
|
'schemaVersion',
|
|
'pluginId',
|
|
'contractVersion',
|
|
'scope',
|
|
'adapterId',
|
|
'requiresBackend',
|
|
'display',
|
|
'skills',
|
|
'tools',
|
|
'surfaces',
|
|
...UNSUPPORTED_COMPONENT_KEYS,
|
|
]);
|
|
|
|
const AUTHOR_KEYS = new Set(['name']);
|
|
const EXTENSIONS_KEYS = new Set(['com.makelore']);
|
|
const MAKELore_EXTENSION_KEYS = new Set(['capabilityManifest']);
|
|
const DISPLAY_KEYS = new Set(['name', 'description']);
|
|
const SKILL_KEYS = new Set(['id', 'entry', 'grants']);
|
|
const TOOL_KEYS = new Set([
|
|
'name',
|
|
'label',
|
|
'description',
|
|
'capabilityId',
|
|
'operation',
|
|
'roles',
|
|
'mutation',
|
|
'projectWriteLease',
|
|
'permissions',
|
|
'inputSchema',
|
|
]);
|
|
const SURFACE_KEYS = new Set(['projectSettings', 'previewRuntime']);
|
|
|
|
const V2_CAPABILITY_KEYS = new Set([
|
|
'schemaVersion',
|
|
'pluginId',
|
|
'contractVersion',
|
|
'scope',
|
|
'runtime',
|
|
'skills',
|
|
'tools',
|
|
]);
|
|
const V2_RUNTIME_KEYS = new Set(['kind', 'protocol']);
|
|
const V2_SKILL_KEYS = new Set(['id', 'entry', 'grants']);
|
|
const V2_TOOL_KEYS = new Set([
|
|
'name',
|
|
'label',
|
|
'description',
|
|
'capabilityId',
|
|
'operation',
|
|
'roles',
|
|
'mutation',
|
|
'projectWriteLease',
|
|
'permissions',
|
|
'executionMode',
|
|
'inputSchema',
|
|
'outputSchema',
|
|
]);
|
|
const HOSTED_PERMISSION_PATTERN = /^hosted\.[a-z][a-z0-9._-]{0,127}$/u;
|
|
const V2_PROPERTY_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/u;
|
|
const V2_SCHEMA_TYPES = new Set(['array', 'boolean', 'integer', 'number', 'object', 'string']);
|
|
const V2_SCHEMA_KEYS = new Set([
|
|
'type',
|
|
'additionalProperties',
|
|
'required',
|
|
'properties',
|
|
'items',
|
|
'minLength',
|
|
'maxLength',
|
|
'minimum',
|
|
'maximum',
|
|
'maxItems',
|
|
'const',
|
|
]);
|
|
|
|
export interface CodingPluginManifestParseOptions {
|
|
packageRoot?: string;
|
|
rootManifestPath?: string;
|
|
capabilityManifestPath?: string;
|
|
/** Metadata supplied by the trusted bundle/package-store owner. */
|
|
runtimeKind?: CodingPluginRuntimeKind;
|
|
acquisitionMode?: CodingPluginAcquisitionMode;
|
|
releaseId?: string | null;
|
|
provenance?: CodingPluginPackageProvenance;
|
|
}
|
|
|
|
export type CodingPluginLoadOptions = Pick<
|
|
CodingPluginManifestParseOptions,
|
|
'runtimeKind' | 'acquisitionMode' | 'releaseId' | 'provenance'
|
|
>;
|
|
|
|
type UnknownRecord = Record<string, unknown>;
|
|
|
|
export class CodingPluginManifestError extends Error {
|
|
readonly code = 'plugin_manifest_invalid' as const;
|
|
|
|
constructor(
|
|
readonly filePath: string,
|
|
readonly field: string,
|
|
message: string,
|
|
) {
|
|
super(`Invalid plugin manifest (${filePath}, ${field}): ${message}`);
|
|
this.name = 'CodingPluginManifestError';
|
|
}
|
|
}
|
|
|
|
function isRecord(value: unknown): value is UnknownRecord {
|
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
}
|
|
|
|
function freezeDeep<T>(value: T): T {
|
|
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
for (const child of Object.values(value as UnknownRecord)) freezeDeep(child);
|
|
Object.freeze(value);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function fail(filePath: string, field: string, message: string): never {
|
|
throw new CodingPluginManifestError(filePath, field, message);
|
|
}
|
|
|
|
function assertRecord(
|
|
value: unknown,
|
|
filePath: string,
|
|
field: string,
|
|
): UnknownRecord {
|
|
if (!isRecord(value)) fail(filePath, field, 'expected an object');
|
|
return value;
|
|
}
|
|
|
|
function assertExactKeys(
|
|
value: UnknownRecord,
|
|
allowed: ReadonlySet<string>,
|
|
filePath: string,
|
|
field: string,
|
|
): void {
|
|
for (const key of Object.keys(value)) {
|
|
if (FORBIDDEN_FIELD_PATTERN.test(key)) {
|
|
fail(filePath, `${field}.${key}`, 'field is not allowed in a plugin manifest');
|
|
}
|
|
if (!allowed.has(key)) {
|
|
fail(filePath, `${field}.${key}`, 'field is not part of the exact P0 schema');
|
|
}
|
|
}
|
|
}
|
|
|
|
function unsupportedComponentIsEmpty(value: unknown): boolean {
|
|
if (value === undefined || value === null) return true;
|
|
if (Array.isArray(value)) return value.length === 0;
|
|
if (isRecord(value)) return Object.keys(value).length === 0;
|
|
return false;
|
|
}
|
|
|
|
function rejectUnsupportedComponents(value: UnknownRecord, filePath: string, field: string): void {
|
|
for (const key of UNSUPPORTED_COMPONENT_KEYS) {
|
|
if (key in value && !unsupportedComponentIsEmpty(value[key])) {
|
|
fail(filePath, `${field}.${key}`, 'non-empty component is unsupported in P0');
|
|
}
|
|
}
|
|
}
|
|
|
|
function text(value: unknown, filePath: string, field: string, maximum = MAX_DISPLAY_TEXT): string {
|
|
if (typeof value !== 'string' || value.trim().length === 0 || value.length > maximum) {
|
|
fail(filePath, field, `expected non-empty text of at most ${maximum} characters`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function stableId(
|
|
value: unknown,
|
|
filePath: string,
|
|
field: string,
|
|
pattern: RegExp,
|
|
): string {
|
|
const result = text(value, filePath, field, 64);
|
|
if (!pattern.test(result)) fail(filePath, field, 'identifier has an invalid shape');
|
|
return result;
|
|
}
|
|
|
|
function bool(value: unknown, filePath: string, field: string): boolean {
|
|
if (typeof value !== 'boolean') fail(filePath, field, 'expected a boolean');
|
|
return value;
|
|
}
|
|
|
|
function positiveInteger(value: unknown, filePath: string, field: string): number {
|
|
if (!Number.isSafeInteger(value) || (value as number) < 1) {
|
|
fail(filePath, field, 'expected a positive safe integer');
|
|
}
|
|
return value as number;
|
|
}
|
|
|
|
function uniqueStrings(
|
|
value: unknown,
|
|
filePath: string,
|
|
field: string,
|
|
pattern: RegExp,
|
|
allowEmpty = false,
|
|
): string[] {
|
|
if (!Array.isArray(value)) fail(filePath, field, 'expected an array');
|
|
const result: string[] = [];
|
|
const seen = new Set<string>();
|
|
for (const [index, candidate] of value.entries()) {
|
|
if (typeof candidate !== 'string' || (!allowEmpty && candidate.trim().length === 0)) {
|
|
fail(filePath, `${field}[${index}]`, 'expected a non-empty string');
|
|
}
|
|
if (!pattern.test(candidate)) fail(filePath, `${field}[${index}]`, 'identifier has an invalid shape');
|
|
if (seen.has(candidate)) fail(filePath, `${field}[${index}]`, 'duplicate identifier');
|
|
seen.add(candidate);
|
|
result.push(candidate);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function normalizeCandidatePath(
|
|
packageRoot: string,
|
|
baseDirectory: string,
|
|
candidate: unknown,
|
|
filePath: string,
|
|
field: string,
|
|
): string {
|
|
const value = text(candidate, filePath, field, 512);
|
|
const portable = value.replaceAll('\\', '/');
|
|
if (portable.startsWith('/') || /^[A-Za-z]:\//u.test(portable)
|
|
|| /^[A-Za-z][A-Za-z0-9+.-]*:/u.test(portable)) {
|
|
fail(filePath, field, 'path must be relative to the package root');
|
|
}
|
|
const root = path.resolve(packageRoot);
|
|
const resolved = path.resolve(baseDirectory, value);
|
|
const relative = path.relative(root, resolved);
|
|
if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
fail(filePath, field, 'path escapes the package root');
|
|
}
|
|
return relative.split(path.sep).join('/');
|
|
}
|
|
|
|
function normalizeV2CandidatePath(
|
|
packageRoot: string,
|
|
baseDirectory: string,
|
|
candidate: unknown,
|
|
filePath: string,
|
|
field: string,
|
|
): string {
|
|
if (typeof candidate !== 'string') fail(filePath, field, 'expected a relative path');
|
|
const portable = candidate.replaceAll('\\', '/');
|
|
if (portable !== candidate || portable.split('/').some((segment) => segment === '')) {
|
|
fail(filePath, field, 'path must use canonical relative separators');
|
|
}
|
|
if (portable.split('/').some((segment) => segment === '.')) {
|
|
fail(filePath, field, 'path must not contain dot segments');
|
|
}
|
|
return normalizeCandidatePath(packageRoot, baseDirectory, candidate, filePath, field);
|
|
}
|
|
|
|
function normalizeCapabilityManifestPath(
|
|
packageRoot: string,
|
|
candidate: unknown,
|
|
filePath: string,
|
|
field: string,
|
|
): string {
|
|
if (typeof candidate !== 'string') fail(filePath, field, 'expected a relative path');
|
|
const portable = candidate.replaceAll('\\', '/');
|
|
const segments = portable.split('/');
|
|
if (portable !== candidate || segments.some((segment, index) => (
|
|
segment === '' || segment === '..' || (segment === '.' && index !== 0)
|
|
))) {
|
|
fail(filePath, field, 'path must use the canonical capability manifest location');
|
|
}
|
|
return normalizeCandidatePath(packageRoot, packageRoot, candidate, filePath, field);
|
|
}
|
|
|
|
function validateSchemaNode(value: unknown, filePath: string, field: string, root = false): void {
|
|
const schema = assertRecord(value, filePath, field);
|
|
const allowed = root
|
|
? new Set(['type', 'additionalProperties', 'required', 'properties'])
|
|
: new Set([
|
|
'type',
|
|
'additionalProperties',
|
|
'required',
|
|
'properties',
|
|
'items',
|
|
'pattern',
|
|
'minLength',
|
|
'maxLength',
|
|
'minimum',
|
|
'maximum',
|
|
'maxItems',
|
|
'const',
|
|
]);
|
|
assertExactKeys(schema, allowed, filePath, field);
|
|
if (typeof schema.type !== 'string' || !['array', 'boolean', 'integer', 'object', 'string'].includes(schema.type)) {
|
|
fail(filePath, `${field}.type`, 'unsupported input schema type');
|
|
}
|
|
if ('additionalProperties' in schema && typeof schema.additionalProperties !== 'boolean') {
|
|
fail(filePath, `${field}.additionalProperties`, 'must be a boolean');
|
|
}
|
|
if ('required' in schema) {
|
|
uniqueStrings(schema.required, filePath, `${field}.required`, /^[A-Za-z_][A-Za-z0-9_]*$/u);
|
|
}
|
|
if ('properties' in schema) {
|
|
const properties = assertRecord(schema.properties, filePath, `${field}.properties`);
|
|
for (const [name, propertySchema] of Object.entries(properties)) {
|
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name)) {
|
|
fail(filePath, `${field}.properties.${name}`, 'input property name is invalid');
|
|
}
|
|
validateSchemaNode(propertySchema, filePath, `${field}.properties.${name}`);
|
|
}
|
|
if ('required' in schema) {
|
|
for (const required of schema.required as string[]) {
|
|
if (!(required in properties)) {
|
|
fail(filePath, `${field}.required`, `required property is not declared: ${required}`);
|
|
}
|
|
}
|
|
}
|
|
} else if ('required' in schema && (schema.required as unknown[]).length > 0) {
|
|
fail(filePath, `${field}.required`, 'required properties need a properties object');
|
|
}
|
|
if ('items' in schema) validateSchemaNode(schema.items, filePath, `${field}.items`);
|
|
for (const numericKey of ['minLength', 'maxLength', 'minimum', 'maximum', 'maxItems']) {
|
|
if (numericKey in schema && (!Number.isSafeInteger(schema[numericKey]) || (schema[numericKey] as number) < 0)) {
|
|
fail(filePath, `${field}.${numericKey}`, 'must be a non-negative safe integer');
|
|
}
|
|
}
|
|
if ('const' in schema && schema.const !== true && schema.const !== false) {
|
|
fail(filePath, `${field}.const`, 'only boolean literal constants are supported');
|
|
}
|
|
}
|
|
|
|
function finiteNumber(value: unknown, filePath: string, field: string): number {
|
|
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
fail(filePath, field, 'must be a finite number');
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function boundedInteger(value: unknown, filePath: string, field: string): number {
|
|
if (!Number.isSafeInteger(value) || (value as number) < 0) {
|
|
fail(filePath, field, 'must be a non-negative safe integer');
|
|
}
|
|
return value as number;
|
|
}
|
|
|
|
/**
|
|
* Schema-v2 deliberately accepts a small data-only subset. This validator is
|
|
* separate from the legacy schema-1 validator because the bundled Data Service
|
|
* contract predates the v2 requirement that strings, arrays, and numbers carry
|
|
* explicit bounds.
|
|
*/
|
|
function validateV2SchemaNode(
|
|
value: unknown,
|
|
filePath: string,
|
|
field: string,
|
|
depth = 1,
|
|
): void {
|
|
if (depth > 4) fail(filePath, field, 'schema exceeds the maximum depth of 4');
|
|
const schema = assertRecord(value, filePath, field);
|
|
assertExactKeys(schema, V2_SCHEMA_KEYS, filePath, field);
|
|
if (typeof schema.type !== 'string' || !V2_SCHEMA_TYPES.has(schema.type)) {
|
|
fail(filePath, `${field}.type`, 'unsupported bounded schema type');
|
|
}
|
|
const type = schema.type;
|
|
if ('const' in schema && (type !== 'boolean' || typeof schema.const !== 'boolean')) {
|
|
fail(filePath, `${field}.const`, 'only boolean constants are supported');
|
|
}
|
|
if ('additionalProperties' in schema) {
|
|
if (type !== 'object' || schema.additionalProperties !== false) {
|
|
fail(filePath, `${field}.additionalProperties`, 'only false is supported on object schemas');
|
|
}
|
|
}
|
|
if ('required' in schema && !Array.isArray(schema.required)) {
|
|
fail(filePath, `${field}.required`, 'must be an array');
|
|
}
|
|
if (type === 'object') {
|
|
if (schema.additionalProperties !== false) {
|
|
fail(filePath, `${field}.additionalProperties`, 'object schemas must close additional properties');
|
|
}
|
|
if (!('required' in schema)) {
|
|
fail(filePath, `${field}.required`, 'object schemas must declare required properties');
|
|
}
|
|
const properties = assertRecord(schema.properties, filePath, `${field}.properties`);
|
|
if (Object.keys(properties).length > 64) {
|
|
fail(filePath, `${field}.properties`, 'object schemas may contain at most 64 properties');
|
|
}
|
|
for (const [name, child] of Object.entries(properties)) {
|
|
if (!V2_PROPERTY_NAME_PATTERN.test(name)) {
|
|
fail(filePath, `${field}.properties.${name}`, 'property name is invalid');
|
|
}
|
|
validateV2SchemaNode(child, filePath, `${field}.properties.${name}`, depth + 1);
|
|
}
|
|
const required = schema.required === undefined
|
|
? []
|
|
: uniqueStrings(schema.required, filePath, `${field}.required`, V2_PROPERTY_NAME_PATTERN);
|
|
for (const name of required) {
|
|
if (!(name in properties)) fail(filePath, `${field}.required`, `required property is not declared: ${name}`);
|
|
}
|
|
for (const key of ['items', 'minLength', 'maxLength', 'minimum', 'maximum', 'maxItems']) {
|
|
if (key in schema) fail(filePath, `${field}.${key}`, `${key} is not valid for an object schema`);
|
|
}
|
|
return;
|
|
}
|
|
if (type === 'array') {
|
|
if (!('items' in schema)) fail(filePath, `${field}.items`, 'array schemas must declare items');
|
|
boundedInteger(schema.maxItems, filePath, `${field}.maxItems`);
|
|
validateV2SchemaNode(schema.items, filePath, `${field}.items`, depth + 1);
|
|
for (const key of ['additionalProperties', 'required', 'properties', 'minLength', 'maxLength', 'minimum', 'maximum']) {
|
|
if (key in schema) fail(filePath, `${field}.${key}`, `${key} is not valid for an array schema`);
|
|
}
|
|
return;
|
|
}
|
|
if ('items' in schema || 'properties' in schema || 'required' in schema
|
|
|| 'additionalProperties' in schema || 'maxItems' in schema) {
|
|
fail(filePath, field, 'nested schema keywords do not match the scalar type');
|
|
}
|
|
if (type === 'string') {
|
|
const maxLength = boundedInteger(schema.maxLength, filePath, `${field}.maxLength`);
|
|
if ('minLength' in schema) {
|
|
const minLength = boundedInteger(schema.minLength, filePath, `${field}.minLength`);
|
|
if (minLength > maxLength) fail(filePath, field, 'minLength cannot exceed maxLength');
|
|
}
|
|
for (const key of ['minimum', 'maximum']) {
|
|
if (key in schema) fail(filePath, `${field}.${key}`, `${key} is not valid for a string schema`);
|
|
}
|
|
return;
|
|
}
|
|
if (type === 'integer' || type === 'number') {
|
|
const minimum = finiteNumber(schema.minimum, filePath, `${field}.minimum`);
|
|
const maximum = finiteNumber(schema.maximum, filePath, `${field}.maximum`);
|
|
if (minimum > maximum) fail(filePath, field, 'minimum cannot exceed maximum');
|
|
for (const key of ['minLength', 'maxLength']) {
|
|
if (key in schema) fail(filePath, `${field}.${key}`, `${key} is not valid for a numeric schema`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function validateDestructiveConfirmation(
|
|
schema: UnknownRecord,
|
|
filePath: string,
|
|
field: string,
|
|
): void {
|
|
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
const confirmation = properties.confirmed;
|
|
if (!required.includes('confirmed') || !isRecord(confirmation)
|
|
|| confirmation.type !== 'boolean' || confirmation.const !== true) {
|
|
fail(filePath, field, 'destructive tools must require literal confirmed: true');
|
|
}
|
|
}
|
|
|
|
export function parseAgentPluginsRootManifest(
|
|
value: unknown,
|
|
filePath = PACKAGE_MANIFEST_FILE,
|
|
): AgentPluginsRootManifest {
|
|
const root = assertRecord(value, filePath, 'root');
|
|
assertExactKeys(root, ROOT_KEYS, filePath, 'root');
|
|
rejectUnsupportedComponents(root, filePath, 'root');
|
|
if (root.$schema !== AGENT_PLUGINS_SCHEMA_URL) {
|
|
fail(filePath, '$schema', `must equal ${AGENT_PLUGINS_SCHEMA_URL}`);
|
|
}
|
|
const name = stableId(root.name, filePath, 'name', /^[a-z][a-z0-9.-]{0,127}$/u);
|
|
const version = text(root.version, filePath, 'version', 128);
|
|
if (!isValidSemVer(version)) fail(filePath, 'version', 'must be a valid package semver');
|
|
const description = text(root.description, filePath, 'description');
|
|
const author = assertRecord(root.author, filePath, 'author');
|
|
assertExactKeys(author, AUTHOR_KEYS, filePath, 'author');
|
|
const authorName = text(author.name, filePath, 'author.name', 128);
|
|
const extensions = assertRecord(root.extensions, filePath, 'extensions');
|
|
assertExactKeys(extensions, EXTENSIONS_KEYS, filePath, 'extensions');
|
|
const makeLore = assertRecord(extensions['com.makelore'], filePath, 'extensions.com.makelore');
|
|
assertExactKeys(makeLore, MAKELore_EXTENSION_KEYS, filePath, 'extensions.com.makelore');
|
|
const capabilityManifest = text(
|
|
makeLore.capabilityManifest,
|
|
filePath,
|
|
'extensions.com.makelore.capabilityManifest',
|
|
512,
|
|
);
|
|
return freezeDeep({
|
|
$schema: AGENT_PLUGINS_SCHEMA_URL,
|
|
name,
|
|
version,
|
|
description,
|
|
author: { name: authorName },
|
|
extensions: { 'com.makelore': { capabilityManifest } },
|
|
});
|
|
}
|
|
|
|
function parseSkill(
|
|
value: unknown,
|
|
index: number,
|
|
packageRoot: string,
|
|
capabilityDirectory: string,
|
|
filePath: string,
|
|
): CodingPluginSkillDefinition {
|
|
const skill = assertRecord(value, filePath, `skills[${index}]`);
|
|
assertExactKeys(skill, SKILL_KEYS, filePath, `skills[${index}]`);
|
|
const id = stableId(skill.id, filePath, `skills[${index}].id`, SKILL_ID_PATTERN);
|
|
const entryPath = normalizeCandidatePath(
|
|
packageRoot,
|
|
capabilityDirectory,
|
|
skill.entry,
|
|
filePath,
|
|
`skills[${index}].entry`,
|
|
);
|
|
const grants = uniqueStrings(
|
|
skill.grants,
|
|
filePath,
|
|
`skills[${index}].grants`,
|
|
CAPABILITY_ID_PATTERN,
|
|
);
|
|
for (const grant of grants) {
|
|
if (!DATA_SERVICE_CAPABILITY_IDS.includes(grant as (typeof DATA_SERVICE_CAPABILITY_IDS)[number])) {
|
|
fail(filePath, `skills[${index}].grants`, `unknown capability grant: ${grant}`);
|
|
}
|
|
}
|
|
return { id, entryPath, grants };
|
|
}
|
|
|
|
function parseTool(
|
|
value: unknown,
|
|
index: number,
|
|
filePath: string,
|
|
): CodingPluginToolDefinition {
|
|
const tool = assertRecord(value, filePath, `tools[${index}]`);
|
|
assertExactKeys(tool, TOOL_KEYS, filePath, `tools[${index}]`);
|
|
const name = stableId(tool.name, filePath, `tools[${index}].name`, TOOL_NAME_PATTERN);
|
|
const label = text(tool.label, filePath, `tools[${index}].label`);
|
|
const description = text(tool.description, filePath, `tools[${index}].description`);
|
|
const capabilityId = stableId(tool.capabilityId, filePath, `tools[${index}].capabilityId`, CAPABILITY_ID_PATTERN);
|
|
if (!DATA_SERVICE_CAPABILITY_IDS.includes(capabilityId as (typeof DATA_SERVICE_CAPABILITY_IDS)[number])) {
|
|
fail(filePath, `tools[${index}].capabilityId`, `unknown capability: ${capabilityId}`);
|
|
}
|
|
const operation = stableId(tool.operation, filePath, `tools[${index}].operation`, OPERATION_ID_PATTERN);
|
|
const roles = uniqueStrings(tool.roles, filePath, `tools[${index}].roles`, /^[a-z]+$/u);
|
|
if (roles.length !== 1 || roles[0] !== 'parent') {
|
|
fail(filePath, `tools[${index}].roles`, 'P0 tools must be exactly parent-only');
|
|
}
|
|
const mutation = tool.mutation;
|
|
if (mutation !== 'read' && mutation !== 'write' && mutation !== 'destructive') {
|
|
fail(filePath, `tools[${index}].mutation`, 'unknown tool mutation');
|
|
}
|
|
const projectWriteLease = bool(tool.projectWriteLease, filePath, `tools[${index}].projectWriteLease`);
|
|
const permissions = uniqueStrings(
|
|
tool.permissions,
|
|
filePath,
|
|
`tools[${index}].permissions`,
|
|
/^[a-z][a-z0-9._-]{0,63}$/u,
|
|
);
|
|
for (const permission of permissions) {
|
|
if (!CODE_OWNED_PLUGIN_PERMISSION_IDS.includes(permission as (typeof CODE_OWNED_PLUGIN_PERMISSION_IDS)[number])) {
|
|
fail(filePath, `tools[${index}].permissions`, `unknown code-owned permission: ${permission}`);
|
|
}
|
|
}
|
|
validateSchemaNode(tool.inputSchema, filePath, `tools[${index}].inputSchema`, true);
|
|
const schema = tool.inputSchema as UnknownRecord;
|
|
if (schema.type !== 'object' || schema.additionalProperties !== false) {
|
|
fail(filePath, `tools[${index}].inputSchema`, 'tool input schema must be an exact object');
|
|
}
|
|
if (mutation === 'destructive') {
|
|
validateDestructiveConfirmation(schema, filePath, `tools[${index}].inputSchema`);
|
|
}
|
|
return {
|
|
name,
|
|
label,
|
|
description,
|
|
capabilityId,
|
|
operation,
|
|
roles: ['parent'],
|
|
mutation: mutation as PluginToolMutation,
|
|
projectWriteLease,
|
|
permissions,
|
|
inputSchema: freezeDeep(structuredClone(schema)),
|
|
};
|
|
}
|
|
|
|
function validateDataServiceDefinition(
|
|
definition: CodingPluginDefinition,
|
|
filePath: string,
|
|
): void {
|
|
const expectedNames = new Set<string>(DATA_SERVICE_TOOL_NAMES);
|
|
if (definition.id !== DATA_SERVICE_PLUGIN_ID) return;
|
|
if (definition.tools.length !== DATA_SERVICE_TOOL_NAMES.length) {
|
|
fail(filePath, 'tools', `Data Service must declare exactly ${DATA_SERVICE_TOOL_NAMES.length} tools`);
|
|
}
|
|
for (const tool of definition.tools) {
|
|
if (!expectedNames.has(tool.name)) fail(filePath, `tools.${tool.name}`, 'unknown Data Service tool');
|
|
const expectedCapability = DATA_SERVICE_OPERATION_CAPABILITIES[tool.operation];
|
|
if (!expectedCapability || expectedCapability !== tool.capabilityId) {
|
|
fail(filePath, `tools.${tool.name}.operation`, 'operation does not reference its capability');
|
|
}
|
|
}
|
|
const operationNames = definition.tools.map((tool) => tool.operation);
|
|
if (new Set(operationNames).size !== operationNames.length) {
|
|
fail(filePath, 'tools.operation', 'duplicate operation identifier');
|
|
}
|
|
}
|
|
|
|
function trustedMetadata(
|
|
options: CodingPluginManifestParseOptions,
|
|
declaredRuntimeKind: CodingPluginRuntimeKind,
|
|
packageRoot: string,
|
|
schemaVersion: 1 | 2,
|
|
filePath: string,
|
|
): Pick<CodingPluginDefinition, 'runtimeKind' | 'acquisitionMode' | 'releaseId' | 'provenance'> {
|
|
const runtimeKind = options.runtimeKind ?? declaredRuntimeKind;
|
|
if (runtimeKind !== declaredRuntimeKind) {
|
|
fail(filePath, 'trusted metadata.runtimeKind', 'does not match the package runtime declaration');
|
|
}
|
|
const expectedAcquisition: CodingPluginAcquisitionMode = schemaVersion === 1
|
|
? 'system_included'
|
|
: 'user_acquired';
|
|
const acquisitionMode = options.acquisitionMode ?? expectedAcquisition;
|
|
if (acquisitionMode !== expectedAcquisition) {
|
|
fail(filePath, 'trusted metadata.acquisitionMode', `must equal ${expectedAcquisition} for schema ${schemaVersion}`);
|
|
}
|
|
const releaseId = options.releaseId ?? null;
|
|
if (releaseId !== null && (typeof releaseId !== 'string' || releaseId.length === 0 || releaseId.length > 128)) {
|
|
fail(filePath, 'trusted metadata.releaseId', 'must be null or a bounded non-empty string');
|
|
}
|
|
if (schemaVersion === 1 && releaseId !== null) {
|
|
fail(filePath, 'trusted metadata.releaseId', 'bundled schema-1 definitions cannot carry a Release ID');
|
|
}
|
|
const defaultProvenance: CodingPluginPackageProvenance = {
|
|
source: schemaVersion === 1 ? 'bundled' : 'marketplace',
|
|
packageRoot: schemaVersion === 1 ? path.basename(packageRoot) : path.resolve(packageRoot),
|
|
};
|
|
const provenance = options.provenance ?? defaultProvenance;
|
|
if (provenance.source !== defaultProvenance.source
|
|
|| typeof provenance.packageRoot !== 'string'
|
|
|| provenance.packageRoot.length === 0
|
|
|| provenance.packageRoot.length > 1024) {
|
|
fail(filePath, 'trusted metadata.provenance', 'is not valid for this package source');
|
|
}
|
|
return {
|
|
runtimeKind,
|
|
acquisitionMode,
|
|
releaseId,
|
|
provenance: {
|
|
source: provenance.source,
|
|
packageRoot: provenance.packageRoot,
|
|
},
|
|
};
|
|
}
|
|
|
|
function parseV2Skill(
|
|
value: unknown,
|
|
index: number,
|
|
packageRoot: string,
|
|
capabilityDirectory: string,
|
|
filePath: string,
|
|
): CodingPluginSkillDefinition {
|
|
const skill = assertRecord(value, filePath, `skills[${index}]`);
|
|
assertExactKeys(skill, V2_SKILL_KEYS, filePath, `skills[${index}]`);
|
|
const id = stableId(skill.id, filePath, `skills[${index}].id`, SKILL_ID_PATTERN);
|
|
const entryPath = normalizeV2CandidatePath(
|
|
packageRoot,
|
|
capabilityDirectory,
|
|
skill.entry,
|
|
filePath,
|
|
`skills[${index}].entry`,
|
|
);
|
|
const grants = uniqueStrings(
|
|
skill.grants,
|
|
filePath,
|
|
`skills[${index}].grants`,
|
|
CAPABILITY_ID_PATTERN,
|
|
);
|
|
return { id, entryPath, grants };
|
|
}
|
|
|
|
function hostedPermissionMatchesPlugin(permission: string, pluginId: string): boolean {
|
|
if (!HOSTED_PERMISSION_PATTERN.test(permission)) return false;
|
|
const suffix = pluginId.startsWith('makelore.') ? pluginId.slice('makelore.'.length) : pluginId;
|
|
return permission.startsWith(`hosted.${suffix}.`);
|
|
}
|
|
|
|
function parseV2Tool(
|
|
value: unknown,
|
|
index: number,
|
|
pluginId: string,
|
|
filePath: string,
|
|
): CodingPluginToolDefinition {
|
|
const tool = assertRecord(value, filePath, `tools[${index}]`);
|
|
assertExactKeys(tool, V2_TOOL_KEYS, filePath, `tools[${index}]`);
|
|
const name = stableId(tool.name, filePath, `tools[${index}].name`, TOOL_NAME_PATTERN);
|
|
const label = text(tool.label, filePath, `tools[${index}].label`);
|
|
const description = text(tool.description, filePath, `tools[${index}].description`);
|
|
const capabilityId = stableId(tool.capabilityId, filePath, `tools[${index}].capabilityId`, CAPABILITY_ID_PATTERN);
|
|
const operation = stableId(tool.operation, filePath, `tools[${index}].operation`, OPERATION_ID_PATTERN);
|
|
const roles = uniqueStrings(tool.roles, filePath, `tools[${index}].roles`, /^[a-z]+$/u);
|
|
if (roles.length !== 1 || roles[0] !== 'parent') {
|
|
fail(filePath, `tools[${index}].roles`, 'distributed tools must be exactly parent-only');
|
|
}
|
|
const mutation = tool.mutation;
|
|
if (mutation !== 'read' && mutation !== 'write' && mutation !== 'destructive') {
|
|
fail(filePath, `tools[${index}].mutation`, 'unknown tool mutation');
|
|
}
|
|
const projectWriteLease = bool(tool.projectWriteLease, filePath, `tools[${index}].projectWriteLease`);
|
|
if (projectWriteLease) fail(filePath, `tools[${index}].projectWriteLease`, 'distributed tools cannot request a project write lease');
|
|
const permissions = uniqueStrings(
|
|
tool.permissions,
|
|
filePath,
|
|
`tools[${index}].permissions`,
|
|
/^[a-z][a-z0-9._-]{0,127}$/u,
|
|
);
|
|
if (permissions.some((permission) => !hostedPermissionMatchesPlugin(permission, pluginId))) {
|
|
fail(filePath, `tools[${index}].permissions`, 'permission is outside the plugin hosted namespace');
|
|
}
|
|
if (tool.executionMode !== 'synchronous' && tool.executionMode !== 'job') {
|
|
fail(filePath, `tools[${index}].executionMode`, 'must be synchronous or job');
|
|
}
|
|
validateV2SchemaNode(tool.inputSchema, filePath, `tools[${index}].inputSchema`);
|
|
validateV2SchemaNode(tool.outputSchema, filePath, `tools[${index}].outputSchema`);
|
|
const inputSchema = tool.inputSchema as UnknownRecord;
|
|
const outputSchema = tool.outputSchema as UnknownRecord;
|
|
if (inputSchema.type !== 'object' || inputSchema.additionalProperties !== false) {
|
|
fail(filePath, `tools[${index}].inputSchema`, 'tool input schema must be a closed object');
|
|
}
|
|
if (outputSchema.type !== 'object' || outputSchema.additionalProperties !== false) {
|
|
fail(filePath, `tools[${index}].outputSchema`, 'tool output schema must be a closed object');
|
|
}
|
|
if (mutation === 'destructive') validateDestructiveConfirmation(inputSchema, filePath, `tools[${index}].inputSchema`);
|
|
return {
|
|
name,
|
|
label,
|
|
description,
|
|
capabilityId,
|
|
operation,
|
|
roles: ['parent'],
|
|
mutation: mutation as PluginToolMutation,
|
|
projectWriteLease: false,
|
|
permissions,
|
|
executionMode: tool.executionMode as CodingPluginExecutionMode,
|
|
inputSchema: freezeDeep(structuredClone(inputSchema)),
|
|
outputSchema: freezeDeep(structuredClone(outputSchema)),
|
|
};
|
|
}
|
|
|
|
function parseV2CodingPluginManifest(
|
|
root: AgentPluginsRootManifest,
|
|
capability: UnknownRecord,
|
|
packageRoot: string,
|
|
capabilityManifestPath: string,
|
|
options: CodingPluginManifestParseOptions,
|
|
): CodingPluginDefinition {
|
|
assertExactKeys(capability, V2_CAPABILITY_KEYS, capabilityManifestPath, 'root');
|
|
rejectUnsupportedComponents(capability, capabilityManifestPath, 'root');
|
|
if (capability.schemaVersion !== 2) fail(capabilityManifestPath, 'schemaVersion', 'must equal 2');
|
|
const pluginId = stableId(capability.pluginId, capabilityManifestPath, 'pluginId', /^[a-z][a-z0-9.-]{0,127}$/u);
|
|
if (pluginId !== root.name) fail(capabilityManifestPath, 'pluginId', 'must match plugin.json name');
|
|
const contractVersion = positiveInteger(capability.contractVersion, capabilityManifestPath, 'contractVersion');
|
|
if (capability.scope !== 'project') fail(capabilityManifestPath, 'scope', 'distributed plugins must be project-scoped');
|
|
const runtime = assertRecord(capability.runtime, capabilityManifestPath, 'runtime');
|
|
assertExactKeys(runtime, V2_RUNTIME_KEYS, capabilityManifestPath, 'runtime');
|
|
const runtimeKind = runtime.kind;
|
|
if (runtimeKind !== 'skill_only' && runtimeKind !== 'platform_hosted') {
|
|
fail(capabilityManifestPath, 'runtime.kind', 'must be skill_only or platform_hosted');
|
|
}
|
|
if (runtimeKind === 'skill_only') {
|
|
if ('protocol' in runtime) fail(capabilityManifestPath, 'runtime.protocol', 'skill_only must not declare a hosted protocol');
|
|
} else if (runtime.protocol !== 'makelore-hosted.v1') {
|
|
fail(capabilityManifestPath, 'runtime.protocol', 'must equal makelore-hosted.v1');
|
|
}
|
|
const skillsValue = capability.skills;
|
|
if (!Array.isArray(skillsValue) || skillsValue.length === 0) {
|
|
fail(capabilityManifestPath, 'skills', 'must contain at least one Skill');
|
|
}
|
|
const capabilityDirectory = path.dirname(capabilityManifestPath);
|
|
const skills = skillsValue.map((value, index) => parseV2Skill(
|
|
value,
|
|
index,
|
|
packageRoot,
|
|
capabilityDirectory,
|
|
capabilityManifestPath,
|
|
));
|
|
if (new Set(skills.map((skill) => skill.id)).size !== skills.length) {
|
|
fail(capabilityManifestPath, 'skills', 'duplicate Skill identifier');
|
|
}
|
|
if (new Set(skills.map((skill) => skill.entryPath)).size !== skills.length) {
|
|
fail(capabilityManifestPath, 'skills', 'duplicate Skill entry path');
|
|
}
|
|
if (!Array.isArray(capability.tools)) fail(capabilityManifestPath, 'tools', 'must be an array');
|
|
if (runtimeKind === 'skill_only' && capability.tools.length !== 0) {
|
|
fail(capabilityManifestPath, 'tools', 'skill_only packages must declare an empty tools array');
|
|
}
|
|
if (runtimeKind === 'platform_hosted' && capability.tools.length === 0) {
|
|
fail(capabilityManifestPath, 'tools', 'platform_hosted packages must declare at least one tool');
|
|
}
|
|
const tools = capability.tools.map((value, index) => parseV2Tool(
|
|
value,
|
|
index,
|
|
pluginId,
|
|
capabilityManifestPath,
|
|
));
|
|
if (new Set(tools.map((tool) => tool.name)).size !== tools.length) {
|
|
fail(capabilityManifestPath, 'tools.name', 'duplicate tool identifier');
|
|
}
|
|
const mappingKeys = tools.map((tool) => `${tool.capabilityId}\u0000${tool.operation}`);
|
|
if (new Set(mappingKeys).size !== mappingKeys.length) {
|
|
fail(capabilityManifestPath, 'tools', 'duplicate capability operation mapping');
|
|
}
|
|
const grants = new Set(skills.flatMap((skill) => skill.grants));
|
|
for (const tool of tools) {
|
|
if (!grants.has(tool.capabilityId)) {
|
|
fail(capabilityManifestPath, `tools.${tool.name}.capabilityId`, 'tool capability has no Skill grant');
|
|
}
|
|
}
|
|
if (runtimeKind === 'skill_only' && skills.some((skill) => skill.grants.length > 0)) {
|
|
fail(capabilityManifestPath, 'skills', 'skill_only Skills must declare empty grants');
|
|
}
|
|
const metadata = trustedMetadata(options, runtimeKind, packageRoot, 2, capabilityManifestPath);
|
|
return freezeDeep({
|
|
id: pluginId,
|
|
version: root.version,
|
|
contractVersion,
|
|
displayName: root.name,
|
|
description: root.description,
|
|
...metadata,
|
|
scope: 'project' as const,
|
|
adapterId: '',
|
|
requiresBackend: runtimeKind === 'platform_hosted',
|
|
skills,
|
|
tools,
|
|
operations: tools.map(({ capabilityId, operation, name }) => ({ capabilityId, operation, toolName: name })),
|
|
surfaces: {},
|
|
});
|
|
}
|
|
|
|
export function parseCodingPluginManifest(
|
|
rootValue: unknown,
|
|
capabilityValue: unknown,
|
|
options: CodingPluginManifestParseOptions = {},
|
|
): CodingPluginDefinition {
|
|
const packageRoot = path.resolve(options.packageRoot ?? path.dirname(options.capabilityManifestPath ?? '.'));
|
|
const rootManifestPath = path.resolve(
|
|
options.rootManifestPath ?? path.join(packageRoot, PACKAGE_MANIFEST_FILE),
|
|
);
|
|
const root = parseAgentPluginsRootManifest(rootValue, rootManifestPath);
|
|
const capabilityManifestPath = path.resolve(
|
|
options.capabilityManifestPath ?? path.join(packageRoot, CAPABILITY_MANIFEST_RELATIVE_PATH),
|
|
);
|
|
const capabilityRelativePath = path.relative(packageRoot, capabilityManifestPath);
|
|
if (!capabilityRelativePath || capabilityRelativePath === '..'
|
|
|| capabilityRelativePath.startsWith(`..${path.sep}`) || path.isAbsolute(capabilityRelativePath)) {
|
|
fail(capabilityManifestPath, 'root', 'capability manifest must be inside the package root');
|
|
}
|
|
const declaredCapabilityPath = normalizeCapabilityManifestPath(
|
|
packageRoot,
|
|
root.extensions['com.makelore'].capabilityManifest,
|
|
rootManifestPath,
|
|
'extensions.com.makelore.capabilityManifest',
|
|
);
|
|
if (declaredCapabilityPath !== CAPABILITY_MANIFEST_RELATIVE_PATH) {
|
|
fail(
|
|
rootManifestPath,
|
|
'extensions.com.makelore.capabilityManifest',
|
|
`must point to ${CAPABILITY_MANIFEST_RELATIVE_PATH}`,
|
|
);
|
|
}
|
|
const capability = assertRecord(capabilityValue, capabilityManifestPath, 'root');
|
|
if (capability.schemaVersion === 2) {
|
|
return parseV2CodingPluginManifest(
|
|
root,
|
|
capability,
|
|
packageRoot,
|
|
capabilityManifestPath,
|
|
options,
|
|
);
|
|
}
|
|
if (capability.schemaVersion !== 1) fail(capabilityManifestPath, 'schemaVersion', 'must equal 1 or 2');
|
|
assertExactKeys(capability, CAPABILITY_KEYS, capabilityManifestPath, 'root');
|
|
rejectUnsupportedComponents(capability, capabilityManifestPath, 'root');
|
|
const pluginId = stableId(capability.pluginId, capabilityManifestPath, 'pluginId', /^[a-z][a-z0-9.-]{0,127}$/u);
|
|
if (pluginId !== root.name) fail(capabilityManifestPath, 'pluginId', 'must match plugin.json name');
|
|
const contractVersion = positiveInteger(capability.contractVersion, capabilityManifestPath, 'contractVersion');
|
|
if (capability.scope !== 'project') fail(capabilityManifestPath, 'scope', 'P0 plugins must be project-scoped');
|
|
const adapterId = text(capability.adapterId, capabilityManifestPath, 'adapterId', 64);
|
|
if (!BUNDLED_CODING_PLUGIN_ADAPTER_IDS.includes(adapterId as (typeof BUNDLED_CODING_PLUGIN_ADAPTER_IDS)[number])) {
|
|
fail(capabilityManifestPath, 'adapterId', `adapter is not code-owned: ${adapterId}`);
|
|
}
|
|
const requiresBackend = bool(capability.requiresBackend, capabilityManifestPath, 'requiresBackend');
|
|
const display = assertRecord(capability.display, capabilityManifestPath, 'display');
|
|
assertExactKeys(display, DISPLAY_KEYS, capabilityManifestPath, 'display');
|
|
const displayName = text(display.name, capabilityManifestPath, 'display.name');
|
|
const description = text(display.description, capabilityManifestPath, 'display.description');
|
|
const capabilityDirectory = path.dirname(capabilityManifestPath);
|
|
const skillsValue = capability.skills;
|
|
if (!Array.isArray(skillsValue) || skillsValue.length === 0) {
|
|
fail(capabilityManifestPath, 'skills', 'must contain at least one Skill');
|
|
}
|
|
const skills = skillsValue.map((value, index) => parseSkill(
|
|
value,
|
|
index,
|
|
packageRoot,
|
|
capabilityDirectory,
|
|
capabilityManifestPath,
|
|
));
|
|
if (new Set(skills.map((skill) => skill.id)).size !== skills.length) {
|
|
fail(capabilityManifestPath, 'skills', 'duplicate Skill identifier');
|
|
}
|
|
if (!Array.isArray(capability.tools) || capability.tools.length === 0) {
|
|
fail(capabilityManifestPath, 'tools', 'must contain at least one tool');
|
|
}
|
|
const tools = capability.tools.map((value, index) => parseTool(value, index, capabilityManifestPath));
|
|
if (new Set(tools.map((tool) => tool.name)).size !== tools.length) {
|
|
fail(capabilityManifestPath, 'tools.name', 'duplicate tool identifier');
|
|
}
|
|
if (new Set(tools.map((tool) => tool.operation)).size !== tools.length) {
|
|
fail(capabilityManifestPath, 'tools.operation', 'duplicate operation identifier');
|
|
}
|
|
const grants = new Set(skills.flatMap((skill) => skill.grants));
|
|
for (const tool of tools) {
|
|
if (!grants.has(tool.capabilityId)) {
|
|
fail(capabilityManifestPath, `tools.${tool.name}.capabilityId`, 'tool capability has no Skill grant');
|
|
}
|
|
}
|
|
const surfacesValue = capability.surfaces;
|
|
const surfaces = assertRecord(surfacesValue, capabilityManifestPath, 'surfaces');
|
|
assertExactKeys(surfaces, SURFACE_KEYS, capabilityManifestPath, 'surfaces');
|
|
const normalizedSurfaces: { projectSettings?: string; previewRuntime?: string } = {};
|
|
if (surfaces.projectSettings !== undefined) {
|
|
const projectSettings = text(surfaces.projectSettings, capabilityManifestPath, 'surfaces.projectSettings', 64);
|
|
if (!BUNDLED_CODING_PLUGIN_SETTINGS_SURFACES.includes(projectSettings as (typeof BUNDLED_CODING_PLUGIN_SETTINGS_SURFACES)[number])) {
|
|
fail(capabilityManifestPath, 'surfaces.projectSettings', `surface is not code-owned: ${projectSettings}`);
|
|
}
|
|
normalizedSurfaces.projectSettings = projectSettings;
|
|
}
|
|
if (surfaces.previewRuntime !== undefined) {
|
|
const previewRuntime = text(surfaces.previewRuntime, capabilityManifestPath, 'surfaces.previewRuntime', 64);
|
|
if (!BUNDLED_CODING_PLUGIN_PREVIEW_SURFACES.includes(previewRuntime as (typeof BUNDLED_CODING_PLUGIN_PREVIEW_SURFACES)[number])) {
|
|
fail(capabilityManifestPath, 'surfaces.previewRuntime', `surface is not code-owned: ${previewRuntime}`);
|
|
}
|
|
normalizedSurfaces.previewRuntime = previewRuntime;
|
|
}
|
|
const definition = freezeDeep({
|
|
id: pluginId,
|
|
version: root.version,
|
|
contractVersion,
|
|
displayName,
|
|
description,
|
|
...trustedMetadata(options, 'bundled_typed', packageRoot, 1, capabilityManifestPath),
|
|
scope: 'project' as const,
|
|
adapterId,
|
|
requiresBackend,
|
|
skills,
|
|
tools,
|
|
operations: pluginId === DATA_SERVICE_PLUGIN_ID
|
|
? DATA_SERVICE_OPERATION_DEFINITIONS
|
|
: tools.map(({ capabilityId, operation, name }) => ({ capabilityId, operation, toolName: name })),
|
|
surfaces: normalizedSurfaces,
|
|
});
|
|
validateDataServiceDefinition(definition, capabilityManifestPath);
|
|
return definition;
|
|
}
|
|
|
|
function readJson(source: string, filePath: string): unknown {
|
|
try {
|
|
return JSON.parse(source) as unknown;
|
|
} catch (error) {
|
|
fail(filePath, 'root', `invalid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
}
|
|
}
|
|
|
|
export async function loadCodingPluginDefinition(
|
|
packageRoot: string,
|
|
options: CodingPluginLoadOptions = {},
|
|
): Promise<CodingPluginDefinition> {
|
|
const root = path.resolve(packageRoot);
|
|
const pluginManifestPath = path.join(root, PACKAGE_MANIFEST_FILE);
|
|
const rootValue = readJson(await readFile(pluginManifestPath, 'utf8'), pluginManifestPath);
|
|
const rootManifest = parseAgentPluginsRootManifest(rootValue, pluginManifestPath);
|
|
const capabilityManifest = normalizeCapabilityManifestPath(
|
|
root,
|
|
rootManifest.extensions['com.makelore'].capabilityManifest,
|
|
pluginManifestPath,
|
|
'extensions.com.makelore.capabilityManifest',
|
|
);
|
|
if (capabilityManifest !== CAPABILITY_MANIFEST_RELATIVE_PATH) {
|
|
fail(pluginManifestPath, 'extensions.com.makelore.capabilityManifest', `must point to ${CAPABILITY_MANIFEST_RELATIVE_PATH}`);
|
|
}
|
|
const capabilityManifestPath = path.join(root, capabilityManifest);
|
|
const capabilityValue = readJson(await readFile(capabilityManifestPath, 'utf8'), capabilityManifestPath);
|
|
const definition = parseCodingPluginManifest(rootValue, capabilityValue, {
|
|
packageRoot: root,
|
|
rootManifestPath: pluginManifestPath,
|
|
capabilityManifestPath,
|
|
...options,
|
|
});
|
|
for (const skill of definition.skills) {
|
|
try {
|
|
await readFile(path.join(root, skill.entryPath), 'utf8');
|
|
} catch (error) {
|
|
fail(
|
|
capabilityManifestPath,
|
|
`skills.${skill.id}.entry`,
|
|
`Skill entry could not be read: ${error instanceof Error ? error.message : String(error)}`,
|
|
);
|
|
}
|
|
}
|
|
return definition;
|
|
}
|
|
|
|
export function loadCodingPluginDefinitionSync(
|
|
packageRoot: string,
|
|
options: CodingPluginLoadOptions = {},
|
|
): CodingPluginDefinition {
|
|
const root = path.resolve(packageRoot);
|
|
const pluginManifestPath = path.join(root, PACKAGE_MANIFEST_FILE);
|
|
const rootValue = readJson(readFileSync(pluginManifestPath, 'utf8'), pluginManifestPath);
|
|
const rootManifest = parseAgentPluginsRootManifest(rootValue, pluginManifestPath);
|
|
const capabilityManifest = normalizeCapabilityManifestPath(
|
|
root,
|
|
rootManifest.extensions['com.makelore'].capabilityManifest,
|
|
pluginManifestPath,
|
|
'extensions.com.makelore.capabilityManifest',
|
|
);
|
|
if (capabilityManifest !== CAPABILITY_MANIFEST_RELATIVE_PATH) {
|
|
fail(pluginManifestPath, 'extensions.com.makelore.capabilityManifest', `must point to ${CAPABILITY_MANIFEST_RELATIVE_PATH}`);
|
|
}
|
|
const capabilityManifestPath = path.join(root, capabilityManifest);
|
|
const capabilityValue = readJson(readFileSync(capabilityManifestPath, 'utf8'), capabilityManifestPath);
|
|
const definition = parseCodingPluginManifest(rootValue, capabilityValue, {
|
|
packageRoot: root,
|
|
rootManifestPath: pluginManifestPath,
|
|
capabilityManifestPath,
|
|
...options,
|
|
});
|
|
for (const skill of definition.skills) {
|
|
try {
|
|
readFileSync(path.join(root, skill.entryPath), 'utf8');
|
|
} catch (error) {
|
|
fail(
|
|
capabilityManifestPath,
|
|
`skills.${skill.id}.entry`,
|
|
`Skill entry could not be read: ${error instanceof Error ? error.message : String(error)}`,
|
|
);
|
|
}
|
|
}
|
|
return definition;
|
|
}
|
|
|
|
export function resolveBundledCodingPluginRootPaths(resourcesRoot: string): string[] {
|
|
const root = path.resolve(resourcesRoot);
|
|
return BUNDLED_CODING_PLUGIN_ROOTS.map((relativeRoot) => path.join(root, relativeRoot));
|
|
}
|
|
|
|
export const resolveBundledPluginRoots = resolveBundledCodingPluginRootPaths;
|
|
|
|
function validateBundledDefinitions(definitions: readonly CodingPluginDefinition[]): void {
|
|
const pluginIds = new Set<string>();
|
|
const capabilityIds = new Set<string>();
|
|
const skillIds = new Set<string>();
|
|
const operationIds = new Set<string>();
|
|
const toolIds = new Set<string>();
|
|
for (const definition of definitions) {
|
|
if (pluginIds.has(definition.id)) throw new Error(`Duplicate bundled plugin identifier: ${definition.id}`);
|
|
pluginIds.add(definition.id);
|
|
for (const skill of definition.skills) {
|
|
if (skillIds.has(skill.id)) throw new Error(`Duplicate bundled Skill identifier: ${skill.id}`);
|
|
skillIds.add(skill.id);
|
|
for (const grant of skill.grants) capabilityIds.add(grant);
|
|
}
|
|
for (const tool of definition.tools) {
|
|
if (toolIds.has(tool.name)) throw new Error(`Duplicate bundled tool identifier: ${tool.name}`);
|
|
if (operationIds.has(tool.operation)) throw new Error(`Duplicate bundled operation identifier: ${tool.operation}`);
|
|
toolIds.add(tool.name);
|
|
operationIds.add(tool.operation);
|
|
capabilityIds.add(tool.capabilityId);
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function loadBundledCodingPluginDefinitions(
|
|
resourcesRoot: string,
|
|
): Promise<readonly CodingPluginDefinition[]> {
|
|
const roots = resolveBundledCodingPluginRootPaths(resourcesRoot);
|
|
const definitions = await Promise.all(roots.map(async (root, index) => {
|
|
const expectedRoot = BUNDLED_CODING_PLUGIN_ROOTS[index];
|
|
const definition = await loadCodingPluginDefinition(root, {
|
|
runtimeKind: 'bundled_typed',
|
|
acquisitionMode: 'system_included',
|
|
releaseId: null,
|
|
provenance: { source: 'bundled', packageRoot: expectedRoot },
|
|
});
|
|
if (definition.id !== `makelore.${expectedRoot}`) {
|
|
throw new CodingPluginManifestError(
|
|
path.join(root, PACKAGE_MANIFEST_FILE),
|
|
'name',
|
|
`fixed bundle root ${expectedRoot} contains ${definition.id}`,
|
|
);
|
|
}
|
|
return definition;
|
|
}));
|
|
validateBundledDefinitions(definitions);
|
|
return Object.freeze(definitions);
|
|
}
|
|
|
|
export function loadBundledCodingPluginDefinitionsSync(
|
|
resourcesRoot: string,
|
|
): readonly CodingPluginDefinition[] {
|
|
const roots = resolveBundledCodingPluginRootPaths(resourcesRoot);
|
|
const definitions = roots.map((root, index) => {
|
|
const expectedRoot = BUNDLED_CODING_PLUGIN_ROOTS[index];
|
|
const definition = loadCodingPluginDefinitionSync(root, {
|
|
runtimeKind: 'bundled_typed',
|
|
acquisitionMode: 'system_included',
|
|
releaseId: null,
|
|
provenance: { source: 'bundled', packageRoot: expectedRoot },
|
|
});
|
|
if (definition.id !== `makelore.${expectedRoot}`) {
|
|
throw new CodingPluginManifestError(
|
|
path.join(root, PACKAGE_MANIFEST_FILE),
|
|
'name',
|
|
`fixed bundle root ${expectedRoot} contains ${definition.id}`,
|
|
);
|
|
}
|
|
return definition;
|
|
});
|
|
validateBundledDefinitions(definitions);
|
|
return Object.freeze(definitions);
|
|
}
|
|
|
|
export const parseBundledCodingPlugins = loadBundledCodingPluginDefinitions;
|
|
export const loadBundledPluginDefinitions = loadBundledCodingPluginDefinitions;
|