Skip to content

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

Merged
AdrianCurtin merged 47 commits into
mainfrom
release/5.8.0
Oct 7, 2026
Merged

AdrianCurtin merged 47 commits into
mainfrom
release/5.8.0

Conversation

@AdrianCurtin

@AdrianCurtin AdrianCurtin commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Release 5.8.0: configurable MCP access and better retrieval, Parse Server 9.10.3 alignment, and a hardening pass over inherited model, query, batch, ACL, and host-integration code found by an audit of the original parse-stack internals.

Per-agent access policies

  • Parse::Agent.new(fields: { Customer => %i[display_name timezone] }) narrows a class's agent_fields for one agent. A policy never widens past agent_fields; sub-agents and nested agents intersect, and threads or fibers a tool starts inherit it.
  • The effective set applies to projection, includes, pipelines, Atlas Search, schema output, completion, exports, describe, and semantic_search chunk text, reranker input, and filter fields.
  • Hidden-field inference closed across query_class, count_objects, export_data, explain_query, text and faceted search, subqueries ($inQuery, $select, $relatedTo), hybrid profiles, and aggregation ($$ROOT references, $lookup join keys, and pass-through pipelines). Keys are checked and queried under one name rule.

Deployment patterns

  • MCPRackApp.user_scoped (signed-in users, sessions owned by the verified user id, no master-key fallback, admin tier refused) and MCPRackApp.master_analytics (requires an operator principal_resolver).
  • Session ownership on every request (403 for another principal's session, 404 for an unknown one), bounded per-principal bindings, rate-charged initialize, subscribe, and stream attach, pinned live and subscribed sessions, and revalidation that tolerates a Parse Server outage without dropping streams or evicting tokens.

Retrieval

  • Server-configured profiles on semantic_search (hybrid, reranking by registered name, budgets that count chunk metadata, observable fallback), a sanitized parse.retrieval.search event, and Parse::Retrieval::Benchmark.
  • Contextualized Voyage batches pack by an estimated token budget and split on recognized size errors.

Index tooling and output naming

  • Parse::VectorSearch::IndexDefinition generates Atlas vector index definitions with preview, diff, quantization:, and drift detection.
  • field_names: :server returns data under exact server field names.

Parse Server 9.10.3 alignment and protected fields

  • CLPScope.protected_fields_for resolves protectedFields as Parse Server does.
  • Scoped Atlas Search and vector search refuse protected-field paths, filters, sorts, highlights, and facets on every entry point.
  • The test stack pins Parse Server 9.10.3 with LiveQuery CLP roles, with REST regression and role-granted LiveQuery integration tests.

Audit fixes to inherited code

  • Records: mass assignment can no longer change an object's id or bypass the protected-key filter; persisted?, destroy, equality, and cache keys follow ActiveRecord; reads no longer dirty records.
  • Values: a :number type keeps integers and decimals; in-place hash edits, nils in arrays, nested dates, GeoPoint keywords, and US phone numbers are stored as assigned; values a type cannot represent raise instead of being guessed.
  • ACL: revocations after a save, in-place edits undone by rollback!, delete(:public)/delete(role), and owner policy on batch saves.
  • Batches and retries: atomic transactions, per-request results when a chunk fails, no collapsed duplicate requests, 502/504 as errors, and retries limited to routes Parse Server deduplicates.
  • Queries: same-field constraints merge, or_where and and keep constraints, limit(0) returns nothing, paging has an objectId tiebreaker, and aggregate helpers window before grouping.
  • Host apps: loads without railties, no Symbol#id/Symbol#size, pluralized aliases stay in the model's namespace, schema migration and the model builder handle relations and clashing columns, and Atlas loads where it is used.
  • Sessions and MFA: logins and MFA checks never carry the master key (it let any code pass and wiped the enrolled secret), an explicit token always wins, with_session(nil) is anonymous, and logouts, password changes, deletions, and role saves invalidate cached identities and roles.
  • Response cache: keys carry the app id and credential in both layouts, users/me is never cached, and writes retire every cached variant and every cached query over the class.
  • Mongo-direct parity: direct reads are scoped to the client's session, enforce readUserFields and pointer permissions before paging, refuse filters, sorts, copies, and joins that touch protected fields, apply joined classes' ownership rules, and decode rows like REST.
  • Webhooks: before_save keeps the client's write, after_find can no longer crash Parse Server or blank rows, before_delete can deny, and signed deliveries cannot be replayed.
  • Associations: relation operations are cleared after save and staged without loading, atomic array operations keep local state in step with the server, wrong-class values are refused, and reading acl after a partial fetch loads the stored ACL.

Fixes that also affect earlier releases

  • Queries send a property's explicit field: name as declared; Parse::Query resolves models through parse_class; vector search uses a vector property's stored column; the cache middleware no longer raises NameError without Redis.

Release tooling

  • Integration runs fail when a service dies or required coverage is skipped; the release workflow validates the exact tagged commit; provider contracts run on a schedule; ActiveModel/ActiveSupport 7.1, 7.2, and 8.0 lanes; an MCP client smoke test.

Behavior changes for upgrading apps

CHANGELOG.md lists each with migration guidance. The ones most likely to need code changes:

  • Agent tools refuse filters and sorts on fields outside agent_fields; MCP requests on another principal's session get 403 and unknown sessions 404.
  • Scoped Atlas Search must name fields: when the caller has protected fields.
  • Same-field query constraints all apply, limit(0) returns nothing, and aggregate helpers apply order, skip, and limit before grouping.
  • Symbol#id and Symbol#size are gone (:field.pointer_id, :field.array_size).
  • Invalid assignments raise Parse::Properties::TypecastError; unsaved objects compare by identity; destroy keeps the id; a pointer to an unsaved object fails the save; rows without an ACL key get a nil acl; as: accepts only server-side owners.
  • Array#save/destroy raise on arrays with no Parse objects.
  • Mongo-direct reads from non-master clients are scoped (session or public) instead of master, and refuse protected-field filters, sorts, copies, and joins.
  • with_session(nil) runs anonymously; signup no longer sends the master key.
  • before_save webhooks keep the client's write; after_find can no longer rewrite rows; unregistered functions error.
  • Collection add!/remove! return booleans, associations refuse wrong-class values, and pointer setters raise.
  • A vector index on the Ruby name of a multi-word property must be recreated on the stored column.

Testing

  • bundle exec rake test:unit passes on each commit.
  • The full gated integration suite ran against Parse Server 9.10.3 and MongoDB 9.0.2. The two files that failed exposed behavior changes in this release, not regressions: the deleted-object tests relied on destroy clearing the id, and the transaction tests passed only because the SDK never sent the transaction flag. Both are updated; the test MongoDB now runs as a single-node replica set, and the transaction file passes repeatedly.
  • Audit findings were reproduced from scripts before each fix and kept as unit tests; mutation checks cover field policy, session ownership, and vector storage paths.

Upgrade notes

  • Parse::Object.transaction now runs as a real Parse Server transaction, which needs MongoDB as a replica set (or mongos). On a standalone server it fails rather than running without atomicity; the error names the replica-set requirement and points to Array#save for a non-atomic batch.
  • Parse Server intermittently answers a transaction with a bare 500 (it runs the requests concurrently on one MongoDB session). A 500 does not prove nothing was applied, so the SDK only resends on a 251 conflict by default; transaction(retry_server_errors: true) opts into resending on a 500 for writes that are safe to repeat. Gateway 502/503/504 responses are never resent.

…rors

Contextualized embedding requests now pack whole documents within the
document cap, the response-size budget, and an estimated 120k input-token
budget (three bytes per token, a conservative estimate; the SDK has no
tokenizer). When Voyage still rejects a request as too large, the request
is halved by document and each half resent, with results kept aligned to
the input and the halving bounded. A document rejected as too large on its
own raises an actionable BadRequestError naming its index.

BadRequestError now carries the HTTP status and the provider's error text
(bounded and terminal-sanitized), with request_too_large? and
input_too_long? predicates. Only positively identified size errors
(Voyage's batch-size and tokens-per-batch messages, or HTTP 413) trigger a
split; any other 400, including a single input longer than the context
window, propagates unchanged.
Adds Parse::VectorSearch::IndexDefinition (build / preview / diff) and
Parse::Schema.vector_index_definition, which derive an Atlas vectorSearch
definition from the :vector property (path, dimensions, similarity,
quantization), agent_searchable filter_fields (pointer fields use their
_p_ storage path), and the agent_tenant_scope field. Output is
deterministic. The vector_search_index model macro registers a generated
declaration that SearchIndexMigrator plans and applies explicitly, with
the definition computed at plan time.

:vector properties accept an optional quantization: (:scalar or :binary),
validated at declaration and emitted only into the index definition;
stored data and write paths are unchanged. First-query drift verification
now reports a quantization mismatch between declaration and live index.
Parse::Agent.new(fields: { Klass => [:a, :b], default: [...] }) narrows the
class agent_fields ceiling for one agent, so MCP deployments in one process
can expose different subsets of the same models. A policy can never widen
past the ceiling, and a sub-agent intersects its parent's policy.

MetadataRegistry.field_allowlist now returns the effective set for the
agent whose tool is executing (Tools.invoke scopes the policy fiber-locally
via Parse::Agent::FieldPolicy), so projection, where/keys checks, pipelines,
Atlas Search fields, schema output, include projections, exports, describe,
and semantic_search chunk text and filter fields all narrow without
per-site changes. class_field_allowlist exposes the unnarrowed ceiling.

Closes a hidden-field inference gap: query_class, count_objects, and
export now refuse a caller where:/order: on a field outside the effective
allowlist (group_by, distinct, and aggregation already did). Fixes
assert_fields_in_allowlist!, which passed its refusal Hash into
AccessDenied's class-name slot, garbling the message and dropping kind.
Parse::Query looked up its table's model with Parse::Model.const_get(@table),
which raises for Parse class names that are not valid Ruby constants
("contacts", "_User") and misses models whose parse_class differs from
the Ruby class name. Most call sites rescued the error to nil, so pointer
fields were silently treated as plain fields: mongo-direct pipelines
addressed `owner` instead of `_p_owner` and pointer values were not
converted to storage form. Query#table_model_class now resolves through
Parse::Model.find_class, falling back to the constant lookup.
MCPRackApp.user_scoped builds agents from a Parse session token on every
request. A missing, blank, invalid, expired, or revoked token is refused
with 401 before any agent is built, with no master-key fallback. Tokens are
re-checked against Parse Server per request by default (response cache
bypassed; a failure also evicts the token from client.authorization), or
through the identity cache with session_validation: :cached. tenant_from
pins tenant_id server-side and fails closed. Long-lived listening streams
are re-checked every session_revalidate_interval (default 60s) and closed,
tearing down their subscriptions, once the session stops validating.

MCPRackApp.master_analytics builds read-only (by default) master-key
agents and refuses to construct without a principal_resolver, so operators
sharing an endpoint have distinct session ownership; an unresolved operator
gets 401. Both factories forward agent_options to Parse::Agent.new and
refuse options that set identity or authority.

Cancellation and elicitation replies now require the session's owner: a
caller who knows or chose another principal's Mcp-Session-Id gets a silent
202 without cancelling its requests or answering its approvals.

MCPSubscriptions::Manager reaps orphaned sessions (subscriptions held with
no listening stream attached) after orphan_ttl (default 300s), checking
attachment and removing subscriptions under one lock so a concurrent attach
cannot race the reap.

Documents both patterns, session ownership, revocation intervals, and
orphan reaping in the MCP guide.
Parse::Retrieval::Profiles registers named, server-configured strategies
for the semantic_search tool (k/max_k, hybrid, a reranker referenced by
registered name, candidate and top_n counts, per-document text cap,
timeout, failure mode, response budget), validated at registration. The
tool gains an optional profile: argument; without it behavior is
unchanged. Profiles never accept caller-supplied providers, endpoints, or
credentials.

BudgetedReranker cuts each document's text before it leaves the process,
charges estimated rerank tokens to the tenant's SpendCap, bounds the call
with a timeout, and on failure either keeps the retrieval order with an
observable rerank_fallback flag or raises. Each call emits one sanitized
parse.retrieval.search event. Parse::Retrieval::Benchmark scores profiles
on labeled cases (recall@k, MRR, hit rate, latency, forbidden-id
violations, estimated tokens), with a small fixture set.
Parse::Authorization#lookup_user_id called current_user without
cache: false, so with the response cache enabled a revoked or expired token
could re-resolve from a cached /users/me response after its identity entry
was evicted or invalidated. The identity plane is now the only cache on the
path, so its TTL and invalidation hooks bound revocation.
A signed-in user reads a field directly through Parse; a session-token
agent narrowed with fields: neither returns it nor allows filtering on it;
a broader analytics agent in the same process can expose it.
Parse::Agent.new(field_names: :server) and Aggregation#results(field_names:
:server) return data fields in the exact names Parse returns or the model
declares through field_map, with no snake_case conversion. Omission keeps
every API's existing output; unsupported values raise. The agent mode is
scoped per tool call (Parse::Agent::FieldNames, alongside FieldPolicy in
Tools.invoke), sub-agents inherit it, and it never changes access
restrictions, which resolve against canonical names before formatting.

AggregationResult gains a :server mode: String keys kept verbatim, nested
values untouched, colliding snake_case forms (totalPlays and total_plays)
both preserved, and a snake_case method name matching several keys raises
instead of guessing.

Fixes call_method serialization: a returned Parse::Object was built from
#attributes (the model's field-type map), so it emitted field types
instead of values; it now serializes from #as_json. A returned
AggregationResult was emitted as its inspect String; it is now a Hash in
the agent's naming mode.
…ontracts

Integration runs (rake test:integration) now check Parse Server /health
before each file and after each failure. A server that is down stops the
run, names the file it went down before or during, and fails the task
instead of letting later files skip. A skip-log reporter (PSNEXT_SKIP_LOG)
records every skip reason, and skips caused by an unreachable Parse Server,
MongoDB, or Redis fail their file; Atlas-only, missing-key, and ffmpeg
skips stay skips (PSNEXT_FAIL_ON_INFRA_SKIP=false reports without failing).

The test stack's Redis cache adapter no longer crashes Parse Server when its
client is closed: it opens the client lazily, waits for readiness with a
bound, and treats any command failure as a cache miss. The db-1 guard is
unchanged.

Release workflow: a validate job requires the tag to match version.rb, a
successful Gem Tests run on the exact commit, and a passing unit suite on
that commit before the gem is pushed. Adds a scheduled/manual provider
contract workflow (never on pull_request) and ActiveModel/ActiveSupport
7.1, 7.2, and 8.0 compatibility lanes (json < 3 there, since ActiveSupport
before 8.1 passes quirks_mode, which json 3 rejects).

Adds an MCP client smoke test over real HTTP (initialize through DELETE,
including completion, logging, and a streamed tools/call) and pins the
dotted-path null comparisons MongoDB 9.0 changed, with a guide note.
Parse::Query.format_field camel-cased every key, ignoring a model's explicit
field: names, so where/order/keys/include on account_id or authId_sub
compiled to accountId/authIdSub and matched nothing. While a query builds
or compiles, it now publishes its model's explicit aliases (field_map
entries that differ from the default formatting) in a fiber-local scope that
format_field consults, for both Ruby and remote names. Subqueries scope to
their own class. Mongo-direct entry points are wrapped with their exact
signatures so client: stays introspectable.
…eams

