parse-stack-next 5.7.6 → 5.8.1

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.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1148 -0
  3. data/README.md +39 -23
  4. data/docs/TEST_SERVER.md +2 -2
  5. data/docs/acl_clp_guide.md +7 -0
  6. data/docs/atlas_vector_search_guide.md +181 -13
  7. data/docs/caching.md +50 -0
  8. data/docs/client_sdk_guide.md +11 -0
  9. data/docs/mcp_guide.md +332 -9
  10. data/docs/mongodb_direct_guide.md +27 -0
  11. data/docs/usage_guide.md +38 -0
  12. data/docs/webhooks_guide.md +126 -17
  13. data/lib/parse/acl_scope.rb +230 -50
  14. data/lib/parse/agent/approval_gate.rb +0 -0
  15. data/lib/parse/agent/constraint_translator.rb +42 -15
  16. data/lib/parse/agent/describe.rb +11 -3
  17. data/lib/parse/agent/field_names.rb +53 -0
  18. data/lib/parse/agent/field_policy.rb +74 -0
  19. data/lib/parse/agent/mcp_deployments.rb +426 -0
  20. data/lib/parse/agent/mcp_dispatcher.rb +8 -1
  21. data/lib/parse/agent/mcp_rack_app.rb +424 -45
  22. data/lib/parse/agent/mcp_server.rb +23 -1
  23. data/lib/parse/agent/mcp_subscriptions.rb +124 -6
  24. data/lib/parse/agent/metadata_registry.rb +67 -8
  25. data/lib/parse/agent/prompt_hardening.rb +9 -3
  26. data/lib/parse/agent/tools.rb +378 -29
  27. data/lib/parse/agent.rb +298 -21
  28. data/lib/parse/api/batch.rb +10 -1
  29. data/lib/parse/api/schema.rb +23 -4
  30. data/lib/parse/api/sessions.rb +6 -2
  31. data/lib/parse/api/users.rb +88 -14
  32. data/lib/parse/atlas_search/index_manager.rb +5 -1
  33. data/lib/parse/atlas_search/protected_paths.rb +236 -0
  34. data/lib/parse/atlas_search.rb +144 -24
  35. data/lib/parse/authorization.rb +289 -4
  36. data/lib/parse/cache/sub_cache.rb +26 -0
  37. data/lib/parse/client/authentication.rb +19 -1
  38. data/lib/parse/client/batch.rb +264 -35
  39. data/lib/parse/client/body_builder.rb +21 -0
  40. data/lib/parse/client/caching.rb +371 -27
  41. data/lib/parse/client/request.rb +26 -14
  42. data/lib/parse/client/response.rb +49 -6
  43. data/lib/parse/client.rb +227 -40
  44. data/lib/parse/clp_scope.rb +317 -25
  45. data/lib/parse/console.rb +2 -2
  46. data/lib/parse/embeddings/voyage.rb +181 -17
  47. data/lib/parse/graphql/type_generator.rb +3 -0
  48. data/lib/parse/live_query/client.rb +72 -2
  49. data/lib/parse/lock_backend.rb +4 -3
  50. data/lib/parse/model/acl.rb +119 -21
  51. data/lib/parse/model/associations/belongs_to.rb +25 -3
  52. data/lib/parse/model/associations/collection_proxy.rb +146 -17
  53. data/lib/parse/model/associations/has_many.rb +49 -9
  54. data/lib/parse/model/associations/has_one.rb +3 -1
  55. data/lib/parse/model/associations/pointer_collection_proxy.rb +145 -17
  56. data/lib/parse/model/associations/relation_collection_proxy.rb +229 -38
  57. data/lib/parse/model/bytes.rb +13 -5
  58. data/lib/parse/model/classes/role.rb +95 -0
  59. data/lib/parse/model/classes/session.rb +304 -1
  60. data/lib/parse/model/classes/user.rb +104 -8
  61. data/lib/parse/model/core/actions.rb +287 -70
  62. data/lib/parse/model/core/builder.rb +100 -8
  63. data/lib/parse/model/core/create_lock.rb +27 -2
  64. data/lib/parse/model/core/describe.rb +2 -0
  65. data/lib/parse/model/core/fetching.rb +50 -5
  66. data/lib/parse/model/core/field_guards.rb +16 -8
  67. data/lib/parse/model/core/pluralized_aliases.rb +8 -4
  68. data/lib/parse/model/core/properties.rb +488 -39
  69. data/lib/parse/model/core/querying.rb +7 -0
  70. data/lib/parse/model/core/schema.rb +5 -3
  71. data/lib/parse/model/core/search_indexing.rb +63 -0
  72. data/lib/parse/model/core/vector_searchable.rb +35 -6
  73. data/lib/parse/model/file.rb +9 -2
  74. data/lib/parse/model/geopoint.rb +61 -13
  75. data/lib/parse/model/model.rb +160 -9
  76. data/lib/parse/model/object.rb +317 -18
  77. data/lib/parse/model/phone.rb +54 -5
  78. data/lib/parse/model/pointer.rb +40 -6
  79. data/lib/parse/model/push.rb +11 -1
  80. data/lib/parse/mongodb.rb +189 -61
  81. data/lib/parse/pipeline_security.rb +415 -26
  82. data/lib/parse/query/constraint.rb +31 -1
  83. data/lib/parse/query/constraints.rb +89 -59
  84. data/lib/parse/query/cursor.rb +3 -1
  85. data/lib/parse/query/operation.rb +62 -8
  86. data/lib/parse/query/ordering.rb +34 -6
  87. data/lib/parse/query.rb +1871 -167
  88. data/lib/parse/retrieval/agent_tool.rb +225 -8
  89. data/lib/parse/retrieval/benchmark.rb +149 -0
  90. data/lib/parse/retrieval/profiles.rb +320 -0
  91. data/lib/parse/retrieval/retriever.rb +10 -1
  92. data/lib/parse/retrieval.rb +2 -0
  93. data/lib/parse/schema/search_index_migrator.rb +23 -5
  94. data/lib/parse/schema.rb +74 -18
  95. data/lib/parse/stack/tasks.rb +6 -4
  96. data/lib/parse/stack/version.rb +1 -1
  97. data/lib/parse/stack.rb +91 -14
  98. data/lib/parse/two_factor_auth/user_extension.rb +14 -2
  99. data/lib/parse/two_factor_auth.rb +11 -0
  100. data/lib/parse/vector_search/hybrid.rb +110 -33
  101. data/lib/parse/vector_search/index_definition.rb +237 -0
  102. data/lib/parse/vector_search.rb +259 -23
  103. data/lib/parse/webhooks/payload.rb +136 -6
  104. data/lib/parse/webhooks/replay_protection.rb +58 -20
  105. data/lib/parse/webhooks.rb +715 -40
  106. metadata +8 -1
data/CHANGELOG.md CHANGED
@@ -1,5 +1,1153 @@
1
1
  ## parse-stack-next Changelog
2
2
 
