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 * http://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 */
016
017package com.soklet;
018
019import com.soklet.internal.mcp.protocol.McpTaskOriginPersistedStateCodec;
020import org.jspecify.annotations.NonNull;
021import org.jspecify.annotations.Nullable;
022
023import javax.annotation.concurrent.ThreadSafe;
024
025import static java.util.Objects.requireNonNull;
026
027/**
028 * Immutable, non-wire origin of one MCP task-augmented operation.
029 *
030 * <p>Soklet makes an invocation-scoped {@link McpTaskCreationContext} available before
031 * invoking a task-capable handler and derives this value only if application
032 * code requests it or the invocation produces a task handle. The persisted
033 * state is versioned framework data that lets a later request, potentially
034 * handled by another process or deployment generation, recover the original
035 * endpoint, operation, validated arguments, and output contract. It is
036 * intentionally opaque so later Tasks revisions can add origin data without
037 * widening this Java API.
038 *
039 * <p>An application that returns a task handle must store this complete value
040 * atomically with its durable work description. Its configured
041 * {@link McpTaskManager} must return a semantically equal origin with every
042 * snapshot of that task. Persist the exact text returned by
043 * {@link #toPersistedString()} byte-for-byte in an opaque text or binary field
044 * and restore it with {@link #fromPersistedString(String)}. Do not route it
045 * through a database JSON type or another parse-and-render cycle that may
046 * normalize or discard representation details. The parser accepts
047 * insignificant whitespace, member reordering, and equivalent decimal scales,
048 * but exact storage is the forward-compatible durability boundary. Soklet
049 * compares the origin before returning the task handle and uses it to apply the
050 * original tool's output protections when the task completes.
051 *
052 * <p>Origin state may contain tool arguments or other sensitive application
053 * data. Store it with the same confidentiality and integrity protections as
054 * the task itself, do not log it, and do not expose it to clients. It is not an
055 * authorization decision or task-ownership binding; the task manager must
056 * independently bind and authorize each task using the admitted request.
057 *
058 * @author <a href="https://www.revetkn.com">Mark Allen</a>
059 */
060@ThreadSafe
061public final class McpTaskOrigin {
062        @NonNull
063        private final McpJsonObject persistedState;
064        @NonNull
065        private final String persistedString;
066
067        /**
068         * Reconstructs an origin from framework state previously supplied by Soklet.
069         *
070         * <p>Applications must preserve every member and value. Soklet canonicalizes
071         * object-member order and decimal scale for durable equality, then validates
072         * the supported version and semantics when the origin is used; construction
073         * establishes an immutable copy boundary.
074         *
075         * @param persistedState complete persisted origin state
076         * @return immutable task origin
077         * @throws NullPointerException if {@code persistedState} is null
078         * @throws IllegalArgumentException if the value cannot be represented by the
079         *                                  bounded canonical origin codec
080         */
081        @NonNull
082        public static McpTaskOrigin fromPersistedState(
083                        @NonNull McpJsonObject persistedState) {
084                return new McpTaskOrigin(persistedState);
085        }
086
087        /**
088         * Reconstructs an origin from its bounded durable JSON representation.
089         *
090         * <p>Store the exact representation as an opaque, non-normalizing text or
091         * binary value. Parsing accepts equivalent JSON object representations,
092         * including insignificant whitespace, member reordering, and numerically
093         * equivalent decimal scales, but {@link #toPersistedString()} always returns
094         * Soklet's canonical form.</p>
095         *
096         * @param persistedString persisted origin JSON
097         * @return immutable task origin
098         * @throws NullPointerException if {@code persistedString} is null
099         * @throws IllegalArgumentException if the value is not a bounded JSON object
100         */
101        @NonNull
102        public static McpTaskOrigin fromPersistedString(
103                        @NonNull String persistedString) {
104                McpTaskOriginPersistedStateCodec.DecodedState decoded =
105                                McpTaskOriginPersistedStateCodec.decode(
106                                                requireNonNull(persistedString));
107                return new McpTaskOrigin(decoded.persistedState(),
108                                decoded.persistedString());
109        }
110
111        private McpTaskOrigin(@NonNull McpJsonObject persistedState) {
112                this(requireNonNull(persistedState),
113                                McpTaskOriginPersistedStateCodec.encode(persistedState));
114        }
115
116        private McpTaskOrigin(@NonNull McpJsonObject persistedState,
117                        @NonNull String persistedString) {
118                this.persistedState = requireNonNull(persistedState);
119                this.persistedString = requireNonNull(persistedString);
120        }
121
122        /**
123         * Returns the complete opaque state that must be durably preserved.
124         *
125         * <p>The returned object may contain sensitive operation arguments. It is
126         * not MCP response metadata and must never be sent to a client.
127         *
128         * @return immutable persisted origin state
129         */
130        @NonNull
131        public McpJsonObject getPersistedState() {
132                return this.persistedState;
133        }
134
135        /**
136         * Returns the bounded canonical JSON form for durable storage.
137         *
138         * <p>The returned text may contain sensitive operation arguments. Store its
139         * exact UTF-8 representation in a non-normalizing text or binary field with
140         * the same confidentiality and integrity protections as the task. Do not
141         * parse and re-emit it, log it, or expose it to clients.</p>
142         *
143         * @return canonical persisted origin JSON
144         */
145        @NonNull
146        public String toPersistedString() {
147                return this.persistedString;
148        }
149
150        /** @return whether the other origin contains semantically equal JSON state */
151        @Override
152        public boolean equals(@Nullable Object other) {
153                if (this == other)
154                        return true;
155                if (!(other instanceof McpTaskOrigin origin))
156                        return false;
157                return this.persistedString.equals(origin.persistedString);
158        }
159
160        /** @return semantic persisted-state hash code */
161        @Override
162        public int hashCode() {
163                return this.persistedString.hashCode();
164        }
165
166        /** @return a diagnostic rendering that does not expose origin state */
167        @Override
168        @NonNull
169        public String toString() {
170                return "McpTaskOrigin{persistedState=<redacted>}";
171        }
172}