Skip to content

v5.8.0 - Agent Access Policies, Retrieval Profiles, Security Hardening

Choose a tag to compare

@AdrianCurtin AdrianCurtin released this 07 Oct 03:31
· 7 commits to main since this release
85a5d6e

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 a where:, order:, or filter: on a field outside agent_fields with :field_denied. Migration: add the field to agent_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 DELETE authenticates 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 _User email protection. Migration: pass fields: naming the fields to search, or run such queries with master: 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: use master: true or use_master_key = true where 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 _User CLPs should pass use_master_key: true.
  • BREAKING: Several constraints on one field all apply (query(:plays => 5, :plays.gt => 1) keeps both), limit(0) returns no rows, and sum/average/min/max/count_distinct/group_by apply order, skip, and limit before grouping. Migration: clear an earlier constraint with query.clear(:where); use limit(nil) to remove a limit.
  • BREAKING: The query DSL no longer defines Symbol#id or Symbol#size, which broke ActiveRecord queries (#83) and sort_by(&:size). Migration: write :field.pointer_id and :field.array_size.
  • BREAKING: Assigning a value a property cannot represent raises Parse::Properties::TypecastError instead of storing a guess (true on an :integer used to erase the column). Parse::GeoPoint raises on non-numeric coordinates instead of becoming (0, 0). Migration: convert untrusted input before assigning it, or rescue TypecastError.
  • BREAKING: Records behave like ActiveRecord: unsaved objects compare by identity, destroy keeps the id and sets destroyed? (a later save is refused), a pointer to an unsaved object fails the save instead of storing objectId: null, and a server row without an ACL key has a nil acl. Migration: check destroyed? instead of id.nil?, and save referenced objects first.
  • BREAKING: Collections and associations validate their contents: array has_many and belongs_to refuse objects of another class, add!/remove! return booleans, and setters on a bare Parse::Pointer raise. Array#save raises on an array with no Parse objects.
  • BREAKING: Webhooks follow Parse Server's contract: a before_save that makes no change keeps the client's write (it used to erase it), after_find can no longer rewrite rows, an unregistered function returns an error, and replay dedup needs a request id, nonce, or signature. Migration: move result filtering from after_find to before_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's agent_fields for 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, and semantic_search.
  • FIXED: Hidden-field inference is closed across filters, sorts, subqueries ($inQuery, $select, $relatedTo), aggregation ($$ROOT, $lookup join keys, pass-through pipelines), and embedded objects in call_method results. 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.register defines server-side strategies for semantic_search (hybrid search, a registered reranker, candidate, token, and time budgets, observable fallback), with a sanitized parse.retrieval.search event and Parse::Retrieval::Benchmark.
  • NEW: Parse::VectorSearch::IndexDefinition generates 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 new cache_key methods follow ActiveRecord.
  • NEW: A :number property type keeps integral values as Integer and decimals as Float; Parse Number columns use it.
  • FIXED: In-place edits to hashes and arrays are saved, arrays keep nil elements, nested dates are stored as Parse Dates, US phone numbers no longer get a Swiss country code, and reading acl after 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.transaction is 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 /batch is never replayed, and retry: 0 disables retries.

Queries return the rows they describe

  • FIXED: or_where and Parse::Query.and keep every constraint, automatic paging adds an objectId tiebreaker so tied rows are not duplicated or skipped, first no longer changes the query, and Set/Range values 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: readUserFields and pointer permissions are enforced (and applied before paging), joined classes apply their own ownership and protected fields, $getField cannot read a protected field under another name, and Parse.without_master_key is respected.
  • FIXED: $inQuery, $select, and $containedBy work on the direct path, including inside $or/$and, and direct rows decode like REST.

Webhooks, associations, and host apps

  • FIXED: before_delete can deny, after_destroy fires 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_for resolves protectedFields the 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.transaction needs 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 with enableLiveQueryClassLevelPermissionRoles set.

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)
end

Commit: 85a5d6e
Author: Adrian Curtin
Date: October 6, 2026