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.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1148 -0
  3. data/README.md +39 -23
  4. data/docs/TEST_SERVER.md +2 -2
  5. data/docs/acl_clp_guide.md +7 -0
  6. data/docs/atlas_vector_search_guide.md +181 -13
  7. data/docs/caching.md +50 -0
  8. data/docs/client_sdk_guide.md +11 -0
  9. data/docs/mcp_guide.md +332 -9
  10. data/docs/mongodb_direct_guide.md +27 -0
  11. data/docs/usage_guide.md +38 -0
  12. data/docs/webhooks_guide.md +126 -17
  13. data/lib/parse/acl_scope.rb +230 -50
  14. data/lib/parse/agent/approval_gate.rb +0 -0
  15. data/lib/parse/agent/constraint_translator.rb +42 -15
  16. data/lib/parse/agent/describe.rb +11 -3
  17. data/lib/parse/agent/field_names.rb +53 -0
  18. data/lib/parse/agent/field_policy.rb +74 -0
  19. data/lib/parse/agent/mcp_deployments.rb +426 -0
  20. data/lib/parse/agent/mcp_dispatcher.rb +8 -1
  21. data/lib/parse/agent/mcp_rack_app.rb +424 -45
  22. data/lib/parse/agent/mcp_server.rb +23 -1
  23. data/lib/parse/agent/mcp_subscriptions.rb +124 -6
  24. data/lib/parse/agent/metadata_registry.rb +67 -8
  25. data/lib/parse/agent/prompt_hardening.rb +9 -3
  26. data/lib/parse/agent/tools.rb +378 -29
  27. data/lib/parse/agent.rb +298 -21
  28. data/lib/parse/api/batch.rb +10 -1
  29. data/lib/parse/api/schema.rb +23 -4
  30. data/lib/parse/api/sessions.rb +6 -2
  31. data/lib/parse/api/users.rb +88 -14
  32. data/lib/parse/atlas_search/index_manager.rb +5 -1
  33. data/lib/parse/atlas_search/protected_paths.rb +236 -0
  34. data/lib/parse/atlas_search.rb +144 -24
  35. data/lib/parse/authorization.rb +289 -4
  36. data/lib/parse/cache/sub_cache.rb +26 -0
  37. data/lib/parse/client/authentication.rb +19 -1
  38. data/lib/parse/client/batch.rb +264 -35
  39. data/lib/parse/client/body_builder.rb +21 -0
  40. data/lib/parse/client/caching.rb +371 -27
  41. data/lib/parse/client/request.rb +26 -14
  42. data/lib/parse/client/response.rb +49 -6
  43. data/lib/parse/client.rb +227 -40
  44. data/lib/parse/clp_scope.rb +317 -25
  45. data/lib/parse/console.rb +2 -2
  46. data/lib/parse/embeddings/voyage.rb +181 -17
  47. data/lib/parse/graphql/type_generator.rb +3 -0
  48. data/lib/parse/live_query/client.rb +72 -2
  49. data/lib/parse/lock_backend.rb +4 -3
  50. data/lib/parse/model/acl.rb +119 -21
  51. data/lib/parse/model/associations/belongs_to.rb +25 -3
  52. data/lib/parse/model/associations/collection_proxy.rb +146 -17
  53. data/lib/parse/model/associations/has_many.rb +49 -9
  54. data/lib/parse/model/associations/has_one.rb +3 -1
  55. data/lib/parse/model/associations/pointer_collection_proxy.rb +145 -17
  56. data/lib/parse/model/associations/relation_collection_proxy.rb +229 -38
  57. data/lib/parse/model/bytes.rb +13 -5
  58. data/lib/parse/model/classes/role.rb +95 -0
  59. data/lib/parse/model/classes/session.rb +304 -1
  60. data/lib/parse/model/classes/user.rb +104 -8
  61. data/lib/parse/model/core/actions.rb +287 -70
  62. data/lib/parse/model/core/builder.rb +100 -8
  63. data/lib/parse/model/core/create_lock.rb +27 -2
  64. data/lib/parse/model/core/describe.rb +2 -0
  65. data/lib/parse/model/core/fetching.rb +50 -5
  66. data/lib/parse/model/core/field_guards.rb +16 -8
  67. data/lib/parse/model/core/pluralized_aliases.rb +8 -4
  68. data/lib/parse/model/core/properties.rb +488 -39
  69. data/lib/parse/model/core/querying.rb +7 -0
  70. data/lib/parse/model/core/schema.rb +5 -3
  71. data/lib/parse/model/core/search_indexing.rb +63 -0
  72. data/lib/parse/model/core/vector_searchable.rb +35 -6
  73. data/lib/parse/model/file.rb +9 -2
  74. data/lib/parse/model/geopoint.rb +61 -13
  75. data/lib/parse/model/model.rb +160 -9
  76. data/lib/parse/model/object.rb +317 -18
  77. data/lib/parse/model/phone.rb +54 -5
  78. data/lib/parse/model/pointer.rb +40 -6
  79. data/lib/parse/model/push.rb +11 -1
  80. data/lib/parse/mongodb.rb +189 -61
  81. data/lib/parse/pipeline_security.rb +415 -26
  82. data/lib/parse/query/constraint.rb +31 -1
  83. data/lib/parse/query/constraints.rb +89 -59
  84. data/lib/parse/query/cursor.rb +3 -1
  85. data/lib/parse/query/operation.rb +62 -8
  86. data/lib/parse/query/ordering.rb +34 -6
  87. data/lib/parse/query.rb +1871 -167
  88. data/lib/parse/retrieval/agent_tool.rb +225 -8
  89. data/lib/parse/retrieval/benchmark.rb +149 -0
  90. data/lib/parse/retrieval/profiles.rb +320 -0
  91. data/lib/parse/retrieval/retriever.rb +10 -1
  92. data/lib/parse/retrieval.rb +2 -0
  93. data/lib/parse/schema/search_index_migrator.rb +23 -5
  94. data/lib/parse/schema.rb +74 -18
  95. data/lib/parse/stack/tasks.rb +6 -4
  96. data/lib/parse/stack/version.rb +1 -1
  97. data/lib/parse/stack.rb +91 -14
  98. data/lib/parse/two_factor_auth/user_extension.rb +14 -2
  99. data/lib/parse/two_factor_auth.rb +11 -0
  100. data/lib/parse/vector_search/hybrid.rb +110 -33
  101. data/lib/parse/vector_search/index_definition.rb +237 -0
  102. data/lib/parse/vector_search.rb +259 -23
  103. data/lib/parse/webhooks/payload.rb +136 -6
  104. data/lib/parse/webhooks/replay_protection.rb +58 -20
  105. data/lib/parse/webhooks.rb +715 -40
  106. metadata +8 -1