- call_method results project every embedded object through its own
  class's effective allowlist, not only the top-level object.
- $inQuery/$notInQuery/$select/$dontSelect predicates and $select keys are
  checked against their target class's effective allowlist.
- Deployment factories keep one rate limiter per principal (bounded LRU)
  so rate_limit accumulates across requests; an injected rate_limiter is
  honored.
- Listening-stream heartbeat and revalidation threads start under the close
  lock and never after the stream closed.
- A superseded listening stream (reconnect overlap) detaches nothing; the
  latest stream owns the session and DELETE still tears it down.
…vectors

- Field policies keep exact server alias names (PublicText) verbatim.
- A caller's k can no longer raise retrieval above rerank_candidates.
- Listener register/unregister run under the manager lock, so an old
  stream's teardown cannot remove a replacement stream's delivery.
- Vector search, hybrid search, index discovery, drift checks, and the
  index generator use a vector property's stored column (bodyEmbedding),
  not its Ruby name; the guide's index example is corrected.
- Session DELETE passes the Origin policy, authenticates through the agent
  factory, and is refused for a session owned by another principal.
- The response budget counts parent documents; under a profile it is
  mandatory. Failed searches emit a sanitized retrieval event.
…ling

Security review:
- atlas_text_search checks filter: against the allowlist and defaults to the
  readable fields; faceted queries and hybrid profile lexical branches are
  restricted the same way; explain_query checks where:.
