parse-stack-next 5.7.4 → 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: ef10cccc964e68942152a64d489ea8044a99eb0b76a3fea00d75c311ddf94d23
4
- data.tar.gz: 357793ece65c1bb58147e160fb8c95782e3496deac3a1b843a82faf9dcd06737
3
+ metadata.gz: e52d26b7117b0f69c691813a88cde7be0429024b7e95624a0b4fc54e883c799f
4
+ data.tar.gz: e97f5cc5e069020a1580a1175e7ce04810790222ccd03e981f502982532112f5
5
5
  SHA512:
6
- metadata.gz: 44ed4c55b5388cd068c723f01bfa02c3efa4fafe61adfa328197a76d0bcf4006eeab313d7d3a42a8b2088b43a4a6a25bd9b89868fe183daa744c3f324c0ef0f2
7
- data.tar.gz: 721eebb0297114c0e439babab1c0c39bb1bb9697788ff4197b6d9cf072d80b1fb7d3783f1c629ea0062314321b0695565bd7e252c4e7c39e1940bcf93adabcb7
6
+ metadata.gz: 87157a0beba280ad3c62d854989687edad86d994ed2e61b574c1394241d3151cb8f158b318d424eab95eb441a18ee48b15fc02a3debd5736d98e7c397971b3b9
7
+ data.tar.gz: 64d779b52738ee8c276fcbf8f6e6e6d848817b90b3841b2deceb2514ffc32c1e59c1ba8a6061d8b59c58c40e62d3085aa187dc6e5b32048ce8ac8250f7429758
data/CHANGELOG.md CHANGED
@@ -1,5 +1,195 @@
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
+
29
+ ### 5.7.5
30
+
31
+ #### MongoDB 9.0 support
32
+
33
+ Parse Server 9.10 and the SDK's mongo-direct paths run against MongoDB 9.0.
34
+ This release moves the test stack to 9.0, re-runs reads that 9.0 kills
35
+ mid-flight, and documents the server-side behavior changes that reach SDK
36
+ callers.
37
+
38
+ - **CHANGED**: The integration test stack runs `mongo:9` by default. Set
39
+ `MONGO_VERSION=8` to run it against the previous major. A data volume
40
+ written by one major is not guaranteed to start under another, so switch
41
+ with `docker-compose down -v` or a separate `PSNEXT_PREFIX`. Atlas Local
42
+ stays on 8.0, since no 9.x image of it is published yet.
43
+ - **CHANGED**: Applications using `Parse::MongoDB`, `Parse::AtlasSearch`,
44
+ or the `*_direct` query methods against a 9.0 server should require the
45
+ `mongo` driver 2.26 or newer, which adds handling for MongoDB 9.0's
46
+ overload (Intelligent Workload Management) errors. The gem's development
47
+ lock is already on 2.26.0.
48
+ - **NEW**: Mongo-direct reads (`Parse::MongoDB.aggregate`,
49
+ `Parse::MongoDB.find`, everything routed through them such as
50
+ `results_direct`, and `Parse::AtlasSearch` searches) re-run a query the
51
+ server killed with `QueryPlanKilled` (code 175; the 9.0 release notes call
52
+ it a `QueryKilledError`). MongoDB 9.0 kills a running query this way when an
53
+ indexed field it references becomes multikey because a concurrent write
54
+ stored an array there. The read is retried up to
55
+ `Parse::MongoDB::QUERY_KILLED_RETRIES` (2) times before the error
56
+ propagates. Retries are immediate, since the kill means a concurrent write
57
+ changed the index rather than that the server is overloaded. Each retry
58
+ emits a `parse.mongodb.query_killed_retry` notification. Other driver
59
+ errors are not retried.
60
+
61
+ #### Voyage `voyage-code-4` and contextualized chunk embeddings
62
+
63
+ - **NEW**: `Parse::Embeddings::Voyage` accepts `voyage-code-4` (1024
64
+ native, Matryoshka 256/512/1024/2048, 32k tokens). It routes through
65
+ `/v1/embeddings` like the other text models.
66
+ - **NEW**: `voyage-context-4` and `voyage-context-3` are supported. These
67
+ models embed each chunk together with the document it came from, through
68
+ Voyage's `/v1/contextualizedembeddings` endpoint. `embed_text` sends each
69
+ string as a one-chunk document, which is the right shape for queries. The
70
+ `embed` class macro uses the same path, so stored fields are embedded
71
+ without surrounding-document context; call `embed_chunks` for that. These
72
+ models default to `embed_batch_size: 32` rather than 128, since every input
73
+ is a whole document and Voyage caps a request at 120k tokens.
74
+ - **NEW**: `Voyage#embed_chunks(documents, input_type:)` takes one Array of
75
+ chunk Strings per document and returns one Array of chunk vectors per
76
+ document, aligned with the input. It enforces Voyage's per-request limits
77
+ (1,000 documents and 16,000 chunks) before any network call, and raises
78
+ `BadRequestError` on a model that is not contextualized. The endpoint has
79
+ no `truncation` field, so none is sent for these models. Large inputs are
80
+ sent as several requests, grouping whole documents so each response stays
81
+ within the provider's response-size cap; a document is never split across
82
+ requests. That split is sized by response vectors, not input tokens, so
83
+ it does not by itself keep a request under Voyage's 120k-token input cap.
84
+ - **NEW**: `Parse::Retrieval::Reranker::Voyage` wraps Voyage's `/v1/rerank`
85
+ and plugs into `Parse::Retrieval.retrieve(rerank:)` like the Cohere
86
+ reranker. It defaults to `rerank-3` and accepts `rerank-3-lite` and the
87
+ 2.5 and 2 series. An Atlas model API key (`al-` prefix) routes to the
88
+ Atlas Embedding and Reranking API automatically. `truncation: false`
89
+ makes over-length inputs an error instead of truncating them.
90
+
91
+ #### MCP server speaks protocol version 2025-11-25
92
+
93
+ - **NEW**: The MCP server negotiates `2025-11-25` and advertises it as its
94
+ preferred version; `2025-06-18`, `2025-03-26`, and `2024-11-05` are still
95
+ accepted. `serverInfo` now carries `title` and `description`.
96
+ - **NEW**: `completion/complete` is implemented and the `completions`
97
+ capability advertised. Prompt arguments named `class_name`,
98
+ `parent_class`, `child_class`, or `classes`, and the `{className}`
99
+ variable of the `parse://` resource templates, complete to the class
100
+ names the connecting agent can see. `group_by` and `pointer_field`
101
+ complete to field names of the class given in the request's
102
+ `context.arguments`. Candidates come from the same tools that back
103
+ `resources/list` and `get_schema`, so hidden classes and fields are never
104
+ offered. Each completion runs those tools through `agent.execute`, so it
105
+ counts against the agent's rate limiter; clients should debounce.
106
+ - **NEW**: `logging/setLevel` is implemented. On a streaming request,
107
+ `notifications/message` events at or above the session's level are sent
108
+ on that request's response stream. Nothing is sent until the client sets a
109
+ level. The `logging` capability is advertised only by `MCPRackApp` with
110
+ streaming on, since no other transport can deliver the messages. A level
111
+ can be set only for a session that was initialized by the same principal,
112
+ so one caller cannot change another session's level. Tools emit messages with
113
+ `agent.log(level, data, logger:)`, and failed tool calls are logged at
114
+ `warning` with the tool name and error code.
115
+ - **FIXED**: Approval prompts are sent only to clients that accept form-mode
116
+ elicitation. Under `2025-11-25` a client declares its modes, and one that
117
+ declares only `url` would reject the form; the approval was then refused
118
+ and reported as a user cancellation. Such a client is now treated like one
119
+ without elicitation, so the destructive call is refused up front with
120
+ that reason. An empty `elicitation: {}` (the earlier shape) still means
121
+ form support.
122
+ - **CHANGED**: `initialize` with an `Mcp-Session-Id` already bound to a
123
+ different principal is refused with 403 instead of rebinding the session
124
+ to the new caller. Previously, knowing another session's id was enough to
125
+ take over its owner binding, and with it the listening stream, the
126
+ recorded elicitation capability, and the log level. The owning principal
127
+ can still re-initialize its own session.
128
+ - **CHANGED**: A `tools/call` whose `arguments` is not a JSON object now
129
+ returns a tool result with `isError: true` instead of an internal error,
130
+ as `2025-11-25` requires for input validation failures.
131
+
132
+ #### `server/discover` no longer fails the MCP version check
133
+
134
+ - **FIXED**: Newer MCP clients send `server/discover` before `initialize`,
135
+ carrying a protocol version this server does not support. The transport
136
+ rejected it with a 400 before it reached the dispatcher, so the client
137
+ treated the server as broken instead of falling back to `initialize`. The
138
+ request now reaches the dispatcher, which answers `-32601`, and the client
139
+ negotiates a supported version through `initialize`. Other methods still
140
+ get a 400 for an unsupported version.
141
+
142
+ #### Embedding cache entries are separated by deployment
143
+
144
+ - **FIXED**: `Parse::Embeddings::Cache` keyed entries by provider class,
145
+ model, dimensions, and input type, but not by endpoint. Two providers of
146
+ the same class and model pointed at different deployments (two
147
+ self-hosted `LocalHTTP` servers serving different weights under one model
148
+ name, or a provider behind a proxy) shared cache entries, so one could be
149
+ served the other's vector for the same query. The key now includes the
150
+ provider's deployment identity: the endpoint's scheme, host, non-default
151
+ port, and path, never credentials, userinfo, or a query string. Built-in
152
+ HTTP providers derive it from their `base_url`; `Provider#cache_identity`
153
+ can be overridden. Providers with no endpoint keep their existing keys,
154
+ so custom providers are unaffected. Existing cached entries for built-in
155
+ providers miss once and are re-filled.
156
+
157
+ #### Test infrastructure
158
+
159
+ - **CHANGED**: Development and CI use Bundler 4.0.22. The lockfile now
160
+ includes gem checksums; dependency versions are unchanged.
161
+ - **CHANGED**: The CI matrix's `3.5` lane, which resolved to a 2025
162
+ `3.5.0preview1` build, is replaced by Ruby 4.0. The unit suite passes on
163
+ Ruby 4.0.6.
164
+ - **NEW**: Snapshot fixtures pin the `$vectorSearch` pipeline (master,
165
+ user-session, strict-role, and caller-filter scopes), the native
166
+ `$rankFusion` pipeline (including that the ACL `$match` and final `$limit`
167
+ run after fusion), and `clp_scope` protected-field resolution and
168
+ redaction.
169
+ - **FIXED**: A streaming heartbeat test asserted how many heartbeats fired
170
+ before a tool's first progress report, which depends on scheduler timing
171
+ and failed intermittently on slower CI runners. It now asserts the
172
+ property under test: no heartbeat follows the first report.
173
+
174
+ ### Behavior Notes
175
+
176
+ These are MongoDB 9.0 server changes, not SDK changes. They apply to REST
177
+ queries (Parse Server passes them to MongoDB) and to mongo-direct queries
178
+ alike.
179
+
180
+ - Equality and range comparisons against `null` (`$eq`, `$ne`, `$in`,
181
+ `$nin`, `$gte`, `$lte`, and `$lookup` equality) treat a dotted path that
182
+ traverses an array and resolves to no non-null value as `null`. For
183
+ `{ a: [] }` or `{ a: [1] }`, a query for `"a.b"` equal to `nil` now
184
+ matches, and `"a.b"` not equal to `nil` no longer does.
185
+ - `$group` rejects an accumulator with an empty field name.
186
+ - A query fails with `QueryPlanKilled` if an indexed field becomes
187
+ multikey while it runs. The SDK's mongo-direct reads re-run it (see
188
+ above); REST queries surface Parse Server's error.
189
+ - `$where`, `$function`, and `$accumulator` are no longer deprecated in
190
+ 9.0. The SDK continues to block them in every pipeline and constraint it
191
+ validates.
192
+
3
193
  ### 5.7.4
