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
|
@@ -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
|
-
|
|
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 }
|
data/lib/parse/agent/describe.rb
CHANGED
|
@@ -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
|
-
|
|
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
|