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}