mirror of
https://github.com/ruvnet/RuView
synced 2026-07-31 18:51:42 +00:00
90b29595fb
Adds the accepted ADR-285 Homecore metaharness, WASM-first kernel, read-only MCP guidance, guarded local host adapters, reviewed memory, and provenance-only npm release gates.
108 lines
3.5 KiB
JavaScript
108 lines
3.5 KiB
JavaScript
// SPDX-License-Identifier: MIT
|
|
// Default-deny MCP authority policy.
|
|
|
|
export const TOOL_POLICY = Object.freeze({
|
|
homecore_guidance: { class: 'read', readOnly: true },
|
|
homecore_wasm_status: { class: 'read', readOnly: true },
|
|
homecore_doctor: { class: 'read', readOnly: true },
|
|
homecore_memory_search: { class: 'read', readOnly: true },
|
|
homecore_verify: {
|
|
class: 'execute',
|
|
readOnly: false,
|
|
writesBuildArtifacts: true,
|
|
mcpExposed: false,
|
|
},
|
|
});
|
|
|
|
function typeMatches(value, type) {
|
|
if (type === 'array') return Array.isArray(value);
|
|
if (type === 'object') return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
if (type === 'number') return typeof value === 'number' && Number.isFinite(value);
|
|
if (type === 'integer') return Number.isSafeInteger(value);
|
|
return typeof value === type;
|
|
}
|
|
|
|
export function validateArguments(schema, value, path = '$') {
|
|
const errors = [];
|
|
const type = schema.type || 'object';
|
|
if (!typeMatches(value, type)) return [`${path} must be ${type}`];
|
|
|
|
if (type === 'object') {
|
|
const properties = schema.properties || {};
|
|
for (const key of schema.required || []) {
|
|
if (!(key in value)) errors.push(`${path}.${key} is required`);
|
|
}
|
|
for (const [key, item] of Object.entries(value)) {
|
|
if (!Object.hasOwn(properties, key)) {
|
|
if (schema.additionalProperties !== true) errors.push(`${path}.${key} is not allowed`);
|
|
continue;
|
|
}
|
|
errors.push(...validateArguments(properties[key], item, `${path}.${key}`));
|
|
}
|
|
}
|
|
|
|
if (type === 'array') {
|
|
if (schema.maxItems !== undefined && value.length > schema.maxItems) {
|
|
errors.push(`${path} exceeds maxItems`);
|
|
}
|
|
if (schema.items) {
|
|
value.forEach((item, index) => {
|
|
errors.push(...validateArguments(schema.items, item, `${path}[${index}]`));
|
|
});
|
|
}
|
|
}
|
|
|
|
if (schema.enum && !schema.enum.includes(value)) {
|
|
errors.push(`${path} must be one of ${schema.enum.join(', ')}`);
|
|
}
|
|
if (type === 'string') {
|
|
if (schema.minLength !== undefined && value.length < schema.minLength) {
|
|
errors.push(`${path} is too short`);
|
|
}
|
|
if (schema.maxLength !== undefined && value.length > schema.maxLength) {
|
|
errors.push(`${path} is too long`);
|
|
}
|
|
}
|
|
if (type === 'number' || type === 'integer') {
|
|
if (schema.minimum !== undefined && value < schema.minimum) {
|
|
errors.push(`${path} is below minimum`);
|
|
}
|
|
if (schema.maximum !== undefined && value > schema.maximum) {
|
|
errors.push(`${path} exceeds maximum`);
|
|
}
|
|
}
|
|
return errors;
|
|
}
|
|
|
|
export function authorizeTool(name, args, context = {}) {
|
|
const policy = TOOL_POLICY[name] || { class: 'unknown', denied: true };
|
|
if (policy.denied) return { ok: false, reason: 'policy_missing', policy };
|
|
if (context.source === 'mcp' && policy.mcpExposed === false) {
|
|
return { ok: false, reason: 'mcp_not_exposed', policy };
|
|
}
|
|
if (context.source !== 'mcp' || policy.readOnly) return { ok: true, policy };
|
|
if (policy.confirmField && args?.[policy.confirmField] !== true) {
|
|
return { ok: false, reason: 'not_confirmed', policy };
|
|
}
|
|
const grants = new Set(context.grants || []);
|
|
if (!grants.has(policy.class)) {
|
|
return {
|
|
ok: false,
|
|
reason: 'authority_denied',
|
|
requiredGrant: policy.class,
|
|
policy,
|
|
};
|
|
}
|
|
return { ok: true, policy };
|
|
}
|
|
|
|
export function mcpAnnotations(name) {
|
|
const policy = TOOL_POLICY[name] || {};
|
|
return {
|
|
readOnlyHint: policy.readOnly === true,
|
|
destructiveHint: false,
|
|
idempotentHint: policy.readOnly === true,
|
|
openWorldHint: false,
|
|
};
|
|
}
|