- fields: String keys resolve to the Parse class name (_User); nested agent
  scopes intersect instead of replacing.
- Owner bindings of live sessions (listener or pending approval) are pinned
  against LRU eviction; user_scoped refuses master_atlas/allow_mutations.

Correctness review:
- The where:/order: guard resolves keys the way queries send them, so
  snake_case and _p_ keys are no longer wrongly refused.
- The alias scope covers sum/average/min/max and group-by helpers, aliases
  only explicit field: names (formatter nil and built-ins unchanged), never
  an internal column, keeps wrapped private methods private, and keys its
  cache by map contents.
- Profiles refuse max_k above the tool cap; IndexDefinition.diff matches the
  vector entry by path and reports malformed values as drift; benchmark
  token counts are per runner thread and MRR is cut off at k.
- Release validation requires the latest Gem Tests run to have succeeded;
  core infrastructure skips are classified before legitimate-skip wording.
@AdrianCurtin AdrianCurtin changed the title Release/5.8.0 v5.8.0 - Per-Agent Access Policies, Deployment Patterns, Retrieval Profiles Oct 6, 2026
Comment thread test/lib/parse/agent/mcp_deployments_test.rb Fixed
CodeQL flagged the reassignment of current as useless because it does not
follow the lookup closure that reads it. Clearing and replacing the set in
place is equivalent (the closure takes a copy) and leaves nothing to flag.
…review

