parse-stack-next 5.7.6 → 5.8.1
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 +1148 -0
- data/README.md +39 -23
- data/docs/TEST_SERVER.md +2 -2
- data/docs/acl_clp_guide.md +7 -0
- data/docs/atlas_vector_search_guide.md +181 -13
- data/docs/caching.md +50 -0
- data/docs/client_sdk_guide.md +11 -0
- data/docs/mcp_guide.md +332 -9
- data/docs/mongodb_direct_guide.md +27 -0
- data/docs/usage_guide.md +38 -0
- data/docs/webhooks_guide.md +126 -17
- data/lib/parse/acl_scope.rb +230 -50
- data/lib/parse/agent/approval_gate.rb +0 -0
- data/lib/parse/agent/constraint_translator.rb +42 -15
- data/lib/parse/agent/describe.rb +11 -3
- 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_dispatcher.rb +8 -1
- 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 +298 -21
- 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/index_manager.rb +5 -1
- data/lib/parse/atlas_search/protected_paths.rb +236 -0
- data/lib/parse/atlas_search.rb +144 -24
- data/lib/parse/authorization.rb +289 -4
- data/lib/parse/cache/sub_cache.rb +26 -0
- data/lib/parse/client/authentication.rb +19 -1
- data/lib/parse/client/batch.rb +264 -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 +227 -40
- data/lib/parse/clp_scope.rb +317 -25
- 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/live_query/client.rb +72 -2
- data/lib/parse/lock_backend.rb +4 -3
- 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 +146 -17
- data/lib/parse/model/associations/has_many.rb +49 -9
- data/lib/parse/model/associations/has_one.rb +3 -1
- data/lib/parse/model/associations/pointer_collection_proxy.rb +145 -17
- data/lib/parse/model/associations/relation_collection_proxy.rb +229 -38
- data/lib/parse/model/bytes.rb +13 -5
- data/lib/parse/model/classes/role.rb +95 -0
- data/lib/parse/model/classes/session.rb +304 -1
- data/lib/parse/model/classes/user.rb +104 -8
- data/lib/parse/model/core/actions.rb +287 -70
- 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 +50 -5
- data/lib/parse/model/core/field_guards.rb +16 -8
- 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 +317 -18
- data/lib/parse/model/phone.rb +54 -5
- data/lib/parse/model/pointer.rb +40 -6
- data/lib/parse/model/push.rb +11 -1
- data/lib/parse/mongodb.rb +189 -61
- data/lib/parse/pipeline_security.rb +415 -26
- data/lib/parse/query/constraint.rb +31 -1
- data/lib/parse/query/constraints.rb +89 -59
- 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 +1871 -167
- data/lib/parse/retrieval/agent_tool.rb +225 -8
- 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 +91 -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 +110 -33
- data/lib/parse/vector_search/index_definition.rb +237 -0
- data/lib/parse/vector_search.rb +259 -23
- data/lib/parse/webhooks/payload.rb +136 -6
- data/lib/parse/webhooks/replay_protection.rb +58 -20
- data/lib/parse/webhooks.rb +715 -40
- metadata +8 -1
data/docs/mcp_guide.md
CHANGED
|
@@ -396,6 +396,177 @@ Common uses for the direct dispatcher:
|
|
|
396
396
|
|
|
397
397
|
---
|
|
398
398
|
|
|
399
|
+
## Deployment Patterns
|
|
400
|
+
|
|
401
|
+
`MCPRackApp.new(agent_factory: ...)` accepts any factory. For the two common
|
|
402
|
+
production shapes, two factories package the identity rules so a deployment
|
|
403
|
+
cannot drift from one access mode into the other by accident:
|
|
404
|
+
|
|
405
|
+
| Factory | Use it for | Identity | Data access |
|
|
406
|
+
|---|---|---|---|
|
|
407
|
+
| `MCPRackApp.user_scoped` | Personal assistants, application users | A Parse session token on every request; missing or invalid is 401 | The user's own ACL/CLP, enforced by Parse Server on REST and by the SDK on mongo-direct paths. No master-key fallback |
|
|
408
|
+
| `MCPRackApp.master_analytics` | Analytics, reporting, trusted operational tools | A verified operator from a required `principal_resolver`; unresolved is 401 | Master authority, narrowed by the agent's `tools:`, `classes:`, `filters:`, and field policy. Read-only by default |
|
|
409
|
+
|
|
410
|
+
Both factories take `agent_options:` (extra `Parse::Agent.new` options such
|
|
411
|
+
as `tools:`, `methods:`, `classes:`, `filters:`) and pass the remaining
|
|
412
|
+
keywords to `MCPRackApp.new` (`transport:`, `logger:`, `allowed_origins:`,
|
|
413
|
+
...). Options that set identity or authority are refused in
|
|
414
|
+
`agent_options:` with `ArgumentError` at construction: `session_token`,
|
|
415
|
+
`acl_user`, `acl_role`, `impersonate_user`, `impersonation_user`,
|
|
416
|
+
`impersonate_mint`, `impersonation_mint`, `impersonate_label`,
|
|
417
|
+
`impersonation_label`, `tenant_id`, `client`, `permissions`, `permission`, and
|
|
418
|
+
`parent`. `user_scoped` also refuses `master_atlas` and `allow_mutations`,
|
|
419
|
+
since a signed-in user's agent never receives authority beyond its session.
|
|
420
|
+
The factory owns the Rack options `agent_factory:`, `principal_resolver:`,
|
|
421
|
+
`listening_stream_revalidator:`, and `listening_stream_revalidate_interval:`,
|
|
422
|
+
so passing any of them is refused as well. A deliberate single-operator
|
|
423
|
+
master-key endpoint is still built with `MCPRackApp.new` directly.
|
|
424
|
+
|
|
425
|
+
### Personal assistant (user-scoped)
|
|
426
|
+
|
|
427
|
+
```ruby
|
|
428
|
+
# config/routes.rb (Rails), or `run` it from config.ru
|
|
429
|
+
MCP_APP = Parse::Agent::MCPRackApp.user_scoped(
|
|
430
|
+
transport: :streamable_http,
|
|
431
|
+
permissions: :write, # writes still go only through agent_methods
|
|
432
|
+
tenant_from: ->(env, user_id) { Workspace.id_for_user(user_id) }, # pinned server-side
|
|
433
|
+
agent_options: { classes: %w[Post Comment], methods: %w[archive] },
|
|
434
|
+
)
|
|
435
|
+
mount MCP_APP, at: "/mcp"
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
```ruby
|
|
439
|
+
# The only write this assistant can make, declared by the application.
|
|
440
|
+
class Post < Parse::Object
|
|
441
|
+
property :archived, :boolean
|
|
442
|
+
property :archive_reason, :string
|
|
443
|
+
|
|
444
|
+
agent_method :archive, permission: :write, supports_dry_run: true, permitted_keys: [:reason]
|
|
445
|
+
def archive(reason:, agent: nil, dry_run: false, **)
|
|
446
|
+
return { would: "archive #{id}", reason: reason } if dry_run
|
|
447
|
+
self.archived = true
|
|
448
|
+
self.archive_reason = reason
|
|
449
|
+
save
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# Require a human approval (MCP elicitation) before any :write call runs.
|
|
454
|
+
Parse::Agent.require_approval_for = [:write]
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
The client sends the user's session token as `Authorization: Bearer
|
|
458
|
+
<token>` (or `X-Parse-Session-Token`; pass `session_token_from: ->(env) {
|
|
459
|
+
... }` for anything else). Every request builds a fresh
|
|
460
|
+
`Parse::Agent.new(session_token: token, ...)`, so the agent acts as that user
|
|
461
|
+
and tool arguments cannot change who it is or which tenant it is bound to.
|
|
462
|
+
`tenant_from` returning nil refuses the request rather than running unscoped.
|
|
463
|
+
|
|
464
|
+
On logout the token stops validating, so the next request gets 401 and any
|
|
465
|
+
open listening stream closes within the revalidation interval. To reconnect,
|
|
466
|
+
the client logs in again and starts a new MCP session (`initialize`) with the
|
|
467
|
+
new token. A session id is bound to the principal that initialized it, so
|
|
468
|
+
the new token cannot take over the old session, and the old session's
|
|
469
|
+
subscriptions are torn down when its stream closes (or reaped as orphans,
|
|
470
|
+
below).
|
|
471
|
+
|
|
472
|
+
### Read-only analytics endpoint (master key)
|
|
473
|
+
|
|
474
|
+
```ruby
|
|
475
|
+
ANALYTICS = Parse::Agent::MCPRackApp.master_analytics(
|
|
476
|
+
# Required. Resolve the VERIFIED operator behind the request. Returning
|
|
477
|
+
# nil or "" refuses the request with 401.
|
|
478
|
+
principal_resolver: ->(_agent, env) { env["warden"]&.user&.email },
|
|
479
|
+
transport: :streamable_http,
|
|
480
|
+
agent_options: {
|
|
481
|
+
classes: %w[Post Subscription PostMetric],
|
|
482
|
+
tools: %w[query_class count_objects group_by group_by_date distinct aggregate get_schema],
|
|
483
|
+
},
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
# Audit: every tool call emits parse.agent.tool_call.
|
|
487
|
+
ActiveSupport::Notifications.subscribe("parse.agent.tool_call") do |*, payload|
|
|
488
|
+
AuditLog.record(tool: payload[:tool], correlation_id: payload[:correlation_id])
|
|
489
|
+
end
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
`principal_resolver:` is a required keyword: `master_analytics` raises
|
|
493
|
+
`ArgumentError` at construction when it is missing or does not respond to
|
|
494
|
+
`#call`. Without one every master-key agent has the same
|
|
495
|
+
fingerprint, so two operators sharing the endpoint could attach to, approve,
|
|
496
|
+
or cancel each other's sessions. The operator identity governs session
|
|
497
|
+
ownership and audit; master authority governs what data the agent can reach,
|
|
498
|
+
and the configured tools and classes govern what it may actually do. It
|
|
499
|
+
defaults to `permissions: :readonly`.
|
|
500
|
+
|
|
501
|
+
The master-key check runs per request, not at construction. The factory
|
|
502
|
+
resolves its client on each request (the `client:` you passed, else
|
|
503
|
+
`Parse.client` at that moment), and any request whose client has no master key
|
|
504
|
+
is answered with 401. Construction succeeds even when the master key is not
|
|
505
|
+
configured yet.
|
|
506
|
+
|
|
507
|
+
### Session ownership
|
|
508
|
+
|
|
509
|
+
Under both factories, an `Mcp-Session-Id` is bound to the principal that
|
|
510
|
+
initialized it (the session token for `user_scoped`, the resolved operator for
|
|
511
|
+
`master_analytics`). Another caller who knows or chooses the same id cannot:
|
|
512
|
+
|
|
513
|
+
- re-initialize it (403),
|
|
514
|
+
- attach its listening stream (403),
|
|
515
|
+
- cancel its in-flight requests (`notifications/cancelled` is a silent 202 no-op),
|
|
516
|
+
- answer its approval prompts (the elicitation reply is a silent 202 no-op),
|
|
517
|
+
- change its log level.
|
|
518
|
+
|
|
519
|
+
A session with an attached listening stream or a pending approval keeps its
|
|
520
|
+
owner binding for as long as it is live. When the owner registry is full of
|
|
521
|
+
live sessions, a new `initialize` or listening stream is refused with `503`
|
|
522
|
+
rather than displacing one (see Capacity and eviction under the listening
|
|
523
|
+
stream's owner binding, below).
|
|
524
|
+
|
|
525
|
+
### Revocation intervals
|
|
526
|
+
|
|
527
|
+
How quickly a logged-out, expired, or revoked session (or a role change)
|
|
528
|
+
stops having effect, for a `user_scoped` deployment:
|
|
529
|
+
|
|
530
|
+
| Path | `session_validation: :per_request` (default) | `session_validation: :cached` |
|
|
531
|
+
|---|---|---|
|
|
532
|
+
| New MCP request (any tool) | Next request: the token is re-checked against Parse Server (`GET /users/me`, response cache bypassed) | Identity-cache TTL (`identity_cache_ttl`, default 3600s), or immediately when the `Parse::Cache::Invalidation` `after_logout` / `_User` hooks evict the token |
|
|
533
|
+
| REST tools within an accepted request | Immediate: Parse Server validates the token on every call | Immediate |
|
|
534
|
+
| Mongo-direct reads (identity) | Next request: a failed check also evicts the token from `client.authorization` | Same as the row above |
|
|
535
|
+
| Mongo-direct reads (role change) | Role-cache TTL (`role_cache_ttl`, default 30s), or immediately when the `_Role` invalidation hook fires | Same |
|
|
536
|
+
| Open listening stream / subscriptions | Within `session_revalidate_interval` (default 60s): the stream is re-checked and closed, tearing down its LiveQuery subscriptions | Same (stream re-checks always ask Parse Server) |
|
|
537
|
+
|
|
538
|
+
`:per_request` costs one `/users/me` call per MCP request; `:cached` saves it
|
|
539
|
+
at the price of the identity-TTL window when invalidation hooks are not
|
|
540
|
+
installed. The TTLs are configured with
|
|
541
|
+
`Parse::Authorization.configure(identity_cache_ttl:, role_cache_ttl:)`.
|
|
542
|
+
|
|
543
|
+
### Orphaned subscriptions
|
|
544
|
+
|
|
545
|
+
A session that subscribes to resources but never opens its listening stream
|
|
546
|
+
(or subscribes again after the stream closed) holds LiveQuery subscriptions
|
|
547
|
+
with nowhere to deliver them. Normal teardown (unsubscribe, stream close,
|
|
548
|
+
`DELETE`) does not cover that case, so the subscription manager reaps such
|
|
549
|
+
sessions after a grace period: `orphan_ttl:` on
|
|
550
|
+
`Parse::Agent::MCPSubscriptions::Manager` (default 300 seconds, `nil` to
|
|
551
|
+
disable). Reaping runs whenever a session subscribes or attaches a stream,
|
|
552
|
+
and can be triggered with `manager.reap_orphans!`. A session that attaches
|
|
553
|
+
its stream within the grace period keeps its subscriptions.
|
|
554
|
+
|
|
555
|
+
### Operational notes for deployment factories
|
|
556
|
+
|
|
557
|
+
* **Validation load.** `user_scoped` with the default
|
|
558
|
+
`session_validation: :per_request` asks Parse Server (`GET /users/me`) on
|
|
559
|
+
every request, including requests carrying a random bearer string. Put a
|
|
560
|
+
`pre_auth_rate_limiter:` in front of a public endpoint, or use
|
|
561
|
+
`session_validation: :cached` and rely on the identity cache's TTL and
|
|
562
|
+
invalidation hooks.
|
|
563
|
+
* **Outages fail closed.** A Parse Server error while validating a session is
|
|
564
|
+
treated as an invalid session: the request gets 401 and a listening stream
|
|
565
|
+
is closed at its next revalidation. Clients reconnect once Parse Server is
|
|
566
|
+
back.
|
|
567
|
+
|
|
568
|
+
---
|
|
569
|
+
|
|
399
570
|
## Connecting Claude Desktop (stdio bridge)
|
|
400
571
|
|
|
401
572
|
Parse Stack speaks MCP over **HTTP** (the standalone server and the
|
|
@@ -659,10 +830,36 @@ The principal fingerprint is derived, in order, from: an operator-supplied
|
|
|
659
830
|
per-`MCPRackApp` instance and **single-process** — it does not span Puma workers
|
|
660
831
|
or survive a restart. In a clustered deployment the `initialize` POST and the
|
|
661
832
|
`GET` stream may land on different workers, so the initialize-binding degrades
|
|
662
|
-
to TOFU there.
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
833
|
+
to TOFU there. Blank session ids or blank fingerprints fail closed.
|
|
834
|
+
|
|
835
|
+
**Capacity and eviction.** The registry is LRU-bounded (default 10,000
|
|
836
|
+
sessions) so a stream of `initialize`-without-`DELETE` sessions cannot grow it
|
|
837
|
+
without limit. Only idle bindings are evicted. A live session (one with an
|
|
838
|
+
attached listening stream or a pending approval prompt) is pinned and keeps its
|
|
839
|
+
owner binding under any amount of LRU pressure, so a flood of new sessions
|
|
840
|
+
cannot strip a victim's owner and then answer its approvals or cancel its
|
|
841
|
+
requests. An evicted idle id is unbound, and the next principal to attach a
|
|
842
|
+
stream to it claims it TOFU. When every binding is live and the registry is
|
|
843
|
+
full, new bindings are refused rather than displacing a live one: `initialize`
|
|
844
|
+
and the listening-stream `GET` answer `503` (`-32000`, "Session capacity
|
|
845
|
+
exhausted"). Clients retry once sessions close or are terminated with
|
|
846
|
+
`DELETE`.
|
|
847
|
+
|
|
848
|
+
**Per-principal bound.** Each principal may hold at most 100 session
|
|
849
|
+
bindings. Past that, its own least recently used idle binding is evicted (or
|
|
850
|
+
the new one refused with `503` when all of its bindings are live), so one
|
|
851
|
+
caller flooding `initialize` cannot push other principals' sessions out of
|
|
852
|
+
the registry. `initialize` and `resources/subscribe` are charged against the
|
|
853
|
+
principal's rate limiter and answer `429` when it is exhausted; with
|
|
854
|
+
`user_scoped` and `master_analytics` that limiter is shared across the
|
|
855
|
+
principal's requests.
|
|
856
|
+
|
|
857
|
+
**Requests on another principal's session.** Any POST whose `Mcp-Session-Id`
|
|
858
|
+
is bound to a different principal is refused with `403`.
|
|
859
|
+
`resources/subscribe` additionally requires a session the caller established,
|
|
860
|
+
through `initialize` or by attaching its listening stream, because each
|
|
861
|
+
subscription holds a LiveQuery socket and a slot in the global session limit.
|
|
862
|
+
A stateless client that never initializes can still call tools.
|
|
666
863
|
|
|
667
864
|
---
|
|
668
865
|
|
|
@@ -683,6 +880,15 @@ The approval gate is a pluggable `agent.approval_gate` consulted inside
|
|
|
683
880
|
unit-testable with a fake approver. `Parse::Agent::MCPElicitationGate` is the
|
|
684
881
|
spec-native implementation; `Parse::Agent::NullGate` (the default) approves.
|
|
685
882
|
|
|
883
|
+
A sub-agent built with `parent:` and no gate of its own uses its parent's gate,
|
|
884
|
+
read at call time, so the gate the dispatcher installs on the parent for a
|
|
885
|
+
`tools/call` also covers any sub-agent a tool builds during that call. Before
|
|
886
|
+
5.8.1 a sub-agent fell back to `NullGate` and skipped approval. Assigning a
|
|
887
|
+
gate on the sub-agent itself overrides this, and assigning `nil` removes the
|
|
888
|
+
override so the sub-agent uses its parent's gate again. The dispatcher
|
|
889
|
+
snapshots and restores the agent's own gate (`approval_gate_override`), so
|
|
890
|
+
driving a sub-agent through `tools/call` never pins its parent's gate onto it.
|
|
891
|
+
|
|
686
892
|
Round-trip over the streaming transport:
|
|
687
893
|
|
|
688
894
|
1. A `tools/call` for a gated tier pauses before execution. The server builds an
|
|
@@ -847,6 +1053,51 @@ session's logs. `DELETE` on the session forgets it.
|
|
|
847
1053
|
|
|
848
1054
|
---
|
|
849
1055
|
|
|
1056
|
+
## Server Field Names (`field_names: :server`)
|
|
1057
|
+
|
|
1058
|
+
`Parse::Agent.new(field_names: :server)` asks for data fields in the exact
|
|
1059
|
+
names Parse returns or the model declares through its `field_map`
|
|
1060
|
+
(`createdAt`, `totalPlays`, an alias such as `ExternalID`), with no
|
|
1061
|
+
snake_case conversion anywhere a tool would otherwise apply one. Omit it (or
|
|
1062
|
+
pass `:default`) to keep each tool's existing output; any other value raises
|
|
1063
|
+
`ArgumentError`. Sub-agents inherit the mode unless they set their own.
|
|
1064
|
+
|
|
1065
|
+
```ruby
|
|
1066
|
+
analytics = Parse::Agent.new(field_names: :server, permissions: :readonly)
|
|
1067
|
+
```
|
|
1068
|
+
|
|
1069
|
+
| Output | Default | `field_names: :server` |
|
|
1070
|
+
|---|---|---|
|
|
1071
|
+
| `query_class` / `get_object` rows, Atlas and `semantic_search` source records, exports (CSV headers) | Parse field names | Parse field names (unchanged) |
|
|
1072
|
+
| A Parse object returned by an `agent_method` (`call_method`) | Parse field names and values | Parse field names and values |
|
|
1073
|
+
| An `AggregationResult` returned by an `agent_method` | snake_case keys | keys exactly as the aggregation returned them |
|
|
1074
|
+
|
|
1075
|
+
Most tool output already carried server names, so the visible difference is
|
|
1076
|
+
in values an `agent_method` returns. Two `call_method` serialization fixes
|
|
1077
|
+
ship alongside: a returned Parse object is now emitted with its values (it
|
|
1078
|
+
previously emitted the model's field-type map, e.g. `"title" => "string"`),
|
|
1079
|
+
and a returned `AggregationResult` is emitted as a Hash (it was previously its
|
|
1080
|
+
`inspect` String).
|
|
1081
|
+
|
|
1082
|
+
The option changes **data-field keys only**:
|
|
1083
|
+
|
|
1084
|
+
* MCP protocol keys and SDK envelope keys (`chunks`, `documents`,
|
|
1085
|
+
`object_id`, `next_call`, ...) keep their contracts.
|
|
1086
|
+
* Date, Pointer, and File formatting, vector visibility, and metadata
|
|
1087
|
+
redaction are unchanged.
|
|
1088
|
+
* "Server names" means the application-facing Parse names, never raw MongoDB
|
|
1089
|
+
storage columns such as `_p_author`, `_rperm`, or `_session_token`.
|
|
1090
|
+
* Explicit export column aliases still win over automatic naming.
|
|
1091
|
+
|
|
1092
|
+
Naming is presentation only. ACL/CLP, `protectedFields`, the class
|
|
1093
|
+
`agent_fields` ceiling, and per-agent `fields:` policies resolve against the
|
|
1094
|
+
canonical field names before anything is formatted, so a user-scoped agent
|
|
1095
|
+
and a master-key agent with the same policy return the same fields in either
|
|
1096
|
+
mode. Like `fields:`, the mode is scoped to each tool call, so agents with
|
|
1097
|
+
different modes can run concurrently in one process.
|
|
1098
|
+
|
|
1099
|
+
---
|
|
1100
|
+
|
|
850
1101
|
## Built-in Agent Hardening & Telemetry
|
|
851
1102
|
|
|
852
1103
|
5.2 adds several agent-side controls, all configured on `Parse::Agent`:
|
|
@@ -1095,6 +1346,12 @@ MCPRackApp (per-request factory)
|
|
|
1095
1346
|
|
|
1096
1347
|
The same problem exists in miniature whenever a tool handler constructs a sub-agent inside its block — a fresh `Parse::Agent.new` produces a fresh limiter, so an attacker who can induce delegation amplifies the per-process budget linearly with delegation depth × branching. The v4.2 `parent:` kwarg closes that case automatically (see [Per-Agent Tool Filtering & Sub-Agent Delegation](#per-agent-tool-filtering--sub-agent-delegation-v42)); the shared external limiter pattern below covers the cross-request case at the MCPRackApp boundary.
|
|
1097
1348
|
|
|
1349
|
+
### Deployment factories already share a limiter per principal
|
|
1350
|
+
|
|
1351
|
+
`MCPRackApp.user_scoped` and `MCPRackApp.master_analytics` do not have this problem. Each keeps one in-process `RateLimiter` per principal (the validated user id for `user_scoped`, the resolved operator for `master_analytics`) and hands it to every agent it builds for that principal, so `rate_limit:` and `rate_window:` in `agent_options:` accumulate across requests. The registry is LRU-bounded (default 10,000 principals); an evicted principal starts a fresh window. Passing your own `rate_limiter:` in `agent_options:` bypasses the registry and uses that limiter as-is, which is the way to share one budget across processes.
|
|
1352
|
+
|
|
1353
|
+
The workarounds below apply to a custom `agent_factory:` lambda (including `Parse::Agent.rack_app do |env| ... end`), which builds a fresh agent per request and gets no shared limiter unless you inject one.
|
|
1354
|
+
|
|
1098
1355
|
### The solution
|
|
1099
1356
|
|
|
1100
1357
|
Inject a shared, externally-stateful limiter:
|
|
@@ -1398,7 +1655,7 @@ agent = Parse::Agent.new(methods: [:archive, "Project.set_client_description"])
|
|
|
1398
1655
|
agent = Parse::Agent.new(methods: { except: ["Account.delete_account"] })
|
|
1399
1656
|
```
|
|
1400
1657
|
|
|
1401
|
-
Entries are bare method names (`:archive
|
|
1658
|
+
Entries are bare method names (`:archive`, which matches the method on any class) or qualified names (`"Project.archive"`, which matches only on that class). Both forms coexist in the same Set; matching is an OR. The class part is compared by its Parse class name, so `"User.reset"` and `"_User.reset"` name the same method (likewise `Role`, `Session`, and `Installation`), whichever spelling the entry or the `call_method` caller uses. An entry may also name the model by its Ruby constant (`"Artist.purge"` for `class Artist < Parse::Object; parse_class "Musician"; end`), which is compared by its Parse class name too. A qualified entry whose class resolves to no loaded model warns at construction and is matched by its class part as written. Before 5.8.1 an entry was matched against the raw string the caller passed, so `methods: { except: ["_User.reset"] }` could be bypassed by calling it on `"User"`.
|
|
1402
1659
|
|
|
1403
1660
|
The filter **narrows declared methods** — it cannot expose a method that was not declared via the `agent_method` DSL, and it cannot bypass tier checks (`agent_can_call?`) or env-gates (`PARSE_AGENT_ALLOW_WRITE_TOOLS`, `PARSE_AGENT_ALLOW_SCHEMA_OPS`). A filtered-out invocation returns `error_code: :tool_filtered`.
|
|
1404
1661
|
|
|
@@ -1416,7 +1673,9 @@ class Project < Parse::Object
|
|
|
1416
1673
|
# _wperm so the update only sees rows the agent's scope is allowed
|
|
1417
1674
|
# to modify, defense-in-depth alongside Parse Server's own ACL.
|
|
1418
1675
|
Audit.all(**agent.acl_scope_kwargs).each { |a| a.cancel! } if agent&.acl_scope
|
|
1419
|
-
|
|
1676
|
+
self.archived = true # property :archived, :boolean
|
|
1677
|
+
self.archive_reason = reason # property :archive_reason, :string
|
|
1678
|
+
save
|
|
1420
1679
|
{ archived: true, objectId: id }
|
|
1421
1680
|
end
|
|
1422
1681
|
end
|
|
@@ -1464,7 +1723,7 @@ Parse::Agent.new(classes: { only: [Pots] }, strict_class_filter: true) # per-in
|
|
|
1464
1723
|
|
|
1465
1724
|
`except:` is never validated — an operator may proactively block a class not yet loaded.
|
|
1466
1725
|
|
|
1467
|
-
**Sub-agent inheritance: intersect, never widen.**
|
|
1726
|
+
**Sub-agent inheritance: intersect, never widen.** Like `tools:` and `methods:`, `classes:` is **intersected** with the parent's effective set so a sub-agent can NEVER widen the parent's data reach. A child `only:` that has no overlap with the parent's `only:` raises `ArgumentError` at construction. A child that omits `classes:` inherits the parent's filter verbatim. `except:` sets are unioned (a sub-agent cannot un-deny a class the parent denied).
|
|
1468
1727
|
|
|
1469
1728
|
**Schema-catalog filtering.** `get_all_schemas` omits classes outside the per-agent allowlist from the catalog response so the LLM doesn't waste a tool call discovering classes it would be refused on.
|
|
1470
1729
|
|
|
@@ -1543,6 +1802,7 @@ Parse::Agent::Tools.register(
|
|
|
1543
1802
|
| `session_token` | Yes (security-critical) | Without it, a session-token parent silently produces a master-key sub-agent — the constructor default is `nil`, which means master-key mode. This was the v4.2 advisor-flagged blocker; do not undo. |
|
|
1544
1803
|
| `acl_user` (v4.4.0) | Yes (security-critical) | When the parent was constructed with `acl_user:` and the child supplies none of `session_token:` / `acl_user:` / `acl_role:`, the parent's identity inherits verbatim. Inheritance is conditional on the child supplying NO identity at all — explicit overrides on the child resolve normally and then face the subset check below. |
|
|
1545
1804
|
| `acl_role` (v4.4.0) | Yes (security-critical) | Same rule as `acl_user`. A child that omits identity inherits the parent's role scope; one that supplies its own identity falls through to the subset check. |
|
|
1805
|
+
| `master_atlas` | Yes (security-critical) | `nil` inherits the parent's setting and `false` drops it. `true` is accepted only when the parent has it: since 5.8.1 a sub-agent of a parent without `master_atlas` raises `ArgumentError`, because master-key faceting would return every row's buckets and results under the parent's scoped identity. |
|
|
1546
1806
|
| `tenant_id` | Yes (security-critical) | Without it, a tenant-bound parent produces an unbound sub-agent that escapes `agent_tenant_scope` rules. |
|
|
1547
1807
|
| `recursion_depth` | Always (decremented) | The parent's budget is authoritative — the explicit `recursion_depth:` kwarg is ignored on inherited construction. |
|
|
1548
1808
|
|
|
@@ -1552,12 +1812,74 @@ Parse::Agent::Tools.register(
|
|
|
1552
1812
|
|-------|---------|
|
|
1553
1813
|
| `permissions` | The default of `:readonly` means `Parse::Agent.new(parent: write_agent)` produces a `:readonly` sub-agent. A sub-agent is at most as privileged as the parent by tier; this is enforced by a clamp check at construction, not by inheritance. An explicit override is accepted only if `≤ parent.permissions` — `Parse::Agent.new(parent: readonly_parent, permissions: :admin)` raises `ArgumentError`. Pass `permissions: parent.permissions` to maintain parity intentionally. |
|
|
1554
1814
|
| `client` | The constructor default `:default` resolves to the same client in standard single-app deployments. Explicit passes through. |
|
|
1555
|
-
|
|
1815
|
+
|
|
1816
|
+
**`tools:` and `methods:` narrow only (5.8.1).** A sub-agent's `tools:` and `methods:` filters can only remove from what its parent allows. A child `tools: { only: }` is intersected with the parent's allowlist and `except:` sets are unioned; a child that omits `tools:` inherits the parent's filter. For `methods:`, the parent's filters stay in force alongside the child's, so a call must pass every one (entries may be bare or `Class.method`, which plain set intersection would match wrongly). A child `only:` list with nothing its parent allows (after the parent's allowlist and `except:` lists apply) raises `ArgumentError` at construction; an explicitly empty `only: []` is the strictest narrowing and is accepted. Before 5.8.1 a child's filter replaced the parent's, so a sub-agent could enable a tool or agent method within its tier that the parent was not given.
|
|
1556
1817
|
|
|
1557
1818
|
**The clamp invariant:** `sub.permissions ≤ parent.permissions` always holds. The default `:readonly` is always safe regardless of parent tier; only explicit overrides hit the clamp check, and overrides that exceed the parent's tier raise at construction. This is the structural guarantee that a `delegate_to_subagent` chain cannot escape the parent's tier through sub-agent construction — the only path to a more-privileged agent is at the MCP factory, where the explicit elevation is auditable.
|
|
1558
1819
|
|
|
1559
1820
|
**ACL-scope subset invariant (v4.4.0):** when the parent carries a resolved ACL scope (session_token / acl_user / acl_role), an explicit child override must resolve to a `permission_strings` set that is a SUBSET of the parent's. A tool handler that tries `Parse::Agent.new(parent: user_scoped, acl_role: "admin")` raises `ArgumentError` at construction because the child's claim set would include `"role:admin"`, which the parent's claim set does not. The same applies to a different `acl_user:` (different user_id), or to a child that resolves to master-key while the parent was scoped. This closes the analogous footgun for the acl_user / acl_role identity axis — the precedent of session_token swap is misleading because session tokens are externally verified by Parse Server, while `acl_user:` and `acl_role:` are unverified constructor assertions. A master-key parent (`@acl_scope.nil?`) allows any child scope because the parent already has unrestricted reach.
|
|
1560
1821
|
|
|
1822
|
+
### `fields:` per-agent field narrowing (5.8)
|
|
1823
|
+
|
|
1824
|
+
A class's `agent_fields` is the most any agent may read. `fields:` narrows
|
|
1825
|
+
that ceiling for one agent, so two MCP deployments in the same process can
|
|
1826
|
+
expose different subsets of the same model: a user-facing assistant sees
|
|
1827
|
+
less than an analytics endpoint. Effective access is the intersection of the
|
|
1828
|
+
caller's Parse ACL/CLP, the class `agent_fields`, the agent's `fields:`
|
|
1829
|
+
policy, its tenant scope, and its tool and method filters.
|
|
1830
|
+
|
|
1831
|
+
```ruby
|
|
1832
|
+
class Customer < Parse::Object
|
|
1833
|
+
property :display_name, :string
|
|
1834
|
+
property :timezone, :string
|
|
1835
|
+
property :plan, :string
|
|
1836
|
+
property :billing_email, :string
|
|
1837
|
+
agent_fields :display_name, :timezone, :plan, :billing_email # ceiling for every agent
|
|
1838
|
+
end
|
|
1839
|
+
|
|
1840
|
+
assistant = Parse::Agent.new(session_token: token,
|
|
1841
|
+
fields: { Customer => %i[display_name timezone] })
|
|
1842
|
+
analytics = Parse::Agent.new(permissions: :readonly,
|
|
1843
|
+
fields: { Customer => %i[display_name plan] })
|
|
1844
|
+
```
|
|
1845
|
+
|
|
1846
|
+
* **Narrow only.** A field outside `agent_fields` stays hidden even if a
|
|
1847
|
+
policy lists it. A class without `agent_fields` is narrowed to exactly the
|
|
1848
|
+
listed fields (plus `objectId`, `createdAt`, `updatedAt`).
|
|
1849
|
+
* **`default:`** narrows every class the policy does not name.
|
|
1850
|
+
* **Sub-agents intersect.** `Parse::Agent.new(parent: assistant, fields: ...)`
|
|
1851
|
+
sees only fields both policies permit; omitting `fields:` inherits the
|
|
1852
|
+
parent's policy.
|
|
1853
|
+
* **Everywhere the class allowlist applied.** Query projection, `keys:`,
|
|
1854
|
+
include projections, aggregation pipelines, Atlas Search fields,
|
|
1855
|
+
`get_schema` and `completion/complete` field names, exports,
|
|
1856
|
+
`agent.describe`, and `semantic_search` chunk text, reranker input, and
|
|
1857
|
+
filter fields all use the effective set.
|
|
1858
|
+
* **No inference through filters.** `query_class`, `count_objects`, and
|
|
1859
|
+
`export_data` refuse a `where:` or `order:` on a field outside the
|
|
1860
|
+
effective set with `:field_denied` (filtering or sorting on a hidden field
|
|
1861
|
+
reveals it through which rows match). `group_by`, `distinct`, and
|
|
1862
|
+
aggregation already refused. This also applies to the class ceiling with
|
|
1863
|
+
no `fields:` policy.
|
|
1864
|
+
* **Writes stay on declared methods.** `fields:` governs what the agent
|
|
1865
|
+
reads; writes still go through `agent_method`s and the per-agent
|
|
1866
|
+
`methods:` filter.
|
|
1867
|
+
|
|
1868
|
+
The policy is applied for the duration of each tool call (fiber-local), so
|
|
1869
|
+
agents serving concurrent requests never see each other's policy.
|
|
1870
|
+
|
|
1871
|
+
**Known limits.**
|
|
1872
|
+
|
|
1873
|
+
* `call_method` projects returned objects (anything carrying `className` and
|
|
1874
|
+
`objectId`) through their class policy, but rows a method returns from its
|
|
1875
|
+
own aggregation (`$group` output, `$lookup` documents) are the method
|
|
1876
|
+
author's responsibility: the SDK cannot tell which class such a row came
|
|
1877
|
+
from.
|
|
1878
|
+
* `get_schema` lists index keys and class-level permission entries that may
|
|
1879
|
+
name fields outside the agent's policy (names only, never values).
|
|
1880
|
+
|
|
1881
|
+
---
|
|
1882
|
+
|
|
1561
1883
|
### Developer introspection — `agent.describe` / `describe_for` / `would_permit?` (v4.4.0)
|
|
1562
1884
|
|
|
1563
1885
|
Three helpers on every agent for answering "why is this agent refusing this call?" and "what can this agent actually see?" without parsing audit payloads or tracing through tool implementations. NOT exposed to the LLM — operator-side observability only.
|
|
@@ -2474,6 +2796,7 @@ Every tool call dispatched through `Agent#execute` fires the `"parse.agent.tool_
|
|
|
2474
2796
|
| `:tools_except` | Array<Symbol> | v4.3.0+ — when the agent was constructed with `tools: { except: [...] }`. |
|
|
2475
2797
|
| `:methods_only` | Array<String> | v4.3.0+ — when the agent was constructed with `methods: { only: [...] }`. Bare names and `"Class.method"` qualified names mix. |
|
|
2476
2798
|
| `:methods_except` | Array<String> | v4.3.0+ — when the agent was constructed with `methods: { except: [...] }`. |
|
|
2799
|
+
| `:methods_layers` | Array<Hash> | 5.8.1+, sub-agents that inherit at least one `methods:` filter (including a single parent layer when the sub-agent has none of its own). Every `methods:` filter in force, parent layers first and the agent's own last, each `{only:, except:}` with sorted name strings (nil when absent). `:methods_only` / `:methods_except` describe only the agent's own layer. `describe[:methods][:layers]` carries the same list. |
|
|
2477
2800
|
| `:filters` | Hash<String,Array<String>> | v4.4.0+ — when the agent was constructed with `filters: {...}`. Maps each filtered class name (or `"default"`) to the list of FIELD NAMES the filter constrains. Filter VALUES are intentionally NOT echoed — `filters: { Account => { user_id: "abc123" } }` would otherwise emit the user-identifying value on every audit-log line. Subscribers that need the actual constraint can call `agent.filter_for(class_name)` directly. |
|
|
2478
2801
|
| `:denial_kind` | Symbol | v4.3.0+, AccessDenied failure path only — one of `:hidden_class` (global `agent_hidden`), `:class_filter` (per-agent `classes:` narrowing), `:field_denied` (outside `agent_fields`), or `:storage_form_field_ref` (referenced `_p_*` pointer-storage column). Lets SOC tooling distinguish operator narrowing from policy-level denials without parsing the message prose. |
|
|
2479
2802
|
|
|
@@ -2483,7 +2806,7 @@ Every tool call dispatched through `Agent#execute` fires the `"parse.agent.tool_
|
|
|
2483
2806
|
|
|
2484
2807
|
**Server-assigned on `initialize`:** when the client omits the header on the `initialize` request, `MCPRackApp` generates a UUID, binds it to `agent.correlation_id`, and returns it in the `Mcp-Session-Id` response header. Clients echo that id on subsequent requests. A client-supplied `Mcp-Session-Id` on `initialize` is echoed back unchanged; a factory-bound `correlation_id` always wins over both. Only the `initialize` response carries the header — non-init responses don't, so the id is never leaked on every reply. The SDK does not maintain a server-side session store: the id is best-effort correlation only (audit threading + cancellation routing), and a subsequent request carrying an "unknown" id is NOT refused.
|
|
2485
2808
|
|
|
2486
|
-
**Session termination via `DELETE /`:** a `DELETE` carrying `Mcp-Session-Id` cancels every in-flight request registered under that correlation id and returns `204 No Content`. The header value is sanitized with the same regex as the request setter; missing or invalid values return `400`.
|
|
2809
|
+
**Session termination via `DELETE /`:** a `DELETE` carrying `Mcp-Session-Id` cancels every in-flight request registered under that correlation id and returns `204 No Content`. The header value is sanitized with the same regex as the request setter; missing or invalid values return `400`. Since 5.8, termination is gated like every other session operation: it passes the Origin policy, authenticates through the agent factory (`401` when the factory refuses), and is refused with `403` when the session is bound to a different principal. An unclaimed session can be terminated by any authenticated caller.
|
|
2487
2810
|
|
|
2488
2811
|
- **Factory path (for application-bound sessions):** application code that already has an internal session identifier can override the client-supplied header by setting it inside the agent factory:
|
|
2489
2812
|
|
|
@@ -469,6 +469,33 @@ The `Parse::MongoDB.to_mongodb_date(value)` helper coerces `Date`,
|
|
|
469
469
|
`DateTime`, `Time`, ISO 8601 strings, and Unix timestamps to a UTC `Time`
|
|
470
470
|
suitable for matching.
|
|
471
471
|
|
|
472
|
+
### MongoDB 9.0 null semantics
|
|
473
|
+
|
|
474
|
+
MongoDB 9.0 changed how `null` comparisons treat a dotted path that runs
|
|
475
|
+
through an array. When the path resolves to no non-null value (the field is
|
|
476
|
+
an empty array, an array of scalars, or contains a nested array), it now
|
|
477
|
+
compares equal to `null`. For `{ meta: [] }` or `{ meta: [1] }`,
|
|
478
|
+
`{ "meta.owner": null }` now matches and `{ "meta.owner": { $ne: null } }` no
|
|
479
|
+
longer does. `$exists` is unchanged, and `$lookup` equality follows the new
|
|
480
|
+
rule. This applies to REST queries (Parse Server passes them to MongoDB) and
|
|
481
|
+
mongo-direct queries alike.
|
|
482
|
+
|
|
483
|
+
The SDK never adds a `null` comparison on a dotted path by itself; ACL
|
|
484
|
+
scoping only checks the top-level `_rperm`. It compiles one only when you
|
|
485
|
+
name a dotted field. Which constraints are affected:
|
|
486
|
+
|
|
487
|
+
| Constraint on a dotted field | Compiles to | Changed in 9.0 |
|
|
488
|
+
|---|---|---|
|
|
489
|
+
| `:"meta.owner" => nil` | `{ "meta.owner": null }` | Yes |
|
|
490
|
+
| `:"meta.owner".not => nil`, `.null => false` | `{ $ne: null }` | Yes |
|
|
491
|
+
| `:"meta.owner".in => [nil, ...]` | `{ $in: [null, ...] }` | Yes |
|
|
492
|
+
| `:"meta.items".empty_or_nil`, `.not_empty` | pipeline with `$eq` / `$ne: null` | Yes |
|
|
493
|
+
| `:"meta.owner".null => true`, `.exists => false` | `{ $exists: false }` | No |
|
|
494
|
+
|
|
495
|
+
If a dotted path in your queries can traverse an array, review those
|
|
496
|
+
queries before upgrading, or use `.exists` when you mean "the field is
|
|
497
|
+
present".
|
|
498
|
+
|
|
472
499
|
---
|
|
473
500
|
|
|
474
501
|
## Pointer joins and `parse_reference`
|
data/docs/usage_guide.md
CHANGED
|
@@ -118,6 +118,44 @@ Song.query.aggregate([
|
|
|
118
118
|
])
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
+
### Field names in aggregation results
|
|
122
|
+
|
|
123
|
+
Custom aggregation rows (`$group`, `$project`, ...) come back as
|
|
124
|
+
`Parse::AggregationResult`. By default `#to_h` converts field names to
|
|
125
|
+
snake_case symbols. Pass `field_names: :server` to `#results` to keep the
|
|
126
|
+
names exactly as the aggregation returned them (5.8):
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
agg = Song.query.aggregate([
|
|
130
|
+
{ "$group" => { "_id" => "$genre", "totalPlays" => { "$sum" => "$plays" } } },
|
|
131
|
+
])
|
|
132
|
+
|
|
133
|
+
row = agg.results.first
|
|
134
|
+
row.to_h # => { _id: "Rock", total_plays: 500 } (default)
|
|
135
|
+
|
|
136
|
+
row = agg.results(field_names: :server).first
|
|
137
|
+
row.to_h # => { "_id" => "Rock", "totalPlays" => 500 }
|
|
138
|
+
row["totalPlays"] # => 500
|
|
139
|
+
row.total_plays # => 500 (a snake_case name still resolves when it matches one key)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
In `:server` mode keys are Strings, nested values are untouched, and keys
|
|
143
|
+
whose snake_case forms collide all survive. A name that is itself a key always
|
|
144
|
+
wins: with both `"totalPlays"` and `"total_plays"` stored, `row.total_plays`
|
|
145
|
+
reads `"total_plays"`. A snake_case name that is not itself a key but matches
|
|
146
|
+
more than one key (`"totalPlays"` and `"TotalPlays"` both stored) is
|
|
147
|
+
ambiguous: the method form (`row.total_plays`) raises `ArgumentError` naming
|
|
148
|
+
the candidates instead of guessing, and `row["total_plays"]` returns nil. Read
|
|
149
|
+
such a field by its exact key. Parse::Object rows are unaffected; `#as_json` already
|
|
150
|
+
returns their server field names. This is a presentation option only, and it
|
|
151
|
+
is separate from:
|
|
152
|
+
|
|
153
|
+
* `#raw`, which returns the unwrapped Hash exactly as received;
|
|
154
|
+
* `Parse::Query.field_formatter`, which controls how field names in OUTGOING
|
|
155
|
+
queries are compiled, not response keys;
|
|
156
|
+
* the REST aggregate `raw_field_names:` / `raw_values:` flags, which change
|
|
157
|
+
what Parse Server itself returns (and are never enabled by `field_names:`).
|
|
158
|
+
|
|
121
159
|
## Transactions
|
|
122
160
|
|
|
123
161
|
```ruby
|