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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +856 -0
- data/README.md +15 -4
- data/docs/TEST_SERVER.md +2 -2
- data/docs/acl_clp_guide.md +7 -0
- data/docs/atlas_vector_search_guide.md +190 -14
- data/docs/client_sdk_guide.md +11 -0
- data/docs/mcp_guide.md +318 -6
- data/docs/mongodb_direct_guide.md +27 -0
- data/docs/usage_guide.md +38 -0
- data/docs/webhooks_guide.md +74 -17
- data/lib/parse/acl_scope.rb +159 -41
- data/lib/parse/agent/approval_gate.rb +0 -0
- data/lib/parse/agent/constraint_translator.rb +42 -15
- data/lib/parse/agent/describe.rb +3 -1
- data/lib/parse/agent/field_names.rb +53 -0
- data/lib/parse/agent/field_policy.rb +74 -0
- data/lib/parse/agent/mcp_deployments.rb +426 -0
- data/lib/parse/agent/mcp_rack_app.rb +424 -45
- data/lib/parse/agent/mcp_server.rb +23 -1
- data/lib/parse/agent/mcp_subscriptions.rb +124 -6
- data/lib/parse/agent/metadata_registry.rb +67 -8
- data/lib/parse/agent/prompt_hardening.rb +9 -3
- data/lib/parse/agent/tools.rb +378 -29
- data/lib/parse/agent.rb +93 -1
- data/lib/parse/api/batch.rb +10 -1
- data/lib/parse/api/schema.rb +23 -4
- data/lib/parse/api/sessions.rb +6 -2
- data/lib/parse/api/users.rb +88 -14
- data/lib/parse/atlas_search/protected_paths.rb +236 -0
- data/lib/parse/atlas_search.rb +95 -23
- data/lib/parse/authorization.rb +54 -1
- data/lib/parse/client/batch.rb +231 -35
- data/lib/parse/client/body_builder.rb +21 -0
- data/lib/parse/client/caching.rb +371 -27
- data/lib/parse/client/request.rb +26 -14
- data/lib/parse/client/response.rb +49 -6
- data/lib/parse/client.rb +201 -38
- data/lib/parse/clp_scope.rb +281 -23
- data/lib/parse/console.rb +2 -2
- data/lib/parse/embeddings/voyage.rb +181 -17
- data/lib/parse/graphql/type_generator.rb +3 -0
- data/lib/parse/model/acl.rb +119 -21
- data/lib/parse/model/associations/belongs_to.rb +25 -3
- data/lib/parse/model/associations/collection_proxy.rb +138 -17
- data/lib/parse/model/associations/has_many.rb +38 -9
- data/lib/parse/model/associations/has_one.rb +3 -1
- data/lib/parse/model/associations/pointer_collection_proxy.rb +109 -17
- data/lib/parse/model/associations/relation_collection_proxy.rb +134 -28
- data/lib/parse/model/bytes.rb +13 -5
- data/lib/parse/model/classes/role.rb +72 -0
- data/lib/parse/model/classes/session.rb +43 -0
- data/lib/parse/model/classes/user.rb +78 -3
- data/lib/parse/model/core/actions.rb +269 -67
- data/lib/parse/model/core/builder.rb +100 -8
- data/lib/parse/model/core/create_lock.rb +27 -2
- data/lib/parse/model/core/describe.rb +2 -0
- data/lib/parse/model/core/fetching.rb +21 -3
- data/lib/parse/model/core/pluralized_aliases.rb +8 -4
- data/lib/parse/model/core/properties.rb +488 -39
- data/lib/parse/model/core/querying.rb +7 -0
- data/lib/parse/model/core/schema.rb +5 -3
- data/lib/parse/model/core/search_indexing.rb +63 -0
- data/lib/parse/model/core/vector_searchable.rb +35 -6
- data/lib/parse/model/file.rb +9 -2
- data/lib/parse/model/geopoint.rb +61 -13
- data/lib/parse/model/model.rb +160 -9
- data/lib/parse/model/object.rb +265 -17
- data/lib/parse/model/phone.rb +54 -5
- data/lib/parse/model/pointer.rb +40 -6
- data/lib/parse/mongodb.rb +170 -60
- data/lib/parse/pipeline_security.rb +415 -26
- data/lib/parse/query/constraint.rb +30 -0
- data/lib/parse/query/constraints.rb +58 -32
- data/lib/parse/query/cursor.rb +3 -1
- data/lib/parse/query/operation.rb +62 -8
- data/lib/parse/query/ordering.rb +34 -6
- data/lib/parse/query.rb +1100 -134
- data/lib/parse/retrieval/agent_tool.rb +290 -17
- data/lib/parse/retrieval/benchmark.rb +149 -0
- data/lib/parse/retrieval/profiles.rb +320 -0
- data/lib/parse/retrieval/retriever.rb +10 -1
- data/lib/parse/retrieval.rb +2 -0
- data/lib/parse/schema/search_index_migrator.rb +23 -5
- data/lib/parse/schema.rb +74 -18
- data/lib/parse/stack/tasks.rb +6 -4
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +72 -14
- data/lib/parse/two_factor_auth/user_extension.rb +14 -2
- data/lib/parse/two_factor_auth.rb +11 -0
- data/lib/parse/vector_search/hybrid.rb +36 -18
- data/lib/parse/vector_search/index_definition.rb +237 -0
- data/lib/parse/vector_search.rb +46 -17
- data/lib/parse/webhooks/payload.rb +93 -6
- data/lib/parse/webhooks/replay_protection.rb +58 -20
- data/lib/parse/webhooks.rb +412 -40
- 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
|