返回 DeepSeek-Reasonix
externalize.go
根目录 / internal / extension / sidecar / externalize.go
1 package sidecar
2
3 import (
4 "bytes"
5 "crypto/sha256"
6 "encoding/hex"
7 "encoding/json"
8 "fmt"
9 "reflect"
10
11 "reasonix/internal/extension/protocol"
12 )
13
14 // Content-ref externalization for the intercept and event calls. The wire
15 // contract: a field tagged externalizable whose serialized value exceeds
16 // protocol.ExternalizeFieldBytes travels as null in its owner document, and
17 // the owner's externalized envelope carries one descriptor per moved field
18 // naming the content ref to page back through host/content/read. Only the
19 // host creates refs (the protocol registers host/content/read in the
20 // Extension → Host direction alone), so an extension answering with a content
21 // ref always names an object in this connection's host-side store.
22
23 // externalizablePointer resolves the schema-registered externalizable JSON
24 // pointer for one field of typ, failing loudly on schema drift instead of
25 // silently externalizing a field the schema no longer marks.
26 func externalizablePointer(typ reflect.Type, want string) (string, error) {
27 for _, pointer := range protocol.ExternalizablePointers(typ) {
28 if pointer == want {
29 return pointer, nil
30 }
31 }
32 return "", fmt.Errorf("sidecar: %s has no externalizable %s field (schema drift)", typ, want)
33 }
34
35 // externalizePayload moves an over-threshold payload into the connection's
36 // content store and returns the null placeholder plus the wire envelope.
37 // Below the protocol.ExternalizeFieldBytes threshold the payload passes
38 // through inline with a nil envelope, keeping small calls byte-identical.
39 func externalizePayload(store *Store, pointer string, payload json.RawMessage) (json.RawMessage, []ExternalizedField, error) {
40 field, err := MaybeExternalize(store, pointer, payload)
41 if err != nil {
42 return nil, nil, err
43 }
44 if field == nil {
45 return payload, nil, nil
46 }
47 return nil, []ExternalizedField{*field}, nil
48 }
49
50 // externalizeInterceptParams applies the outbound content-ref rule to one
51 // intercept call's payload.
52 func (c *Client) externalizeInterceptParams(params *protocol.InterceptParams) error {
53 pointer, err := externalizablePointer(reflect.TypeOf(*params), "/payload")
54 if err != nil {
55 return err
56 }
57 payload, externalized, err := externalizePayload(c.store, pointer, params.Payload)
58 if err != nil {
59 return err
60 }
61 params.Payload = payload
62 params.Externalized = externalized
63 return nil
64 }
65
66 // externalizeEventParams applies the outbound content-ref rule to one event
67 // notification's payload.
68 func (c *Client) externalizeEventParams(params *protocol.EventParams) error {
69 pointer, err := externalizablePointer(reflect.TypeOf(*params), "/payload")
70 if err != nil {
71 return err
72 }
73 payload, externalized, err := externalizePayload(c.store, pointer, params.Payload)
74 if err != nil {
75 return err
76 }
77 params.Payload = payload
78 params.Externalized = externalized
79 return nil
80 }
81
82 // resolveExternalizedReplacement rehydrates an intercept result whose
83 // replacement traveled as a content-ref envelope instead of inline JSON. The
84 // ref is paged out of this connection's store with the exact
85 // host/content/read chunking rules (Store.Read), the reassembled bytes are
86 // verified against the descriptor's byte count and SHA-256, and only then
87 // substituted for the strict decode the dispatcher performs. An inline
88 // replacement alongside an envelope, a pointer outside the schema's
89 // externalizable set, or unverifiable content is a protocol error: the
90 // dispatcher must never decode bytes the peer did not prove.
91 func (c *Client) resolveExternalizedReplacement(result *protocol.InterceptResult) error {
92 if len(result.Externalized) == 0 {
93 return nil
94 }
95 if inline := bytes.TrimSpace(result.Replacement); len(inline) > 0 && !bytes.Equal(inline, []byte("null")) {
96 return &protocol.ProtocolError{Reason: protocol.ErrProtocolError, Message: "intercept result carries both an inline replacement and an externalized envelope"}
97 }
98 pointer, err := externalizablePointer(reflect.TypeOf(*result), "/replacement")
99 if err != nil {
100 return err
101 }
102 if len(result.Externalized) != 1 || result.Externalized[0].JSONPointer != pointer {
103 return &protocol.ProtocolError{Reason: protocol.ErrProtocolError, Message: fmt.Sprintf(
104 "intercept result externalized envelope must hold exactly the %s descriptor", pointer)}
105 }
106 descriptor := result.Externalized[0]
107 if descriptor.TotalBytes > protocol.ContentRefObjectBytes {
108 return &protocol.ProtocolError{Reason: protocol.ErrFrameTooLarge, Message: fmt.Sprintf(
109 "externalized replacement is %d bytes, above the %d byte object cap", descriptor.TotalBytes, protocol.ContentRefObjectBytes)}
110 }
111 reassembled, err := c.store.readAll(descriptor.ContentRef)
112 if err != nil {
113 return err
114 }
115 if int64(len(reassembled)) != descriptor.TotalBytes {
116 return &protocol.ProtocolError{Reason: protocol.ErrProtocolError, Message: fmt.Sprintf(
117 "externalized replacement reassembled to %d bytes, want %d", len(reassembled), descriptor.TotalBytes)}
118 }
119 sum := sha256.Sum256(reassembled)
120 if hex.EncodeToString(sum[:]) != descriptor.SHA256 {
121 return &protocol.ProtocolError{Reason: protocol.ErrProtocolError, Message: "externalized replacement SHA-256 mismatch"}
122 }
123 result.Replacement = reassembled
124 return nil
125 }
126
127 // readAll pages a whole content ref out of the store in
128 // protocol.ContentRefChunkBytes chunks, mirroring how an extension pages
129 // host/content/read.
130 func (s *Store) readAll(ref string) ([]byte, error) {
131 var out []byte
132 var offset int64
133 for {
134 chunk, next, _, _, err := s.Read(ref, offset)
135 if err != nil {
136 return nil, err
137 }
138 out = append(out, chunk...)
139 if next == nil {
140 return out, nil
141 }
142 offset = *next
143 }
144 }
145
145 lines GO