@@ -138,6 +138,10 @@ module Parse
138
138
  # `^foo.*` is not).
139
139
  REDOS_NESTED_QUANTIFIER_RE = /\([^)]*[+*][^)]*\)[+*?]/.freeze
140
140
 
141
+ # Fiber-storage key holding the class whose field names are being
142
+ # resolved; see {#translate}.
143
+ CLASS_SCOPE_KEY = :parse_agent_constraint_class
144
+
141
145
  # Translate JSON constraints to Parse query format.
142
146
  # Validates all operators against the security whitelist.
143
147
  #
@@ -153,9 +157,38 @@ module Parse
153
157
  # — continues to parse as a single positional Hash under Ruby 3+
154
158
  # kwargs separation. Adding a kwarg would have turned the same call
155
159
  # into "empty kwargs + missing positional arg."
156
- def translate(constraints, agent = nil)
160
+ #
161
+ # @param class_name [String, nil] the class the constraints apply to.
162
+ # When given, field keys are resolved the way the field-policy check
163
+ # resolves them ({Parse::Agent::MetadataRegistry.wire_field_names}):
164
+ # a declared `field:` name such as `PublicText` is kept exactly, so the
165
+ # column that was checked is the column that is queried. Embedded
166
+ # `$inQuery`/`$select` clauses resolve against their own className.
167
+ def translate(constraints, agent = nil, class_name = nil)
157
168
  return {} if constraints.nil? || constraints.empty?
169
+ previous = Fiber[CLASS_SCOPE_KEY]
170
+ Fiber[CLASS_SCOPE_KEY] = class_name&.to_s
171
+ begin
172
+ translate_in_scope(constraints, agent)
173
+ ensure
174
+ Fiber[CLASS_SCOPE_KEY] = previous
175
+ end
176
+ end
177
+
178
+ # Check if constraints are valid without raising.
179
+ #
180
+ # @param constraints [Hash] the query constraints
181
+ # @return [Boolean] true if valid, false otherwise
182
+ def valid?(constraints)
183
+ translate(constraints)
184
+ true
185
+ rescue ConstraintSecurityError, InvalidOperatorError
186
+ false
187
+ end
188
+
189
+ private
158
190
 
