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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2d06f0d8932e328bae92fde22a568b9669614a02e75a5deed1a220884213d43e
4
- data.tar.gz: edaaf3bb456a5396fdc08706188ca07d66aba66b17c7fce95220f130041644d8
3
+ metadata.gz: e52d26b7117b0f69c691813a88cde7be0429024b7e95624a0b4fc54e883c799f
4
+ data.tar.gz: e97f5cc5e069020a1580a1175e7ce04810790222ccd03e981f502982532112f5
5
5
  SHA512:
6
- metadata.gz: 202606fe6edad9aafda2d48a216addfca376d24b01f1a8a39febce4fa511fcf5253f736632bf7aeff64645fe398d015fccb694ddf622688f3a78a6431b570ed2
7
- data.tar.gz: a09f557be7a37d9f12937e0c70da314c617bd8ec18993cef2952336b7b55b6d1d9bbe3e0381933b70a4f3bdc3b2dcf3a71125edf83c460f5d15d4b0c58d3b3c8
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`. Required only for models with more
63
- # than one `embed` text source (otherwise inferred). Must name one of
64
- # the class's declared embed sources — an arbitrary field is refused so
65
- # the chunk `content` can't disclose a non-embedded field.
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
- # allowlist. nil/blank → nil (retrieve infers; works for single-source
335
- # models, raises AmbiguousTextField for multi-source so the agent knows
336
- # to pass one).
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
- return nil if text_field.nil? || text_field.to_s.strip.empty?
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
@@ -6,6 +6,6 @@ module Parse
6
6
  # The Parse Server SDK for Ruby
7
7
  module Stack
8
8
  # The current version.
9
- VERSION = "5.7.5"
9
+ VERSION = "5.7.6"
10
10
  end
11
11
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: parse-stack-next
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.7.5
4
+ version: 5.7.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adrian Curtin