Project unsaved embedded objects, refuse text search with no readable
fields, keep strict profile budgets, restrict hybrid lexical fields to
readable ones, refuse new sessions with 503 when the owner registry is
full, honor an explicit nil rate_limiter, require principal_resolver for
master_analytics, and refuse rerank options without a reranker. Relabel
the upgrade-affecting changes as breaking with migration notes.
…Parse Server 9.10.3

Refuse POSTs on another principal's session and subscriptions on sessions
the caller did not establish, bound session bindings per principal, and
charge initialize and subscribe to the principal limiter. Check $relatedTo
keys against field policy, strip internal columns from aggregation rows,
and refuse scoped Atlas text searches that would match on protected
fields. Cap semantic_search query length, refuse the admin tier in
user_scoped, wake the stream revalidator instead of killing it, and keep
field policy scopes in inheritable fiber storage.

Resolve protectedFields the way Parse Server does, pin the test stack to
Parse Server 9.10.3 with LiveQuery CLP roles enabled, and add REST
regression coverage. Run query blocks outside the field-alias scope, give
fetch and cursor paths their own class scope, and cache alias and class
lookups. Close the mutation-test gaps and correct the guides.
Parse Number columns hold floats, so :number properties and schema-built
Number fields no longer truncate to integers. The cache middleware no
longer references Redis error classes when Redis is not loaded, and
Parse::Object.new accepts objects such as ActionController::Parameters
through to_h.
Comment thread lib/parse/query.rb Fixed
Comment thread lib/parse/query.rb Fixed
The wrappers interpolated method names and a table expression into
module_eval source. They now take a proc for the table lookup, with the
same per-call cost.
…s, search, models

