Skip to main content
Glama

IT-MCP

by acampkin95
policyEnforcer.ts11 kB
/** * PolicyEnforcer Service * * Evaluates tool invocations against policy rules and enforces capability-based access control. * Integrates with approval workflow for high-risk operations. */ import type { AuthorizationContext, PolicyDecision, OperationPolicy, RiskLevel, } from "../types/policy.js"; import { getPolicyForTool } from "../config/policies.js"; import { CommandQueueService } from "./commandQueue.js"; /** * Policy enforcement service for IT-MCP tools */ export class PolicyEnforcer { private commandQueue: CommandQueueService; private auditLog: (entry: any) => void; constructor( commandQueue: CommandQueueService, auditLogger?: (entry: any) => void ) { this.commandQueue = commandQueue; this.auditLog = auditLogger || (() => {}); } /** * Evaluate a tool invocation against policy rules * * @param context Authorization context containing caller, tool, operation, and capabilities * @returns Policy decision (allow, deny, or require_approval) */ async evaluateToolInvocation( context: AuthorizationContext ): Promise<PolicyDecision> { // 1. Get policy for this tool/operation const policy = getPolicyForTool(context.tool, context.operation); if (!policy) { // No policy defined - deny by default (fail-safe) return { action: "deny", reason: `No policy defined for ${context.tool}.${context.operation}`, riskLevel: "CRITICAL", requiresApproval: false, }; } // 2. Check capability authorization const capabilityCheck = this.checkCapabilities( context.userCapabilities, policy.requires ); if (!capabilityCheck.authorized) { return { action: "deny", reason: `Missing required capabilities: ${capabilityCheck.missing.join(", ")}`, riskLevel: policy.danger, requiresApproval: false, missingCapabilities: capabilityCheck.missing, }; } // 3. Assess need for approval const needsApproval = await this.assessNeedForApproval(context, policy); if (needsApproval) { return { action: "require_approval", reason: this.generateApprovalReason(context, policy), riskLevel: policy.danger, requiresApproval: true, approvalReason: this.generateApprovalReason(context, policy), }; } // 4. Allow execution return { action: "allow", reason: `Authorized: ${context.tool}.${context.operation} (${policy.danger} risk)`, riskLevel: policy.danger, requiresApproval: false, }; } /** * Check if user has required capabilities * * @param userCapabilities Capabilities assigned to the caller * @param requiredCapabilities Capabilities required by the policy * @returns Authorization status and missing capabilities */ private checkCapabilities( userCapabilities: readonly string[], requiredCapabilities: readonly string[] ): { authorized: boolean; missing: string[] } { const missing: string[] = []; for (const required of requiredCapabilities) { if (!userCapabilities.includes(required)) { missing.push(required); } } return { authorized: missing.length === 0, missing, }; } /** * Assess whether operation requires human approval * * @param context Authorization context * @param policy Operation policy * @returns True if approval is required */ private async assessNeedForApproval( context: AuthorizationContext, policy: OperationPolicy ): Promise<boolean> { // 1. Explicit interactiveOnly flag if (policy.interactiveOnly === true) { return true; } // 2. CRITICAL risk level always requires approval if (policy.danger === "CRITICAL") { return true; } // 3. HIGH risk with dangerous parameters if (policy.danger === "HIGH" && this.hasDangerousParams(context.args)) { return true; } // 4. Sudo operations always require approval if ( policy.requires.includes("local-sudo") && context.args.requiresSudo === true ) { return true; } // 5. Remote execution with sudo if ( (policy.requires.includes("ssh-linux") || policy.requires.includes("ssh-mac")) && context.args.requiresSudo === true ) { return true; } return false; } /** * Check if arguments contain dangerous patterns * * @param args Tool invocation arguments * @returns True if dangerous patterns detected */ private hasDangerousParams(args: Record<string, any>): boolean { const argsStr = JSON.stringify(args).toLowerCase(); const dangerousPatterns = [ "rm -rf", "dd if=", "mkfs", "fdisk", "parted", "format", "systemctl stop", "systemctl disable", "kill -9", "pkill", "iptables -f", "ufw delete", "firewall-cmd --remove", "> /dev/", "curl | sh", "wget | sh", "eval", "chmod 777", "chown root", ]; for (const pattern of dangerousPatterns) { if (argsStr.includes(pattern)) { return true; } } // Check for --force flags if (args.force === true || args.noConfirm === true) { return true; } return false; } /** * Generate human-readable approval reason * * @param context Authorization context * @param policy Operation policy * @returns Approval reason message */ private generateApprovalReason( context: AuthorizationContext, policy: OperationPolicy ): string { const reasons: string[] = []; if (policy.interactiveOnly) { reasons.push("Operation marked as interactiveOnly"); } if (policy.danger === "CRITICAL") { reasons.push("CRITICAL risk level"); } if (policy.requires.includes("local-sudo")) { reasons.push("Requires elevated privileges (sudo)"); } if (policy.requires.includes("system-modify")) { reasons.push("Modifies system configuration"); } if (policy.requires.includes("firewall-admin")) { reasons.push("Firewall rule modification"); } if (policy.requires.includes("service-control")) { reasons.push("Service lifecycle control"); } if (this.hasDangerousParams(context.args)) { reasons.push("Potentially destructive parameters detected"); } if (reasons.length === 0) { reasons.push(`${policy.danger} risk operation`); } return `Approval required: ${reasons.join(", ")}`; } /** * Request approval for a high-risk operation * * Submits the operation to the approval queue and returns a job ID * for tracking. * * @param context Authorization context * @param decision Policy decision * @returns Job ID for tracking approval status */ async requestApproval( context: AuthorizationContext, decision: PolicyDecision ): Promise<{ jobId: string }> { // Submit to command queue with 'queued' status // Human approver will mark as 'approved' or 'rejected' const jobId = await this.commandQueue.submitCommand({ toolName: context.tool, params: context.args, requestedCapabilities: [...decision.missingCapabilities || []], targetAgentId: context.targetAgent, priority: this.riskLevelToPriority(decision.riskLevel), }); // Audit log the approval request this.auditLog({ type: "approval_requested", jobId, context, decision, timestamp: new Date().toISOString(), }); return { jobId }; } /** * Convert risk level to command queue priority * * @param riskLevel Risk level from policy * @returns Priority level for command queue */ private riskLevelToPriority( riskLevel: RiskLevel ): "low" | "normal" | "high" | "urgent" { switch (riskLevel) { case "CRITICAL": return "urgent"; case "HIGH": return "high"; case "MEDIUM": return "normal"; case "LOW": return "low"; default: return "normal"; } } /** * Check if a queued command has been approved * * @param jobId Job ID from requestApproval() * @returns Approval status and approver info */ async checkApprovalStatus(jobId: string): Promise<{ approved: boolean; status: string; approver?: string; approvedAt?: string; }> { const stats = await this.commandQueue.getQueueStats(); // This is a simplified check - in production, you'd query the // approval metadata directly from the database const job = await this.commandQueue.getCommandById(jobId); if (!job) { return { approved: false, status: "not_found", }; } // In a full implementation, add approval metadata to command_queue schema // For now, we use status field as a proxy const approved = job.status === "picked" || job.status === "executing"; return { approved, status: job.status, // TODO: Add approver and approvedAt from metadata when schema extended }; } /** * Deny approval for a queued command * * @param jobId Job ID from requestApproval() * @param reason Rejection reason * @param rejectedBy User who rejected the request */ async denyApproval( jobId: string, reason: string, rejectedBy: string ): Promise<void> { // Mark command as failed in queue await this.commandQueue.markCommandFailed(jobId, reason); // Audit log the rejection this.auditLog({ type: "approval_denied", jobId, reason, rejectedBy, timestamp: new Date().toISOString(), }); } /** * Grant approval for a queued command * * @param jobId Job ID from requestApproval() * @param approvedBy User who approved the request */ async grantApproval(jobId: string, approvedBy: string): Promise<void> { // This moves the command to 'picked' state, making it eligible for execution // The actual execution will be handled by the tool handler // Audit log the approval this.auditLog({ type: "approval_granted", jobId, approvedBy, timestamp: new Date().toISOString(), }); // In production, you'd update approval metadata in the database // For now, the command remains in queue until picked by executor } } /** * Singleton instance for global access * Initialized by server.ts with dependencies */ let policyEnforcerInstance: PolicyEnforcer | null = null; export function initializePolicyEnforcer( commandQueue: CommandQueueService, auditLogger?: (entry: any) => void ): PolicyEnforcer { policyEnforcerInstance = new PolicyEnforcer(commandQueue, auditLogger); return policyEnforcerInstance; } export function getPolicyEnforcer(): PolicyEnforcer { if (!policyEnforcerInstance) { throw new Error( "PolicyEnforcer not initialized. Call initializePolicyEnforcer() first." ); } return policyEnforcerInstance; }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/acampkin95/MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server