Repository navigation
v5.8.0 - Agent Access Policies, Retrieval Profiles, Security Hardening
Feature Release
An access-control, retrieval, and hardening release: per-agent field policies and supported MCP deployment patterns, server-configured retrieval profiles and vector index tooling, Parse Server 9.10.3 alignment, and a security and data-integrity pass over the inherited model, query, batch, ACL, session, webhook, and mongo-direct code. Several fixes change long-standing behavior; read Breaking Changes before upgrading, and see CHANGELOG.md for the full list with migration notes.
Breaking Changes
- BREAKING: Agent tools (
query_class,count_objects,export_data,explain_query,atlas_text_search) refuse awhere:,order:, orfilter:on a field outsideagent_fieldswith:field_denied. Migration: add the field toagent_fields, or drop the constraint. - BREAKING: MCP sessions are owned by the principal that created them: requests on another principal's session get 403, an unknown session gets 404 (re-initialize), and
DELETEauthenticates through the agent factory. Migration: send the same credentials on every request of a session. - BREAKING: Session-scoped Atlas Search, vector, and mongo-direct queries refuse to match, sort, filter, copy, or join on a CLP protected field (the field used to be stripped from results while still deciding which rows matched), and apply Parse Server's default
_Useremail protection. Migration: passfields:naming the fields to search, or run such queries withmaster: true. - BREAKING: Mongo-direct reads (
results_direct,count_direct, and auto-routed queries) from a non-master client run as that client's session or in the public scope instead of as master.Parse.with_session(nil)runs anonymously. Migration: usemaster: trueoruse_master_key = truewhere a master read is intended. - BREAKING: Logins, MFA verification, and signup are never sent with the master key, so Parse Server enforces MFA and returns a session token on signup. Migration: deployments that create users server-side with closed
_UserCLPs should passuse_master_key: true. - BREAKING: Several constraints on one field all apply (
query(:plays => 5, :plays.gt => 1)keeps both),limit(0)returns no rows, andsum/average/min/max/count_distinct/group_byapplyorder,skip, andlimitbefore grouping. Migration: clear an earlier constraint withquery.clear(:where); uselimit(nil)to remove a limit. - BREAKING: The query DSL no longer defines
Symbol#idorSymbol#size, which broke ActiveRecord queries (#83) andsort_by(&:size). Migration: write:field.pointer_idand:field.array_size. - BREAKING: Assigning a value a property cannot represent raises
Parse::Properties::TypecastErrorinstead of storing a guess (trueon an:integerused to erase the column).Parse::GeoPointraises on non-numeric coordinates instead of becoming (0, 0). Migration: convert untrusted input before assigning it, or rescueTypecastError. - BREAKING: Records behave like ActiveRecord: unsaved objects compare by identity,
destroykeeps the id and setsdestroyed?(a latersaveis refused), a pointer to an unsaved object fails the save instead of storingobjectId: null, and a server row without anACLkey has a nilacl. Migration: checkdestroyed?instead ofid.nil?, and save referenced objects first. - BREAKING: Collections and associations validate their contents: array
has_manyandbelongs_torefuse objects of another class,add!/remove!return booleans, and setters on a bareParse::Pointerraise.Array#saveraises on an array with no Parse objects. - BREAKING: Webhooks follow Parse Server's contract: a
before_savethat makes no change keeps the client's write (it used to erase it),after_findcan no longer rewrite rows, an unregistered function returns an error, and replay dedup needs a request id, nonce, or signature. Migration: move result filtering fromafter_findtobefore_find, ACLs, or CLPs.
Changes
MCP deployments can expose less than their users can read
- NEW:
Parse::Agent.new(fields: { Customer => %i[display_name timezone] })narrows a class'sagent_fieldsfor one agent, so deployments in one process expose different subsets of the same model. The narrowed set applies to projection, includes, pipelines, Atlas Search, schema output, completion, exports, andsemantic_search. - FIXED: Hidden-field inference is closed across filters, sorts, subqueries (
$inQuery,$select,$relatedTo), aggregation ($$ROOT,$lookupjoin keys, pass-through pipelines), and embedded objects incall_methodresults. Fields are checked and queried under one name rule.
Supported user-scoped and analytics deployment patterns
- NEW:
MCPRackApp.user_scoped(...)serves signed-in users with no master-key fallback, sessions owned by the verified user id, and server-pinned tenant;MCPRackApp.master_analytics(principal_resolver:, ...)serves operators with a required principal. - IMPROVED: Per-principal rate limiters and bounded session bindings, revalidated listening streams that tolerate a Parse Server outage, orphaned subscription reaping, and documented revocation intervals.
Retrieval profiles and vector tooling
- NEW:
Parse::Retrieval::Profiles.registerdefines server-side strategies forsemantic_search(hybrid search, a registered reranker, candidate, token, and time budgets, observable fallback), with a sanitizedparse.retrieval.searchevent andParse::Retrieval::Benchmark. - NEW:
Parse::VectorSearch::IndexDefinitiongenerates Atlas vector index definitions from the model, with preview, diff,quantization:, and drift detection. Vector search uses a property's stored column (bodyEmbedding), not its Ruby name. - IMPROVED: Contextualized Voyage batches pack by an estimated token budget and split on recognized size errors.
Records, values, and ACLs are saved as written
- FIXED: Mass assignment can no longer change an object's id or bypass the protected-key filter;
persisted?,destroy, equality, and newcache_keymethods follow ActiveRecord. - NEW: A
:numberproperty type keeps integral values as Integer and decimals as Float; ParseNumbercolumns use it. - FIXED: In-place edits to hashes and arrays are saved, arrays keep
nilelements, nested dates are stored as Parse Dates, US phone numbers no longer get a Swiss country code, and readingaclafter a partial fetch loads the stored ACL instead of nil. - FIXED: ACL revocations after a save are sent,
rollback!undoes in-place ACL edits,acl.delete(:public)/delete(role)work, and batch saves apply the owner ACL policy.
Batches, transactions, and retries
- FIXED:
Parse::Object.transactionis sent as one atomic batch (the transaction flag used to be dropped and large transactions split into parallel batches). A failed chunk no longer moves results onto the wrong objects, identical requests are not collapsed, and 502/504 responses are errors instead of successes. - FIXED: Retries only replay routes Parse Server deduplicates;
POST /batchis never replayed, andretry: 0disables retries.
Queries return the rows they describe
- FIXED:
or_whereandParse::Query.andkeep every constraint, automatic paging adds anobjectIdtiebreaker so tied rows are not duplicated or skipped,firstno longer changes the query, andSet/Rangevalues expand in$in. - FIXED: Queries send a property's declared
field:name exactly as written, and model lookup and field aliases survive class redefinition.
Sessions, MFA, and the response cache
- FIXED: With the master key on the client, logins skipped MFA and overwrote the enrolled TOTP secret. Logins and MFA checks now never carry the master key.
- FIXED: An explicit session token is never replaced by an ambient one; logout, password change, user deletion, and role saves invalidate cached identities and role closures.
- FIXED: The response cache never stores
users/me, binds entries to the app id and credential, retires every cached query over a class on a write, and cannot store a stale response from a read that overlapped a write.
Mongo-direct reads match REST access rules
- FIXED:
readUserFieldsand pointer permissions are enforced (and applied before paging), joined classes apply their own ownership and protected fields,$getFieldcannot read a protected field under another name, andParse.without_master_keyis respected. - FIXED:
$inQuery,$select, and$containedBywork on the direct path, including inside$or/$and, and direct rows decode like REST.
Webhooks, associations, and host apps
- FIXED:
before_deletecan deny,after_destroyfires for afterDelete, signed deliveries cannot be replayed with an altered nonce, and the response log is redacted. - FIXED: Relation adds and removes are cleared after save and staged without loading the relation; atomic
add!/remove!keep local arrays in step with the server. - FIXED: The gem loads when Rails is defined without railties, pluralized aliases stay in the model's namespace, schema migration handles relations and special column types, and Atlas Search loads where it is used.
Parse Server 9.10.3
- FIXED:
Parse::CLPScope.protected_fields_forresolvesprotectedFieldsthe way Parse Server does; role-only and user-only entries used to protect nothing on mongo-direct paths. - CHANGED: The integration stack pins Parse Server 9.10.3 with LiveQuery CLP roles enabled and runs MongoDB as a single-node replica set.
Behavior Notes
Parse::Object.transactionneeds MongoDB as a replica set (or mongos); on a standalone server it fails and the error says so. Parse Server answers both an aborted transaction and a failed commit with a bare 500, so the SDK resends only on a confirmed 251 conflict;transaction(retry_server_errors: true)also resends on a 500 for writes that are safe to repeat.- A vector index whose path is the Ruby name of a multi-word property (
body_embedding) must be recreated on the stored column (bodyEmbedding). - Parse Server 9.10.3 honors
role:CLP grants for LiveQuery only withenableLiveQueryClassLevelPermissionRolesset.
Code Example
# A user-facing MCP endpoint that exposes less than the model allows.
app = Parse::Agent::MCPRackApp.user_scoped(
agent_options: { fields: { Post => %i[title status] } },
)
# Same-field constraints now combine instead of replacing each other.
Song.query(:plays => 5, :plays.gt => 1).compile_where
# => { "plays" => { "$eq" => 5, "$gt" => 1 } }
# Transactions are atomic; opt in to resending after a bare 500.
Parse::Object.transaction(retry_server_errors: true) do |batch|
batch.add(order)
batch.add(inventory)
endCommit: 85a5d6e
Author: Adrian Curtin
Date: October 6, 2026