parse-stack-next 5.7.5 → 5.7.6
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 +26 -0
- data/README.md +1 -0
- data/docs/atlas_vector_search_guide.md +9 -1
- data/docs/mcp_guide.md +1 -0
- data/lib/parse/retrieval/agent_tool.rb +65 -9
- data/lib/parse/stack/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e52d26b7117b0f69c691813a88cde7be0429024b7e95624a0b4fc54e883c799f
|
|
4
|
+
data.tar.gz: e97f5cc5e069020a1580a1175e7ce04810790222ccd03e981f502982532112f5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 87157a0beba280ad3c62d854989687edad86d994ed2e61b574c1394241d3151cb8f158b318d424eab95eb441a18ee48b15fc02a3debd5736d98e7c397971b3b9
|
|
7
|
+
data.tar.gz: 64d779b52738ee8c276fcbf8f6e6e6d848817b90b3841b2deceb2514ffc32c1e59c1ba8a6061d8b59c58c40e62d3085aa187dc6e5b32048ce8ac8250f7429758
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
## parse-stack-next Changelog
|
|
2
2
|
|
|
3
|
+
### 5.7.6
|
|
4
|
+
|
|
5
|
+
#### `semantic_search` no longer returns hidden fields as chunk content
|
|
6
|
+
|
|
7
|
+
A focused security fix for the `semantic_search` agent tool. When a class
|
|
8
|
+
embedded a field that its `agent_fields` allowlist hides from agents (for
|
|
9
|
+
example, searching on `body` while exposing only `title`), the tool returned
|
|
10
|
+
that field's text as `chunks[].content`, even though the `documents` map
|
|
11
|
+
correctly omitted it. The chunk text is now restricted to fields the agent may
|
|
12
|
+
read.
|
|
13
|
+
|
|
14
|
+
- **FIXED**: `semantic_search` built chunk content from the raw value of the
|
|
15
|
+
embedded text source before the per-record `agent_fields` projection ran,
|
|
16
|
+
and its `text_field` check compared against the class's embed sources but
|
|
17
|
+
not its `agent_fields` allowlist. A text source is now usable only when it
|
|
18
|
+
is both an embed source and inside `agent_fields`. An explicit `text_field`
|
|
19
|
+
naming an embedded-but-hidden field is refused with `AccessDenied`
|
|
20
|
+
(`kind: :field_denied`) before any search runs. When `text_field` is
|
|
21
|
+
omitted, the tool infers the sole readable source, refuses with
|
|
22
|
+
`:field_denied` when no embedded source is readable, and asks for an
|
|
23
|
+
explicit `text_field` when several are. Reranker input is built from the
|
|
24
|
+
same text source, so it is covered by the same check. Classes without an
|
|
25
|
+
`agent_fields` allowlist behave as before, and direct
|
|
26
|
+
`Parse::Retrieval.retrieve` callers, which are application code rather than
|
|
27
|
+
agents, are unaffected.
|
|
28
|
+
|
|
3
29
|
### 5.7.5
|
|
4
30
|
|
|
5
31
|
#### MongoDB 9.0 support
|
data/README.md
CHANGED
|
@@ -6,6 +6,7 @@ A full-featured Ruby client SDK for [Parse Server](http://parseplatform.org/). [
|
|
|
6
6
|
|
|
7
7
|
## What's new in 5.7
|
|
8
8
|
|
|
9
|
+
- **5.7.6: `semantic_search` respects `agent_fields` for chunk content.** A class that embedded a field hidden by its `agent_fields` allowlist returned that field's text as chunk content. Chunk text now comes only from embedded fields the agent may read; a hidden text source is refused with `:field_denied` before the search runs. See [CHANGELOG.md](./CHANGELOG.md)
|
|
9
10
|
- **5.7.5: MongoDB 9.0 support.** The test stack runs MongoDB 9 by default (`MONGO_VERSION=8` selects the previous major), and use the `mongo` driver 2.26 or newer for 9.0's overload handling. MongoDB 9.0 changes how `null` comparisons treat dotted paths through arrays; see the behavior notes in [CHANGELOG.md](./CHANGELOG.md)
|
|
10
11
|
- **5.7.5: `voyage-code-4` and contextualized chunk embeddings.** The Voyage provider accepts `voyage-code-4`, `voyage-context-4`, and `voyage-context-3`. `Voyage#embed_chunks` embeds whole chunked documents so each chunk's vector carries its document's context. See [CHANGELOG.md](./CHANGELOG.md)
|
|
11
12
|
- **5.7.5: Voyage reranker and MCP 2025-11-25.** `Parse::Retrieval::Reranker::Voyage` adds `rerank-3` and `rerank-3-lite` reranking, including through the Atlas endpoint. The MCP server negotiates protocol `2025-11-25` and adds `completion/complete` (class and field names scoped to the agent) and `logging/setLevel`. Mongo-direct reads re-run queries MongoDB 9.0 kills mid-flight. See [CHANGELOG.md](./CHANGELOG.md)
|
|
@@ -715,7 +715,15 @@ Because this model embeds **two** text sources (`:title` and `:body`),
|
|
|
715
715
|
`semantic_search` cannot guess which one to chunk and return as the
|
|
716
716
|
result `content`. Pass `text_field:` to choose (it must name one of the
|
|
717
717
|
embedded sources); a single-source model infers it automatically and the
|
|
718
|
-
parameter is optional
|
|
718
|
+
parameter is optional.
|
|
719
|
+
|
|
720
|
+
Through the agent tool, the chosen source must also be inside the class's
|
|
721
|
+
`agent_fields` allowlist, because chunk `content` is that field's text. A
|
|
722
|
+
class may embed a field it hides from agents (search the `body`, expose only
|
|
723
|
+
the `title`), but `semantic_search` then refuses that source with
|
|
724
|
+
`:field_denied` instead of returning it, and infers only among the readable
|
|
725
|
+
sources. Direct `Parse::Retrieval.retrieve` calls are application code and
|
|
726
|
+
are not subject to `agent_fields`:
|
|
719
727
|
|
|
720
728
|
```ruby
|
|
721
729
|
# via the agent tool (LLM-facing parameter)
|
data/docs/mcp_guide.md
CHANGED
|
@@ -912,6 +912,7 @@ front avoids discovering each only on impact:
|
|
|
912
912
|
| Reserved underscore key | A `filter:` / `vector_filter:` / `where:` contains an underscore-prefixed key (`_rperm`, `_p_*`, …) at any depth | `ArgumentError` / `ValidationError` (recursive refusal) |
|
|
913
913
|
| Filter-field allowlist | A `filter:` / `vector_filter:` names a field not in the class's `agent_searchable filter_fields:` | `ValidationError` naming the offending field(s) |
|
|
914
914
|
| `text_field` not embedded | `semantic_search` `text_field:` names a field that isn't a declared `embed` source | `ValidationError` listing the allowed sources |
|
|
915
|
+
| `text_field` hidden | The `semantic_search` text source (explicit or inferred) is an `embed` source outside the class's `agent_fields` allowlist | `Parse::Agent::AccessDenied` (`kind: :field_denied`), before any search runs |
|
|
915
916
|
| Tool filtered | A tool/method removed by a per-instance `tools:` / `methods:` filter is invoked | `error_code: :tool_filtered` |
|
|
916
917
|
| Approval denied/unavailable | A gated write/admin op is rejected or the approver is unreachable | `error_code: :approval_denied` |
|
|
917
918
|
|
|
@@ -59,10 +59,12 @@ module Parse
|
|
|
59
59
|
|
|
60
60
|
# @param agent [Parse::Agent]
|
|
61
61
|
# @param text_field [String, Symbol, nil] which embedded text source to
|
|
62
|
-
# chunk and return as `content`.
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
62
|
+
# chunk and return as `content`. Must name one of the class's declared
|
|
63
|
+
# embed sources that is also inside its `agent_fields` allowlist: an
|
|
64
|
+
# arbitrary field is refused so chunk `content` can't disclose a
|
|
65
|
+
# non-embedded field, and an embedded-but-hidden field is refused with
|
|
66
|
+
# `:field_denied` so it can't disclose a field the agent may not read.
|
|
67
|
+
# When omitted, it is inferred from the readable embed sources.
|
|
66
68
|
# @param max_chunks_per_document [Integer, nil] cap on chunks emitted per
|
|
67
69
|
# matched document (forwarded to the chunker).
|
|
68
70
|
# @param max_total_tokens [Integer, nil] ceiling on total returned
|
|
@@ -329,22 +331,76 @@ module Parse
|
|
|
329
331
|
.flat_map(&:sources).map(&:to_s).uniq
|
|
330
332
|
end
|
|
331
333
|
|
|
334
|
+
# @!visibility private
|
|
335
|
+
# The embed text sources the agent may also READ: those whose wire
|
|
336
|
+
# column is inside the class's `agent_fields` allowlist. Chunk
|
|
337
|
+
# `content` (and any reranker input) is built from the chosen source's
|
|
338
|
+
# raw value BEFORE the per-record projection strips disallowed fields,
|
|
339
|
+
# so the source itself must be readable. With no allowlist every
|
|
340
|
+
# source is readable, as before.
|
|
341
|
+
#
|
|
342
|
+
# @return [Array<String>, nil] readable sources, or nil when the class
|
|
343
|
+
# declares no `agent_fields` allowlist.
|
|
344
|
+
def readable_text_fields(klass)
|
|
345
|
+
allowlist = Parse::Agent::MetadataRegistry.field_allowlist(klass.parse_class)
|
|
346
|
+
return nil if allowlist.nil? || allowlist.empty?
|
|
347
|
+
permitted = allowlist.map(&:to_s)
|
|
348
|
+
searchable_text_fields(klass).select do |source|
|
|
349
|
+
permitted.include?(Parse::Retrieval.send(:wire_name, klass, source))
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
332
353
|
# @!visibility private
|
|
333
354
|
# Validate a caller-supplied text_field against the embedded-source
|
|
334
|
-
#
|
|
335
|
-
#
|
|
336
|
-
#
|
|
355
|
+
# list and the `agent_fields` allowlist, or infer one.
|
|
356
|
+
#
|
|
357
|
+
# * Explicit field that is not an embed source: ValidationError.
|
|
358
|
+
# * Explicit embed source outside `agent_fields`: AccessDenied
|
|
359
|
+
# (`:field_denied`), raised before any search runs.
|
|
360
|
+
# * Omitted, class has no `agent_fields`: nil, so retrieve infers as
|
|
361
|
+
# before (single source) or raises AmbiguousTextField (several).
|
|
362
|
+
# * Omitted, class has `agent_fields`: the sole readable source; a
|
|
363
|
+
# `:field_denied` refusal when none is readable; a ValidationError
|
|
364
|
+
# asking for `text_field` when several are.
|
|
337
365
|
def normalize_text_field!(text_field, klass)
|
|
338
|
-
|
|
366
|
+
readable = readable_text_fields(klass)
|
|
367
|
+
|
|
368
|
+
if text_field.nil? || text_field.to_s.strip.empty?
|
|
369
|
+
return nil if readable.nil?
|
|
370
|
+
return readable.first.to_sym if readable.length == 1
|
|
371
|
+
if readable.empty?
|
|
372
|
+
raise text_field_denied(klass, searchable_text_fields(klass).first)
|
|
373
|
+
end
|
|
374
|
+
raise Parse::Agent::ValidationError,
|
|
375
|
+
"semantic_search: this class embeds several readable text sources; pass " \
|
|
376
|
+
"text_field (allowed: #{readable.inspect})."
|
|
377
|
+
end
|
|
378
|
+
|
|
339
379
|
allowed = searchable_text_fields(klass)
|
|
340
380
|
unless allowed.include?(text_field.to_s)
|
|
341
381
|
raise Parse::Agent::ValidationError,
|
|
342
382
|
"semantic_search: text_field #{text_field.to_s.inspect} is not an embedded " \
|
|
343
|
-
"text source for this class (allowed: #{allowed.inspect})."
|
|
383
|
+
"text source for this class (allowed: #{(readable || allowed).inspect})."
|
|
384
|
+
end
|
|
385
|
+
if readable && !readable.include?(text_field.to_s)
|
|
386
|
+
raise text_field_denied(klass, text_field.to_s)
|
|
344
387
|
end
|
|
345
388
|
text_field.to_sym
|
|
346
389
|
end
|
|
347
390
|
|
|
391
|
+
# @!visibility private
|
|
392
|
+
def text_field_denied(klass, source)
|
|
393
|
+
allowlist = Parse::Agent::MetadataRegistry.field_allowlist(klass.parse_class)
|
|
394
|
+
Parse::Agent::AccessDenied.new(
|
|
395
|
+
klass.parse_class,
|
|
396
|
+
"semantic_search: text source #{source.to_s.inspect} is outside the agent_fields " \
|
|
397
|
+
"allowlist for class '#{klass.parse_class}', so it cannot be returned as chunk content.",
|
|
398
|
+
kind: :field_denied,
|
|
399
|
+
denied_field: source.to_s,
|
|
400
|
+
allowed_fields: allowlist&.map(&:to_s),
|
|
401
|
+
)
|
|
402
|
+
end
|
|
403
|
+
|
|
348
404
|
# @!visibility private
|
|
349
405
|
# Refuse any top-level filter key not in the class's declared
|
|
350
406
|
# `filter_fields` allowlist (compound operators included — the
|
data/lib/parse/stack/version.rb
CHANGED