feat(coding): add marketplace package trust primitives
This commit is contained in:
272
tests/unit/coding-plugin-marketplace-contract.test.ts
Normal file
272
tests/unit/coding-plugin-marketplace-contract.test.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
// @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' });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user