3
+ ### 5.8.1
4
+
5
+ A correctness release for webhooks, sessions, associations, queries, search,
6
+ and agent policies. No new defaults and no API changes; every corrected
7
+ outcome is listed under Behavior Notes.
8
+
9
+ #### Webhooks
10
+
11
+ - **FIXED**: A client create through a `before_save` webhook whose handler
12
+ returns `parse_object` now gets the ACL its class policy resolves, exactly
13
+ as an SDK-side create would. Owner-based policies (`:owner_else_private`,
14
+ the default, plus `:owner_else_public` and `:owner_but_public_read`) take the
15
+ owner from the declared `owner:` field, or from the requesting user when that
16
+ field is empty. Before, a signed-in client got the policy's no-owner
17
+ fallback (`{}` under `:owner_else_private`) and could not read its own
18
+ record. A request without a user, or a master-key request, gets the policy
19
+ fallback. A `_User` class declared with `owner: :self` is owned by the new
20
+ user, never the requester. An ACL the client sent is kept, and an ACL the
21
+ handler sets always wins (through `acl=` or mass assignment), even when it
22
+ equals the class default. A `guard :acl, :master_only` revert on a create
23
+ returns the ACL to the class policy instead of `{}`. A handler-assigned ACL
24
+ is written whatever the handler returns (`parse_object`, `true`, `nil`, or
25
+ a Hash that does not set `ACL` itself).
26
+ - **FIXED**: When a `before_save` handler changes a record, a sub-document the
27
+ client changed is written back as one-level dotted paths (`"meta.count":
28
+ {...}`) instead of the whole field, so concurrent writes to other sub-keys
29
+ survive. A removed sub-key is written as a `Delete`. Dotted keys in a
30
+ returned Hash are folded into the field or `field.sub` write they belong to,
31
+ with operators (`Increment`, `Add`, `AddUnique`, `Remove`, `Delete`) applied
32
+ rather than stored. Hash keys may use Ruby names.
33
+
34
+ #### Sessions and master-key scope
35
+
36
+ - **FIXED**: Deleting `Parse::Session` or `Parse::User` objects, one at a time
37
+ or with a batch `Array#destroy`, drops their identity-cache entries when the
38
+ delete succeeds or reports the row already gone. A denied delete leaves the
39
+ cache alone. A session that does not carry its token or owner (built from
40
+ its objectId alone, fetched without the master key, or fetched with `keys:`
41
+ leaving them out) has them looked up first, in one query per batch: with the
42
+ master key when the client has one, otherwise with the delete's `session:`.
43
+ A lookup that finds no row leaves nothing to forget; only a lookup that
44
+ fails resets the client's identity cache, at most once every 5 seconds per
45
+ client. When the lookup finds no row, the delete uses the owner recorded
46
+ when a `_Session` query or `Parse::Session.session` last fetched that
47
+ session through the same client (only the user id, never the token, in a
48
+ store separate from the identity plane,
49
+ `client.authorization.session_owner_cache`). Without one, a batch delete
50
+ that applied or reported the row gone falls back to the rate-limited reset;
51
+ a denied delete never resets, and a single `destroy` never resets. The
52
+ looked-up token is used only for the delete, is never kept on the object,
53
+ and never enters the response cache. A token resolution in flight when an
54
+ invalidation lands, in this process or, on the Redis identity plane, in any
55
+ other, does not cache its answer: the resolver checks a shared invalidation
56
+ marker before and after writing and evicts its own write when the marker
57
+ moved.
58
+ - **FIXED**: `Parse::User#signup!`, signup on save, and `upgrade_anonymous!`
59
+ store `created_at` / `updated_at` as `Parse::Date` values. Reading
60
+ `updated_at` on a newly signed-up user no longer marks it dirty.
61
+ - **FIXED**: Inside `Parse.without_master_key`, an explicit `master: true`
62
+ passed to `Parse::MongoDB.aggregate`, `Parse::ACLScope`, `Parse::Query`
63
+ direct reads, vector and hybrid search, Atlas Search, or agent tools for
64
+ master-key agents no longer runs as master. The call runs in the public
65
+ scope, as the REST request does once the block strips its master key, and
66
+ the `parse.mongodb.aggregate` notification reports `master_dropped: true`.
67
+ The SDK's own metadata reads keep the master key without lifting the block:
68
+ the class schema and role graph read to enforce a scope, index statistics,
69
+ and the Atlas Search index listing. Before, a schema read refused inside the
70
+ block was cached and denied every scoped mongo-direct read of that class,
71
+ for all callers, for 5 seconds. `Parse::AtlasSearch.faceted_search` raises
72
+ `FacetedSearchNotACLSafe` inside the block, because its bucket counts are
73
+ not ACL-filtered. The metadata marker rides only on the outgoing request
74
+ copy, never on `response.request` or an error's request. A schema read that
75
+ fails inside the block is retried at most every 2 seconds there. A LiveQuery
76
+ admin client that first connected inside the block keeps withholding its
77
+ master key on every reconnect until `allow_master_key_connection!`.
78
+
79
+ #### Agent policies
80
+
81
+ - **FIXED**: A sub-agent's `tools:` and `methods:` filters now only narrow its
82
+ parent's, as `classes:` already did. A child `tools: { only: }` is
83
+ intersected with the parent's allowlist and `except:` sets are unioned; for
84
+ `methods:` the parent's filters stay in force alongside the child's. Before,
85
+ the child's filter replaced the parent's, so a sub-agent could enable a tool
86
+ or `agent_method` within its tier that the parent was not given. A child
87
+ `only:` list with nothing its parent allows raises `ArgumentError` at
88
+ construction.
89
+ - **FIXED**: A sub-agent built with `parent:` uses its parent's approval gate,
90
+ read at call time, unless given its own. Before, it fell back to
91
+ `NullGate`, so a sub-agent built inside an MCP `tools/call` ran tools in
92
+ `require_approval_for` tiers without approval.
93
+ - **FIXED**: Qualified `methods:` entries compare the class by its Parse class
94
+ name, so `"User.x"` and `"_User.x"` (and the `Role`, `Session`, and
95
+ `Installation` aliases) match whichever spelling the entry or the
96
+ `call_method` caller uses. Before, `methods: { except: ["_User.x"] }` could
97
+ be bypassed by calling the method on `"User"`.
98
+ - **FIXED**: A sub-agent can no longer turn on `master_atlas` when its parent
99
+ does not have it. `Parse::Agent.new(parent: scoped_parent, master_atlas:
100
+ true)` raises `ArgumentError`; before, the child's `atlas_faceted_search` ran
101
+ with master-key semantics and returned every row under the parent's scoped
102
+ identity. Omitting `master_atlas` still inherits, and `false` still drops
103
+ it.
104
+ - **FIXED**: The MCP dispatcher snapshots and restores an agent's own approval
105
+ gate, so driving a sub-agent through `tools/call` no longer pins its
106
+ parent's gate onto it. Assigning `approval_gate = nil` makes a sub-agent use
107
+ its parent's gate again.
108
+ - **FIXED**: Qualified `methods:` entries may name a model by its Ruby
109
+ constant (`"Artist.purge"` for a class whose `parse_class` is `"Musician"`)
110
+ and are compared by Parse class name, so such an `except:` entry is no
111
+ longer bypassed through the Parse class name. An entry whose class resolves
112
+ to no loaded model warns at construction.
113
+ - **IMPROVED**: The `parse.agent.tool_call` audit payload adds
114
+ `methods_layers` whenever a sub-agent inherits a `methods:` filter
115
+ (including a single parent layer), and `describe[:methods][:layers]` lists
116
+ every `methods:` filter in force, parent first.
117
+
118
+ #### Associations
119
+
120
+ - **FIXED**: `add!`, `add_unique!`, and `remove!` on a
121
+ `has_many through: :array` collection take the array Parse Server returns,
122
+ as plain array properties already did. A stale local copy is corrected,
123
+ objects already fetched locally are kept, and the field is not marked
124
+ changed. The array is adopted only when every entry is a pointer to the
125
+ declared class; otherwise the operation is applied locally.
126
+ - **FIXED**: Saving a relation collection on its own (`owner.likes.save`)
127
+ sends the staged additions and removals and returns whether they were
128
+ applied. Before, it silently sent nothing.
129
+ - **FIXED**: `Parse::Object#clear_changes!` (also called by `reload!`) drops
130
+ a relation's staged additions and removals and clears array collection
131
+ dirty state. Before, discarded relation changes were sent with the next
132
+ change to that relation. A fetch (`fetch!`, partial fetch, autofetch, with
133
+ or without `preserve_changes:`) keeps a relation's staged additions and
134
+ removals and leaves the relation marked changed, including when the response
135
+ carries the relation's descriptor, so the next save sends them.
136
+ `rollback!` drops a relation's staged additions and removals too, including
137
+ after a fetch.
138
+ - **IMPROVED**: `RelationCollectionProxy#save` accepts `session:` to send the
139
+ staged operations as a given user (`nil` for no session token). Relation
140
+ writes on an owner whose objectId was assigned client-side during a create
141
+ are staged rather than sent until the create returns.
142
+ - **FIXED**: Atomic relation operations (`add!` / `remove!` on a
143
+ `through: :relation` collection) write to the relation's Parse column when
144
+ its name differs from the Ruby name (`has_many :liked_items` maps to
145
+ `likedItems`, or a relation declared with `field:`). Before, they targeted
146
+ the Ruby name.
147
+
148
+ #### Queries
149
+
150
+ - **FIXED**: An OR whose branch has no constraints now matches every row.
151
+ Previously `query | Model.query` and `Parse::Query.or(query, Model.query)`
152
+ dropped that branch and returned only the other side. An empty receiver of
153
+ `|` still starts a new OR, as `or_where` does, so
154
+ `[q1, q2].reduce(Model.query, :|)` builds `q1 OR q2`.
155
+ - **FIXED**: `Model.query(:or => [{...}, {...}])` builds a `$or` group ANDed
156
+ with the query's other constraints, instead of a constraint on a literal
157
+ field named `or`. Branches can be constraint hashes, `Parse::Query` objects
158
+ of the same class, or arrays of constraints.
159
+ - **FIXED**: A constraint that needs the aggregation pipeline or mongo-direct
160
+ (`readable_by`, `:ACL.readable_by`, `:field.array_size`, the mongo-direct geo
161
+ operators) raises `ArgumentError` inside an OR (`:or`, `or_where`, `|`,
162
+ `Parse::Query.or`). Before, the branch was emitted as `{}` and matched every
163
+ row, or lost part of its conditions, including in push targeting, which
164
+ could reach every installation.
165
+ - **FIXED**: `Parse::Query#clone`, `|`, `or_where`, `Parse::Query.or`, `:or`
166
+ branches (including Hash branches with `session:` / `use_master_key:` keys
167
+ or nested scoped queries), and subqueries (`$inQuery`, `$notInQuery`,
168
+ `$select`, `$dontSelect`) keep the query's authority and application:
169
+ session token, a session-bound client from `Parse::Client#become`, scoped
170
+ user or role, master-key flag, read preference, and the Parse application
171
+ the query targets. Queries combined this way must run under the same
172
+ authority against the same application. An unscoped query takes the
173
+ other's authority, and different authorities or applications raise
174
+ `ArgumentError`. Before, a session combined with `scope_to_user` ran as the
175
+ scoped user, a `become`-client query composed into an OR ran as the master
176
+ key, a scoped subquery ran under the outer query's master key, and a clone
177
+ could send one application's session token to the default application.
178
+ Changing the authority or client of a combined query raises and leaves the
179
+ query unchanged. A combined query re-checks its authority before it runs,
180
+ and explicit auth passed to `results_direct`, `count_direct`,
181
+ `distinct_direct`, or `atlas_search` must match it.
182
+ - **FIXED**: LiveQuery subscriptions and push targeting refuse constraints that
183
+ only the aggregation pipeline or mongo-direct can run (`array_size`,
184
+ `set_equals`, `not_empty`, `elem_match`, `readable_by`, mongo-direct geo
185
+ operators). Before, those constraints were dropped and the subscription or
186
+ push reached a wider audience. A subquery with such a constraint is also
187
+ refused, where it used to match every row. `Parse::Query#subscribe` and
188
+ `client.subscribe(query)` run under the query's own session or `become`
189
+ client, and such a subscription is refused with `ArgumentError` when the
190
+ LiveQuery client targets a different Parse application id than the query,
191
+ so one application's session token is never sent to another application's
192
+ LiveQuery server.
193
+ - **FIXED**: A field repeated in `order` is sent once, at its first position
194
+ with the last direction given, matching what MongoDB applies on direct
195
+ reads. String forms such as `"-title"` are parsed as descending, so they
196
+ deduplicate too, and the direct `$sort` stage sorts on the real field.
197
+ `order("title,-plays")` is parsed as two fields.
198
+ - **FIXED**: A bare objectId compared to a declared pointer field
199
+ (`Song.query(:artist => "abc123")`, `:artist.ne`, `:artist.in`) now matches
200
+ on REST, in `Parse::Push` targeting, and in LiveQuery subscriptions from
201
+ `Parse::Query#subscribe`, `Model.subscribe`, and `client.subscribe(query)`.
202
+ Equality and `$in` used to return no rows and `$ne` every row, while direct
203
+ and aggregate reads already matched. The new
204
+ `Parse::Query#compile_rest_where` returns the where clause in that form.
205
+
206
+ #### Search
207
+
208
+ - **FIXED**: Atlas Search (`Parse::AtlasSearch.search`, `search_with_stage`,
209
+ `autocomplete`, `Parse::Query#atlas_search`) and native hybrid search apply
210
+ the CLP `pointerFields` / `readUserFields` ownership constraint before the
211
+ page is cut. For native hybrid, the lexical input filters before its
212
+ per-input `$limit`, and the fused result is filtered again before the final
213
+ `$limit`. Rows used to be filtered after the page was cut, so when the
214
+ top-ranked hits belonged to other users a scoped caller got a short or empty
215
+ page. Inaccessible rows are still never returned.
216
+ - **IMPROVED**: `Parse::VectorSearch.search` and native hybrid search enforce
217
+ the `pointerFields` ownership constraint server-side across the whole
218
+ candidate window. When every owner field is declared as a scalar pointer
219
+ (`belongs_to`) and its storage path (`_p_<field>`) is a `type: "filter"`
220
+ field of the vectorSearch index, the constraint also goes
221
+ into `$vectorSearch.filter`, so other users' rows no longer use up the
222
+ candidate window. The prefilter is used only while the index is READY and
223
+ serving its latest definition. If Atlas refuses it (for example mid-rebuild),
224
+ the search reruns once without it and skips it for the index cache TTL. A
225
+ failed index lookup is also remembered for that TTL.
226
+
227
+ - **FIXED**: A `$vectorSearch` refusal caused by the caller's own
228
+ `vector_filter` on an unindexed path no longer switches off the owner
229
+ prefilter for every caller on that index or clears the index cache; only a
230
+ refusal naming an owner `_p_<field>` path does. Native hybrid search uses
231
+ `Parse::VectorSearch.default_index` when the vector branch names no index.
232
+
233
+ #### Documentation
234
+
235
+ - **FIXED**: README and query YARD examples use `:field.pointer_id` and
236
+ `:field.array_size`, the names the DSL has used since 5.8.0.
237
+
238
+ #### Behavior Notes
239
+
240
+ - An OR that includes an unconstrained branch now returns every row. The one
241
+ exception is an empty receiver of `|`, which starts the OR. Code that passed
242
+ an empty query for "no filter" should leave that branch out. `:or => []`
243
+ matches no rows.
244
+ - A pipeline-only constraint inside an OR now raises `ArgumentError`. Apply it
245
+ outside the OR, or run the branches as separate queries.
246
+ - Combining queries that run under different authorities or against
247
+ different Parse applications now raises `ArgumentError`. That covers OR
248
+ composition, `:or` Hash branches, and subqueries, and includes different
249
+ session tokens, a session against `scope_to_user` / `scope_to_role` / the
250
+ master key, and different scoped users or roles. Setting a new session
251
+ token, master-key flag, scope, or client on a query that took its authority
252
+ from another query also raises, as does passing different explicit auth to
253
+ a direct or Atlas terminal. Query-shaping options (`limit`, `order`, `keys`,
254
+ `skip`, `include`) inside an `:or` branch raise.
255
+ - LiveQuery subscriptions, push targeting, and subqueries with pipeline-only
256
+ constraints raise `ArgumentError`. Subscribing to a `scope_to_user` /
257
+ `scope_to_role` query, or passing a `session_token:` that differs from the
258
+ query's session, raises.
259
+ - REST queries, LiveQuery subscriptions, and push targeting that compare a
260
+ bare objectId to a declared pointer field now return the matching rows.
261
+ Undeclared fields are unchanged. If a model declares `belongs_to` for a
262
+ field whose server column holds a plain String, a bare string compared to
263
+ that field is now sent as a Pointer and stops matching those values;
264
+ declare such a field as a `:string` property instead.
265
+ - Code that relied on `master: true` keeping master authority inside
266
+ `Parse.without_master_key` should wrap that call in `Parse.with_master_key`.
267
+ A dropped `master: true` raises `ACLRequired` under either
268
+ `Parse::ACLScope.require_session_token` or
269
+ `Parse::AtlasSearch.require_session_token`, with a message naming the block,
270
+ and no longer emits the no-ACL banner. The role-graph helpers
271
+ (`users_in_role_subtree` and related) raise `ACLRequired` for
272
+ `master: true` inside the block; pass `as:` instead.
273
+ - A LiveQuery connection that already connected with the master key outside
274
+ `Parse.without_master_key` stays elevated for subscriptions made inside the
275
+ block, because Parse Server authorizes per connection; the SDK warns.
276
+ - `Parse.without_master_key` guards against accidental master-key use and is
277
+ not an isolation boundary.
278
+ - `Parse::Role.all_for_user` and `Parse::Role#all_parent_role_names` without
279
+ `master:` or `as:` return the full role closure inside
280
+ `Parse.without_master_key`, because the role graph is read as SDK metadata.
281
+ - `Parse::Session._preload_identity_for_destroy!` and the
282
+ `_after_batch_destroy` hooks are now private.
283
+ - A session deleted elsewhere whose owner this process never recorded keeps
284
+ resolving until the next rate-limited reset or `identity_cache_ttl`.
285
+ - A custom identity plane gets the cross-process revocation-race guarantee by
286
+ implementing `invalidation_nonce` and `bump_invalidation_nonce`, as the
287
+ built-in Redis plane does. One with generation counters only is covered for
288
+ single-token and per-user invalidations, but a lookup racing another
289
+ process's full reset is bounded by `identity_cache_ttl` when the plane's
290
+ `clear` also deletes its counters.
291
+ - A sub-agent's `tools:` or `methods:` `only:` list that leaves nothing once
292
+ the parent's `only:` and `except:` lists apply now raises `ArgumentError`.
293
+ An explicitly empty `only: []` is accepted. A sub-agent passing
294
+ `master_atlas: true` under a parent without it raises `ArgumentError`.
295
+ - Parse Server sends a `before_save` webhook only the resulting value of a
296
+ dotted operator. When the handler changes the record, the SDK writes back
297
+ changed sub-keys one level deep and deletes removed ones by path. Two
298
+ concurrent writes inside the same sub-key (including deeper paths under it)
299
+ can still overwrite each other, an array is written whole, and a changed
300
+ sub-key carrying a typed value (Date, Bytes, Pointer) makes the whole field
301
+ be written. A client's whole replace of a sub-document becomes a merge. The
302
+ save response reports the dotted keys; the JavaScript SDK applies them, and
303
+ other clients may need a fetch. A handler operator that cannot be folded,
304
+ or two conflicting dotted keys from the handler, fail the save with an
305
+ error.
306
+ - A `before_save` handler that returns `true`, `nil`, or a Hash keeps the
307
+ client's write as sent, so a client create without an ACL through such a
308
+ handler is stored public read and write, as before, unless the handler
309
+ assigns an ACL on `parse_object`.
310
+ - Saving a relation collection on its own sends removals and additions as two
311
+ requests. If the second fails, the first stays applied and the unsent
312
+ operations stay staged.
313
+ - Owner fields stored as arrays of pointers, undeclared, or not filter-indexed
314
+ get no `$vectorSearch` prefilter. Vector search then fills results only from
315
+ its existing candidate window.
316
+ - A custom identity cache with neither `invalidate_value` nor generation
317
+ support (`generation`, `generation_current?`, `bump_generation`) only
318
+ expires a user's tokens at `identity_cache_ttl` after a password change,
319
+ account deletion, `logout_all!`, or a batch destroy of sessions.
320
+
321
+ ### 5.8.0
322
+
323
+ #### Breaking Changes
324
+
325
+ - **BREAKING**: `query_class`, `count_objects`, `export_data`,
326
+ `explain_query`, and `atlas_text_search` refuse a caller `where:`,
327
+ `order:`, or `filter:` (including inside `$inQuery`/`$select` subqueries)
328
+ on a field outside the class's `agent_fields`, with `AccessDenied`
329
+ (`kind: :field_denied`). Before 5.8 these calls ran and let an agent infer a
330
+ hidden field's value from which rows matched or how they were ordered.
331
+ **Migration:** add the field to `agent_fields` if agents should filter or
332
+ sort on it, or drop the constraint. Classes without `agent_fields` are
333
+ unaffected.
334
+ - **BREAKING**: Session termination (`DELETE` with `Mcp-Session-Id`) passes the
335
+ Origin policy, authenticates through the agent factory (401 when refused),
336
+ and is refused with 403 for a session owned by another principal.
337
+ **Migration:** send the same credentials on `DELETE` as on the session's
338
+ other requests.
339
+ - **BREAKING**: An MCP request whose `Mcp-Session-Id` is bound to another
340
+ principal is refused with 403 (`notifications/cancelled` and elicitation
341
+ replies are instead ignored with a silent 202, so they reveal nothing).
342
+ `resources/subscribe` needs a session the caller established through
343
+ `initialize` or its listening stream; an unknown session id gets 404, the
344
+ MCP signal to re-initialize. Previously a caller who knew a session id
345
+ could unsubscribe its resources, fill its subscription cap, or route an
346
+ approval prompt to its stream, and any signed-in caller could invent
347
+ session ids to fill the global subscription limit. **Migration:** clients
348
+ that follow the MCP lifecycle need no change; send the same credentials on
349
+ every request of a session and re-initialize on 404.
350
+ - **BREAKING**: Atlas Search and vector search refuse a session-, user-, or
351
+ role-scoped query that lets a CLP protected field decide which rows match,
352
+ how they rank, or how they are filtered. This covers `fields:`, builder
353
+ block and `search_with_stage` paths (nested operators, wildcard and `multi`
354
+ path objects, `queryString`), highlight and autocomplete fields, `sort:`
355
+ keys, filter keys (nested, dotted, and `_p_` forms), vector fields and
356
+ filters, the native `$rankFusion` path, and faceted search, and a search
357
+ that names no fields while the scope has protected fields. Before, the
358
+ field was only stripped from results, so its value could be probed through
359
+ matching. Master scopes and classes with nothing protected are unaffected.
360
+ **Migration:** pass `fields:` naming the fields to search, and drop
361
+ protected fields from filters, sorts, and highlights.
362
+ - **BREAKING**: Several constraints on the same field now all apply.
363
+ `query(:plays => 5, :plays.gt => 1)` compiles to
364
+ `{"plays" => {"$eq" => 5, "$gt" => 1}}` instead of keeping only the last
365
+ one; conflicting constraints (the same operator twice with different
366
+ values, two equalities, two regexes) are combined in a top-level `$and`.
367
+ **Migration:** code that relied on a later `where` replacing an earlier
368
+ constraint on the same field should clear it with `query.clear(:where)` or
369
+ build a fresh query.
370
+ - **BREAKING**: `limit(0)` (and a negative limit) returns no rows without a
371
+ request, and aggregate helpers on such a query return their empty result.
372
+ Before, the limit was omitted and the server default of 100 rows came back.
373
+ **Migration:** use `limit(nil)` to clear a limit.
374
+ - **BREAKING**: `sum`, `average`, `min`, `max`, `count_distinct`, and
375
+ `group_by` apply the query's `order`, `skip`, and `limit` before grouping,
376
+ so `query.skip(10).sum(:plays)` sums the rows after the first ten instead of
377
+ returning nil, and `order(:plays.desc).limit(2).average(:plays)` averages
378
+ the top two rows. `distinct` and `group_by_date` are unchanged.
379
+ **Migration:** sort or trim grouped output in Ruby, or use `group_by`'s own
380
+ `.order(...)`.
381
+ - **BREAKING**: The query DSL no longer defines `Symbol#id` or `Symbol#size`.
382
+ `Symbol#id` made every symbol look like a record to ActiveRecord, so
383
+ `where(status: :draft)` raised on Rails 8.1 (#83), and `Symbol#size`
384
+ replaced Ruby's own method, so `sort_by(&:size)` raised. **Migration:**
385
+ write `:tags.array_size => 2` for `:tags.size => 2` and
386
+ `:author.pointer_id => id` for `:author.id => id`.
387
+ - **BREAKING**: Assigning a value a property cannot represent raises
388
+ `Parse::Properties::TypecastError` (an `ArgumentError`) instead of storing
389
+ a guess. Previously `true` on an `:integer` became a Delete that erased the
390
+ column, `""` became `0`, `"1.5"` became `1`, and NaN or Infinity were sent
391
+ as null. `:string` refuses Arrays and Hashes and stores `false` as
392
+ `"false"`, `:date` refuses Numerics, `:boolean` reads `"no"` and `"n"` as
393
+ false, and `Parse::GeoPoint` raises on non-numeric or non-finite
394
+ coordinates instead of becoming (0, 0). Blank strings clear the field, and
395
+ data loaded from the server never raises. **Migration:** convert untrusted
396
+ input before assigning it (for example `Integer(params[:n], exception:
397
+ false)`, `Time.at(epoch)`), or rescue `Parse::Properties::TypecastError`
398
+ where form input is assigned directly.
399
+ - **BREAKING**: Unsaved objects (no objectId) compare equal only to
400
+ themselves and hash by identity, so `uniq`, `Set`, and Hash keys no longer
401
+ merge distinct new records. **Migration:** an object's hash changes when it
402
+ is saved; rebuild a Set or Hash keyed by unsaved objects after saving.
403
+ - **BREAKING**: `destroy` keeps the objectId and sets `destroyed?`;
404
+ `persisted?` is then false, and `save` returns false (`save!` raises
405
+ `Parse::RecordNotSaved`) instead of recreating the record. **Migration:**
406
+ check `destroyed?` rather than `id.nil?` after `destroy`.
407
+ - **BREAKING**: Saving a record that references an unsaved object in a
408
+ pointer, array, or relation field fails instead of storing a pointer with a
409
+ null objectId. Single saves return false with an error on the field; batch
410
+ and transaction saves raise `Parse::RecordNotSaved`. **Migration:** save the
411
+ referenced object first, or in a `before_save` callback.
412
+ - **BREAKING**: An object built from a server row with no `ACL` key has a
413
+ nil `acl` (the row is public on the server) instead of the class default
414
+ ACL. **Migration:** handle a nil `acl` on such rows.
415
+ - **BREAKING**: The `as:` owner option is honored only as a Symbol key and
416
+ only as a `Parse::User`, a pointer to `_User`, or a user objectId String. A
417
+ String `"as"` key (as in form or JSON params) is dropped, and `"*"`,
418
+ `role:` keys, and other objects raise `ArgumentError`, so request input can
419
+ no longer choose a record's ACL owner or make it public. **Migration:** pass
420
+ the owner as `as: user` from server code.
421
+ - **BREAKING**: `Array#save` and `Array#destroy` raise `ArgumentError` on a
422
+ non-empty array that contains no Parse objects, instead of doing nothing and
423
+ returning a truthy `BatchOperation`. When one chunk of a batch raises, the
424
+ chunks that succeeded are still applied before the exception is re-raised,
425
+ and `responses` holds one entry per request. **Migration:** call them only
426
+ on arrays of `Parse::Object`, and read per-request responses.
427
+
428
+ - **BREAKING**: Mongo-direct reads (`results_direct`, `count_direct`,
429
+ `first_direct`, `distinct_direct`, and queries that auto-route there) no
430
+ longer fall back to the master key for non-master clients. A client from
431
+ `Parse::Client#become`, `Parse::User#session_client`, or a webhook
432
+ `user_client` is scoped to its session, and a client without a master key,
433
+ `Parse.client_mode`, or `use_master_key = false` reads in the public scope,
434
+ as REST does. **Migration:** set `use_master_key = true` on a master-keyed
435
+ client, or pass `master: true`, where a master read is intended.
436
+ - **BREAKING**: Scoped mongo-direct queries refuse to filter, sort, or join
437
+ on a protectedFields column, or to copy one through `$$ROOT`,
438
+ `$$CURRENT`, `$getField`, or a join sub-pipeline, with
439
+ `Parse::CLPScope::Denied`, as REST refuses with error 119. They also apply
440
+ Parse Server's default `_User` protection (other users' `email`) and each
441
+ included or joined class's protected fields, and a `$graphLookup` into a
442
+ class with protected fields is refused. **Migration:** run such queries
443
+ with `master: true`, and set `Parse::CLPScope.default_protected_fields` if
444
+ your server's `protectedFields` option differs from the default.
445
+ - **BREAKING**: `Parse.with_session(nil)`, and `with_session` given a user or
446
+ session with no token, run the block anonymously (no session token and no
447
+ master key) instead of with the master key. **Migration:** pass
448
+ `use_master_key: true` on calls that need it; `Parse.anonymous_session?`
449
+ reports the state.
450
+ - **BREAKING**: Signup (`Parse::User#signup!`, `.create`, `.signup`,
451
+ `.anonymous_signup`, `.autologin_service`) no longer sends the master key
452
+ or an ambient session, so Parse Server returns a session token and
453
+ `upgrade_anonymous!` works. **Migration:** deployments that close `_User`
454
+ create and provision users server-side should pass `use_master_key: true`.
455
+ - **BREAKING**: A `before_save` webhook that makes no change keeps the
456
+ client's write as sent (it used to erase the whole write), and one that
457
+ does change the object replies with the client's full write plus its
458
+ changes, so operators, undeclared fields, and signup fields survive. An
459
+ `after_find` webhook can no longer rewrite results (Parse Server blanks
460
+ them and crashed on `nil`); it keeps the rows or denies the find. A call to
461
+ an unregistered function returns an error, and replay dedup runs only with
462
+ a request id, a nonce header, or a signature. **Migration:** remove
463
+ undeclared fields explicitly in `before_save`, move row filtering to
464
+ `before_find`, ACLs, or CLPs, and register every function Parse Server
465
+ calls.
466
+ - **BREAKING**: `add!`, `add_unique!`, and `remove!` on collections return
467
+ `true` or `false`, and an array `has_many` or `belongs_to` refuses `nil`,
468
+ non-object values, and objects of another class when the declared class is
469
+ a registered model (an objectId String becomes a pointer of the declared
470
+ class). Setting a field on a bare `Parse::Pointer` raises instead of
471
+ writing a hidden copy that was never saved. **Migration:** check the
472
+ boolean, pass objects of the declared class, and fetch a pointer before
473
+ editing it.
474
+
475
+ #### MCP deployments can expose less than their users can read
476
+
477
+ - **NEW**: `Parse::Agent.new(fields: { Customer => %i[display_name timezone], default: [...] })`
478
+ narrows a class's `agent_fields` ceiling for one agent, so two MCP
479
+ deployments in one process can expose different subsets of the same model.
480
+ A policy can never widen past `agent_fields`, a class without `agent_fields`
481
+ is narrowed to exactly the listed fields, and a sub-agent intersects its
482
+ parent's policy. The effective set applies everywhere the class allowlist
483
+ did: projection, `keys:`, include projections, aggregation pipelines, Atlas
484
+ Search fields, `get_schema`, `completion/complete`, exports,
485
+ `agent.describe`, and `semantic_search` chunk text, reranker input, and
486
+ filter fields. The policy lives in fiber storage for the duration of each
487
+ tool call, so concurrent agents never see each other's policy, and threads
488
+ or fibers a custom tool starts inherit it.
489
+ - **FIXED**: `query_class`, `count_objects`, and `export_data` accepted a
490
+ caller `where:` or `order:` on a field outside `agent_fields`, so an agent
491
+ could infer a hidden field's value from which rows matched or how they were
492
+ ordered. They now refuse with `AccessDenied` (`kind: :field_denied`), as
493
+ `group_by`, `distinct`, and aggregation already did. Keys are resolved the
494
+ way queries send them, so `play_count` is checked as `playCount` and a
495
+ `_p_` prefix is ignored. Server-owned tenant, per-agent, and canonical
496
+ filters are not affected. A `fields:` String key such as `"User"` resolves
497
+ to the class's Parse name (`_User`), and a tool that invokes another agent
498
+ runs under both agents' policies.
499
+ - **FIXED**: `atlas_text_search` accepted a `filter:` on a field outside the
500
+ allowlist, and without `fields:` it (and a non-empty faceted search query)
501
+ searched every field; `explain_query` never checked its `where:`. Each could
502
+ reveal a hidden field's value. Filters are now checked, text search defaults
503
+ to the readable fields when an allowlist applies (and is refused when none
504
+ is readable, rather than falling back to a wildcard), hybrid retrieval
505
+ profiles restrict their lexical branch to readable fields, and
506
+ `explain_query` refuses a hidden field in `where:`.
507
+ - **FIXED**: Subqueries could reach fields a direct query would be refused:
508
+ `$inQuery`, `$notInQuery`, `$select`, and `$dontSelect` predicates (and a
509
+ `$select` key) are now checked against their target class's effective
510
+ allowlist, so an equivalent subquery is no longer an oracle for a hidden
511
+ field.
512
+ - **FIXED**: `call_method` results projected only the top-level object, so a
513
+ returned object's embedded children (or object JSON inside a returned Hash,
514
+ saved or unsaved) could carry fields outside their own class's allowlist.
515
+ Every embedded object is now projected through its own class's effective
516
+ allowlist.
517
+ - **FIXED**: Field refusals from `group_by`, `group_by_date`, and `distinct`
518
+ passed the refusal into `AccessDenied`'s class-name slot, so the message was
519
+ a stringified Hash and `kind`, `denied_field`, and `allowed_fields` were
520
+ lost. They now carry the structured details like every other refusal.
521
+
522
+ #### Supported user-scoped and analytics deployment patterns
523
+
524
+ - **FIXED**: A `$relatedTo` constraint's `key` was not checked against the
525
+ owning class's field policy, so `count_objects` on `_User` with
526
+ `$relatedTo: { object: Post#X, key: "flaggedBy" }` revealed a relation
527
+ hidden from `Post`'s allowlist. The key is now checked like `$select` keys,
528
+ with the owner resolved from a pointer hash, a `Parse::Pointer`, or a
529
+ `"Class$id"` string.
530
+ - **FIXED**: Agent tools checked a `where:`, `order:`, or filter key under one
531
+ field name and queried another. A declared column such as `PublicText`
532
+ passed the check and was sent as `publicText`, and two properties whose
533
+ Ruby and remote names crossed could be checked under one and queried under
534
+ the other. Validation and execution now share one name rule (a Ruby
535
+ property name maps to its declared column first, then an exact declared
536
+ column is kept), and `order:` is sent under the checked names.
537
+ - **FIXED**: Aggregation through agent tools could still read hidden fields:
538
+ a pipeline that never projects (`[{ "$limit" => 1 }]`) returned whole
539
+ documents, `$$ROOT.secret` and `$$CURRENT` references were not checked, and
540
+ `$lookup` / `$graphLookup` join keys (`localField`, `foreignField`, `let`,
541
+ `startWith`, `connectFromField`, `connectToField`,
542
+ `restrictSearchWithMatch`) were not checked. Rows whose source document
543
+ reaches the output are now projected to the allowlist, and every one of
544
+ those references is checked against the right class.
545
+ - **FIXED**: An `agent_method` returning aggregation rows
546
+ (`Parse::AggregationResult`) passed Parse Server's internal columns
547
+ (`_rperm`, `_hashed_password`, `_auth_data_*`) through to the caller. They
548
+ are now removed at every depth, as on every other aggregation path.
549
+ - **IMPROVED**: Each principal may hold at most 100 session bindings (the
550
+ shared master-key principal of an endpoint without a `principal_resolver`
551
+ is bounded only by the registry). Past that its own least recently used
552
+ idle binding is
553
+ evicted, so one caller flooding `initialize` can no longer push other
554
+ principals' idle sessions out of the registry and then claim their ids.
555
+ `initialize` and `resources/subscribe` are charged against the principal's
556
+ rate limiter (429 when exhausted). A skipped live session moves to the back
557
+ of the eviction order, so a registry full of live sessions no longer makes
558
+ every new binding rescan them under the lock. An owner's ordinary requests
559
+ refresh its session's position, sessions holding resource subscriptions are
560
+ never evicted, and a listening stream is rate-charged and capacity-checked
561
+ before it claims a session id.
562
+ - **IMPROVED**: A listening stream's revalidation thread is woken on close
563
+ rather than killed, so it can no longer be interrupted inside a REST call
564
+ and return a pooled connection mid-response. A revalidator that reports the
565
+ session invalid closes the stream at once; one that raises is retried, and
566
+ the third consecutive raise closes it. `user_scoped` now separates a
567
+ rejected session from a Parse Server outage: an outage (unreachable server,
568
+ 5xx) fails the request with 401 but keeps the token cached and counts as a
569
+ transient stream error, instead of closing every open stream and evicting
570
+ the token.
571
+ - **FIXED**: A listening-stream body closed before Rack iterated it detached
572
+ the session's active stream, and a close that raced the attach left a
573
+ listener registered with no stream. A body now detaches only a listener it
574
+ attached.
575
+
576
+ - **NEW**: `Parse::Agent::MCPRackApp.user_scoped(...)` builds an endpoint for
577
+ signed-in application users. The session token comes from
578
+ `Authorization: Bearer` or `X-Parse-Session-Token` (or a custom
579
+ `session_token_from:`); a missing, blank, or invalid token gets 401 before
580
+ any agent is built, and it never falls back to the master key. Identity and
581
+ tenant are pinned server-side (`tenant_from:`), and `agent_options:` passes
582
+ extra agent settings such as `fields:` while refusing identity or authority
583
+ keys (and `master_atlas` and `allow_mutations`). `permissions:` accepts
584
+ `:readonly` (default) or `:write`; `:admin` is refused because it skips the
585
+ spend cap and score quantization for every signed-in user. Sessions are
586
+ owned by the verified user id, so a refreshed token keeps its session.
587
+ `session_validation:` (`:per_request` or `:cached`) and
588
+ `session_revalidate_interval:` (default 60s) control revocation, and
589
+ `MCPRackApp.new` accepts the underlying `listening_stream_revalidator:` and
590
+ `listening_stream_revalidate_interval:`.
591
+ - **NEW**: `Parse::Agent::MCPRackApp.master_analytics(principal_resolver:, ...)`
592
+ builds a shared master-key endpoint (read-only by default) that refuses to
593
+ construct without a callable `principal_resolver`: without one every
594
+ master-key caller fingerprints as the same principal and shares stream,
595
+ approval, and cancellation ownership. An unresolved operator gets 401.
596
+ Direct `MCPRackApp.new` construction and single-operator master-key use are
597
+ unchanged.
598
+ - **IMPROVED**: The owner binding of a live session (an attached listening
599
+ stream or a pending approval prompt) is never evicted under LRU pressure,
600
+ so flooding new sessions cannot strip a victim's owner. `user_scoped`
601
+ refuses `master_atlas` and `allow_mutations` in `agent_options`.
602
+ - **FIXED**: Session termination ran before authentication, so any caller who
603
+ knew a session id could cancel its requests and drop its approvals,
604
+ subscriptions, and log level (see Breaking Changes).
605
+ - **IMPROVED**: When the session registry is full of live sessions, a new
606
+ session or stream is refused with 503 rather than admitted without an
607
+ owner.
608
+ - **FIXED**: `notifications/cancelled` and elicitation replies were bound only
609
+ to the session id, so a caller who knew another principal's
610
+ `Mcp-Session-Id` could cancel its requests or answer its approval prompts.
611
+ A session bound to an owner now accepts them only from that owner; a
612
+ mismatch is a silent 202.
613
+ - **FIXED**: `Parse::Authorization` resolved a session through `/users/me`
614
+ without bypassing the response cache, so a revoked token could re-resolve
615
+ from a cached response. Revocation is now bounded by the identity cache's
616
+ TTL and invalidation hooks. The guide documents the interval for each path
617
+ (REST, mongo-direct, listening streams).
618
+ - **IMPROVED**: The deployment factories keep one rate limiter per
619
+ principal (the session's user, or the resolved operator), bounded to the
620
+ most recently seen 10,000, so `rate_limit:` accumulates across requests
621
+ instead of resetting with each request's fresh agent. An injected
622
+ `rate_limiter:` in `agent_options` is used as-is.
623
+ - **NEW**: Orphaned subscription sessions (subscribed but never streamed, or
624
+ whose stream closed without `DELETE`) become eligible for reaping after
625
+ `orphan_ttl` (default 300 seconds, `nil` disables), releasing their
626
+ LiveQuery subscriptions. Reaping runs on the next `subscribe` or stream
627
+ attach; on an otherwise idle server, call
628
+ `MCPSubscriptions::Manager#reap_orphans!` on a timer.
629
+ - **FIXED**: A client that reconnected a listening stream before the old one
630
+ closed lost its subscriptions: the old stream's close detached the session,
631
+ unregistering the new stream's delivery and tearing down its LiveQuery
632
+ subscriptions. A superseded stream now detaches nothing; the latest stream
633
+ owns the session, and `DELETE` still tears it down.
634
+ - **FIXED**: A listening stream that closed while its first frame was being
635
+ written could start its heartbeat and identity-revalidation threads after
636
+ closing, leaving them running indefinitely. Both start under the close lock
637
+ and never once the stream is closed.
638
+
639
+ #### Opt-in server field names in returned data
640
+
641
+ - **NEW**: `Parse::Agent.new(field_names: :server)` and
642
+ `aggregation.results(field_names: :server)` return data keyed by the exact
643
+ server field names (`createdAt`, `totalPlays`, an explicit `field_map`
644
+ alias such as `ExternalID`) as Strings. Omitting the option keeps every
645
+ API's current behavior. Most agent output already used server names; the
646
+ option matters for `AggregationResult#to_h` and `#keys`, which default to
647
+ snake_case Symbols, and keeps distinct keys such as `totalPlays` and
648
+ `total_plays` instead of collapsing them. In server mode a snake_case
649
+ method name resolves when it matches exactly one key and raises naming the
650
+ candidates when it matches several. The mode is scoped per tool call, is
651
+ inherited by sub-agents, and never changes field restrictions: access
652
+ policies apply identically in either mode. Unsupported values raise
653
+ `ArgumentError`. It does not enable the REST aggregate `raw_field_names:`
654
+ or `raw_values:` flags.
655
+ - **FIXED**: `call_method` serialized a returned Parse object from its
656
+ field-type map, emitting `{"title" => "string"}` instead of values; it now
657
+ serializes the object's data (still projected and redacted). A returned
658
+ `AggregationResult` was emitted as its `inspect` string; it is now a hash.
659
+
660
+ #### Retrieval profiles for `semantic_search`
661
+
662
+ - **FIXED**: On a class without `agent_fields`, a hybrid profile's lexical
663
+ branch searched every column (`wildcard: "*"`), so CLP `protectedFields`
664
+ could decide which documents matched. It now searches the embedded text
665
+ sources unless the profile names fields.
666
+ - **FIXED**: `semantic_search` filters could use an `agent_searchable
667
+ filter_fields:` entry outside `agent_fields` unless a per-agent `fields:`
668
+ policy also applied. Filterable fields are now always bounded by the
669
+ readable fields. A profile's configured lexical field that is an exact
670
+ declared server name (`title_exact`) is kept as written instead of being
671
+ recased and refused. The strict profile budget counts chunk metadata and
672
+ per-chunk overhead, so many tiny chunks no longer return several times the
673
+ budget.
674
+ - **CHANGED**: `semantic_search` refuses a `query` longer than 4,000
675
+ characters, and a profile reranker receives at most 2,000 characters of
676
+ it. The query is paired with every candidate document in a rerank call, so
677
+ an unbounded query multiplied the provider cost of every call.
678
+
679
+ - **NEW**: `Parse::Retrieval::Profiles.register(name, ...)` defines
680
+ server-configured strategies (result counts, hybrid search, a reranker
681
+ referenced by `Parse::Retrieval.register_reranker` name, candidate and
682
+ top-n counts, a per-document text cap, a timeout, a failure mode, and a
683
+ response budget). The `semantic_search` tool takes an optional `profile:`;
684
+ without it behavior is unchanged. Profiles are validated at registration,
685
+ an unknown name at call time is refused with the available list, and an
686
+ agent can never supply a provider, endpoint, or credential.
687
+ - **NEW**: Reranking under a profile cuts each document's text before it
688
+ leaves the process, charges estimated tokens to the tenant's `SpendCap`,
689
+ and is bounded by a timeout. On a timeout or provider error,
690
+ `on_rerank_failure: :fallback` keeps the retrieval order and adds
691
+ `rerank_fallback: true` and `rerank_fallback_reason` to the result;
692
+ `:raise` fails the call.
693
+ - **NEW**: Each `semantic_search` call emits one `parse.retrieval.search`
694
+ notification with the profile, counts, rerank stats, and timings, and never
695
+ the query, document text, field values, URLs, or credentials. A failed call
696
+ emits one too, naming only the error class. Rerank token counts are SDK
697
+ estimates, not provider-reported usage.
698
+ - **CHANGED**: The response token budget covers the whole response: each
699
+ returned parent document counts once, alongside chunk text, so a caller
700
+ passing `max_total_tokens:` may get fewer chunks than in 5.7. Under a
701
+ profile the budget is mandatory: the caller can lower it but not raise or
702
+ disable it, and it is never exceeded, even by an oversized first result. A
703
+ caller's `k` can never raise retrieval above the profile's
704
+ `rerank_candidates`, and rerank options set without a `reranker:` are
705
+ refused at registration.
706
+ - **NEW**: `Parse::Retrieval::Benchmark` scores profiles on a labeled case set
707
+ (recall@k, MRR, hit rate, mean and p95 latency, forbidden-id violations,
708
+ estimated tokens), overall and per tag, through the real `semantic_search`
709
+ tool.
710
+
711
+ #### Contextualized embedding batches adapt to provider limits
712
+
713
+ - **IMPROVED**: Contextualized requests are packed by document count, an
714
+ estimated 120k-token budget (bytes divided by 3, deliberately
715
+ conservative), and the 5.7.5 response-size cap, never splitting a
716
+ document. When Voyage rejects a request as too large, it is halved by
717
+ document and retried (bounded), keeping vectors aligned with their inputs.
718
+ Only recognized size errors trigger a split; a single document that is
719
+ still too large raises an error naming its index. This is robust
720
+ adaptation, not exact token counting.
721
+ - **CHANGED**: Voyage 4xx errors now carry the provider's sanitized error
722
+ detail (`BadRequestError#status`, `#detail`) and include it in the message.
723
+
724
+ #### Vector index definitions from the model
725
+
726
+ - **NEW**: `Parse::VectorSearch::IndexDefinition.build(klass)` (also
727
+ `Parse::Schema.vector_index_definition`) derives an Atlas `vectorSearch`
728
+ definition from the `:vector` property, `agent_searchable filter_fields:`,
729
+ and `agent_tenant_scope`, with deterministic output. `preview` and `diff`
730
+ compare it with a live index, and the `vector_search_index` model macro
731
+ registers it with `SearchIndexMigrator`. Applying stays an explicit
732
+ `apply_search_indexes!` call.
733
+ - **NEW**: `property :embedding, :vector, ..., quantization: :scalar` (or
734
+ `:binary`) adds Atlas automatic quantization to the generated index
735
+ definition, cutting index memory roughly 4x or 32x. Stored vectors and the
736
+ write path are unchanged. Drift detection reports a quantization mismatch
737
+ between the declaration and the live index.
738
+
739
+ #### Queries use a property's declared remote name
740
+
741
+ - **FIXED**: A property declared with an explicit remote name, such as
742
+ `property :account_id, :string, field: :account_id` or
743
+ `property :auth_id_sub, :string, field: :authId_sub`, was saved and read
744
+ under that name, but queries camel-cased it anyway: `where(account_id:)`
745
+ compiled to `accountId`, `order(:auth_id_sub.desc)` to `authIdSub`, and even
746
+ the string key `"authId_sub"` became `authIdSub`, so queries against systems
747
+ whose columns use underscores or mixed casing silently matched nothing.
748
+ Queries now send a declared `field:` name exactly as declared, for both the
749
+ Ruby name and the remote name, across `where`, operators, `order`, `keys`,
750
+ `include`, subqueries (each with its own class's names), aggregation
751
+ helpers (`sum`, `average`, `min`, `max`, group-by), and the mongo-direct
752
+ entry points. Only explicit `field:` names that differ from the default
753
+ camelCase are aliased, never an internal `_` column; other names, the
754
+ built-in system fields, and `Parse::Query.field_formatter` (including
755
+ `nil`) behave exactly as before.
756
+
757
+ - **FIXED**: A block passed to `results`, `first`, `results_direct`, and
758
+ other query methods now runs outside the query's field-alias scope.
759
+ Before, a `Pointer#fetch(keys:)`, a partial `fetch!`, or a cursor used
760
+ inside the block formatted another class's keys with the outer model's
761
+ declared `field:` names, sending `account_id` where the other class expects
762
+ `accountId`.
763
+ - **FIXED**: Partial fetches (`Pointer#fetch`, `fetch!`, `fetch_json`),
764
+ `fetched_keys` tracking, and cursor pagination constraints use the fetched
765
+ class's own declared `field:` names, so `keys: [:account_id]` on a model
766
+ that declares `field: :account_id` requests the right column.
767
+ - **IMPROVED**: Field-alias resolution is cached per class and refreshed when
768
+ a model, `parse_class`, or field is declared, and re-entering the scope for
769
+ the same class skips setup. `Parse::Model.find_class` caches lookups for
770
+ class names with no Ruby model, so queries on such classes are no slower
771
+ than in 5.7.
772
+ - **CHANGED**: The query field-alias scope lives in inheritable fiber storage,
773
+ so threads and fibers started while a query compiles see the same names.
774
+
775
+ #### Vector search uses the stored column for multi-word vector properties
776
+
777
+ - **FIXED**: A `:vector` property is saved under its `field_map` name
778
+ (`property :body_embedding, :vector` is stored as `bodyEmbedding`), but
779
+ `find_similar`, hybrid search, index auto-discovery, and drift checks used
780
+ the Ruby name (`body_embedding`) as the vector path, so a multi-word vector
781
+ property searched a path holding no vectors and auto-discovery could not
782
+ find its index. They now use the stored column, as does the new index
783
+ generator. Single-word properties such as `embedding` are unaffected. An
784
+ index created with the Ruby name as its path must be recreated with the
785
+ stored name; drift detection reports the mismatch.
786
+
787
+ #### Records keep their identity, values, and writes
788
+
789
+ - **NEW**: `:number` is a property type for Parse Number columns: integral
790
+ values read back as Integer and fractional values as Float, so `4.75` is no
791
+ longer truncated to `4` and `5` does not become `5.0`. Server `Number`
792
+ columns (`Parse::Schema`, `auto_generate_models!`) use it, and GraphQL
793
+ exposes it as `Float`. `:integer` and `:float` keep their casting.
794
+ - **NEW**: `destroyed?`, `cache_key`, `cache_version`, and
795
+ `cache_key_with_version` (ActiveRecord-compatible, so fragment caches no
796
+ longer collide across classes or never expire), and `attribute_values`,
797
+ the value form of `attributes` (which stays the property type map that
798
+ ActiveModel serialization reads).
799
+ - **FIXED**: Mass assignment (`attributes=`, `apply_attributes!`, ActiveModel
800
+ `assign_attributes`) could change an object's `id`, so a params hash with
801
+ `"id"` redirected the next save to another record, and `assign_attributes`
802
+ skipped the protected-key filter (`session_token`, timestamps). Both are
803
+ closed, and assigning `objectId` no longer triggers an autofetch.
804
+ - **FIXED**: `persisted?` means the record exists on the server, as
805
+ ActiveModel expects. Before, an object with unsaved changes or missing
806
+ timestamps reported false, so a Rails edit form re-rendered after a failed
807
+ validation submitted to create. Reading `updated_at` or `created_at` after
808
+ `create` no longer marks the record dirty.
809
+ - **FIXED**: In-place edits to `:object` hashes (`obj.meta["k"] = 1`) and to
810
+ hashes inside arrays are detected and saved; they were silently dropped.
811
+ `:array` keeps `nil` elements instead of shifting later positions, assigned
812
+ arrays and hashes are copied so the caller's data is not changed through
813
+ the property, array change history records the real previous value, and
814
+ `rollback!` restores arrays.
815
+ - **FIXED**: Dates nested inside `:array` and `:object` values are saved as
816
+ Parse Dates rather than ISO strings. `Parse::GeoPoint.new(lat:, lng:)` (and
817
+ `latitude:`/`longitude:`, hashes, numeric strings) no longer becomes (0, 0).
818
+ Without phonelib, a US number entered without `+` was saved with a Swiss
819
+ country code; 10-digit North American numbers now get `+1`, results match
820
+ phonelib, and `Parse::Phone.default_country_code` (default `"1"`) controls
821
+ national numbers. `Parse::Bytes` encodes without newlines and
822
+ `attributes=` works, and a `Parse::File` from server data no longer guesses
823
+ `image/jpeg`.
824
+ - **FIXED**: `<field>_increment!` and `_decrement!` applied the amount twice
825
+ to the local value, and `op_increment!` truncated fractional amounts.
826
+ Changes to array fields were never marked saved after a save, so the next
827
+ save resent the whole array.
828
+ - **FIXED**: `Parse::Object.new` converts only strong parameters, Structs,
829
+ and OpenStructs through `to_h` (permitted `ActionController::Parameters`
830
+ now work; unpermitted ones still raise), and with
831
+ `strict_property_redefinition = false` the redeclaration warning reports
832
+ the existing type rather than the new one.
833
+ - **FIXED**: The cache middleware's rescue named Redis error classes
834
+ directly, so with a non-Redis store any other store error became
835
+ `NameError: uninitialized constant Redis`. Redis and `connection_pool`
836
+ classes are listed only when loaded, and `Redis::BaseConnectionError`,
837
+ `Redis::ReadOnlyError`, `RedisClient::Error`, and `Errno::ECONNREFUSED` now
838
+ disable caching for the request instead of failing it.
839
+
840
+ #### ACL edits and revocations are always saved
841
+
842
+ - **FIXED**: Revoking an ACL grant after a save was not sent: a stale
843
+ pre-save snapshot made the revocation compare unchanged, and `save`
844
+ returned true without writing it.
845
+ - **FIXED**: `rollback!` now undoes in-place ACL edits. Copying an ACL shared
846
+ its permissions table, so a rolled-back grant was still sent on the next
847
+ save and change history showed identical old and new ACLs.
848
+ - **FIXED**: `acl.delete(:public)`, `acl.delete("*")`, and `acl.delete(role)`
849
+ remove the entry (only user ids matched before), and
850
+ `Permission#read!`/`#write!`/`#no_read!`/`#no_write!` mark the ACL changed,
851
+ so a revoke through a `Permission` on a fetched object is saved.
852
+ - **FIXED**: Batch (`Array#save`) and transaction saves apply the
853
+ `acl_policy` owner ACL to new records; owner-only records were created
854
+ public.
855
+ - **FIXED**: `apply_role("role:Admin")` no longer writes `role:role:Admin`,
856
+ the string `"false"` is a denial rather than a grant, and ACL `eql?`/`hash`
857
+ agree with `==`.
858
+
859
+ #### Batches, transactions, and retries apply exactly what succeeded
860
+
861
+ - **FIXED**: Transactions are sent as one `POST /batch` with
862
+ `transaction: true`. The flag was dropped and transactions over 50
863
+ operations were split into parallel batches, so a failure could leave a
864
+ partial commit while the SDK rolled local state back. The retry on
865
+ conflict code 251 now runs.
866
+ - **FIXED**: A batch chunk that fails with an HTTP error or a malformed
867
+ response fails every request in that chunk; before, later responses
868
+ shifted onto the wrong objects, and a short or non-array response counted
869
+ as success with objects marked clean but missing ids. Identical requests
870
+ (two Increments) are no longer collapsed into one, an object whose
871
+ attribute write and relation write split keeps the failed part dirty, a
872
+ batch create sets `updated_at`, `Array#destroy` marks objects destroyed,
873
+ new objects' relation additions are sent in the create, and a success body
874
+ with a `code` or `error` column is no longer treated as a failure.
875
+ - **FIXED**: `save_all` reports a failing last page, visits records the block
876
+ leaves unchanged instead of stopping early, no longer mutates the caller's
877
+ constraints, and a query-scoped `save_all` with a block no longer runs a
878
+ second forced save over every match.
879
+ - **FIXED**: A 502 or 504 with a JSON body that has no Parse error (as from a
880
+ gateway or load balancer) raises `Parse::Error::ServiceUnavailableError`;
881
+ before, it counted as success and saves returned true with no id.
882
+ - **FIXED**: With `assume_server_idempotency`, retries only replay routes
883
+ Parse Server deduplicates (object, user, and installation writes,
884
+ functions, jobs); `POST /batch`, files, push, login, schemas, and config
885
+ are never replayed. Automatic request ids are left off functions, jobs,
886
+ push, logout, sessions, events, and password reset (the exclusion never
887
+ matched). A retried DELETE that finds the object gone reports success,
888
+ bodies with `__op` at any depth are not retried, error 143 is no longer a
889
+ `TimeoutError`, `retry: 0` disables retries, and the caller's headers hash
890
+ is no longer modified.
891
+ - **CHANGED**: The `first_or_create!(synchronize: true)` lock lease defaults
892
+ to the client's worst-case time for a find and a create with retries
893
+ (about 222s with default timeouts) instead of 3 seconds, so it no longer
894
+ expires mid-call and admits a duplicate. The maximum `ttl:` rises from 30 to
895
+ 300 seconds.
896
+
897
+ #### Queries return the rows they describe
898
+
899
+ - **FIXED**: `or_where` and `|` keep constraints added next to an existing
900
+ `$or`, and `Parse::Query.and` keeps every `$or` it combines.
901
+ - **FIXED**: Automatic paging (`limit(:max)`, `all`, `results { }`) appends
902
+ `objectId` as the final sort key, so rows that tie on the sort field are no
903
+ longer returned twice or skipped (live: 1,200 rows returned, 617 unique).
904
+ - **FIXED**: `latest` and `last_updated` work on a query that already has an
905
+ order, and `first`, `latest`, and `last_updated` no longer change the query
906
+ they are called on.
907
+ - **FIXED**: A bare objectId compared to a pointer field in aggregate and
908
+ mongo-direct queries is matched in pointer storage form (`$ne` matched every
909
+ row; equality matched none), and a `Set` or `Range` passed to `in`, `nin`,
910
+ `all`, or `contained_by` is expanded into its members.
911
+ - **FIXED**: `Parse::Model.find_class` no longer returns a class left behind
912
+ by `remove_const` after a model is redefined (code reload, tests), so field
913
+ aliases and `_p_` pointer detection use the current class, and a class named
914
+ after an earlier lookup is found. `belongs_to` and `has_many` refresh the
915
+ cached aliases, and linked-pointer constraints use the linked class's
916
+ declared names.
917
+
918
+ #### The gem coexists with Rails and other libraries
919
+
920
+ - **FIXED**: The gem loads when `Rails` is defined without railties (for
921
+ example by rails-html-sanitizer).
922
+ - **CHANGED**: Query DSL methods on `Symbol` (`:plays.gt`, `:name.desc`) live
923
+ in included modules, so a method Ruby or another library (Mongoid, Sequel
924
+ core extensions) defines on `Symbol` is never replaced, whichever loads
925
+ first. Skipped names are listed in `Parse::Operation.symbol_conflicts`, and
926
+ Mongoid query keys passed to a Parse query are translated.
927
+ - **FIXED**: Automatic pluralized aliases are created only in the namespace
928
+ that defines the model (`::Posts` for `Post`), never in the module where
929
+ the lookup happened, and a frozen namespace no longer raises `FrozenError`.
930
+ - **FIXED**: Schema migration no longer crashes on relation fields, creates
931
+ Pointer and Relation columns with their `targetClass`, creates `:vector`
932
+ columns as Array, and no longer reports `:vector`, `:timezone`, `:phone`,
933
+ or `:email` fields as mismatched.
934
+ - **FIXED**: `Parse.auto_generate_models!` exposes a server column that
935
+ clashes with a Ruby method (`class`, `hash`, `send`) as `<name>_field`,
936
+ skips a second column that underscores to an existing name with a warning
937
+ instead of aborting, and generated classes query their real server class.
938
+ - **FIXED**: Search index migrations, `describe(:atlas)`, and the agent's
939
+ Atlas tools load Atlas Search on first use instead of failing with
940
+ `NameError`. `enable_mcp!` no longer needs webrick (not a dependency), and
941
+ prompt-marker scrubbing works without the MCP client loaded.
942
+ - **CHANGED**: `Parse::User.model_name` is relative to the `Parse`
943
+ namespace, so Rails forms and routes use `params[:user]` and `users_path`.
944
+
945
+ #### Sessions, MFA, and the response cache
946
+
947
+ - **FIXED**: `login`, `login_with_mfa`, `verify_password`,
948
+ `request_password_reset`, and `request_email_verification` are always
949
+ sent without the master key or any session token. With the master key,
950
+ Parse Server skipped MFA validation and saved the submitted
951
+ `authData.mfa` over the enrolled TOTP secret, so any code was accepted and
952
+ MFA was silently disabled for the account.
953
+ - **FIXED**: An explicit session token (a call's `session_token:` or an
954
+ `X-Parse-Session-Token` header) is never replaced by the ambient
955
+ `with_session` token or a client-bound token. `Parse::User.session(a)`
956
+ inside `with_session(b)` returned user B and cached token A as B.
957
+ - **FIXED**: Logout, `logout_all!`, session destroy, password change, user
958
+ deletion, and `Role` saves invalidate the cached identity and role
959
+ closures, so revocations and role changes reach mongo-direct and Atlas
960
+ paths immediately instead of after the identity or role TTL; atomic
961
+ `role.users` and `role.roles` operations invalidate the same caches. The MCP
962
+ session check treats Parse Server's answer for a revoked token (HTTP 400,
963
+ code 209) as a rejection.
964
+ - **FIXED**: The response cache never stores `users/me`, `sessions/me`,
965
+ `login`, `verifyPassword`, or `logout`. Cache keys in both layouts carry
966
+ the application id and a digest of the credential sent, so a client with
967
+ other keys sharing the store cannot read another's entries. A write
968
+ retires every cached variant of the resource and every cached query over
969
+ its class (including batch sub-requests), so a revoked row is not served
970
+ from a cached query until it expires. Reads establish their cache versions
971
+ before the request and skip storing a response when a version changed in
972
+ flight, so a read that overlaps an ACL write cannot restore the revoked
973
+ row.
974
+ - **FIXED**: `Parse::Client#send_request` honors the request's own
975
+ `session_token:`, `use_master_key:`, `cache:`, and `retry:` options; a
976
+ request opting out of the master key still sent it.
977
+ - **FIXED**: A password-only login on an MFA account raises
978
+ `Parse::MFA::RequiredError`, and a wrong code raises
979
+ `Parse::MFA::VerificationError`, instead of `ServiceUnavailableError`.
980
+ - **CHANGED**: Session tokens are redacted from `Parse::User`,
981
+ `Parse::Session`, and `Parse::Query` `inspect` output, and
982
+ `Parse::Session#as_json` omits `sessionToken` unless
983
+ `include_session_token: true` is passed.
984
+
985
+ #### Mongo-direct reads match REST access rules
986
+
987
+ - **FIXED**: `readUserFields` and pointer-permission CLPs are enforced on
988
+ mongo-direct, Atlas Search, vector, and hybrid reads (every authenticated
989
+ user saw every row) and are applied before `$skip`/`$limit`/`$count`, so
990
+ `count_direct` and pages are correct. Joins into another class apply that
991
+ class's ownership rules and fail closed on a denied CLP.
992
+ - **FIXED**: A user keeps their own protected `_User` fields only when no
993
+ pipeline stage can rewrite `_id`; a pipeline that set another user's `_id`
994
+ to the caller's exposed that user's email.
995
+ - **FIXED**: Direct queries inside `with_session` were denied by CLP because
996
+ the schema lookup sent the session token; it now uses the master key.
997
+ - **FIXED**: `$inQuery`, `$notInQuery`, `$select`, `$dontSelect`, and
998
+ `$containedBy` work on `results_direct` and `count_direct`, and direct rows
999
+ decode like REST: included objects carry their class, `includes` with
1000
+ `keys` keeps them, File columns decode as files, and a dotted key returns
1001
+ its whole column.
1002
+ - **CHANGED**: protectedFields stripping removes top-level columns only,
1003
+ matching Parse Server.
1004
+ - **FIXED**: Scoped mongo-direct pipelines on classes with protected fields
1005
+ accept a `$getField`, `$setField`, or `$unsetField` name only as a plain
1006
+ string or a `$literal` string, so a name read from the document cannot
1007
+ alias a protected field.
1008
+ - **FIXED**: `results_direct`, `count_direct`, `distinct_direct`, the
1009
+ mongo-direct auto-route, and the Atlas Search bridge respect
1010
+ `Parse.without_master_key`, resolving to the session or public scope as
1011
+ REST does instead of reading as master.
1012
+ - **FIXED**: Subqueries (`in_query`, `not_in_query`, `select`,
1013
+ `dont_select`) combined with `or_where` or nested in `$and`, `$or`, or
1014
+ `$nor` compile to their own joins on the direct path, with the joined
1015
+ class's ACL, CLP, and protected-field checks. A subquery in a position that
1016
+ cannot be translated, or nested in a query-derived aggregation, raises
1017
+ `ArgumentError` instead of reaching MongoDB raw (one under `$not` used to
1018
+ invert the filter).
1019
+
1020
+ #### Webhooks follow Parse Server's contract
1021
+
1022
+ - **FIXED**: `before_delete` can deny a delete, and `after_destroy` fires for
1023
+ afterDelete webhooks. `Payload#parse_query` reads constraints only from
1024
+ `where` and applies `limit`, `skip`, `order`, `keys`, and `include`. The
1025
+ response log is redacted, an unexpected handler exception returns a JSON
1026
+ error without its message, and a trigger whose body names a different
1027
+ class than its URL is refused.
1028
+ - **FIXED**: A signed webhook delivery is deduplicated on its signature, so a
1029
+ captured request cannot be replayed within the timestamp window by
1030
+ altering or dropping the unsigned nonce. A signature may cover the
1031
+ delivery nonce (`"#{timestamp}.#{nonce}.#{body}"`), so identical bodies
1032
+ sent in the same second each get their own signature.
1033
+ - **NEW**: `error!(message, code:)` and `Parse::Webhooks::ResponseError#code`
1034
+ carry a Parse error code (Parse Server's HTTP adapter still reports 141).
1035
+
1036
+ #### Associations save exactly what changed
1037
+
1038
+ - **FIXED**: Relation additions and removals are cleared after a save and
1039
+ deduplicated (a stale `RemoveRelation` was resent on every later save),
1040
+ staged without loading the relation, and never queried with a null owner.
1041
+ A relation declared with `field:` reads the remote column, and `rollback!`
1042
+ works on relations.
1043
+ - **FIXED**: `clear` on an array collection is saved, and atomic
1044
+ `add!`/`add_unique!`/`remove!` keep the local array in step with the
1045
+ server instead of emptying it (the next save overwrote the server array).
1046
+ A clean array adopts the array the server returns; unsaved local edits are
1047
+ kept and still sent.
1048
+ - **FIXED**: Nested partial fetches cover multi-word `belongs_to` fields, a
1049
+ query `has_many` on an unsaved owner returns a chainable empty query, and
1050
+ `CollectionProxy#replace` is added.
1051
+ - **FIXED**: Reading `acl` on an object fetched with `keys:` that left the
1052
+ ACL out fetches the stored ACL instead of returning nil, so code that
1053
+ edits it cannot replace the record's real ACL. Saving other fields never
1054
+ sends the ACL, and the partial-fetch tracking survives an update, so the
1055
+ ACL is still fetched on read after a save. Reassigning a property to itself
1056
+ after an in-place edit keeps the edit.
1057
+
1058
+ #### `protectedFields` resolution matches Parse Server
1059
+
1060
+ - **FIXED**: `Parse::CLPScope.protected_fields_for` now resolves
1061
+ `protectedFields` the way Parse Server does: it intersects only the groups
1062
+ present in the map that apply to the caller (`*`, `authenticated`, each
1063
+ `role:` claim, and the caller's user id). Previously a map with no `"*"`
1064
+ entry started from an empty set and intersected every role and user entry
1065
+ away, so `{ "role:Restricted" => ["secret"] }` stripped nothing for a member
1066
+ of `Restricted`. Mongo-direct queries, Atlas Search, vector search, and
1067
+ pipeline validation could therefore return fields the server would have
1068
+ hidden. The `authenticated` group is now applied too.
1069
+ - **CHANGED**: The integration stack pins Parse Server 9.10.3 (was 9.10.0)
1070
+ and enables `enableLiveQueryClassLevelPermissionRoles`. New integration
1071
+ coverage checks empty versus omitted `username` and `password` on `_User`
1072
+ updates, refusal of anonymous, cross-user, and redirected `_User` updates,
1073
+ `_Session` create honoring the class's `create` and `addField`
1074
+ permissions, and role-granted LiveQuery subscriptions.
1075
+
1076
+ #### Release checks prove the intended coverage ran
1077
+
1078
+ - **CHANGED**: `rake test:integration` checks Parse Server's health before
1079
+ each file and after each failure, and fails the run (naming the file) when
1080
+ the server goes down instead of passing with skips. Skips caused by an
1081
+ unreachable Parse Server, MongoDB, or Redis fail their file; Atlas-only,
1082
+ missing-credential, and missing-tool skips remain skips.
1083
+ `PSNEXT_FAIL_ON_INFRA_SKIP=false` reports them without failing.
1084
+ - **FIXED**: The integration stack's Parse Server cache adapter could crash
1085
+ Parse Server when its Redis client was closed (an unawaited rejected `put`
1086
+ became an uncaught exception). Cache commands now connect lazily, wait for
1087
+ readiness with a bound, and treat any command error as a cache miss.
1088
+ - **NEW**: The release workflow validates the tagged commit before
1089
+ publishing: the tag must match `version.rb`, the unit-test workflow must
1090
+ have passed on that exact commit, and the unit suite runs again; otherwise
1091
+ nothing is pushed. Provider contract tests run in a separate scheduled or
1092
+ manual workflow, never on pull requests.
1093
+ - **NEW**: CI runs the unit suite against ActiveModel/ActiveSupport 7.1, 7.2,
1094
+ and 8.0 (`gemfiles/`), and an MCP client smoke test drives the Rack app
1095
+ over real HTTP through initialize, tool and prompt listing, completion,
1096
+ logging, a streamed tool call, and session termination.
1097
+ - **NEW**: The MongoDB direct guide documents MongoDB 9.0 `null` semantics on
1098
+ dotted paths. The SDK never compares a dotted path to `null`
1099
+ on its own; only caller-supplied constraints are affected, and those
1100
+ shapes are pinned by tests.
1101
+
1102
+ #### Query model resolution works for any Parse class name
1103
+
1104
+ - **FIXED**: `Parse::Query` looked up its table's model with
1105
+ `Parse::Model.const_get`, which fails for Parse class names that are not
1106
+ Ruby constants (`"contacts"`, `"_User"`) and misses models whose
1107
+ `parse_class` differs from the Ruby class name. The error was rescued to
1108
+ nil, so pointer fields were silently treated as plain fields: mongo-direct
1109
+ pipelines addressed `owner` instead of `_p_owner` and pointer values were
1110
+ not converted to storage form. Models now resolve through
1111
+ `Parse::Model.find_class`.
1112
+
1113
+ ### Behavior Notes
1114
+
1115
+ - An Atlas vector index whose path is the Ruby name of a multi-word
1116
+ `:vector` property (`body_embedding`) must be recreated with the stored
1117
+ column (`bodyEmbedding`); drift detection reports the mismatch. Such an
1118
+ index never matched stored vectors, so search results do not get worse.
1119
+ - On a shared master-key endpoint without a `principal_resolver`, every
1120
+ caller is the same principal, so owner binding does not separate them; use
1121
+ `MCPRackApp.master_analytics`, which requires a resolver.
1122
+ - `Parse::Authorization` resolves a session through `/users/me` with the
1123
+ response cache bypassed, adding one uncached round trip per identity-cache
1124
+ miss.
1125
+ - Field policies, field-name mode, and query field aliases live in fiber
1126
+ storage for each tool call or query, so a thread or fiber a custom tool starts inherits them. A thread
1127
+ created before the call (a long-lived pool) does not, and runs unnarrowed.
1128
+ - Without a tenant scope, every `user_scoped` caller charges the shared
1129
+ default `SpendCap` bucket, so one user can use up the embedding and
1130
+ reranking budget for all. Declare `agent_tenant_scope` on the searched
1131
+ classes and pass `tenant_from:` so each tenant has its own budget.
1132
+ - `Parse::Object.transaction` now really runs as a Parse Server transaction,
1133
+ which needs MongoDB to be a replica set or mongos. On a standalone server it
1134
+ fails with a 500 instead of running without atomicity, and the raised
1135
+ `Parse::Error` says so and points to `Array#save` for a non-atomic batch.
1136
+ Parse Server runs a transaction's requests concurrently on one session,
1137
+ which MongoDB intermittently rejects with a bare 500. Because a 500 does
1138
+ not prove the transaction was not applied (a failed commit looks the
1139
+ same), the SDK resends only on a 251 conflict by default;
1140
+ `transaction(retry_server_errors: true)` also resends on a 500 for writes
1141
+ that are safe to repeat. A 502, 503, or 504 is never resent.
1142
+ - Mongo-direct, Atlas Search, and vector results for callers in a `role:` or
1143
+ `authenticated` protectedFields group now omit those fields, matching
1144
+ Parse Server.
1145
+ - Parse Server 9.10.3 honors `role:` entries in class-level permissions for
1146
+ LiveQuery subscriptions only when `enableLiveQueryClassLevelPermissionRoles`
1147
+ is set (default `false`). Without it, a role member is refused a
1148
+ subscription the equivalent REST query would serve, which affects MCP
1149
+ `resources/subscribe` on such classes.
1150
+
3
1151
  ### 5.7.6
4
1152
 
5
1153
  #### `semantic_search` no longer returns hidden fields as chunk content