273 lines
12 KiB
TypeScript
273 lines
12 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('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.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 project lease', (value: Record<string, unknown>) => {
|
|
(value.tools as Array<Record<string, unknown>>)[0]!.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' });
|
|
});
|
|
});
|