4
194
 
5
195
  #### Reset connections are retried instead of surfacing a raw Faraday error
data/README.md CHANGED
@@ -6,6 +6,10 @@ 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)
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)
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)
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)
9
13
  - **5.7.0: Reserved, app-scoped cache keyspace.** `Parse::Cache::Keyspace` lays out response, identity, and role-cache keys on a shared backend (`parse-stack:v1:<app_scope>[:<namespace>]:<family>[:T:<tenant>]:<rest>`) and owns the glob patterns that clear them again, so key generation and eviction can no longer drift apart. `app_scope` is a digest of the application id and server URL, so two apps sharing one Redis no longer collide. Enable with `cache_keyspace: true` on `Parse.setup`; left unset, behavior is unchanged. See [CHANGELOG.md](./CHANGELOG.md)
10
14
  - **5.7.0: `clear_cache!` stops falling back to `FLUSHDB`.** With `cache_keyspace: true`, `Parse::Client#clear_cache!` performs a scoped SCAN inside the client's own keys instead of flushing the whole database, which previously could destroy co-tenant data and drop `first_or_create!` create-locks on a shared Redis. `flush_db!` remains the explicit opt-in for a full flush. See [CHANGELOG.md](./CHANGELOG.md)
11
15
  - **5.7.0: Response-cache auth separation enforced by construction.** `Parse::Cache::Keyspace#cache_key` now requires an `auth:` discriminator for the response-cache family and refuses to build a key without one, so a master-key body and a session-token body can no longer land under the same key by accident. A non-GET write now invalidates every auth variant of a resource in one scoped pattern instead of only the variants the process has already seen. See [CHANGELOG.md](./CHANGELOG.md)
