001/* 002 * Copyright 2022-2026 Revetware LLC. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package com.soklet; 017 018import org.jspecify.annotations.NonNull; 019 020import javax.annotation.concurrent.ThreadSafe; 021 022import static java.util.Objects.requireNonNull; 023 024/** 025 * Immutable policy separating Skills access from discovery. 026 * 027 * <p>Discovery checks access first and invokes the discovery evaluator only for 028 * accessible registrations. An accessible but undiscoverable registration is 029 * omitted from listing but remains eligible for direct access. Direct skill 030 * lookup and file reads freshly evaluate access; an earlier list is not a grant. 031 * Denial supplies no grant through that registration, rather than vetoing a 032 * shared file independently granted through another accessible owner. 033 * 034 * <p>Evaluators receive canonical registrations and must support concurrent 035 * invocation. The immutable policy does not make captured application state 036 * thread-safe. Callback failures and null results fail closed; no partially 037 * authorized result is published. Policies retain reference identity and do not 038 * inspect evaluator capabilities for equality or diagnostic rendering. 039 * 040 * @author <a href="https://www.revetkn.com">Mark Allen</a> 041 */ 042@ThreadSafe 043public final class McpSkillAccessPolicy { 044 @NonNull 045 private static final McpSkillAccessPolicy ALLOW_ALL = new McpSkillAccessPolicy( 046 (requestContext, skillRegistration, invocationFeatures) -> true, 047 (requestContext, skillRegistration, invocationFeatures) -> true); 048 @NonNull 049 private final AccessEvaluator accessEvaluator; 050 @NonNull 051 private final DiscoveryEvaluator discoveryEvaluator; 052 053 private McpSkillAccessPolicy(@NonNull AccessEvaluator accessEvaluator, 054 @NonNull DiscoveryEvaluator discoveryEvaluator) { 055 this.accessEvaluator = requireNonNull(accessEvaluator); 056 this.discoveryEvaluator = requireNonNull(discoveryEvaluator); 057 } 058 059 /** 060 * Creates a policy from two required application-owned evaluators. 061 * 062 * @param accessEvaluator registration access evaluator 063 * @param discoveryEvaluator listing eligibility evaluator 064 * @return immutable policy 065 * @throws NullPointerException if either evaluator is null 066 */ 067 @NonNull 068 public static McpSkillAccessPolicy fromEvaluators(@NonNull AccessEvaluator accessEvaluator, 069 @NonNull DiscoveryEvaluator discoveryEvaluator) { 070 return new McpSkillAccessPolicy(accessEvaluator, discoveryEvaluator); 071 } 072 073 /** @return shared policy permitting access to and discovery of every registered skill */ 074 @NonNull 075 public static McpSkillAccessPolicy allowAllInstance() { 076 return ALLOW_ALL; 077 } 078 079 boolean isSkillAccessible(@NonNull McpRequestContext requestContext, 080 @NonNull McpSkillRegistration skillRegistration, 081 @NonNull McpInvocationFeatures invocationFeatures) throws Exception { 082 return requireNonNull(this.accessEvaluator.isSkillAccessible( 083 requireNonNull(requestContext), requireNonNull(skillRegistration), 084 requireNonNull(invocationFeatures)), 085 "The MCP skill access evaluator returned null."); 086 } 087 088 boolean isSkillDiscoverable(@NonNull McpRequestContext requestContext, 089 @NonNull McpSkillRegistration skillRegistration, 090 @NonNull McpInvocationFeatures invocationFeatures) throws Exception { 091 return requireNonNull(this.discoveryEvaluator.isSkillDiscoverable( 092 requireNonNull(requestContext), requireNonNull(skillRegistration), 093 requireNonNull(invocationFeatures)), 094 "The MCP skill discovery evaluator returned null."); 095 } 096 097 /** @return diagnostic rendering without application-owned evaluator data */ 098 @Override 099 @NonNull 100 public String toString() { 101 return "McpSkillAccessPolicy{evaluators=<redacted>}"; 102 } 103 104 /** 105 * Concurrently callable registration access evaluator. 106 * 107 * @author <a href="https://www.revetkn.com">Mark Allen</a> 108 */ 109 @ThreadSafe 110 @FunctionalInterface 111 public interface AccessEvaluator { 112 /** 113 * Determines whether this registration may grant the caller access. 114 * 115 * @param requestContext admitted request context 116 * @param skillRegistration canonical registration being checked 117 * @param invocationFeatures features scoped to this authorization check 118 * @return true if accessible; never null 119 * @throws Exception if access cannot be established 120 */ 121 @NonNull 122 Boolean isSkillAccessible(@NonNull McpRequestContext requestContext, 123 @NonNull McpSkillRegistration skillRegistration, 124 @NonNull McpInvocationFeatures invocationFeatures) throws Exception; 125 } 126 127 /** 128 * Concurrently callable discovery evaluator for accessible registrations. 129 * 130 * @author <a href="https://www.revetkn.com">Mark Allen</a> 131 */ 132 @ThreadSafe 133 @FunctionalInterface 134 public interface DiscoveryEvaluator { 135 /** 136 * Determines whether an accessible registration is a listing candidate. 137 * Discovery does not independently grant access to a skill or its files. 138 * 139 * @param requestContext admitted request context 140 * @param skillRegistration canonical accessible registration 141 * @param invocationFeatures features scoped to this discovery check 142 * @return true if discoverable; never null 143 * @throws Exception if discovery eligibility cannot be established 144 */ 145 @NonNull 146 Boolean isSkillDiscoverable(@NonNull McpRequestContext requestContext, 147 @NonNull McpSkillRegistration skillRegistration, 148 @NonNull McpInvocationFeatures invocationFeatures) throws Exception; 149 } 150}