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}