moving
This commit is contained in:
@@ -33,6 +33,14 @@ import {
|
||||
} from '@waggle/shared';
|
||||
import type { McpRuntime, McpServerState } from '@waggle/agent';
|
||||
import { scanForInjection, type RecordAuditInput } from '@waggle/core';
|
||||
import {
|
||||
MCP_SERVERS,
|
||||
MarketplaceInstaller,
|
||||
createMarketplaceMcpProvenance,
|
||||
type InstallManifest,
|
||||
type MarketplaceMcpProvenance,
|
||||
type McpServerConfig,
|
||||
} from '@waggle/marketplace';
|
||||
import {
|
||||
loadMcpConfig,
|
||||
saveMcpServerEntry,
|
||||
@@ -49,6 +57,13 @@ import { authHeaders, clampStr, clampStrArray } from './validate.js';
|
||||
* under the instance's own 30s per-request timeout. */
|
||||
const TEST_TIMEOUT_MS = 8_000;
|
||||
|
||||
const RESERVED_MARKETPLACE_MCP_NAMES = new Set(
|
||||
[
|
||||
...MCP_CATALOG.map((server) => server.id),
|
||||
...MCP_SERVERS.flatMap((pkg) => pkg.install_manifest?.mcp_config?.name ?? []),
|
||||
],
|
||||
);
|
||||
|
||||
/** Clamp env to ≤64 pairs with bounded key/value lengths. Non-record shapes
|
||||
* (and non-string values) pass through untouched so validateMcpEntry still
|
||||
* rejects them with its precise error message. */
|
||||
@@ -231,28 +246,53 @@ export async function mcpRoutes(fastify: FastifyInstance) {
|
||||
}
|
||||
|
||||
// Resolve catalog id → marketplace package (mcp-registry seeds share ids)
|
||||
const row = db.getRawDb().prepare(
|
||||
"SELECT id FROM packages WHERE name = ? AND waggle_install_type = 'mcp'",
|
||||
).get(mcpId) as { id: number } | undefined;
|
||||
const row = db.getRawDb().prepare(`
|
||||
SELECT p.id, p.name, p.version, p.install_manifest AS installManifest,
|
||||
s.name AS sourceName, s.source_type AS sourceType,
|
||||
s.is_custom AS sourceIsCustom
|
||||
FROM packages p
|
||||
INNER JOIN sources s ON s.id = p.source_id
|
||||
WHERE p.name = ?
|
||||
AND p.waggle_install_type = 'mcp'
|
||||
AND s.name = 'mcp_registry'
|
||||
AND s.source_type = 'registry'
|
||||
AND s.is_custom = 0
|
||||
ORDER BY p.id
|
||||
LIMIT 1
|
||||
`).get(mcpId) as {
|
||||
id: number;
|
||||
name: string;
|
||||
version: string;
|
||||
installManifest: string | InstallManifest;
|
||||
sourceName: string;
|
||||
sourceType: 'registry';
|
||||
sourceIsCustom: number;
|
||||
} | undefined;
|
||||
if (!row) {
|
||||
return reply.code(404).send({ error: `No marketplace MCP package named "${mcpId}"` });
|
||||
}
|
||||
|
||||
// Resolve + validate the manifest BEFORE delegating: a package whose
|
||||
// mcp_config fails the same validation the C4 boot loader applies would
|
||||
// install "successfully" now and then be skipped at every reboot. Reject
|
||||
// it up front (422) instead of half-installing.
|
||||
const pkg = db.getPackage(row.id);
|
||||
const manifest = pkg?.install_manifest as { mcp_config?: { name: string; command: string; args: string[]; env?: Record<string, string> } } | null;
|
||||
const mcpConfig = manifest?.mcp_config;
|
||||
if (!mcpConfig) {
|
||||
return reply.code(422).send({ installed: false, error: 'Package manifest has no mcp_config' });
|
||||
}
|
||||
const manifestInvalid = validateMcpEntry(mcpConfig.name, { command: mcpConfig.command, args: mcpConfig.args, env: mcpConfig.env });
|
||||
if (manifestInvalid) {
|
||||
let expectedMcpProvenance: MarketplaceMcpProvenance;
|
||||
try {
|
||||
const manifest = (typeof row.installManifest === 'string'
|
||||
? JSON.parse(row.installManifest)
|
||||
: row.installManifest) as InstallManifest;
|
||||
const mcpConfig = manifest.mcp_config;
|
||||
if (!mcpConfig) throw new Error('Marketplace MCP snapshot has no configuration.');
|
||||
expectedMcpProvenance = createMarketplaceMcpProvenance(
|
||||
{
|
||||
name: row.sourceName,
|
||||
source_type: row.sourceType,
|
||||
is_custom: Boolean(row.sourceIsCustom),
|
||||
},
|
||||
{ name: row.name, version: row.version },
|
||||
mcpConfig,
|
||||
);
|
||||
} catch (err) {
|
||||
return reply.code(422).send({
|
||||
installed: false,
|
||||
error: `Package mcp_config would not survive a restart (boot-loader validation): ${manifestInvalid}`,
|
||||
success: false,
|
||||
error: `Canonical marketplace MCP snapshot is invalid: ${(err as Error).message}`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -265,12 +305,17 @@ export async function mcpRoutes(fastify: FastifyInstance) {
|
||||
settings: body.settings,
|
||||
force: body.force,
|
||||
forceInsecure: body.forceInsecure,
|
||||
expectedInstallType: 'mcp',
|
||||
expectedMcpProvenance,
|
||||
},
|
||||
});
|
||||
const result = res.json() as {
|
||||
success?: boolean;
|
||||
blocked?: boolean;
|
||||
scanResult?: { blocked?: boolean; overall_severity?: string };
|
||||
mcpSourceConfig?: McpServerConfig;
|
||||
mcpProvenance?: MarketplaceMcpProvenance;
|
||||
errorCode?: 'PACKAGE_IDENTITY_CHANGED';
|
||||
};
|
||||
if (res.statusCode >= 400 || result.success === false) {
|
||||
// A SecurityGate block (route-level 403 OR installer-level 422 with
|
||||
@@ -285,28 +330,58 @@ export async function mcpRoutes(fastify: FastifyInstance) {
|
||||
});
|
||||
}
|
||||
|
||||
// The installer wrote the .mcp.json entry; apply the same env templating
|
||||
// so the runtime registration matches what was persisted.
|
||||
const env = mcpConfig.env ? { ...mcpConfig.env } : undefined;
|
||||
if (body.settings && env) {
|
||||
for (const [key, value] of Object.entries(body.settings)) {
|
||||
for (const envKey of Object.keys(env)) {
|
||||
if (env[envKey] === `\${${key}}` || env[envKey] === '') env[envKey] = value;
|
||||
}
|
||||
}
|
||||
// Only the installer's exact validated source snapshot may reach an
|
||||
// execution sink. The receipt is secret-free; settings are normalized by
|
||||
// the same pure helper used for the installer's own .mcp.json.
|
||||
if (!result.mcpSourceConfig || !result.mcpProvenance) {
|
||||
return reply.code(500).send({
|
||||
installed: false,
|
||||
error: 'Marketplace installer returned no complete validated MCP receipt',
|
||||
});
|
||||
}
|
||||
if (
|
||||
row.name !== mcpId
|
||||
|| !MarketplaceInstaller.mcpProvenanceMatches(result.mcpProvenance, expectedMcpProvenance)
|
||||
|| result.mcpSourceConfig.name !== row.name
|
||||
) {
|
||||
return reply.code(409).send({
|
||||
installed: false,
|
||||
error: 'Marketplace MCP package changed during installation; retry from the refreshed catalog',
|
||||
});
|
||||
}
|
||||
let configured: McpServerConfig;
|
||||
try {
|
||||
configured = MarketplaceInstaller.configureMcpServer(result.mcpSourceConfig, body.settings);
|
||||
} catch (err) {
|
||||
return reply.code(422).send({
|
||||
installed: false,
|
||||
error: `Marketplace MCP receipt validation failed: ${(err as Error).message}`,
|
||||
});
|
||||
}
|
||||
const entry: PersistedMcpEntry = {
|
||||
command: configured.command,
|
||||
args: configured.args,
|
||||
...(configured.env ? { env: configured.env } : {}),
|
||||
provenance: result.mcpProvenance,
|
||||
};
|
||||
const entryInvalid = validateMcpEntry(configured.name, entry);
|
||||
if (entryInvalid) {
|
||||
return reply.code(422).send({
|
||||
installed: false,
|
||||
error: `Configured marketplace MCP would not survive a restart (boot-loader validation): ${entryInvalid}`,
|
||||
});
|
||||
}
|
||||
const entry: PersistedMcpEntry = { command: mcpConfig.command, args: mcpConfig.args, ...(env ? { env } : {}) };
|
||||
// Persist at the server's dataDir too — the installer writes to
|
||||
// WAGGLE_DATA_DIR/~/.waggle, which may differ from a custom dataDir.
|
||||
saveMcpServerEntry(dataDir(), mcpConfig.name, entry);
|
||||
saveMcpServerEntry(dataDir(), configured.name, entry);
|
||||
|
||||
const runtime = getRuntime();
|
||||
let status: McpServerState | 'unregistered' = 'unregistered';
|
||||
let startError: string | undefined;
|
||||
if (runtime) {
|
||||
if (runtime.getServer(mcpConfig.name)) await runtime.removeServer(mcpConfig.name);
|
||||
runtime.addServer({ name: mcpConfig.name, command: entry.command, args: entry.args, env: entry.env });
|
||||
const instance = runtime.getServer(mcpConfig.name)!;
|
||||
if (runtime.getServer(configured.name)) await runtime.removeServer(configured.name);
|
||||
runtime.addServer({ name: configured.name, command: entry.command, args: entry.args, env: entry.env });
|
||||
const instance = runtime.getServer(configured.name)!;
|
||||
try {
|
||||
await withTimeout(instance.start(), TEST_TIMEOUT_MS, 'MCP start');
|
||||
} catch (err) {
|
||||
@@ -327,7 +402,7 @@ export async function mcpRoutes(fastify: FastifyInstance) {
|
||||
const scanSeverity = result.scanResult?.overall_severity;
|
||||
const overrode = result.scanResult?.blocked === true;
|
||||
recordMcpAudit({
|
||||
capabilityName: mcpConfig.name,
|
||||
capabilityName: configured.name,
|
||||
source: 'marketplace',
|
||||
riskLevel: scanSeverity === 'CRITICAL' ? 'critical'
|
||||
: scanSeverity === 'HIGH' ? 'high'
|
||||
@@ -344,8 +419,9 @@ export async function mcpRoutes(fastify: FastifyInstance) {
|
||||
return {
|
||||
installed: true,
|
||||
mcpId,
|
||||
server: mcpConfig.name,
|
||||
server: configured.name,
|
||||
status,
|
||||
mcpProvenance: result.mcpProvenance,
|
||||
...(startError ? { startError } : {}),
|
||||
};
|
||||
});
|
||||
@@ -374,6 +450,11 @@ export async function mcpRoutes(fastify: FastifyInstance) {
|
||||
? { workspaceId: typeof body.workspaceId === 'string' ? clampStr(body.workspaceId, 200) : body.workspaceId }
|
||||
: {}),
|
||||
};
|
||||
if (RESERVED_MARKETPLACE_MCP_NAMES.has(name)) {
|
||||
return reply.code(409).send({
|
||||
error: `MCP server name "${name}" is reserved for verified marketplace installs`,
|
||||
});
|
||||
}
|
||||
const invalid = validateMcpEntry(name, candidate);
|
||||
if (invalid) return reply.code(400).send({ error: invalid });
|
||||
|
||||
@@ -522,36 +603,56 @@ export async function mcpRoutes(fastify: FastifyInstance) {
|
||||
const { id } = request.params as { id: string };
|
||||
const runtime = getRuntime();
|
||||
const hadInstance = !!runtime?.getServer(id);
|
||||
const persisted = loadMcpConfig(dataDir()).mcpServers[id];
|
||||
const removedConfig = removeMcpServerEntry(dataDir(), id);
|
||||
if (!hadInstance && !removedConfig) {
|
||||
return reply.code(404).send({ error: `MCP server "${id}" is not installed` });
|
||||
}
|
||||
await runtime?.removeServer(id); // stops the process if running
|
||||
const provenance = persisted?.provenance;
|
||||
const verifiedMarketplaceOrigin = provenance?.kind === 'marketplace';
|
||||
|
||||
// Keep the marketplace's installed:true annotation honest (A4): if this
|
||||
// server came from a marketplace package, retire that installation row
|
||||
// too — otherwise /api/marketplace/search keeps claiming it's installed.
|
||||
try {
|
||||
const db = fastify.marketplace;
|
||||
const pkgRow = db?.getRawDb().prepare(
|
||||
"SELECT id FROM packages WHERE name = ? AND waggle_install_type = 'mcp'",
|
||||
).get(id) as { id: number } | undefined;
|
||||
if (pkgRow && db!.isInstalled(pkgRow.id)) {
|
||||
db!.markUninstalled(pkgRow.id);
|
||||
if (verifiedMarketplaceOrigin) {
|
||||
try {
|
||||
const db = fastify.marketplace;
|
||||
const pkgRow = db?.getRawDb().prepare(`
|
||||
SELECT p.id
|
||||
FROM packages p
|
||||
INNER JOIN sources s ON s.id = p.source_id
|
||||
WHERE p.name = ?
|
||||
AND p.version = ?
|
||||
AND p.waggle_install_type = 'mcp'
|
||||
AND s.name = ?
|
||||
AND s.source_type = 'registry'
|
||||
AND s.is_custom = 0
|
||||
ORDER BY p.id
|
||||
LIMIT 1
|
||||
`).get(
|
||||
provenance.packageName,
|
||||
provenance.packageVersion,
|
||||
provenance.sourceName,
|
||||
) as { id: number } | undefined;
|
||||
if (pkgRow && db!.isInstalled(pkgRow.id)) {
|
||||
db!.markUninstalled(pkgRow.id);
|
||||
}
|
||||
} catch (err) {
|
||||
fastify.log.warn({ err, id }, 'marketplace bookkeeping on MCP revoke failed (non-blocking)');
|
||||
}
|
||||
} catch (err) {
|
||||
fastify.log.warn({ err, id }, 'marketplace bookkeeping on MCP revoke failed (non-blocking)');
|
||||
}
|
||||
|
||||
recordMcpAudit({
|
||||
capabilityName: id,
|
||||
source: 'mcp',
|
||||
source: verifiedMarketplaceOrigin ? 'marketplace' : 'mcp',
|
||||
version: verifiedMarketplaceOrigin ? provenance.packageVersion : null,
|
||||
riskLevel: 'low',
|
||||
trustSource: 'local_user',
|
||||
trustSource: verifiedMarketplaceOrigin ? 'third_party_verified' : 'local_user',
|
||||
approvalClass: 'standard',
|
||||
action: 'rejected',
|
||||
action: 'uninstalled',
|
||||
initiator: 'user',
|
||||
detail: 'MCP server revoked — removed from runtime and persisted config',
|
||||
detail: `MCP server revoked — removed from runtime and persisted config (${verifiedMarketplaceOrigin ? 'verified marketplace' : 'custom local'} origin)`,
|
||||
});
|
||||
|
||||
return { ok: true, id, stoppedInstance: hadInstance, removedConfig };
|
||||
|
||||
Reference in New Issue
Block a user