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