Agent tools resolve where, order, and filter keys the same way they check
them, project pass-through aggregation rows to the allowlist, refuse
$$ROOT references and hidden $lookup join keys, bound semantic filters
by agent_fields, and count chunk metadata in profile budgets.

MCP sessions under user_scoped are keyed by the verified user id, a
Parse Server outage no longer rejects or evicts a session, unknown
sessions get 404, owner activity refreshes the eviction order, the shared
master-key principal is not capped per principal, stream attach is
charged before it claims an id, sessions holding subscriptions are
pinned, and stream bodies only detach a listener they attached.

Atlas and vector search refuse protected-field paths, filters, sorts,
highlights, and facets for scoped callers on every entry point.

Add a :number property type that keeps integral values as Integer, fix
the increment helpers, make persisted? mean the record exists, restrict
hash-like init input and the as: owner option, report the existing type
in the redefinition warning, and widen the cache store error fallback.
Deep-copy permissions in ACL#initialize_copy so rollback! and change
history see the real previous ACL, make delete resolve public, role, and
user keys, have Permission mutators mark the ACL changed, stop
apply_role double-prefixing, read the string "false" as a denial, and
align eql?/hash with ==.
…ings

Mass assignment can no longer change an object's id and assign_attributes
applies the protected-key filter. Unsaved objects compare by identity,
destroy keeps the id and sets destroyed?, reads no longer dirty records,
ACL revocations after a save are sent, batch saves apply the owner
policy, and save_all reports failures and visits every record.

