返回 DeepSeek-Reasonix
validate.go
根目录 / internal / extension / protocol / validate.go
1 package protocol
2
3 import (
4 "bytes"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "io"
9 "reflect"
10 "regexp"
11 "slices"
12 "sort"
13 "strconv"
14 "strings"
15 )
16
17 type protocolValidatable interface {
18 Validate() error
19 }
20
21 type validationFailure struct{ message string }
22
23 func (e *validationFailure) Error() string { return e.message }
24
25 func validationError(message string) error { return &validationFailure{message: message} }
26
27 var sha256Pattern = regexp.MustCompile(`^[0-9a-f]{64}$`)
28
29 // enumTypes freezes the allowed wire values of every string enum DTO type.
30 // The strict decoder rejects anything outside these sets; the schema
31 // generator emits them as JSON Schema enums.
32 var enumTypes = map[reflect.Type][]string{
33 reflect.TypeFor[Direction](): values(DirectionHostToExtensionRequest, DirectionExtensionToHostRequest, DirectionHostToExtensionNotification, DirectionExtensionToHostNotification),
34 reflect.TypeFor[OperationClass](): values(ClassLifecycle, ClassIntercept, ClassObservation, ClassProvider, ClassUI, ClassContent),
35 reflect.TypeFor[InterceptEvent](): interceptEventValues(),
36 reflect.TypeFor[InterceptDecision](): values(DecisionContinue, DecisionBlock, DecisionReplace, DecisionAllow, DecisionDeny),
37 reflect.TypeFor[UIHostKind](): values(UIHostTUI, UIHostDesktop, UIHostACP, UIHostHeadless),
38 reflect.TypeFor[UISurfaceKind](): values(UISurfaceStatus, UISurfaceCard, UISurfaceForm, UISurfaceNotification),
39 reflect.TypeFor[UIRequestKind](): values(UIRequestConfirm, UIRequestInput, UIRequestSelect, UIRequestMultiselect),
40 reflect.TypeFor[UIFieldKind](): values(UIFieldConfirm, UIFieldInput, UIFieldSelect, UIFieldMultiselect),
41 reflect.TypeFor[UISeverity](): values(UISeverityInfo, UISeverityWarn, UISeverityError),
42 reflect.TypeFor[ProviderRole](): values(ProviderRoleSystem, ProviderRoleUser, ProviderRoleAssistant, ProviderRoleTool),
43 reflect.TypeFor[ProviderChunkType](): values(ChunkText, ChunkReasoning, ChunkToolCallStart, ChunkToolCallDelta, ChunkToolCall, ChunkUsage, ChunkDone, ChunkError),
44 reflect.TypeFor[ProviderErrorCode](): values(ProviderFailed, ProviderInterrupted),
45 reflect.TypeFor[ContentEncoding](): values(ContentUTF8),
46 }
47
48 func init() {
49 contracts := ErrorContracts()
50 reasons := make([]string, len(contracts))
51 for i := range contracts {
52 reasons[i] = string(contracts[i].Reason)
53 }
54 enumTypes[reflect.TypeFor[ErrorReason]()] = reasons
55 }
56
57 // EnumValues returns the frozen wire values of every string enum DTO type,
58 // keyed by the Go type name (e.g. "InterceptEvent" → the 17 hook points). It
59 // is the exported form of enumTypes for code generators: the strict decoder,
60 // the JSON Schema, and the SDK DTO mirror all draw from this one table.
61 func EnumValues() map[string][]string {
62 out := make(map[string][]string, len(enumTypes))
63 for typ, allowed := range enumTypes {
64 out[typ.Name()] = append([]string(nil), allowed...)
65 }
66 return out
67 }
68
69 func interceptEventValues() []string {
70 return InterceptEvents()
71 }
72
73 func values[T ~string](in ...T) []string {
74 out := make([]string, len(in))
75 for i := range in {
76 out[i] = string(in[i])
77 }
78 return out
79 }
80
81 // decodeAndValidate is the single strict decoder every direction helper
82 // shares: required-field presence, DisallowUnknownFields, tag validation, and
83 // semantic Validate methods.
84 func decodeAndValidate(raw json.RawMessage, typ reflect.Type) (any, error) {
85 if typ.Kind() != reflect.Struct {
86 return nil, errors.New("protocol registry params must be structs")
87 }
88 if len(bytes.TrimSpace(raw)) == 0 {
89 raw = json.RawMessage(`{}`)
90 }
91 if err := validateRequiredJSON(raw, typ, "params"); err != nil {
92 return nil, err
93 }
94 ptr := reflect.New(typ)
95 decoder := json.NewDecoder(bytes.NewReader(raw))
96 decoder.DisallowUnknownFields()
97 if err := decoder.Decode(ptr.Interface()); err != nil {
98 return nil, validationError("params do not match the registered type")
99 }
100 if err := ensureJSONEOF(decoder); err != nil {
101 return nil, validationError("params contain trailing JSON")
102 }
103 value := ptr.Elem().Interface()
104 if err := validateDecoded(value); err != nil {
105 return nil, err
106 }
107 return value, nil
108 }
109
110 func ensureJSONEOF(decoder *json.Decoder) error {
111 var extra any
112 err := decoder.Decode(&extra)
113 if errors.Is(err, io.EOF) {
114 return nil
115 }
116 if err == nil {
117 return errors.New("extra JSON value")
118 }
119 return err
120 }
121
122 func validateRequiredJSON(raw json.RawMessage, typ reflect.Type, at string) error {
123 for typ.Kind() == reflect.Pointer {
124 typ = typ.Elem()
125 }
126 if typ.Kind() != reflect.Struct {
127 return nil
128 }
129 var object map[string]json.RawMessage
130 if err := json.Unmarshal(raw, &object); err != nil {
131 return validationError(at + " must be a JSON object")
132 }
133 return validateRequiredObject(object, typ, at)
134 }
135
136 func validateRequiredObject(object map[string]json.RawMessage, typ reflect.Type, at string) error {
137 for i := range typ.NumField() {
138 field := typ.Field(i)
139 if field.PkgPath != "" {
140 continue
141 }
142 name, omitEmpty, skip := jsonField(field)
143 if skip {
144 continue
145 }
146 if field.Anonymous && name == "" {
147 if err := validateRequiredObject(object, field.Type, at); err != nil {
148 return err
149 }
150 continue
151 }
152 fieldRaw, present := object[name]
153 if !omitEmpty && !present {
154 return validationError(fmt.Sprintf("%s.%s is required", at, name))
155 }
156 if !present {
157 continue
158 }
159 if bytes.Equal(bytes.TrimSpace(fieldRaw), []byte("null")) {
160 if field.Tag.Get("nullable") == "true" || field.Tag.Get("externalizable") == "true" {
161 continue
162 }
163 return validationError(fmt.Sprintf("%s.%s must not be null", at, name))
164 }
165 if err := validateNestedRequired(fieldRaw, field.Type, at+"."+name); err != nil {
166 return err
167 }
168 }
169 return nil
170 }
171
172 func validateNestedRequired(raw json.RawMessage, typ reflect.Type, at string) error {
173 for typ.Kind() == reflect.Pointer {
174 typ = typ.Elem()
175 }
176 if typ == reflect.TypeFor[json.RawMessage]() {
177 if len(bytes.TrimSpace(raw)) == 0 || !json.Valid(raw) {
178 return validationError(at + " must contain valid JSON")
179 }
180 return nil
181 }
182 switch typ.Kind() {
183 case reflect.Struct:
184 return validateRequiredJSON(raw, typ, at)
185 case reflect.Slice, reflect.Array:
186 var items []json.RawMessage
187 if err := json.Unmarshal(raw, &items); err != nil {
188 return nil
189 }
190 for i, item := range items {
191 if err := validateNestedRequired(item, typ.Elem(), at+"["+strconv.Itoa(i)+"]"); err != nil {
192 return err
193 }
194 }
195 }
196 return nil
197 }
198
199 func validateDecoded(value any) error {
200 if err := validateValue(reflect.ValueOf(value), "params", false); err != nil {
201 return err
202 }
203 if validatable, ok := value.(protocolValidatable); ok {
204 return validatable.Validate()
205 }
206 return nil
207 }
208
209 func validateValue(value reflect.Value, at string, omitEmpty bool) error {
210 if !value.IsValid() {
211 return nil
212 }
213 if value.Kind() == reflect.Interface {
214 return validateValue(value.Elem(), at, omitEmpty)
215 }
216 if value.Kind() == reflect.Pointer {
217 if value.IsNil() {
218 return nil
219 }
220 return validateValue(value.Elem(), at, false)
221 }
222 typ := value.Type()
223 if typ == reflect.TypeFor[json.RawMessage]() {
224 raw := value.Interface().(json.RawMessage)
225 if len(bytes.TrimSpace(raw)) == 0 {
226 // An empty RawMessage is the zero value of an omitempty field and
227 // never serializes; a present field was already JSON-checked.
228 return nil
229 }
230 if !json.Valid(raw) {
231 return validationError(at + " must contain valid JSON")
232 }
233 return nil
234 }
235 if allowed, enum := enumTypes[typ]; enum {
236 if value.String() == "" && omitEmpty {
237 return nil
238 }
239 if !contains(allowed, value.String()) {
240 return validationError(fmt.Sprintf("%s has invalid enum value %q", at, value.String()))
241 }
242 return nil
243 }
244 switch value.Kind() {
245 case reflect.Struct:
246 for i := range value.NumField() {
247 field := typ.Field(i)
248 if field.PkgPath != "" {
249 continue
250 }
251 name, fieldOmitEmpty, skip := jsonField(field)
252 if skip {
253 continue
254 }
255 childAt := at
256 if name != "" {
257 childAt += "." + name
258 }
259 if err := validateValue(value.Field(i), childAt, fieldOmitEmpty); err != nil {
260 return err
261 }
262 if err := validateTag(value.Field(i), field.Tag.Get("validate"), childAt, fieldOmitEmpty); err != nil {
263 return err
264 }
265 child := value.Field(i)
266 if child.Kind() == reflect.Pointer && child.IsNil() {
267 continue
268 }
269 if child.Kind() == reflect.Pointer {
270 child = child.Elem()
271 }
272 if child.CanInterface() {
273 if validatable, ok := child.Interface().(protocolValidatable); ok {
274 if err := validatable.Validate(); err != nil {
275 return validationError(childAt + ": " + err.Error())
276 }
277 }
278 }
279 }
280 case reflect.Slice, reflect.Array:
281 for i := range value.Len() {
282 if err := validateValue(value.Index(i), fmt.Sprintf("%s[%d]", at, i), false); err != nil {
283 return err
284 }
285 item := value.Index(i)
286 if item.Kind() == reflect.Pointer && !item.IsNil() {
287 item = item.Elem()
288 }
289 if item.CanInterface() {
290 if validatable, ok := item.Interface().(protocolValidatable); ok {
291 if err := validatable.Validate(); err != nil {
292 return validationError(fmt.Sprintf("%s[%d]: %v", at, i, err))
293 }
294 }
295 }
296 }
297 }
298 return nil
299 }
300
301 // validateTag enforces the protocol's validate tag vocabulary: nonempty,
302 // min=, max=, sha256.
303 func validateTag(value reflect.Value, tags, at string, omitEmpty bool) error {
304 if tags == "" || (omitEmpty && value.IsZero()) {
305 return nil
306 }
307 if value.Kind() == reflect.Pointer {
308 if value.IsNil() {
309 return nil
310 }
311 value = value.Elem()
312 }
313 for tag := range strings.SplitSeq(tags, ",") {
314 switch {
315 case tag == "nonempty":
316 if value.Kind() == reflect.String && strings.TrimSpace(value.String()) == "" {
317 return validationError(at + " must be non-empty")
318 }
319 case strings.HasPrefix(tag, "min="):
320 minimum, _ := strconv.ParseFloat(strings.TrimPrefix(tag, "min="), 64)
321 if numericValue(value) < minimum {
322 return validationError(at + " is below its minimum")
323 }
324 case strings.HasPrefix(tag, "max="):
325 maximum, _ := strconv.ParseFloat(strings.TrimPrefix(tag, "max="), 64)
326 if numericValue(value) > maximum {
327 return validationError(at + " exceeds its maximum")
328 }
329 case tag == "sha256":
330 if !sha256Pattern.MatchString(value.String()) {
331 return validationError(at + " must be a lowercase SHA-256 hex value")
332 }
333 }
334 }
335 return nil
336 }
337
338 func numericValue(value reflect.Value) float64 {
339 switch value.Kind() {
340 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
341 return float64(value.Int())
342 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
343 return float64(value.Uint())
344 case reflect.Float32, reflect.Float64:
345 return value.Float()
346 }
347 return 0
348 }
349
350 func contains(items []string, value string) bool {
351 return slices.Contains(items, value)
352 }
353
354 func jsonField(field reflect.StructField) (name string, omitEmpty, skip bool) {
355 tag := field.Tag.Get("json")
356 parts := strings.Split(tag, ",")
357 if len(parts) > 0 && parts[0] == "-" {
358 return "", false, true
359 }
360 if len(parts) > 0 {
361 name = parts[0]
362 }
363 for _, option := range parts[1:] {
364 if option == "omitempty" || option == "omitzero" {
365 omitEmpty = true
366 }
367 }
368 if name == "" && !field.Anonymous {
369 name = field.Name
370 name = strings.ToLower(name[:1]) + name[1:]
371 }
372 return name, omitEmpty, false
373 }
374
375 // ExternalizablePointers lists the schema-level JSON pointer patterns ('*'
376 // for array items) of fields tagged externalizable on typ. Payloads at these
377 // locations may travel as content refs instead of inline JSON when they
378 // exceed ExternalizeFieldBytes.
379 func ExternalizablePointers(typ reflect.Type) []string {
380 var out []string
381 collectExternalizablePointers(typ, "", &out)
382 sort.Strings(out)
383 return out
384 }
385
386 func collectExternalizablePointers(typ reflect.Type, prefix string, out *[]string) {
387 for typ.Kind() == reflect.Pointer {
388 typ = typ.Elem()
389 }
390 switch typ.Kind() {
391 case reflect.Struct:
392 for i := range typ.NumField() {
393 field := typ.Field(i)
394 if field.PkgPath != "" {
395 continue
396 }
397 name, _, skip := jsonField(field)
398 if skip {
399 continue
400 }
401 if field.Anonymous && name == "" {
402 collectExternalizablePointers(field.Type, prefix, out)
403 continue
404 }
405 fieldPointer := prefix + "/" + escapeJSONPointerToken(name)
406 if field.Tag.Get("externalizable") == "true" {
407 *out = append(*out, fieldPointer)
408 continue
409 }
410 collectExternalizablePointers(field.Type, fieldPointer, out)
411 }
412 case reflect.Slice, reflect.Array:
413 collectExternalizablePointers(typ.Elem(), prefix+"/*", out)
414 }
415 }
416
417 func escapeJSONPointerToken(value string) string {
418 return strings.ReplaceAll(strings.ReplaceAll(value, "~", "~0"), "/", "~1")
419 }
420
420 lines GO