@@ -120,8 +120,11 @@ providers:
120
120
  `{ type: "image_url", image_url: { url: ... } }` content rows.
121
121
  * `Parse::Embeddings::Voyage` — voyage-4 family (`voyage-4-large` 2048,
122
122
  Matryoshka; `voyage-4` 1024; `voyage-4-lite` 512; `voyage-4-nano` 256),
123
- voyage-3 family, domain models (`voyage-code-3`, `voyage-finance-2`,
124
- `voyage-law-2`), and `voyage-multimodal-3` (1024-dim, 32k token
123
+ voyage-3 family, domain models (`voyage-code-4`, `voyage-code-3`,
124
+ `voyage-finance-2`, `voyage-law-2`), contextualized chunk models
125
+ (`voyage-context-4`, `voyage-context-3`; route to
126
+ `/v1/contextualizedembeddings`, with whole chunked documents embedded
127
+ through `embed_chunks`), and `voyage-multimodal-3` (1024-dim, 32k token
125
128
  context, routes to `/v1/multimodalembeddings` with the wrapped
126
129
  `{inputs: [{content: [{type: "text", text: ...}]}]}` envelope for
127
130
  text and `{type: "image_url", image_url: <url>}` content rows for
@@ -330,7 +333,8 @@ Every `text:`-overload query funnels through one embed path
330
333
  ```ruby
331
334
  # Opt-in query-embed cache: repeated identical queries skip the
332
335
  # provider round-trip. Keyed by (provider, model, dimensions,
333
- # input_type, SHA-256(input)) — plaintext never lands in the store.
336
+ # input_type, deployment endpoint, SHA-256(input)); plaintext and
337
+ # credentials never land in the store.
334
338
  Parse::Embeddings::Cache.enable!(max_entries: 2048, ttl: 600)
335
339
  Parse::Embeddings::Cache.stats # => { enabled:, hits:, misses:, size: }
336
340
 
@@ -451,6 +455,69 @@ embed-time chunking), use one of these patterns:
451
455
  similarity search against the chunk collection, then hydrate
452
456
  parents as needed.
453
457
 
458
+ ### Contextualized chunk embeddings (`voyage-context-4`)
459
+
460
+ An ordinary embedding of a chunk sees only that chunk. "It ships Friday."
461
+ embeds the same no matter which product the surrounding document is
462
+ about. Voyage's contextualized models (`voyage-context-4`,
463
+ `voyage-context-3`) embed every chunk together with the rest of its
464
+ document, so each chunk vector also carries what the document is about.
465
+
466
+ To get that, send a document's chunks together with `embed_chunks`:
467
+
468
+ ```ruby
469
+ voyage = Parse::Embeddings::Voyage.new(
470
+ api_key: ENV.fetch("VOYAGE_API_KEY"),
471
+ model: "voyage-context-4", # 1024 dims; 256/512/2048 via dimensions:
472
+ )
473
+
474
+ documents = [
475
+ ["The Atlas release slipped a week.", "It ships Friday."], # one document, two chunks
476
+ ["Billing moves to the new provider.", "It ships Friday."],
477
+ ]
478
+ vectors = voyage.embed_chunks(documents, input_type: :search_document)
479
+ # => [[vec_a1, vec_a2], [vec_b1, vec_b2]] one Array per document, aligned with its chunks
480
+ # vec_a2 != vec_b2: the same sentence, embedded with different documents.
481
+
482
+ # Store each chunk vector on its own record. The chunk class declares the
483
+ # vector property but NOT the `embed` macro: `embed :content` would
484
+ # recompute the vector from `content` alone on save, silently replacing the
485
+ # contextual vector with an ordinary one.
486
+ class PostChunk < Parse::Object
487
+ belongs_to :post
488
+ property :content, :string
489
+ property :embedding, :vector, dimensions: 1024
490
+ end
491
+
492
+ documents.zip(vectors).each do |chunks, chunk_vectors|
493
+ chunks.zip(chunk_vectors).each { |text, vec| PostChunk.create!(content: text, embedding: vec) }
494
+ end
495
+
496
+ # Queries are single strings: embed_text sends each as a one-chunk document.
497
+ query_vec = voyage.embed_text(["when does it ship?"], input_type: :search_query).first
498
+ PostChunk.find_similar(vector: query_vec, k: 5)
499
+ ```
500
+
501
+ Two things to know:
502
+
503
+ * **The `embed` macro does not contextualize.** It sends each record's
504
+ source text through `embed_text`, which treats every input as its own
505
+ one-chunk document. Registering a context model as an `embed` provider
506
+ works, but the stored vectors carry no surrounding-document context.
507
+ Use `embed_chunks` when chunk-level context is the point.
508
+ * **Request sizing is partly handled for you.** Voyage caps one request
509
+ at 1,000 documents, 16,000 chunks, and 120k tokens. `embed_chunks`
510
+ checks the document and chunk caps before sending, and splits large
511
+ inputs across several requests by whole document (a document's chunks
512
+ always travel together) so that each **response** stays under the
513
+ SDK's response-size cap. That split is sized by the vectors coming
514
+ back, not by tokens going out, so it does **not** guarantee a request
515
+ stays under the 120k input-token cap. The SDK has no tokenizer to
516
+ check that; keep long documents to a few per call, and expect a
517
+ `BadRequestError` from Voyage if a request exceeds it. Context models
518
+ default to `embed_batch_size: 32` to keep `embed_text` batches clear of
519
+ the token cap for typical inputs, which is a heuristic, not a check.
520
+
454
521
  ---
455
522
 
456
523
  ## Retrieval (RAG)
@@ -541,6 +608,19 @@ when the cluster does not support it; the default `:rrf` always fuses
541
608
  client-side, which is the fully-enforced, deterministic path. `$rankFusion`
542
609
  is admitted to `PipelineSecurity::ALLOWED_STAGES` for the native path.
543
610
 
611
+ *Status:* the native path is shipped and its pipeline shape is pinned by
612
+ unit and snapshot tests. Unlike the client-side path, where each branch
613
+ enforces ACL and CLP before fusion, the native path applies the ACL
614
+ `$match` **after** `$rankFusion`: the stage fuses the unfiltered candidate
615
+ sets, then rows the caller cannot read are dropped. To keep a scoped caller
616
+ from being underfilled by those drops, the branches request a wider
617
+ candidate window and the final `$limit` runs after the ACL match; a caller
618
+ who can read only a small fraction of the collection can still receive
619
+ fewer than `k` results. Fused scores are recomputed from surviving rows so
620
+ an unreadable row's rank does not leak through. It has not yet been
621
+ validated end to end against a live Atlas cluster, which is why it stays
622
+ opt-in rather than the default.
623
+
544
624
  `Parse::Retrieval.retrieve(hybrid: true, ...)` routes through
545
625
  `hybrid_search` and chunks the fused results; pass `hybrid: { lexical:,
546
626
  vector:, fusion: }` to configure the branches. Tenant scope is folded into
@@ -565,6 +645,27 @@ chunks = Parse::Retrieval.retrieve(
565
645
  # Reranked chunks' score is the cross-encoder relevance_score.
566
646
  ```
567
647
 
648
+ Voyage's rerankers plug in the same way. `rerank-3` is the default and
649
+ `rerank-3-lite` is the cheaper option. An Atlas model API key (`al-`
650
+ prefix) routes to MongoDB's Atlas Embedding and Reranking API
651
+ automatically, exactly like the Voyage embeddings provider:
652
+
653
+ ```ruby
654
+ reranker = Parse::Retrieval::Reranker::Voyage.new(
655
+ api_key: ENV.fetch("VOYAGE_API_KEY"), # or an Atlas "al-..." key
656
+ model: "rerank-3-lite",
657
+ truncation: false, # over-length input raises instead of truncating
658
+ )
659
+ chunks = Parse::Retrieval.retrieve(
660
+ query: "reset my password", klass: Article, k: 30,
661
+ rerank: reranker, rerank_top_n: 5,
662
+ )
663
+
664
+ # Or directly, outside retrieve:
665
+ reranker.rerank(query: "capital of France", documents: docs, top_n: 3)
666
+ # => [#<struct Result index=1, relevance_score=0.91>, ...]
667
+ ```
668
+
568
669
  `Reranker::Fixture` is a deterministic, zero-network reranker (lexical
569
670
  token overlap) for tests. The `Reranker::Base` protocol validates inputs,
570
671
  bounds `top_n`, rejects out-of-range indices, and sorts descending —
@@ -614,7 +715,15 @@ Because this model embeds **two** text sources (`:title` and `:body`),
614
715
  `semantic_search` cannot guess which one to chunk and return as the
615
716
  result `content`. Pass `text_field:` to choose (it must name one of the
616
717
  embedded sources); a single-source model infers it automatically and the
617
- 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`:
618
727
 
619
728
  ```ruby
620
729
  # via the agent tool (LLM-facing parameter)
@@ -655,8 +764,9 @@ envelope. See the [MCP guide's Token Economy section](./mcp_guide.md#token-econo
655
764
 
656
765
  `embed_image` is the image-source counterpart to `embed`. The source
657
766
  property must be `:file`-typed; the target must be a `:vector` property
658
- whose declared `provider:` supports multimodal input (currently
659
- `:voyage` with `voyage-multimodal-3`, or `:cohere` with `embed-v4.0`).
767
+ whose declared `provider:` supports multimodal input (`:voyage` with
768
+ `voyage-multimodal-3.5` or `voyage-multimodal-3`, or `:cohere` with
769
+ `embed-v4.0`).
660
770
 
661
771
  Two fetch modes, selected per declaration with `source:`:
662
772
 
@@ -782,7 +892,17 @@ Direct provider calls accept the same shape:
782
892
 
783
893
  ### Save-side semantics
784
894
 
785
- * Digest is the **SHA-256 of the URL String**, not the file bytes.
895
+ * **Private buckets.** When the file adapter returns signed URLs (S3 or GCS
896
+ with `presignedUrl: true`), `file.url` holds the canonical URL with the
897
+ signature stripped and the signed form is kept in `file.presigned_url`.
898
+ Recompute fetches through `file.presigned_url` while it is still valid
899
+ (for both `source: :url` and `source: :bytes`) and falls back to the bare
900
+ URL otherwise, so a private object is reachable without making the
901
+ bucket public. Make sure the URL's lifetime outlasts the save: an expired
902
+ signature falls back to the bare URL, which a private bucket refuses.
903
+ * Digest is the **SHA-256 of the canonical URL String** (signature
904
+ stripped), not the file bytes, so a save that only rotates the
905
+ signature does not re-embed.
786
906
  Replacing the `Parse::File` with one pointing at a different URL
787
907
  re-embeds; resaving the same URL is a no-op (zero provider calls).
788
908
  Parse-managed file URLs are stable unless overwritten in place — if
@@ -841,9 +961,12 @@ each row's digest sibling (so the save-path recompute cannot elide the
841
961
  provider call), and saves. Unlike `embed_pending!` — which only fills
842
962
  NULL vectors — `reembed!` recomputes populated rows too. Run it with a
843
963
  master-key client (or pass `save_opts:` with a session token that can
844
- write every row). Each row's save makes one provider call; pace bulk
845
- runs against provider rate limits (see `BatchEmbedder` below for the
846
- pattern, or just throttle the loop).
964
+ write every row). `batch_size:` is the query page size: it controls how
965
+ many records are fetched per round, not how many inputs go into a provider
966
+ request. Each row's save makes its own provider call; pace bulk runs
967
+ against provider rate limits (see `BatchEmbedder` below for the pattern,
968
+ or just throttle the loop). `embed_pending!(batch_size:)` pages the same
969
+ way.
847
970
 
848
971
  ### Changed-width migrations: dual-field workflow
849
972
 
@@ -932,7 +1055,7 @@ Payload contract (keys always present; values may be nil):
932
1055
  | `:input_count` | `Integer` | batch size |
933
1056
  | `:input_type` | `Symbol` | `:search_query` / `:search_document` |
934
1057
  | `:total_tokens`| `Integer`/nil | provider-reported usage; nil for Fixture and providers without usage |
935
- | `:cached` | `Boolean` | always false in v5.0; reserved for v5.1 embed cache |
1058
+ | `:cached` | `Boolean` | true when `Parse::Embeddings::Cache` served the vector (no provider call) |
936
1059
  | `:error` | `String`/nil | `exception.class.name` when the block raised — class name only |
937
1060
 
938
1061
  Notes:
data/docs/mcp_guide.md CHANGED
@@ -766,6 +766,87 @@ out-of-band / clustered publisher that needs the lower-level `publish` seam.
766
766
 
767
767
  ---
768
768
 
769
+ ## Argument Completion (`completion/complete`)
770
+
771
+ The server advertises the `completions` capability, so clients that support
772
+ it (MCP Inspector, IDE integrations) can autocomplete prompt arguments and
773
+ resource-template variables:
774
+
775
+ | Argument | Completes to |
776
+ |---|---|
777
+ | `class_name`, `parent_class`, `child_class` on any prompt; `{className}` in `parse://{className}/...` | Class names the connecting agent can see |
778
+ | `classes` (comma-separated) | The last segment of the list |
779
+ | `group_by`, `pointer_field` | Field names of the class named by `class_name` (or `child_class`) in the request's `context.arguments` |
780
+
781
+ ```json
782
+ {"jsonrpc":"2.0","id":7,"method":"completion/complete","params":{
783
+ "ref":{"type":"ref/prompt","name":"count_by"},
784
+ "argument":{"name":"group_by","value":"st"},
785
+ "context":{"arguments":{"class_name":"Post"}}}}
786
+ ```
787
+
788
+ ```json
789
+ {"jsonrpc":"2.0","id":7,"result":{"completion":{"values":["status","state"],"total":2,"hasMore":false}}}
790
+ ```
791
+
792
+ Candidates come from the same `get_all_schemas` / `get_schema` tools that back
793
+ `resources/list`, so `agent_hidden` classes, the `classes:` allowlist, and
794
+ `agent_fields` all apply: a completion never offers a name the agent could not
795
+ already see. Each completion is a tool call for rate-limiting and audit
796
+ purposes, so clients that complete on every keystroke should debounce. Custom
797
+ prompts get class-name completion automatically by naming an argument
798
+ `class_name`, `parent_class`, `child_class`, or `classes`.
799
+
800
+ ---
801
+
802
+ ## Logging (`logging/setLevel`)
803
+
804
+ On `MCPRackApp` with streaming on, the server advertises the `logging`
805
+ capability. A client opts in by setting a minimum level for its session; from
806
+ then on, log messages at or above that level ride the response stream of each
807
+ streamed request as `notifications/message`, ahead of the final response.
808
+ Nothing is sent before the client sets a level, and the WEBrick `MCPServer`
809
+ and non-streaming Rack mounts do not advertise the capability at all.
810
+
811
+ ```json
812
+ {"jsonrpc":"2.0","id":3,"method":"logging/setLevel","params":{"level":"warning"}}
813
+ ```
814
+
815
+ Built-in behavior: a failed `tools/call` logs at `warning` with the tool name
816
+ and error code. Custom tools log through the agent:
817
+
818
+ ```ruby
819
+ Parse::Agent::Tools.register(
820
+ name: :reindex_posts,
821
+ description: "Rebuild the Post search index",
822
+ parameters: { type: "object", properties: {} },
823
+ permission: :readonly,
824
+ handler: lambda do |agent, **_args|
825
+ agent.log(:info, { "step" => "scanning", "rows" => 1200 }, logger: "reindex")
826
+ # ...
827
+ agent.log(:warning, "3 rows skipped: missing title")
828
+ { reindexed: 1197 }
829
+ end,
830
+ )
831
+ ```
832
+
833
+ `agent.log` takes an RFC 5424 level (`:debug`, `:info`, `:notice`,
834
+ `:warning`, `:error`, `:critical`, `:alert`, `:emergency`; anything else
835
+ raises `ArgumentError` on every transport) and any JSON-serializable data.
836
+ It is a no-op when no client is listening. Do not log secrets or rows the
837
+ agent's scope cannot read; the message goes to whoever holds the session.
838
+
839
+ A level is stored per `Mcp-Session-Id` and can be set only by the principal
840
+ that initialized that session, so one caller cannot silence or flood another
841
+ session's logs. `DELETE` on the session forgets it.
842
+
843
+ > **Spec note.** MCP `2026-07-28` removes `logging/setLevel` in favor of a
844
+ > per-request log level in `_meta` and deprecates the logging feature. This
845
+ > server targets `2025-11-25` and earlier, where `logging/setLevel` is the
846
+ > mechanism. `agent.log` is the stable API either way.
847
+
848
+ ---
849
+
769
850
  ## Built-in Agent Hardening & Telemetry
770
851
 
771
852
  5.2 adds several agent-side controls, all configured on `Parse::Agent`:
@@ -831,6 +912,7 @@ front avoids discovering each only on impact:
831
912
  | Reserved underscore key | A `filter:` / `vector_filter:` / `where:` contains an underscore-prefixed key (`_rperm`, `_p_*`, …) at any depth | `ArgumentError` / `ValidationError` (recursive refusal) |
832
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) |
833
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 |
834
916
  | Tool filtered | A tool/method removed by a per-instance `tools:` / `methods:` filter is invoked | `error_code: :tool_filtered` |
835
917
  | Approval denied/unavailable | A gated write/admin op is rejected or the approver is unreachable | `error_code: :approval_denied` |
836
918
 
@@ -0,0 +1,11 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ module Parse
5
+ class Agent
6
+ # RFC 5424 severities used by MCP logging, least to most severe. Shared
7
+ # by {Parse::Agent#log} and {Parse::Agent::MCPDispatcher} so a level is
8
+ # validated the same way whether or not a transport is attached.
9
+ LOG_LEVELS = %w[debug info notice warning error critical alert emergency].freeze
10
+ end
11
+ end