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 com.soklet.internal.mcp.skills.McpSkillRuntimeBridge;
019import org.jspecify.annotations.NonNull;
020import org.jspecify.annotations.Nullable;
021
022import javax.annotation.concurrent.NotThreadSafe;
023import javax.annotation.concurrent.ThreadSafe;
024import java.net.URI;
025import java.util.List;
026import java.util.Locale;
027import java.util.Objects;
028import java.util.Optional;
029import java.util.Set;
030
031import static java.util.Objects.requireNonNull;
032
033/**
034 * Immutable association between a Skills snapshot and its root resource URI.
035 *
036 * <p>Construction validates the generated manifest but does not publish endpoint
037 * resources. The URI identifies the root {@code SKILL.md}, not a directory.
038 * Locale and cache settings do not grant authorization to read the bundle.
039 *
040 * @author <a href="https://www.revetkn.com">Mark Allen</a>
041 */
042@ThreadSafe
043public final class McpSkillRegistration {
044        @NonNull private final URI uri;
045        @NonNull private final McpSkillBundle skillBundle;
046        @NonNull private final Set<@NonNull McpProtocolVersion> protocolVersions;
047        @Nullable private final Locale locale;
048        @NonNull private final McpCachePolicy cachePolicy;
049        @NonNull private final List<@NonNull Resource> resources;
050        private final McpSkillRuntimeBridge.@NonNull Registration runtimeRegistration;
051
052        private McpSkillRegistration(@NonNull Builder builder) {
053                this.uri = builder.uri;
054                this.skillBundle = builder.skillBundle;
055                this.protocolVersions = builder.protocolVersions;
056                this.locale = builder.locale;
057                this.cachePolicy = builder.cachePolicy;
058                this.runtimeRegistration = McpSkillRuntimeBridge.register(this.uri,
059                                this.skillBundle.runtimeBundle(), this.cachePolicy);
060                this.resources = this.runtimeRegistration.resources().stream().map(Resource::new).toList();
061        }
062
063        /**
064         * Starts registration of a bundle at its root document URI.
065         *
066         * @param uri absolute normalized hierarchical ASCII URI ending in literal
067         * {@code /SKILL.md}, without query, fragment, dot segments, or encoded separators
068         * @param skillBundle validated immutable snapshot whose name matches the URI's
069         * final skill-directory segment
070         * @param protocolVersions nonempty exact revisions serving this Skill
071         * @return mutable builder with no locale and private zero-TTL cache policy
072         * @throws NullPointerException if either argument is null
073         */
074        @NonNull
075        public static Builder withUriAndSkillBundle(@NonNull URI uri,
076                        @NonNull McpSkillBundle skillBundle,
077                        @NonNull Set<@NonNull McpProtocolVersion> protocolVersions) {
078                return new Builder(uri, skillBundle,
079                                McpProtocolVersion.requiredSet(protocolVersions));
080        }
081
082        /** @return original root {@code SKILL.md} URI */
083        @NonNull
084        public URI getUri() { return this.uri; }
085
086        /** @return the supplied immutable bundle */
087        @NonNull
088        public McpSkillBundle getSkillBundle() { return this.skillBundle; }
089
090        /** @return exact protocol revisions on which this Skill is available */
091        @NonNull
092        public Set<@NonNull McpProtocolVersion> getProtocolVersions() {
093                return this.protocolVersions;
094        }
095
096        /** @return configured locale, or empty when omitted */
097        @NonNull
098        public Optional<@NonNull Locale> getLocale() { return Optional.ofNullable(this.locale); }
099
100        /** @return configured cache policy, private zero-TTL when omitted */
101        @NonNull
102        public McpCachePolicy getCachePolicy() { return this.cachePolicy; }
103
104        /**
105         * Returns framework-derived file manifest entries without copying file bytes.
106         *
107         * @return cached immutable list in canonical bundle path order, root document first
108         */
109        @NonNull
110        public List<McpSkillRegistration.@NonNull Resource> getResources() { return this.resources; }
111
112        McpSkillRuntimeBridge.@NonNull Registration runtimeRegistration() { return this.runtimeRegistration; }
113
114        /** @return whether URI identity, exact bundle content, locale, and cache policy match */
115        @Override
116        public boolean equals(@Nullable Object other) {
117                return this == other || other instanceof McpSkillRegistration registration
118                                && this.uri.equals(registration.uri) && this.skillBundle.equals(registration.skillBundle)
119                                && this.protocolVersions.equals(registration.protocolVersions)
120                                && Objects.equals(this.locale, registration.locale) && this.cachePolicy.equals(registration.cachePolicy);
121        }
122
123        /** @return structural registration hash code */
124        @Override
125        public int hashCode() { return Objects.hash(this.uri, this.skillBundle,
126                        this.protocolVersions, this.locale, this.cachePolicy); }
127
128        /** @return diagnostic rendering without registration or bundle content */
129        @Override
130        @NonNull
131        public String toString() { return "McpSkillRegistration[redacted]"; }
132
133        /**
134         * Immutable, framework-generated resource identity and content fingerprint.
135         *
136         * @author <a href="https://www.revetkn.com">Mark Allen</a>
137         */
138        @ThreadSafe
139        public static final class Resource {
140                @NonNull private final URI uri;
141                @NonNull private final String digest;
142                @NonNull private final Long sizeInBytes;
143
144                private Resource(McpSkillRuntimeBridge.@NonNull Resource resource) {
145                        this.uri = resource.uri();
146                        this.digest = resource.digest();
147                        this.sizeInBytes = resource.sizeInBytes();
148                }
149
150                /** @return canonical absolute file URI */
151                @NonNull
152                public URI getUri() { return this.uri; }
153
154                /** @return {@code sha256:} followed by lowercase hexadecimal SHA-256 of served bytes */
155                @NonNull
156                public String getDigest() { return this.digest; }
157
158                /** @return exact raw file size in bytes */
159                @NonNull
160                public Long getSizeInBytes() { return this.sizeInBytes; }
161
162                /** @return whether URI identity, digest, and byte size match */
163                @Override
164                public boolean equals(@Nullable Object other) {
165                        return this == other || other instanceof Resource resource
166                                        && this.uri.equals(resource.uri) && this.digest.equals(resource.digest)
167                                        && this.sizeInBytes.equals(resource.sizeInBytes);
168                }
169
170                /** @return structural resource hash code */
171                @Override
172                public int hashCode() { return Objects.hash(this.uri, this.digest, this.sizeInBytes); }
173
174                /** @return diagnostic rendering without URI, digest, or byte size */
175                @Override
176                @NonNull
177                public String toString() { return "McpSkillRegistration.Resource[redacted]"; }
178        }
179
180        /**
181         * Mutable builder for an immutable Skills registration.
182         *
183         * @author <a href="https://www.revetkn.com">Mark Allen</a>
184         */
185        @NotThreadSafe
186        public static final class Builder {
187                @NonNull private final URI uri;
188                @NonNull private final McpSkillBundle skillBundle;
189                @NonNull private final Set<@NonNull McpProtocolVersion> protocolVersions;
190                @Nullable private Locale locale;
191                @NonNull private McpCachePolicy cachePolicy = McpCachePolicy.privateNoCacheInstance();
192
193                private Builder(@NonNull URI uri, @NonNull McpSkillBundle skillBundle,
194                                @NonNull Set<@NonNull McpProtocolVersion> protocolVersions) {
195                        this.uri = requireNonNull(uri, "A Skills root URI is required.");
196                        this.skillBundle = requireNonNull(skillBundle, "A Skills bundle is required.");
197                        this.protocolVersions = requireNonNull(protocolVersions);
198                }
199
200                /**
201                 * Selects the locale associated with this registration.
202                 *
203                 * @param locale non-null registration locale
204                 * @return this builder
205                 * @throws NullPointerException if the locale is null
206                 */
207                @NonNull
208                public Builder locale(@NonNull Locale locale) {
209                        this.locale = requireNonNull(locale, "A Skills locale is required.");
210                        return this;
211                }
212
213                /**
214                 * Replaces the registration's cache policy.
215                 *
216                 * @param cachePolicy non-null policy; public scope is appropriate only for
217                 * content that does not vary by private identity or authorization
218                 * @return this builder
219                 * @throws NullPointerException if the policy is null
220                 */
221                @NonNull
222                public Builder cachePolicy(@NonNull McpCachePolicy cachePolicy) {
223                        this.cachePolicy = requireNonNull(cachePolicy, "A Skills cache policy is required.");
224                        return this;
225                }
226
227                /**
228                 * Validates URI/name agreement and bounded manifest/resource representations.
229                 *
230                 * <p>Preflight uses representative response wrappers. It cannot account for
231                 * arbitrary request IDs, server metadata, or multi-entry endpoint pages.
232                 * Endpoint-wide checks and final request serialization remain separate;
233                 * construction is not a guarantee that every later response envelope fits.
234                 *
235                 * @return immutable registration snapshot
236                 * @throws IllegalArgumentException if the URI or generated response representation is invalid
237                 */
238                @NonNull
239                public McpSkillRegistration build() { return new McpSkillRegistration(this); }
240        }
241}