191
+ def translate_in_scope(constraints, agent)
159
192
  raise InvalidOperatorError.new(
160
193
  "Constraints must be a Hash, got #{constraints.class}",
161
194
  operator: nil,
@@ -178,19 +211,6 @@ module Parse
178
211
  end
179
212
  end
180
213
 
181
- # Check if constraints are valid without raising.
182
- #
183
- # @param constraints [Hash] the query constraints
184
- # @return [Boolean] true if valid, false otherwise
185
- def valid?(constraints)
186
- translate(constraints)
187
- true
188
- rescue ConstraintSecurityError, InvalidOperatorError
189
- false
190
- end
191
-
192
- private
193
-
194
214
  # Translate a single value, handling nested operators
195
215
  #
196
216
  # @param value [Object] the value to translate
@@ -305,7 +325,7 @@ module Parse
305
325
  # disabling the per-agent class filter on every nested
306
326
  # cross-class hop. Keep this call POSITIONAL.
307
327
  if embedded_where.is_a?(Hash)
308
- translated_where = translate(embedded_where, agent)
328
+ translated_where = translate(embedded_where, agent, embedded_class_name)
309
329
  new_val = val.dup
310
330
  if op == "$select" || op == "$dontSelect"
311
331
  query_part = new_val["query"].transform_keys(&:to_s)
@@ -542,6 +562,13 @@ module Parse
542
562
  def columnize(field)
543
563
  return field if field.start_with?("_") # Preserve special fields like _User
544
564
 
565
+ class_name = Fiber[CLASS_SCOPE_KEY]
566
+ if class_name && !class_name.empty? && defined?(Parse::Agent::MetadataRegistry)
567
+ root, rest = field.to_s.split(".", 2)
568
+ wire = Parse::Agent::MetadataRegistry.wire_field_names(class_name, [root]).first || root
569
+ return rest ? "#{wire}.#{rest}" : wire
570
+ end
571
+
545
572
  # Convert snake_case to camelCase
546
573
  field.to_s.gsub(/_([a-z])/) { ::Regexp.last_match(1).upcase }
547
574
  .sub(/^([A-Z])/) { ::Regexp.last_match(1).downcase }
@@ -176,7 +176,8 @@ module Parse
176
176
  tenant_id: tenant_id,
177
177
  classes: filter_descriptor(@class_filter_only, @class_filter_except),
178
178
  tools: tools_descriptor,
179
- methods: filter_descriptor(@method_filter_only, @method_filter_except, transform: ->(s) { s.to_s }),
179
+ methods: filter_descriptor(@method_filter_only, @method_filter_except, transform: ->(s) { s.to_s })
180
+ .merge(layers: method_filter_layers_descriptor),
180
181
  filters: per_agent_filters_summary,
181
182
  hidden_classes: Parse::Agent::MetadataRegistry.hidden_class_names,
182
183
  per_class: per_class_descriptor,
@@ -298,7 +299,9 @@ module Parse
298
299
  # the wire-format field name Array so the output reads identically to
299
300
  # the schema-enriched `get_schema` echo.
300
301
  def class_field_allowlist(class_name)
301
- list = Parse::Agent::MetadataRegistry.field_allowlist(class_name)
302
+ # Effective allowlist for THIS agent: the class ceiling narrowed by
303
+ # its `fields:` policy.
304
+ list = Parse::Agent::FieldPolicy.with(self) { Parse::Agent::MetadataRegistry.field_allowlist(class_name) }
302
305
  list && list.any? ? list.dup : nil
303
306
  end
304
307
 
@@ -381,10 +384,15 @@ module Parse
381
384
  lines << " except: #{data[:tools][:except].inspect}" if data[:tools][:except]
382
385
  lines << " effective: #{data[:tools][:effective].inspect}"
383
386
 
384
- if data[:methods][:only] || data[:methods][:except]
387
+ inherited_layers = Array(data[:methods][:layers])[0...-1] if data[:methods][:only] || data[:methods][:except]
388
+ inherited_layers ||= Array(data[:methods][:layers])
389
+ if data[:methods][:only] || data[:methods][:except] || inherited_layers.any?
385
390
  lines << " methods:"
386
391
  lines << " only: #{data[:methods][:only].inspect}" if data[:methods][:only]
387
392
  lines << " except: #{data[:methods][:except].inspect}" if data[:methods][:except]
393
+ inherited_layers.each do |layer|
394
+ lines << " inherited: #{layer.compact.inspect}"
395
+ end
388
396
  end
389
397
 
390
398
  if data[:filters]
@@ -0,0 +1,53 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ module Parse
5
+ class Agent
6
+ # Per-agent data-field naming mode for tool output.
7
+ #
8
+ # `Parse::Agent.new(field_names: :server)` asks for data fields in the
9
+ # exact names Parse returns or the model declares through its
10
+ # `field_map` (`createdAt`, `totalPlays`, `ExternalID`), with no
11
+ # snake_case conversion anywhere a tool would otherwise apply one.
12
+ # `:default` (or omitting the option) keeps every tool's existing output.
13
+ #
14
+ # Only data-field keys are affected. MCP protocol keys and SDK envelope
15
+ # keys (`chunks`, `documents`, `object_id`, `next_call`, ...) keep their
16
+ # contracts, and "server names" never means raw MongoDB storage columns
17
+ # (`_p_author`, `_rperm`, `_session_token`). Naming is presentation only:
18
+ # every access check (ACL/CLP, protectedFields, class and per-agent field
19
+ # policies) resolves against canonical field identities before output.
20
+ #
21
+ # Like {FieldPolicy}, the mode is scoped fiber-locally around each tool
22
+ # call by {Parse::Agent::Tools.invoke}, so concurrent agents with
23
+ # different modes never see each other's setting and nothing global is
24
+ # mutated.
25
+ module FieldNames
26
+ SCOPE_KEY = :parse_agent_field_names_scope
27
+
28
+ module_function
29
+
30
+ # Run the block with `agent`'s naming mode in effect.
31
+ def with(agent)
32
+ previous = Fiber[SCOPE_KEY]
33
+ Fiber[SCOPE_KEY] = agent
34
+ yield
35
+ ensure
36
+ Fiber[SCOPE_KEY] = previous
37
+ end
38
+
39
+ # @return [Symbol] `:server` or `:default` for the agent whose tool is
40
+ # executing; `:default` outside a tool call.
41
+ def current_mode
42
+ agent = Fiber[SCOPE_KEY]
43
+ mode = agent.respond_to?(:field_names_mode) ? agent.field_names_mode : nil
44
+ mode || :default
45
+ end
46
+
47
+ # @return [Boolean] true when the executing agent asked for server names.
48
+ def server?
49
+ current_mode == :server
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,74 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ module Parse
5
+ class Agent
6
+ # Per-agent field narrowing.
7
+ #
8
+ # A class's `agent_fields` declaration is the CEILING: the most any agent
9
+ # may read. A `Parse::Agent.new(fields: { "Post" => [:title, :status] })`
10
+ # policy NARROWS that ceiling for one agent (one MCP deployment), so a
11
+ # user-facing assistant and an analytics endpoint in the same process can
12
+ # expose different subsets of the same model. A policy can never widen
13
+ # past the ceiling, and a sub-agent's policy intersects its parent's.
14
+ #
15
+ # The effective allowlist is resolved by
16
+ # {Parse::Agent::MetadataRegistry.field_allowlist}, which every
17
+ # enforcement point already calls (projection, `where:`/`keys:` checks,
18
+ # aggregation pipelines, Atlas Search fields, schema output, exports,
19
+ # `semantic_search` chunk text). This module supplies the narrowing for
20
+ # the agent whose tool is currently executing: {Parse::Agent::Tools.invoke}
21
+ # wraps every tool call in {.with}, so the policy applies without
22
+ # threading the agent through each helper.
23
+ #
24
+ # The scope lives in fiber storage (`Fiber[]`), which child fibers and
25
+ # threads inherit when they are created. Concurrent requests on
26
+ # different threads never see each other's policy, and work a custom
27
+ # tool hands to a thread or fiber it starts stays narrowed.
28
+ module FieldPolicy
29
+ SCOPE_KEY = :parse_agent_field_policy_scope
30
+
31
+ module_function
32
+
33
+ # Run the block with `agent`'s field narrowing in effect.
34
+ #
35
+ # @param agent [Parse::Agent, nil]
36
+ # @return the block's value
37
+ #
38
+ # Scopes nest: a tool that builds another agent and invokes it (even one
39
+ # constructed without `parent:`) runs under BOTH policies, so the inner
40
+ # call can only narrow further, never escape the outer agent's policy.
41
+ def with(agent)
42
+ previous = Fiber[SCOPE_KEY]
43
+ Fiber[SCOPE_KEY] = (previous || []) + [agent]
44
+ yield
45
+ ensure
46
+ Fiber[SCOPE_KEY] = previous
47
+ end
48
+
49
+ # @return [Parse::Agent, nil] the innermost agent whose tool is executing.
50
+ def current_agent
51
+ Fiber[SCOPE_KEY]&.last
52
+ end
53
+
54
+ # Wire-format field names the current agent narrows `class_name` to, or
55
+ # nil when the agent places no narrowing on that class (or no agent is
56
+ # in scope).
57
+ #
58
+ # @param class_name [String]
59
+ # @return [Array<String>, nil]
60
+ def narrowing_for(class_name)
61
+ stack = Fiber[SCOPE_KEY]
62
+ return nil if stack.nil? || stack.empty?
63
+ result = nil
64
+ stack.uniq.each do |agent|
65
+ next unless agent.respond_to?(:field_narrowing_for)
66
+ names = agent.field_narrowing_for(class_name)
67
+ next if names.nil?
68
+ result = result ? (result & names) : names
69
+ end
70
+ result
71
+ end
72
+ end
73
+ end
74
+ end