parse-stack-next 5.5.5 → 5.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +338 -0
- data/lib/parse/agent/mcp_rack_app.rb +29 -11
- data/lib/parse/agent/metadata_dsl.rb +10 -0
- data/lib/parse/embeddings/binding_audit.rb +211 -0
- data/lib/parse/embeddings/media_file.rb +136 -0
- data/lib/parse/embeddings/provider.rb +39 -0
- data/lib/parse/embeddings/streaming_body.rb +170 -0
- data/lib/parse/embeddings/video_source.rb +120 -0
- data/lib/parse/embeddings/voyage.rb +548 -135
- data/lib/parse/embeddings.rb +55 -0
- data/lib/parse/model/core/embed_managed.rb +26 -0
- data/lib/parse/model/core/properties.rb +37 -1
- data/lib/parse/model/core/vector_searchable.rb +30 -3
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/vector_search/hybrid.rb +120 -12
- data/lib/parse/vector_search.rb +132 -7
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d7ea05ce0688ca7c1df6a1077162478dfee52e33d9fac77c7480c5ca818c9eb8
|
|
4
|
+
data.tar.gz: 71e61f7e70da94793dae9d741a3e5d29ffc7f9cd45f2adee6c96cd98b5ac2d24
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c723fbb9589ce57c7651fab142bed386f3a2070d39fc20887fc40b50f676ccb0041e9b758611e8b75d0a9202095de318ce2bbae35e41015be519209d9d2628dd
|
|
7
|
+
data.tar.gz: dbbc09ee718630415da1ab329168fc1c10d5d9cf20ad09e4129b2518087231206ca84d7d5ecd5e1a27f66e2fb75d3704b856592fcd43c6d2f7711fa20c0dba1b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,343 @@
|
|
|
1
1
|
## parse-stack-next Changelog
|
|
2
2
|
|
|
3
|
+
### 5.6.0
|
|
4
|
+
|
|
5
|
+
#### Voyage embeddings reach the Atlas endpoint, video, and streamed media
|
|
6
|
+
|
|
7
|
+
- **NEW**: The Voyage provider now targets MongoDB's Atlas Embedding and
|
|
8
|
+
Reranking API in addition to Voyage's own. The two serve the same models
|
|
9
|
+
over an identical wire contract but do not share credentials — an Atlas
|
|
10
|
+
model API key returns 403 from Voyage's host and vice versa. A key carrying
|
|
11
|
+
the Atlas prefix routes to `https://ai.mongodb.com/v1` automatically; pass
|
|
12
|
+
`endpoint: :atlas` or `:voyage` to be explicit, or a `base_url:` to override
|
|
13
|
+
both. A named endpoint that contradicts an explicit `base_url` is rejected
|
|
14
|
+
rather than silently reconciled, so a credential is never sent to a host the
|
|
15
|
+
caller did not intend. `#endpoint` and `#atlas?` report the resolved target.
|
|
16
|
+
- **NEW**: Added `voyage-3.5`, `voyage-3.5-lite`, `voyage-code-2` (1536-dim),
|
|
17
|
+
and `voyage-multimodal-3.5`. Models the Atlas endpoint does not expose —
|
|
18
|
+
`voyage-3`, `voyage-3-lite`, `voyage-4-nano` — are refused at construction
|
|
19
|
+
when that endpoint is active, with an error naming a current replacement;
|
|
20
|
+
they remain valid against Voyage's own API.
|
|
21
|
+
- **NEW**: `Parse::Embeddings::Voyage#embed_video` embeds video through
|
|
22
|
+
`voyage-multimodal-3.5`, the only model that accepts it. Text, image, and
|
|
23
|
+
video vectors share one space, so a stored text vector is comparable against
|
|
24
|
+
a video vector without re-embedding. `#modalities` reports `[:text, :image,
|
|
25
|
+
:video]` for that model and `[:text, :image]` for `voyage-multimodal-3`.
|
|
26
|
+
- **NEW**: `Parse::Embeddings::MediaFile` wraps a local image or video and
|
|
27
|
+
streams it into the request body instead of buffering it. Serializing media
|
|
28
|
+
with `to_json` costs roughly 2.4x the file size resident — raw bytes, the
|
|
29
|
+
1.33x base64 copy, and the serialized document — which is enough to exhaust
|
|
30
|
+
a small dyno on a single moderate video. `MediaFile` reads only a 16-byte
|
|
31
|
+
header at construction; the payload is then base64-encoded into the socket
|
|
32
|
+
in fixed-size chunks by `Parse::Embeddings::StreamingBody`, so peak memory
|
|
33
|
+
is bounded by the chunk size regardless of file size and nothing spills to
|
|
34
|
+
disk. `Content-Length` is computed exactly so the request avoids chunked
|
|
35
|
+
transfer encoding, and the body replays byte-identically on retry. Passing a
|
|
36
|
+
URL instead keeps the SDK out of the transfer entirely.
|
|
37
|
+
- **FIXED**: Corrected the model dimension table. The entire v4 family
|
|
38
|
+
defaults to 1024 — `voyage-4-large`'s 2048, `voyage-4-lite`'s 512, and
|
|
39
|
+
`voyage-4-nano`'s 256 were recorded as native widths when they are
|
|
40
|
+
Matryoshka options reached only by requesting them. Because the provider
|
|
41
|
+
validates the returned vector width against the declared one, both
|
|
42
|
+
`voyage-4-large` and `voyage-4-lite` raised
|
|
43
|
+
`Parse::Embeddings::InvalidResponseError` on every call. `voyage-4-nano` is
|
|
44
|
+
1024, and `voyage-finance-2` carries a 32,000-token context rather than
|
|
45
|
+
16,000.
|
|
46
|
+
- **CHANGED**: The coarse "Matryoshka-capable models" gate is replaced by
|
|
47
|
+
per-model `MODEL_SUPPORTED_DIMENSIONS`. Any width on a model's ladder is now
|
|
48
|
+
accepted — including one wider than its default, which the old rule rejected
|
|
49
|
+
as "exceeds native" — and a width off the ladder is refused with the
|
|
50
|
+
supported set named. `output_dimension` is sent whenever the configured
|
|
51
|
+
width differs from the model's default, so `voyage-4-lite` at 512 and
|
|
52
|
+
`voyage-4-large` at 2048 both work.
|
|
53
|
+
- **FIXED**: A caller-supplied URL can no longer capture a local file's bytes.
|
|
54
|
+
The streamed body previously marked each payload with a sentinel token and
|
|
55
|
+
located it by searching the serialized JSON, so a URL containing that token
|
|
56
|
+
matched first: the file's base64 was spliced into the URL slot and forwarded
|
|
57
|
+
to the provider as a URL to fetch, disclosing local file contents, while the
|
|
58
|
+
intended slot kept the literal token. Request bodies are now assembled
|
|
59
|
+
structurally — each fragment serialized independently and concatenated in
|
|
60
|
+
order — so payloads are placed by position and caller data is never
|
|
61
|
+
searched.
|
|
62
|
+
- **FIXED**: Mixed `image_url` / `image_base64` batches no longer violate
|
|
63
|
+
Voyage's request contract, which requires a single representation per
|
|
64
|
+
request. A mixed batch is split into one request per representation and
|
|
65
|
+
reassembled into the caller's original order, preserving the 1:1 alignment
|
|
66
|
+
between inputs and returned vectors.
|
|
67
|
+
- **FIXED**: Video validation no longer accepts containers the provider
|
|
68
|
+
rejects. MP4 is the only format Voyage supports, and WebM and QuickTime
|
|
69
|
+
payloads are refused by the API, so both are out of the default allowlist.
|
|
70
|
+
An `ftyp` box no longer implies MP4 on its own — QuickTime and the
|
|
71
|
+
audio-only profiles share the ISO base media container — so major brands are
|
|
72
|
+
matched explicitly and an unrecognized brand is refused rather than assumed.
|
|
73
|
+
Apple's audio-only `M4A` brand is excluded, closing a type confusion in
|
|
74
|
+
which an audio file passed as video.
|
|
75
|
+
- **NEW**: `Parse::Embeddings.max_media_bytes` caps streamed media per file,
|
|
76
|
+
defaulting to the 20 MB Voyage documents. Streaming already prevents an
|
|
77
|
+
oversized file from exhausting memory, but the provider still rejects it, so
|
|
78
|
+
failing locally turns a wasted upload into an immediate error. The Voyage
|
|
79
|
+
adapter enforces the 20 MB ceiling independently, so raising the global knob
|
|
80
|
+
for another provider cannot push an oversized payload onto Voyage.
|
|
81
|
+
|
|
82
|
+
#### Breaking
|
|
83
|
+
|
|
84
|
+
- **BREAKING**: The Voyage provider's default model moves from `voyage-3` to
|
|
85
|
+
`voyage-3.5`. `voyage-3` is retired from the Atlas endpoint, so the old
|
|
86
|
+
default made an Atlas key fail at construction whenever no model was named.
|
|
87
|
+
Vectors from the two models are not comparable. **Migration:** code relying
|
|
88
|
+
on the default must pin `model: "voyage-3"` explicitly to keep existing
|
|
89
|
+
embeddings valid, or re-embed against the new default. A `:vector` property
|
|
90
|
+
that declares `model:` is unaffected — the new binding audit catches the
|
|
91
|
+
mismatch before any request rather than letting the two mix silently.
|
|
92
|
+
|
|
93
|
+
#### Vector search no longer returns fewer results than requested
|
|
94
|
+
|
|
95
|
+
- **FIXED**: `$vectorSearch` set its `limit` to `k`, but Atlas applies that
|
|
96
|
+
limit before the SDK's ACL `$match`, `protectedFields` redaction,
|
|
97
|
+
pointer-field filtering, and any caller-supplied `filter` — so a scoped
|
|
98
|
+
caller who could read 2 of the top 10 documents asked for 10 and received 2,
|
|
99
|
+
even when hundreds of readable matches existed further down the ranking.
|
|
100
|
+
The search now requests a wider internal candidate window, applies every
|
|
101
|
+
enforcement layer, and only then trims to `k`. The window is raised only
|
|
102
|
+
when something can actually drop rows, so a master-key call with no filter
|
|
103
|
+
keeps its previous one-for-one cost. A `candidate_limit:` option on
|
|
104
|
+
`VectorSearch.search` and `find_similar` tunes the window for principals
|
|
105
|
+
whose visibility is unusually narrow. This is a mitigation, not a guarantee:
|
|
106
|
+
a sufficiently selective ACL can still exhaust any finite window, which is
|
|
107
|
+
why the attrition counts below exist. HNSW width stays anchored to `k`, so
|
|
108
|
+
the wider window does not widen the ANN search.
|
|
109
|
+
- **NEW**: `VectorSearch.search` emits a `parse.vector_search.search`
|
|
110
|
+
`ActiveSupport::Notifications` event carrying `candidate_limit`,
|
|
111
|
+
`num_candidates`, `post_filter_count`, `post_pointer_count`,
|
|
112
|
+
`returned_count`, `pointer_attrition`, and `underfilled`. The counts are
|
|
113
|
+
named for where they are measured — obtaining a true pre-`$match` count
|
|
114
|
+
would require a `$facet` — so an underfill is observable rather than silent.
|
|
115
|
+
|
|
116
|
+
#### Vector properties are checked against the provider actually registered
|
|
117
|
+
|
|
118
|
+
- **FIXED**: A `:vector` property's `model:` was recorded and never enforced.
|
|
119
|
+
Because models in the same family usually share a width (`voyage-3` and
|
|
120
|
+
`voyage-3.5` are both 1024), swapping the registered provider's model
|
|
121
|
+
silently mixed incomparable vectors into one index — no error, just
|
|
122
|
+
degraded recall, repairable only by re-embedding. `dimensions:` was
|
|
123
|
+
verified, but only against a vector the provider had already returned and
|
|
124
|
+
billed for. Both are now checked by `Parse::Embeddings::BindingAudit`
|
|
125
|
+
before any request is issued, on the managed-write path and the
|
|
126
|
+
query-embedding path alike. The audit runs ahead of the digest short-circuit
|
|
127
|
+
so an unchanged record still surfaces a drifted binding, and it fails closed:
|
|
128
|
+
a provider that cannot report `#model_name` or `#dimensions` is refused
|
|
129
|
+
rather than skipped, since a declaration that cannot be verified is not a
|
|
130
|
+
declaration that has been satisfied.
|
|
131
|
+
- **NEW**: `Parse::Embeddings::BindingAudit.audit_all!` and
|
|
132
|
+
`.audit_all_or_raise!` check every declared binding at once, for a boot-time
|
|
133
|
+
or CI gate rather than waiting for the first save that happens to touch one.
|
|
134
|
+
- **NEW**: `:vector` properties validate `similarity:` against the functions
|
|
135
|
+
Atlas accepts (`euclidean`, `cosine`, `dotProduct`) at declaration time
|
|
136
|
+
instead of surfacing a typo as an index error later.
|
|
137
|
+
- **FIXED**: Hybrid search applies the same candidate window as the plain
|
|
138
|
+
vector search, so opting into hybrid no longer underfills where a straight
|
|
139
|
+
vector search would not. Each branch's limit is applied before ACL
|
|
140
|
+
enforcement and was narrower than the plain path's window.
|
|
141
|
+
- **FIXED**: Hybrid search separates the rows each branch retains for fusion
|
|
142
|
+
from the rows Atlas considers before ACL. Conflating them meant the branch
|
|
143
|
+
limit was passed as the vector branch's `k` and then multiplied a second
|
|
144
|
+
time by the plain search's own window derivation — a hybrid `k: 10` asked
|
|
145
|
+
Atlas for 1,000 rows instead of the intended 100, an explicit
|
|
146
|
+
`candidate_limit: 250` became 2,500 on the client path while staying 250 on
|
|
147
|
+
the native path, and any `k` above 100 produced a branch `k` beyond
|
|
148
|
+
`VectorSearch::MAX_K` that failed outright. The fusion depth is now bounded
|
|
149
|
+
by `MAX_K` and the candidate window by Atlas's 10,000 ceiling, with both
|
|
150
|
+
paths using the same window. A `candidate_limit` outside that range is
|
|
151
|
+
refused rather than clamped, and `vector: { candidate_limit: }` is forwarded
|
|
152
|
+
through `hybrid_search`.
|
|
153
|
+
- **FIXED**: The native `$rankFusion` pipeline trimmed to `k` in a `$limit`
|
|
154
|
+
stage that runs after its ACL `$match`, reintroducing the underfill the
|
|
155
|
+
candidate window exists to prevent. It now limits to the candidate window
|
|
156
|
+
and trims to `k` once enforcement has run.
|
|
157
|
+
- **NEW**: A `parse.vector_search.hybrid` notification reports `method`,
|
|
158
|
+
`branch_depth`, `candidate_window`, `post_filter_count`, `returned_count`,
|
|
159
|
+
and `underfilled` for both fusion paths. `branch_depth` differs by method by
|
|
160
|
+
design: the client path enforces ACL inside each branch and can retain the
|
|
161
|
+
narrower fusion depth, while the native path enforces after `$rankFusion`
|
|
162
|
+
and must retain the full window. The value reported is the one that ran.
|
|
163
|
+
- **NEW**: A `:vector` property wider than the Atlas vectorSearch index cap is
|
|
164
|
+
refused unless it declares `searchable: false`. `Parse::Vector` tolerates up
|
|
165
|
+
to 16384 dimensions while an Atlas index caps at 8192, so such a property
|
|
166
|
+
was previously declarable, storable, and permanently unsearchable, with the
|
|
167
|
+
failure appearing only at query time. The two limits remain distinct — they
|
|
168
|
+
govern storage and indexing respectively — but the combination now has to be
|
|
169
|
+
acknowledged.
|
|
170
|
+
- **NEW**: `searchable: false` makes a `:vector` property genuinely
|
|
171
|
+
storage-only, whether it opted out to clear the index cap or by choice. It
|
|
172
|
+
is excluded from `find_similar` / `hybrid_search` field resolution, refused
|
|
173
|
+
with an explanation when named directly, and rejected by `agent_searchable`
|
|
174
|
+
at class load rather than at an agent's first query.
|
|
175
|
+
|
|
176
|
+
#### Provider protocol
|
|
177
|
+
|
|
178
|
+
- **NEW**: `Parse::Embeddings::Provider#embed_video` joins `#embed_image` in
|
|
179
|
+
the base protocol with the same `NotImplementedError` default, so video is a
|
|
180
|
+
declared capability rather than a Voyage-only method. `#supports_modality?`
|
|
181
|
+
answers the capability question without rescuing.
|
|
182
|
+
- **NEW**: `rake test:contract` runs live, billable provider contract tests
|
|
183
|
+
that pin request routing, native dimensions, the Matryoshka ladder, accepted
|
|
184
|
+
media, size limits, model availability, and response shape. They skip unless
|
|
185
|
+
`VOYAGE_CONTRACT_KEY` is set and are excluded from both `rake test` and
|
|
186
|
+
`rake test:unit`, so no ordinary run becomes billable because a key happens
|
|
187
|
+
to be exported. Probes that assert a refusal issue raw requests rather than
|
|
188
|
+
going through the SDK — a local guard asserted against itself proves nothing
|
|
189
|
+
about the contract it encodes — and distinguish a genuine refusal from an
|
|
190
|
+
authentication, rate-limit, or 5xx failure so infrastructure trouble cannot
|
|
191
|
+
read as a contract verdict. Mocked tests assert what the SDK believes the
|
|
192
|
+
API does and therefore cannot detect provider drift: every dimension and
|
|
193
|
+
media-format correction in this release was invisible to a fully green
|
|
194
|
+
mocked suite.
|
|
195
|
+
|
|
196
|
+
### Behavior Notes
|
|
197
|
+
|
|
198
|
+
- Audio is not offered by any Voyage model, and neither PDF nor DOCX is
|
|
199
|
+
accepted as a content type. Render document pages to images and embed those;
|
|
200
|
+
the SDK does not perform that conversion.
|
|
201
|
+
- Deterministic result fill under highly selective ACLs would require the
|
|
202
|
+
authorization predicate to run inside the Atlas prefilter rather than after
|
|
203
|
+
it. The post-search `$match` remains the enforcement boundary regardless.
|
|
204
|
+
- `voyage-4-nano` is served by neither hosted endpoint — it is open-weight and
|
|
205
|
+
meant to be self-hosted. It is refused against Voyage's and Atlas's hosts
|
|
206
|
+
with a message pointing at a self-hosted `base_url:` or
|
|
207
|
+
`Parse::Embeddings::LocalHTTP`, and remains usable through either.
|
|
208
|
+
|
|
209
|
+
### Code Example
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
# Endpoint inferred from the key prefix — no base_url needed.
|
|
213
|
+
provider = Parse::Embeddings::Voyage.new(
|
|
214
|
+
api_key: ENV.fetch("ATLAS_MODEL_API_KEY"),
|
|
215
|
+
model: "voyage-multimodal-3.5",
|
|
216
|
+
)
|
|
217
|
+
provider.endpoint # => :atlas
|
|
218
|
+
provider.modalities # => [:text, :image, :video]
|
|
219
|
+
|
|
220
|
+
# Local media streams into the request; the bytes are never held in memory.
|
|
221
|
+
provider.embed_image([Parse::Embeddings::MediaFile.image("page.png")])
|
|
222
|
+
provider.embed_video([Parse::Embeddings::MediaFile.video("demo.mp4")])
|
|
223
|
+
|
|
224
|
+
# A URL keeps the SDK out of the transfer entirely — the provider fetches it.
|
|
225
|
+
provider.embed_image(["https://cdn.example.com/page.png"])
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### 5.5.6
|
|
229
|
+
|
|
230
|
+
#### MCP clients now receive the SSE response instead of hanging
|
|
231
|
+
|
|
232
|
+
- **FIXED**: The Streamable HTTP SSE transport framed its events with custom
|
|
233
|
+
event names — `event: progress` for `notifications/progress` and
|
|
234
|
+
`event: response` for the final JSON-RPC response. MCP defines a single SSE
|
|
235
|
+
event type for JSON-RPC traffic, and clients match only the default
|
|
236
|
+
`message` type, so every frame the SDK emitted was silently discarded: tool
|
|
237
|
+
progress never surfaced, and, critically, the terminating response never
|
|
238
|
+
arrived, leaving the client blocked until its own timeout on any streaming
|
|
239
|
+
`tools/call`. All frames on both the request-scoped POST stream and the
|
|
240
|
+
server-to-client GET notification stream now carry `event: message`, and
|
|
241
|
+
clients discriminate from the JSON-RPC envelope (`method` present for a
|
|
242
|
+
notification, `id` plus `result`/`error` for a response) as the protocol
|
|
243
|
+
intends. Deployments that worked around this with a middleware rewriting
|
|
244
|
+
the event name can drop it.
|
|
245
|
+
|
|
246
|
+
### 5.5.5
|
|
247
|
+
|
|
248
|
+
#### Agent `call_method` runs under the caller's scope, not the master key
|
|
249
|
+
|
|
250
|
+
- **FIXED**: Instance `call_method` resolved its receiver with a bare
|
|
251
|
+
`klass.find` on the master-backed default client, so a scoped agent could
|
|
252
|
+
read — and through a write method, mutate — any object by id. The receiver
|
|
253
|
+
(both the real call and the dry-run existence check) is now fetched through
|
|
254
|
+
the requesting agent's scope: a session scope fetches with the session token
|
|
255
|
+
(Parse Server enforces ACL/CLP), an `acl_user` / `acl_role` scope routes
|
|
256
|
+
mongo-direct (ACL simulation), and a row the scope cannot read fails closed
|
|
257
|
+
to "not found". The method body runs inside the caller's session so its own
|
|
258
|
+
writes inherit the same principal, and an instance write/admin method under
|
|
259
|
+
an `acl_user` / `acl_role` scope — which has no session token to bind — is
|
|
260
|
+
refused rather than run with master authority.
|
|
261
|
+
- **FIXED**: Aggregation pipelines now default-deny joins under an active
|
|
262
|
+
tenant scope. A `$lookup` / `$graphLookup` / `$unionWith` sub-pipeline runs
|
|
263
|
+
in the joined collection's context with no tenant predicate injected, so any
|
|
264
|
+
such join could surface rows from other tenants; all are refused while a
|
|
265
|
+
tenant scope is active. The `$unionWith` bare-string shorthand
|
|
266
|
+
(`{ "$unionWith" => "Class" }`) is now covered by that guard as well as the
|
|
267
|
+
class-allowlist, hidden/underscore, and CLP-`find` gates, which previously
|
|
268
|
+
inspected only the Hash form.
|
|
269
|
+
- **IMPROVED**: Unimplemented agent tools now raise a typed
|
|
270
|
+
`Parse::Agent::NotImplemented` error, and tools without a handler are omitted
|
|
271
|
+
from tool listings.
|
|
272
|
+
|
|
273
|
+
#### Blank or injected credentials no longer escalate to the master key
|
|
274
|
+
|
|
275
|
+
- **FIXED**: An explicitly-supplied blank or whitespace `session_token:` no
|
|
276
|
+
longer falls through to the master key. It now fails closed to an anonymous
|
|
277
|
+
request (master suppressed, no session header) and does not fall back to a
|
|
278
|
+
bound or ambient token. `Parse.with_session` likewise rejects a blank token
|
|
279
|
+
at the source instead of storing a whitespace ambient that the request layer
|
|
280
|
+
would then drop.
|
|
281
|
+
- **FIXED**: The control options `use_master_key` and `session` can no longer
|
|
282
|
+
be set from a string-keyed conditions hash — the "forward a request params
|
|
283
|
+
hash straight into `Query.new` / `where`" pattern. `{"use_master_key" =>
|
|
284
|
+
true}` from untrusted params was an ACL/CLP-bypass mass-assignment; these two
|
|
285
|
+
keys are now honored only when passed as symbols (code-authored), and a
|
|
286
|
+
string form is treated as an ordinary field constraint with a warning.
|
|
287
|
+
|
|
288
|
+
#### Atlas Search and aggregation stay ACL-scoped
|
|
289
|
+
|
|
290
|
+
- **FIXED**: Atlas Search keeps `$search` at pipeline stage 0 while running the
|
|
291
|
+
scoped ACL/CLP enforcement chain (ACL `$match` folded after `$search`,
|
|
292
|
+
protectedFields strip, pointerFields filter, and a protected-field `$expr`
|
|
293
|
+
oracle guard). Query-derived constraints on the builder-block, options, and
|
|
294
|
+
autocomplete paths are converted to MongoDB storage form before the
|
|
295
|
+
post-`$search` `$match`, so a pointer/date/objectId constraint targets the
|
|
296
|
+
correct storage column and no longer fails BSON serialization.
|
|
297
|
+
- **FIXED**: `search_with_stage` rejects a non-`$search` stage and any
|
|
298
|
+
`returnStoredSource`, which could otherwise return only index-stored fields
|
|
299
|
+
(without `_rperm`), be read as public by the ACL match, and leak restricted
|
|
300
|
+
rows.
|
|
301
|
+
- **FIXED**: A scoped `$geoNear` folds the ACL predicate into `$geoNear.query`
|
|
302
|
+
rather than prepending a `$match`, keeping `$geoNear` at stage 0 so the query
|
|
303
|
+
no longer fails under ACL scoping. The fold embeds a copy of the caller's
|
|
304
|
+
existing query so the caller's pipeline is not mutated.
|
|
305
|
+
|
|
306
|
+
#### Outbound fetches and the shipped CLI are hardened
|
|
307
|
+
|
|
308
|
+
- **FIXED**: `parse-console --url` now parses and scheme-validates its argument
|
|
309
|
+
(HTTP(S) only) before fetching, instead of passing it to `Kernel#open` —
|
|
310
|
+
closing a path where a `|command` argument would be executed as a subprocess.
|
|
311
|
+
- **IMPROVED**: The Cohere reranker `base_url` is validated to reject
|
|
312
|
+
credentials embedded in the URL and plaintext HTTP to non-loopback hosts.
|
|
313
|
+
- **IMPROVED**: Remote file/image fetches enforce a streaming size cap that
|
|
314
|
+
aborts mid-download; the per-call `max_bytes:` ceiling is validated as a
|
|
315
|
+
positive integer, so a zero, negative, or non-numeric value is refused up
|
|
316
|
+
front rather than silently rejecting every response.
|
|
317
|
+
|
|
318
|
+
#### Redaction, locking, and API ergonomics
|
|
319
|
+
|
|
320
|
+
- **IMPROVED**: Query-string credential redaction for logging and request
|
|
321
|
+
profiling is now a single shared implementation, so the two cannot drift. It
|
|
322
|
+
redacts the value of any credential-bearing parameter name (matched by a
|
|
323
|
+
generic rule with a safe-list, and aware of percent-encoded names) and uses
|
|
324
|
+
possessive-quantifier matching so it stays linear on pathological URLs.
|
|
325
|
+
- **FIXED**: The in-process fallback mutex registry (used when no shared lock
|
|
326
|
+
store is configured) is bounded, and its eviction can no longer reclaim a
|
|
327
|
+
mutex a caller is about to lock — a pending-acquirer reservation prevents two
|
|
328
|
+
callers from getting distinct mutexes for the same key.
|
|
329
|
+
- **IMPROVED**: `call_function` and `trigger_job` accept request options as
|
|
330
|
+
bare keyword arguments (for example `call_function("f", {}, session_token:
|
|
331
|
+
t)`), merged with the explicit `opts:` hash for back-compat.
|
|
332
|
+
|
|
333
|
+
#### Packaging, CI, and docs
|
|
334
|
+
|
|
335
|
+
- **CHANGED**: The shipped-gem file list was narrowed to an allowlist of the
|
|
336
|
+
library, binaries, docs, examples, and standard metadata.
|
|
337
|
+
- **CHANGED**: The SHA-pinned `ruby/setup-ruby` GitHub action was bumped to
|
|
338
|
+
`v1.318.0` across the CI, docs, and release workflows.
|
|
339
|
+
- **IMPROVED**: README, test-server, and guide updates, plus YARD styling.
|
|
340
|
+
|
|
3
341
|
### 5.5.4
|
|
4
342
|
|
|
5
343
|
#### Dependency updates
|
|
@@ -1183,10 +1183,21 @@ module Parse
|
|
|
1183
1183
|
# Wire format for each SSE event (note: trailing blank line is required
|
|
1184
1184
|
# by the SSE spec):
|
|
1185
1185
|
#
|
|
1186
|
-
# event:
|
|
1186
|
+
# event: message\n
|
|
1187
1187
|
# data: <json>\n
|
|
1188
1188
|
# \n
|
|
1189
1189
|
#
|
|
1190
|
+
# EVERY frame — progress notifications, list-changed notifications,
|
|
1191
|
+
# and the final JSON-RPC response alike — carries the event name
|
|
1192
|
+
# `message`. MCP Streamable HTTP defines exactly one SSE event type
|
|
1193
|
+
# for JSON-RPC traffic; clients discriminate by inspecting the
|
|
1194
|
+
# envelope (`method` present => notification, `id` + `result`/`error`
|
|
1195
|
+
# => response), NOT by the SSE event name. Earlier releases emitted
|
|
1196
|
+
# `event: progress` and `event: response`, which real MCP clients
|
|
1197
|
+
# silently discard — they match only the default `message` type — so
|
|
1198
|
+
# the final response never arrived and the call appeared to hang.
|
|
1199
|
+
# Do not reintroduce custom event names.
|
|
1200
|
+
#
|
|
1190
1201
|
# @api private
|
|
1191
1202
|
class SSEBody
|
|
1192
1203
|
# Sentinel pushed to the queue when the worker is done.
|
|
@@ -1546,6 +1557,9 @@ module Parse
|
|
|
1546
1557
|
# The `total` field is omitted (rather than nil) so the wire
|
|
1547
1558
|
# shape matches the spec's optional-field convention.
|
|
1548
1559
|
#
|
|
1560
|
+
# Emitted as `event: message` — see the SSEBody class docs. The
|
|
1561
|
+
# payload's `method` is what marks it as progress.
|
|
1562
|
+
#
|
|
1549
1563
|
# @param elapsed [Float] seconds elapsed since the stream started.
|
|
1550
1564
|
# @return [String] SSE event string (includes trailing blank line).
|
|
1551
1565
|
def build_progress_event(elapsed)
|
|
@@ -1557,15 +1571,14 @@ module Parse
|
|
|
1557
1571
|
"progress" => elapsed,
|
|
1558
1572
|
},
|
|
1559
1573
|
})
|
|
1560
|
-
"event:
|
|
1574
|
+
"event: message\ndata: #{data}\n\n"
|
|
1561
1575
|
end
|
|
1562
1576
|
|
|
1563
1577
|
# Format a `notifications/tools/list_changed` or
|
|
1564
1578
|
# `notifications/prompts/list_changed` SSE event. Both
|
|
1565
1579
|
# notifications have no `params` — the wire shape is just the
|
|
1566
|
-
# JSON-RPC envelope with `method` set.
|
|
1567
|
-
#
|
|
1568
|
-
# progress event name is reserved for progress notifications).
|
|
1580
|
+
# JSON-RPC envelope with `method` set. Emitted as `event: message`,
|
|
1581
|
+
# like every other frame on this stream.
|
|
1569
1582
|
#
|
|
1570
1583
|
# @param method [String] full MCP method string.
|
|
1571
1584
|
# @return [String] SSE event string (includes trailing blank line).
|
|
@@ -1599,7 +1612,7 @@ module Parse
|
|
|
1599
1612
|
"method" => "notifications/progress",
|
|
1600
1613
|
"params" => params,
|
|
1601
1614
|
})
|
|
1602
|
-
"event:
|
|
1615
|
+
"event: message\ndata: #{data}\n\n"
|
|
1603
1616
|
end
|
|
1604
1617
|
|
|
1605
1618
|
# Build the callback the dispatcher block passes into
|
|
@@ -1631,12 +1644,17 @@ module Parse
|
|
|
1631
1644
|
end
|
|
1632
1645
|
end
|
|
1633
1646
|
|
|
1634
|
-
# Format the final
|
|
1647
|
+
# Format the final JSON-RPC response SSE event.
|
|
1648
|
+
#
|
|
1649
|
+
# Emitted as `event: message` (NOT `event: response`) — an MCP
|
|
1650
|
+
# client matching only the default `message` type would otherwise
|
|
1651
|
+
# discard the response and block until its own timeout. The
|
|
1652
|
+
# envelope's `id` + `result`/`error` is what marks it final.
|
|
1635
1653
|
#
|
|
1636
1654
|
# @param body [Hash] JSON-RPC response envelope.
|
|
1637
1655
|
# @return [String] SSE event string (includes trailing blank line).
|
|
1638
1656
|
def build_response_event(body)
|
|
1639
|
-
"event:
|
|
1657
|
+
"event: message\ndata: #{JSON.generate(body)}\n\n"
|
|
1640
1658
|
end
|
|
1641
1659
|
|
|
1642
1660
|
# Build an internal-error JSON-RPC envelope (id may be nil at this layer).
|
|
@@ -1755,9 +1773,9 @@ module Parse
|
|
|
1755
1773
|
end
|
|
1756
1774
|
end
|
|
1757
1775
|
|
|
1758
|
-
# SSE wire form for a server→client notification. Event name
|
|
1759
|
-
#
|
|
1760
|
-
# SSE path
|
|
1776
|
+
# SSE wire form for a server→client notification. Event name
|
|
1777
|
+
# "message" — the single event type MCP Streamable HTTP defines for
|
|
1778
|
+
# JSON-RPC traffic, matching the request-scoped SSE path.
|
|
1761
1779
|
def format_event(notification)
|
|
1762
1780
|
"event: message\ndata: #{JSON.generate(notification)}\n\n"
|
|
1763
1781
|
end
|
|
@@ -634,6 +634,16 @@ module Parse
|
|
|
634
634
|
"agent_searchable field: :#{field_sym} is not a declared :vector property " \
|
|
635
635
|
"on #{parse_class_name} (declared: #{vector_properties.keys.inspect})."
|
|
636
636
|
end
|
|
637
|
+
# A storage-only property is not eligible for search, so the
|
|
638
|
+
# tool this registers could never run. Refuse at class load
|
|
639
|
+
# rather than at the first agent query.
|
|
640
|
+
if respond_to?(:vector_properties) &&
|
|
641
|
+
vector_properties.dig(field_sym, :searchable) == false
|
|
642
|
+
raise ArgumentError,
|
|
643
|
+
"agent_searchable field: :#{field_sym} on #{parse_class_name} is declared " \
|
|
644
|
+
"`searchable: false` (storage-only, not eligible for search) and cannot " \
|
|
645
|
+
"back a search tool."
|
|
646
|
+
end
|
|
637
647
|
filters = Array(filter_fields).map(&:to_sym)
|
|
638
648
|
@agent_searchable_field = field_sym
|
|
639
649
|
@agent_searchable_filter_fields = filters
|