parse-stack-next 5.7.5 → 5.8.0

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