Files
makelore/tests/unit/coding-plugin-marketplace-contract.test.ts

292 lines
13 KiB
TypeScript

// @vitest-environment node
import { createHash, generateKeyPairSync, sign } from 'node:crypto';
import { describe, expect, it } from 'vitest';
import {
CodingPluginManifestError,
parseCodingPluginManifest,
} from '../../electron/coding-plugins/manifest';
import {
buildPluginReleaseDescriptor,
isMakeLoreVersionCompatible,
parsePluginReleaseDescriptor,
serializePluginReleaseDescriptor,
} from '../../electron/coding-plugins/release-descriptor';
import {
createPluginSignatureVerifier,
verifyPluginReleaseSignature,
} from '../../electron/coding-plugins/signature-verifier';
import {
CODE_OWNED_PLUGIN_SIGNING_KEYS,
createCodeOwnedPluginTrustStore,
} from '../../electron/coding-plugins/trusted-keys';
const PACKAGE_ROOT = '/trusted/plugin';
const ROOT = {
$schema: 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json',
name: 'makelore.example',
version: '1.2.0',
description: 'A declarative example Skill.',
author: { name: 'MakeLore' },
extensions: { 'com.makelore': { capabilityManifest: './com.makelore/capability.json' } },
};
const SKILL_ONLY_CAPABILITY = {
schemaVersion: 2,
pluginId: 'makelore.example',
contractVersion: 1,
scope: 'project',
runtime: { kind: 'skill_only' },
skills: [{ id: 'example-skill', entry: '../skills/example-skill/SKILL.md', grants: [] }],
tools: [],
};
const HOSTED_CAPABILITY = {
schemaVersion: 2,
pluginId: 'makelore.example',
contractVersion: 1,
scope: 'project',
runtime: { kind: 'platform_hosted', protocol: 'makelore-hosted.v1' },
skills: [{ id: 'example-skill', entry: '../skills/example-skill/SKILL.md', grants: ['example.generate'] }],
tools: [{
name: 'example_generate',
label: 'Generate example',
description: 'Generate one example.',
capabilityId: 'example.generate',
operation: 'generate',
roles: ['parent'],
mutation: 'write',
projectWriteLease: false,
permissions: ['hosted.example.generate'],
executionMode: 'synchronous',
inputSchema: {
type: 'object',
additionalProperties: false,
required: ['prompt'],
properties: { prompt: { type: 'string', minLength: 1, maxLength: 4000 } },
},
outputSchema: {
type: 'object',
additionalProperties: false,
required: ['text'],
properties: { text: { type: 'string', maxLength: 20000 } },
},
}],
};
function parse(capability: Record<string, unknown>) {
return parseCodingPluginManifest(ROOT, capability, {
packageRoot: PACKAGE_ROOT,
rootManifestPath: `${PACKAGE_ROOT}/plugin.json`,
capabilityManifestPath: `${PACKAGE_ROOT}/com.makelore/capability.json`,
runtimeKind: capability === SKILL_ONLY_CAPABILITY ? 'skill_only' : 'platform_hosted',
acquisitionMode: 'user_acquired',
releaseId: 'release-1',
provenance: { source: 'marketplace', packageRoot: PACKAGE_ROOT },
});
}
describe('Marketplace Release A package contract', () => {
it('parses immutable skill-only schema 2 with trusted provenance', () => {
const definition = parse(SKILL_ONLY_CAPABILITY);
expect(definition).toMatchObject({
id: 'makelore.example',
version: '1.2.0',
runtimeKind: 'skill_only',
acquisitionMode: 'user_acquired',
releaseId: 'release-1',
provenance: { source: 'marketplace', packageRoot: PACKAGE_ROOT },
skills: [{ id: 'example-skill', grants: [] }],
tools: [],
operations: [],
});
expect(Object.isFrozen(definition)).toBe(true);
expect(Object.isFrozen(definition.skills)).toBe(true);
expect(Object.isFrozen(definition.provenance)).toBe(true);
});
it('parses declarative hosted schema 2 and preserves closed schemas', () => {
const definition = parse(HOSTED_CAPABILITY);
expect(definition).toMatchObject({
runtimeKind: 'platform_hosted',
acquisitionMode: 'user_acquired',
requiresBackend: true,
tools: [{
executionMode: 'synchronous',
outputSchema: HOSTED_CAPABILITY.tools[0]?.outputSchema,
projectWriteLease: false,
}],
operations: [{ capabilityId: 'example.generate', operation: 'generate', toolName: 'example_generate' }],
});
expect(Object.isFrozen(definition.tools[0]?.outputSchema)).toBe(true);
});
it('accepts only the canonical asynchronous job execution mode', () => {
const job = structuredClone(HOSTED_CAPABILITY) as Record<string, unknown>;
(job.tools as Array<Record<string, unknown>>)[0]!.executionMode = 'job';
expect(parse(job).tools[0]?.executionMode).toBe('job');
const accepted = structuredClone(HOSTED_CAPABILITY) as Record<string, unknown>;
(accepted.tools as Array<Record<string, unknown>>)[0]!.executionMode = 'accepted';
expect(() => parse(accepted)).toThrow(CodingPluginManifestError);
});
it('requires the exact capability manifest path even for direct parsing', () => {
const root = structuredClone(ROOT) as Record<string, unknown>;
const extensions = root.extensions as Record<string, unknown>;
const makeLore = extensions['com.makelore'] as Record<string, unknown>;
makeLore.capabilityManifest = './com.makelore/alternate.json';
expect(() => parseCodingPluginManifest(root, SKILL_ONLY_CAPABILITY, {
packageRoot: PACKAGE_ROOT,
rootManifestPath: `${PACKAGE_ROOT}/plugin.json`,
capabilityManifestPath: `${PACKAGE_ROOT}/com.makelore/capability.json`,
})).toThrow(CodingPluginManifestError);
});
it('allows a hosted write tool to request the Main-owned project write lease', () => {
const source = structuredClone(HOSTED_CAPABILITY) as Record<string, unknown>;
(source.tools as Array<Record<string, unknown>>)[0]!.projectWriteLease = true;
expect(parse(source).tools[0]?.projectWriteLease).toBe(true);
});
it.each([
['unknown capability field', (value: Record<string, unknown>) => { value.unknown = true; }],
['unknown runtime field', (value: Record<string, unknown>) => {
(value.runtime as Record<string, unknown>).unknown = true;
}],
['schema-2 adapter privilege', (value: Record<string, unknown>) => { value.adapterId = 'data-service'; }],
['schema-2 local surface privilege', (value: Record<string, unknown>) => { value.surfaces = {}; }],
['skill-only grants', (value: Record<string, unknown>) => {
(value.skills as Array<Record<string, unknown>>)[0]!.grants = ['example.generate'];
}],
['skill-only tools', (value: Record<string, unknown>) => {
value.tools = [{ ...HOSTED_CAPABILITY.tools[0] }];
}],
['hosted local permission', (value: Record<string, unknown>) => {
(value.tools as Array<Record<string, unknown>>)[0]!.permissions = ['project.data.read'];
}],
['hosted foreign permission namespace', (value: Record<string, unknown>) => {
(value.tools as Array<Record<string, unknown>>)[0]!.permissions = ['hosted.makelore.example.generate'];
}],
['hosted read-only project lease', (value: Record<string, unknown>) => {
const tool = (value.tools as Array<Record<string, unknown>>)[0]!;
tool.mutation = 'read';
tool.projectWriteLease = true;
}],
['unbounded schema', (value: Record<string, unknown>) => {
const tool = (value.tools as Array<Record<string, unknown>>)[0]!;
(tool.inputSchema as Record<string, unknown>).properties = {
prompt: { type: 'string' },
};
}],
['implicit required schema', (value: Record<string, unknown>) => {
const tool = (value.tools as Array<Record<string, unknown>>)[0]!;
delete (tool.inputSchema as Record<string, unknown>).required;
}],
])('rejects %s', (_label, mutate) => {
const source = structuredClone(
_label === 'skill-only grants' || _label === 'skill-only tools'
? SKILL_ONLY_CAPABILITY
: HOSTED_CAPABILITY,
) as Record<string, unknown>;
mutate(source);
expect(() => parse(source)).toThrow(CodingPluginManifestError);
});
it('keeps the bundled schema-1 definition byte-compatible', async () => {
const { readFile } = await import('node:fs/promises');
const path = await import('node:path');
const packageRoot = path.resolve('resources/coding-plugins/data-service');
const root = JSON.parse(await readFile(path.join(packageRoot, 'plugin.json'), 'utf8')) as Record<string, unknown>;
const capability = JSON.parse(await readFile(path.join(packageRoot, 'com.makelore/capability.json'), 'utf8')) as Record<string, unknown>;
const definition = parseCodingPluginManifest(root, capability, {
packageRoot,
capabilityManifestPath: path.join(packageRoot, 'com.makelore/capability.json'),
});
expect(definition.id).toBe('makelore.data-service');
expect(definition.runtimeKind).toBe('bundled_typed');
expect(definition.acquisitionMode).toBe('system_included');
expect(definition.tools).toHaveLength(10);
expect(definition.operations).toHaveLength(14);
});
it('serializes and parses the fixed descriptor bytes in field order', () => {
const descriptor = buildPluginReleaseDescriptor({
pluginId: 'makelore.example',
version: '1.2.0',
packageSchemaVersion: 2,
contractVersion: 1,
minMakeloreVersion: '1.0.0',
maxMakeloreVersion: null,
artifact: { sha256: 'a'.repeat(64), sizeBytes: 1234 },
});
const bytes = serializePluginReleaseDescriptor(descriptor);
expect(Buffer.from(bytes).toString('utf8')).toBe(
'{"schema":"makelore-plugin-release.v1","plugin_id":"makelore.example","version":"1.2.0","package_schema_version":2,"contract_version":1,"min_makelore_version":"1.0.0","max_makelore_version":null,"artifact":{"sha256":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","size_bytes":1234}}',
);
expect(parsePluginReleaseDescriptor(bytes)).toEqual(descriptor);
expect(() => parsePluginReleaseDescriptor(
Buffer.from(Buffer.from(bytes).toString('utf8').replace('"schema":"makelore-plugin-release.v1"', '"schema":"other.v1"')),
)).toThrow();
});
it('uses inclusive strict SemVer client bounds', () => {
expect(isMakeLoreVersionCompatible('1.0.0', '1.0.0', null)).toBe(true);
expect(isMakeLoreVersionCompatible('2.0.0', '1.0.0', '2.0.0')).toBe(true);
expect(isMakeLoreVersionCompatible('2.0.1', '1.0.0', '2.0.0')).toBe(false);
expect(isMakeLoreVersionCompatible('1.0.0-alpha.1', '1.0.0', null)).toBe(false);
});
it('verifies Ed25519 signatures only through the injected test key dependency', () => {
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
const artifact = Buffer.from('artifact-bytes');
const descriptor = buildPluginReleaseDescriptor({
pluginId: 'makelore.example', version: '1.2.0', packageSchemaVersion: 2,
contractVersion: 1, minMakeloreVersion: '1.0.0', maxMakeloreVersion: null,
artifact: { sha256: createHash('sha256').update(artifact).digest('hex'), sizeBytes: artifact.length },
});
const descriptorBytes = serializePluginReleaseDescriptor(descriptor);
const signature = sign(null, descriptorBytes, privateKey).toString('base64url');
const verifier = createPluginSignatureVerifier({
keyStore: new Map([['test-key', publicKey.export({ type: 'spki', format: 'der' })]]),
clientVersion: '1.0.0',
});
expect(verifyPluginReleaseSignature({
verifier, keyId: 'test-key', signature, descriptor, artifact,
})).toEqual({ ok: true });
expect(verifyPluginReleaseSignature({
verifier, keyId: 'missing-key', signature, descriptor, artifact,
})).toMatchObject({ ok: false, code: 'plugin_signature_invalid' });
expect(createCodeOwnedPluginTrustStore().get('missing-key')).toBeNull();
expect(Object.keys(CODE_OWNED_PLUGIN_SIGNING_KEYS)).toHaveLength(0);
});
it('rejects bad signature, digest, descriptor identity, and incompatible client', () => {
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
const artifact = Buffer.from('artifact-bytes');
const descriptor = buildPluginReleaseDescriptor({
pluginId: 'makelore.example', version: '1.2.0', packageSchemaVersion: 2,
contractVersion: 1, minMakeloreVersion: '2.0.0', maxMakeloreVersion: null,
artifact: { sha256: createHash('sha256').update(artifact).digest('hex'), sizeBytes: artifact.length },
});
const signature = sign(null, serializePluginReleaseDescriptor(descriptor), privateKey).toString('base64url');
const verifier = createPluginSignatureVerifier({
keyStore: new Map([['test-key', publicKey.export({ type: 'spki', format: 'der' })]]),
clientVersion: '1.0.0',
});
expect(verifyPluginReleaseSignature({ verifier, keyId: 'test-key', signature, descriptor, artifact }))
.toMatchObject({ ok: false, code: 'plugin_incompatible_client' });
const compatible = createPluginSignatureVerifier({
keyStore: new Map([['test-key', publicKey.export({ type: 'spki', format: 'der' })]]),
clientVersion: '2.0.0',
});
expect(verifyPluginReleaseSignature({
verifier: compatible, keyId: 'test-key', signature: `${signature.slice(0, -2)}aa`, descriptor, artifact,
})).toMatchObject({ ok: false, code: 'plugin_signature_invalid' });
expect(verifyPluginReleaseSignature({
verifier: compatible, keyId: 'test-key', signature, descriptor, artifact: Buffer.from('other'),
})).toMatchObject({ ok: false, code: 'plugin_artifact_invalid' });
});
});