Typecasting keeps nils in arrays, tracks in-place hash edits, encodes
nested dates, refuses values a type cannot represent instead of guessing,
and fixes GeoPoint, phone, Bytes, and File handling.

Transactions are sent atomically in one batch, chunk failures stay on
their own requests, identical requests are no longer collapsed, 502/504
are errors, retries only replay routes the server deduplicates, and the
create lock covers a full request.

Queries resolve field names with one rule, merge same-field constraints,
keep or_where and and constraints, honor limit(0), page with an objectId
tiebreaker, window aggregates before grouping, and no longer mutate on
first.
Comment thread lib/parse/client.rb Fixed
Comment thread lib/parse/model/associations/collection_proxy.rb Fixed
… crashes

Load when Rails is defined without railties, move the query DSL into
modules included in Symbol so Ruby and other libraries keep their own
methods (Symbol#id and Symbol#size become pointer_id and array_size),
create pluralized aliases only in the model's own namespace, carry
targetClass and the right column types through schema migration, keep
the model builder working on clashing column names, load Atlas Search
where it is used, make webrick optional for enable_mcp!, always scrub
the untrusted tool-result marker, and name Parse::User relative to the
Parse namespace.
The trailing-slash pattern in api_relative_path backtracked polynomially
on long runs of "/" (CodeQL rb/polynomial-redos).
Relation adds and removes are cleared after save and deduplicated, staged
without loading the relation, and never queried with a null owner.
Array collections mark clear() dirty, keep atomic add!/remove! results
in step with the server, and validate item classes; belongs_to refuses
wrong-class values and accepts objectId strings; relations with field:
read the remote column; rollback! works on relations; nested partial
fetches cover multi-word belongs_to; query has_many on an unsaved owner
returns a chainable empty query; CollectionProxy#replace is added; and
setters on a bare Pointer raise instead of writing a hidden copy.

The transient-revalidation test now feeds each check its outcome rather
than racing a short interval.
A before_save that changes nothing keeps the client's write, and one that
does replies with the full write plus its changes so operators, undeclared
fields, and signup fields survive. after_find always keeps the rows and
can deny instead of crashing Parse Server or blanking results. Replay
dedup needs a request id or nonce header, before_delete can deny,
after_destroy fires on afterDelete, parse_query reads only where, the
response log is redacted, unrouted functions error, unexpected
exceptions return a JSON error, and error! carries a code.

Atomic add!/add_unique!/remove! on a plain array adopt the array Parse
Server returns, correcting a stale local copy.
Comment thread lib/parse/webhooks.rb Fixed
Logins, MFA verification, password reset, and signup are never sent with
the master key, so Parse Server no longer skips MFA and overwrites the
enrolled secret. An explicit session token is never replaced by the
ambient or bound one, with_session(nil) runs anonymously (including on
mongo-direct and parallel fetches), and logout, password changes, user
deletion, session destroys, and role saves invalidate cached identities
and role closures. The response cache never stores users/me, keys
include the app id and credential, writes retire every variant of a
resource, and MFA login errors map to MFA exceptions.

Mongo-direct reads are scoped to a client's bound session or the public
scope instead of master, refuse filters, sorts, and joins on protected
fields, enforce readUserFields and pointer permissions before paging,
strip included classes' protected fields and the default _User email,
fetch CLPs with the master key, and decode includes, files, and dotted
keys like REST. Session tokens are redacted from inspect output.
…am slots, signed replays, partial-fetch ACL

An atomic add! on an array with unsaved edits applies the op locally
instead of adopting the server array, so the edits are still saved.
Reassigning a mutable property after an in-place edit keeps the edit.
Listening streams count and uncount under the close lock, so a close
racing startup no longer leaks a capacity slot. Signed webhook
deliveries are deduplicated on their signature, so altering the unsigned
nonce no longer permits a replay. Reading acl on a partially fetched
object fetches the stored ACL instead of returning nil, so code that
edits it cannot replace the record's real ACL; pointers still never
fetch for it. Also initialize obj before the early return in the
afterDelete chain (CodeQL) and restore unset webhook env vars in the
replay test teardown.
…edential and class version

send_request and request(req) apply a Parse::Request's own options, so a
request with a session token and use_master_key: false no longer sends
the master key. Keyspaced cache keys now carry the credential digest the
legacy layout already used. Every write retires cached query results for
its class (updates, deletes, creates, and batch sub-requests) through a
per-class version in both layouts, bumped again after the response, and
a GET re-sent as a POST method override no longer counts as a write.
…ected fields

A user keeps their own protected _User fields only when no caller stage
can rewrite _id, and joined _User rows are decided on their stored _id
before caller stages run. Scoped pipelines refuse $$ROOT and $$CURRENT
copies, $$ROOT paths into protected fields, and $getField reads of
protected or computed names, and each $facet branch starts by stripping
protected fields. Joins into another class enforce its readUserFields and
pointerFields ownership, strip its protected fields at the head of the
join, refuse on a denied CLP or a $graphLookup into a protected class,
and check sub-pipeline references against the joined class.
…s, accept nonce-signed webhooks

An update returns only updatedAt, so a partially fetched record keeps its
fetched-keys tracking after a save and adds the saved fields to it; the
ACL the partial fetch left out is still fetched on read, instead of
reading nil and letting an added grant replace existing ones. Atomic
role.users and role.roles add!/remove! invalidate cached role closures
like a save does. A webhook signature may cover the delivery nonce
(ts.nonce.body), so identical bodies sent in the same second each get
their own signature; the ts.body form is still accepted.
… restore revoked data

A cold-cache GET read its resource and class versions only after the
response, so a read that overlapped an ACL write adopted the write's new
versions and stored its older, private body under them. Reads now create
or read their versions before dispatch (set-if-absent where the store
supports it), store only under those versions, and skip the store when a
version changed in flight, in both cache layouts.
…, translate nested subqueries

Scoped mongo-direct pipelines on classes with protected fields accept a
$getField, $setField, or $unsetField name only as a plain string or a
$literal string, so a name read from the document cannot alias a
protected field. Direct reads, counts, distinct, the auto-route, and the
Atlas bridge respect Parse.without_master_key and resolve to the session
or public scope as REST does. Subqueries nested in $and, $or, or $nor
compile to their own lookups on the direct path, and positions that
cannot be translated, plus nested subqueries in query-derived
aggregations, fail closed instead of reaching MongoDB raw.
@AdrianCurtin AdrianCurtin changed the title v5.8.0 - Per-Agent Access Policies, Deployment Patterns, Retrieval Profiles v5.8.0 - Agent Access Policies, Retrieval Profiles, Security Hardening Oct 6, 2026
…ion aborts

Real Parse Server transactions need a replica set, so the test MongoDB
now starts as a single-node replica set with a generated key file and a
healthcheck that initiates it, and every test URI uses
directConnection=true. Parse Server runs a transaction's requests
concurrently on one session, which MongoDB intermittently rejects;
Parse Server aborts and answers a bare 500, so the transaction retry
loop now resends on that as it does on a 251 conflict.

The deleted-object integration tests now check the destroy contract
(id kept, destroyed?, save refused, fetch raises), and the mixed
transaction test no longer points at an object created in the same
transaction.
Parse Server answers every aborted transaction with "Internal server
error", so once retries run out the transaction raises a Parse::Error
that names the replica-set requirement and points to Array#save for a
non-atomic batch, keeping the original error as its cause.
…pts in

A bare 500 from Parse Server does not prove a transaction was not
applied (a failed commit looks the same), and a gateway 502/503/504 can
arrive after it committed. The transaction retry now resends only on a
structured 251 conflict code, never on message text, and resends on a
500 only with transaction(retry_server_errors: true). The error raised
after a 500 says the outcome is unknown, names the replica-set
requirement, and points to Array#save. ServiceUnavailableError now
carries its response and HTTP status.
@AdrianCurtin
AdrianCurtin merged commit 85a5d6e into main Oct 7, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants