parse-stack-next 4.5.0 → 5.0.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/.bundle/config +2 -0
- data/.env.sample +17 -3
- data/.github/workflows/codeql.yml +44 -0
- data/.github/workflows/docs.yml +39 -0
- data/.github/workflows/release.yml +32 -0
- data/.github/workflows/ruby.yml +8 -6
- data/.gitignore +4 -0
- data/.vscode/settings.json +3 -0
- data/CHANGELOG.md +305 -72
- data/Gemfile.lock +10 -3
- data/LICENSE.txt +1 -1
- data/README.md +190 -219
- data/Rakefile +1 -1
- data/SECURITY.md +30 -0
- data/assets/parse-stack-next-avatar.png +0 -0
- data/assets/parse-stack-next-avatar.svg +37 -0
- data/assets/parse-stack-next-banner.png +0 -0
- data/assets/parse-stack-next-banner.svg +45 -0
- data/assets/parse-stack-next-social-preview.png +0 -0
- data/docs/atlas_vector_search_guide.md +511 -0
- data/docs/client_sdk_guide.md +1320 -0
- data/docs/mcp_guide.md +225 -104
- data/docs/mongodb_direct_guide.md +21 -4
- data/docs/usage_guide.md +585 -0
- data/examples/transaction_example.rb +28 -28
- data/lib/parse/acl_scope.rb +2 -2
- data/lib/parse/agent/mcp_rack_app.rb +184 -16
- data/lib/parse/agent/metadata_dsl.rb +16 -16
- data/lib/parse/agent/pipeline_validator.rb +28 -1
- data/lib/parse/agent/prompts.rb +5 -5
- data/lib/parse/agent/tools.rb +287 -14
- data/lib/parse/agent.rb +209 -12
- data/lib/parse/api/analytics.rb +27 -5
- data/lib/parse/api/files.rb +6 -2
- data/lib/parse/api/push.rb +21 -4
- data/lib/parse/api/server.rb +59 -0
- data/lib/parse/api/users.rb +26 -2
- data/lib/parse/atlas_search/index_manager.rb +84 -0
- data/lib/parse/atlas_search.rb +37 -9
- data/lib/parse/cache/pool.rb +88 -0
- data/lib/parse/cache/redis.rb +249 -0
- data/lib/parse/client/body_builder.rb +94 -0
- data/lib/parse/client/caching.rb +109 -9
- data/lib/parse/client/response.rb +27 -0
- data/lib/parse/client.rb +74 -3
- data/lib/parse/console.rb +203 -0
- data/lib/parse/embeddings/cohere.rb +484 -0
- data/lib/parse/embeddings/fixture.rb +130 -0
- data/lib/parse/embeddings/jina.rb +454 -0
- data/lib/parse/embeddings/local_http.rb +492 -0
- data/lib/parse/embeddings/openai.rb +520 -0
- data/lib/parse/embeddings/provider.rb +264 -0
- data/lib/parse/embeddings/qwen.rb +431 -0
- data/lib/parse/embeddings/voyage.rb +550 -0
- data/lib/parse/embeddings.rb +225 -0
- data/lib/parse/graphql/scalars.rb +53 -0
- data/lib/parse/graphql/type_generator.rb +264 -0
- data/lib/parse/graphql.rb +48 -0
- data/lib/parse/live_query/client.rb +24 -5
- data/lib/parse/live_query/subscription.rb +17 -6
- data/lib/parse/live_query.rb +9 -4
- data/lib/parse/model/associations/collection_proxy.rb +2 -2
- data/lib/parse/model/associations/has_many.rb +32 -1
- data/lib/parse/model/associations/has_one.rb +17 -0
- data/lib/parse/model/associations/pointer_collection_proxy.rb +3 -3
- data/lib/parse/model/classes/user.rb +307 -11
- data/lib/parse/model/clp.rb +1 -1
- data/lib/parse/model/core/create_lock.rb +14 -2
- data/lib/parse/model/core/embed_managed.rb +296 -0
- data/lib/parse/model/core/fetching.rb +4 -4
- data/lib/parse/model/core/indexing.rb +53 -14
- data/lib/parse/model/core/parse_reference.rb +3 -3
- data/lib/parse/model/core/properties.rb +70 -1
- data/lib/parse/model/core/querying.rb +57 -1
- data/lib/parse/model/core/vector_searchable.rb +285 -0
- data/lib/parse/model/file.rb +16 -4
- data/lib/parse/model/model.rb +26 -10
- data/lib/parse/model/object.rb +63 -6
- data/lib/parse/model/pointer.rb +16 -2
- data/lib/parse/model/shortnames.rb +2 -0
- data/lib/parse/model/validations/uniqueness_validator.rb +3 -3
- data/lib/parse/model/vector.rb +102 -0
- data/lib/parse/mongodb.rb +90 -8
- data/lib/parse/pipeline_security.rb +59 -2
- data/lib/parse/query/constraints.rb +16 -14
- data/lib/parse/query/ordering.rb +1 -1
- data/lib/parse/query.rb +137 -64
- data/lib/parse/stack/generators/templates/model.erb +2 -2
- data/lib/parse/stack/generators/templates/model_installation.rb +1 -1
- data/lib/parse/stack/generators/templates/model_role.rb +1 -1
- data/lib/parse/stack/generators/templates/model_session.rb +1 -1
- data/lib/parse/stack/generators/templates/parse.rb +1 -1
- data/lib/parse/stack/generators/templates/webhooks.rb +1 -1
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +375 -73
- data/lib/parse/two_factor_auth/user_extension.rb +5 -2
- data/lib/parse/vector_search.rb +341 -0
- data/parse-stack-next.gemspec +10 -9
- data/scripts/docker/docker-compose.test.yml +18 -0
- data/scripts/start-parse.sh +6 -0
- data/scripts/vector_prototype/create_vector_index.js +105 -0
- data/scripts/vector_prototype/fetch_embeddings.py +241 -0
- data/scripts/vector_prototype/fixture_manifest.json +9 -0
- data/scripts/vector_prototype/query_prototype.rb +84 -0
- data/scripts/vector_prototype/run.sh +34 -0
- metadata +77 -5
- data/parse-stack.png +0 -0
data/lib/parse/agent.rb
CHANGED
|
@@ -455,6 +455,37 @@ module Parse
|
|
|
455
455
|
WRITE_GATED_TOOLS = %i[create_object update_object delete_object].freeze
|
|
456
456
|
SCHEMA_GATED_TOOLS = %i[create_class delete_class].freeze
|
|
457
457
|
|
|
458
|
+
# Built-in tools that are safe to dispatch when the agent runs on a
|
|
459
|
+
# client (no master_key) with a session_token. Parse Server natively
|
|
460
|
+
# enforces ACL + CLP + protectedFields on these REST endpoints, so the
|
|
461
|
+
# SDK does not need to add an enforcement layer for them.
|
|
462
|
+
#
|
|
463
|
+
# The list is the MODE CEILING in client mode: an operator's `tools:`
|
|
464
|
+
# filter may narrow further, but cannot widen past this set. Anything
|
|
465
|
+
# not in CLIENT_SAFE_READ_TOOLS or CLIENT_SAFE_MUTATION_TOOLS is
|
|
466
|
+
# refused at dispatch when @client_mode is true, including custom
|
|
467
|
+
# registered tools (which must opt in explicitly via
|
|
468
|
+
# `Parse::Agent::Tools.register(client_safe: true, ...)`).
|
|
469
|
+
CLIENT_SAFE_READ_TOOLS = %i[
|
|
470
|
+
list_tools
|
|
471
|
+
get_object
|
|
472
|
+
get_objects
|
|
473
|
+
query_class
|
|
474
|
+
count_objects
|
|
475
|
+
get_sample_objects
|
|
476
|
+
].freeze
|
|
477
|
+
|
|
478
|
+
# Built-in mutation tools that route through session-token REST and
|
|
479
|
+
# are therefore enforceable by Parse Server's native ACL/CLP. Gated
|
|
480
|
+
# additionally by the per-agent +allow_mutations:+ kwarg in client
|
|
481
|
+
# mode (default false) and by the existing process-level env vars
|
|
482
|
+
# (PARSE_AGENT_ALLOW_WRITE_TOOLS + PARSE_AGENT_ALLOW_RAW_CRUD).
|
|
483
|
+
CLIENT_SAFE_MUTATION_TOOLS = %i[
|
|
484
|
+
create_object
|
|
485
|
+
update_object
|
|
486
|
+
delete_object
|
|
487
|
+
].freeze
|
|
488
|
+
|
|
458
489
|
# Truthy ENV-var values. Anything else (including unset) means disabled.
|
|
459
490
|
ENV_TRUTHY_RE = /\A(1|true|yes|on)\z/i.freeze
|
|
460
491
|
|
|
@@ -590,6 +621,24 @@ module Parse
|
|
|
590
621
|
# @return [Parse::Client] the Parse client instance to use
|
|
591
622
|
attr_reader :client
|
|
592
623
|
|
|
624
|
+
# @return [Boolean] whether the agent runs in client mode (its
|
|
625
|
+
# Parse::Client has no master_key). In client mode the dispatchable
|
|
626
|
+
# tool set is restricted to {CLIENT_SAFE_READ_TOOLS},
|
|
627
|
+
# {CLIENT_SAFE_MUTATION_TOOLS} (gated on {#allow_mutations?}), and
|
|
628
|
+
# any registered tool declared +client_safe: true+.
|
|
629
|
+
def client_mode?
|
|
630
|
+
@client_mode == true
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
# @return [Boolean] whether this agent may dispatch raw mutation
|
|
634
|
+
# tools (create_object/update_object/delete_object). Layered with
|
|
635
|
+
# the process-level PARSE_AGENT_ALLOW_WRITE_TOOLS +
|
|
636
|
+
# PARSE_AGENT_ALLOW_RAW_CRUD env vars (all three must be true).
|
|
637
|
+
# Default: +false+ in client mode, +true+ in master-key mode.
|
|
638
|
+
def allow_mutations?
|
|
639
|
+
@allow_mutations == true
|
|
640
|
+
end
|
|
641
|
+
|
|
593
642
|
# @return [Array<Hash>] log of operations performed in this session
|
|
594
643
|
attr_reader :operation_log
|
|
595
644
|
|
|
@@ -604,7 +653,7 @@ module Parse
|
|
|
604
653
|
|
|
605
654
|
# @return [String, nil] caller-supplied identifier that ties multiple
|
|
606
655
|
# tool calls into a single logical conversation. Set by the transport
|
|
607
|
-
# layer (MCPRackApp reads
|
|
656
|
+
# layer (MCPRackApp reads Mcp-Session-Id) or directly by an
|
|
608
657
|
# embedder. Included in every `parse.agent.tool_call` notification
|
|
609
658
|
# payload as `:correlation_id` when present. Sanitized to a max of
|
|
610
659
|
# 128 characters from the set `[A-Za-z0-9._-]` to prevent log
|
|
@@ -1068,7 +1117,8 @@ module Parse
|
|
|
1068
1117
|
tools: nil, methods: nil, classes: nil, filters: nil,
|
|
1069
1118
|
parent: nil, recursion_depth: nil,
|
|
1070
1119
|
strict_tool_filter: nil, strict_class_filter: nil,
|
|
1071
|
-
master_atlas: nil
|
|
1120
|
+
master_atlas: nil,
|
|
1121
|
+
allow_mutations: nil)
|
|
1072
1122
|
# SECURITY: Mutually exclusive identity inputs. `acl_user:` and
|
|
1073
1123
|
# `acl_role:` are unverified constructor assertions (the SDK does
|
|
1074
1124
|
# not round-trip them to Parse Server for validation the way
|
|
@@ -1271,6 +1321,78 @@ module Parse
|
|
|
1271
1321
|
@tenant_id = tenant_id
|
|
1272
1322
|
@master_atlas = master_atlas == true
|
|
1273
1323
|
|
|
1324
|
+
# Client-mode detection. An agent runs in CLIENT MODE when its
|
|
1325
|
+
# underlying Parse::Client has no master_key AND it was constructed
|
|
1326
|
+
# with a non-empty session_token. This is the explicit
|
|
1327
|
+
# "session-token-on-a-public-client" posture: every tool call must
|
|
1328
|
+
# route through a REST endpoint Parse Server natively authorizes
|
|
1329
|
+
# (ACL + CLP + protectedFields) because the SDK has no master-key
|
|
1330
|
+
# fallback to lean on.
|
|
1331
|
+
#
|
|
1332
|
+
# The "no master_key, no session_token" case is NOT treated as
|
|
1333
|
+
# client mode — that's a misconfigured master-key-posture agent
|
|
1334
|
+
# whose REST calls will fail with 401 at dispatch. The existing
|
|
1335
|
+
# one-time master-key warning surfaces this; refusing here would
|
|
1336
|
+
# break compatibility with test harnesses and bootstrap factories
|
|
1337
|
+
# that construct agents before identity is threaded in.
|
|
1338
|
+
#
|
|
1339
|
+
# acl_user / acl_role on a no-master-key client are refused
|
|
1340
|
+
# regardless of session_token presence: they are unverified
|
|
1341
|
+
# constructor assertions with no REST equivalent — Parse Server's
|
|
1342
|
+
# REST surface offers no "act as user-pointer" affordance, so the
|
|
1343
|
+
# SDK cannot honor them without a master key.
|
|
1344
|
+
no_master_key = @client.respond_to?(:master_key) && @client.master_key.nil?
|
|
1345
|
+
session_token_present = !@session_token.nil? && !@session_token.to_s.empty?
|
|
1346
|
+
@client_mode = no_master_key && session_token_present
|
|
1347
|
+
|
|
1348
|
+
if no_master_key && (@acl_user_scope || @acl_role_scope)
|
|
1349
|
+
raise ArgumentError,
|
|
1350
|
+
"Parse::Agent: acl_user: and acl_role: require a Parse::Client " \
|
|
1351
|
+
"with a master_key (they are unverified constructor assertions " \
|
|
1352
|
+
"the SDK can only honor via master-key REST). The supplied " \
|
|
1353
|
+
"client has no master_key. Use session_token: instead, or " \
|
|
1354
|
+
"switch to a master-key client."
|
|
1355
|
+
end
|
|
1356
|
+
|
|
1357
|
+
# Per-agent mutation gate. Layered ON TOP of the process-level
|
|
1358
|
+
# PARSE_AGENT_ALLOW_WRITE_TOOLS / PARSE_AGENT_ALLOW_RAW_CRUD env
|
|
1359
|
+
# vars — BOTH must be true for raw create/update/delete to
|
|
1360
|
+
# dispatch. Defaults:
|
|
1361
|
+
# * Client mode → false (default-deny; opt in per agent)
|
|
1362
|
+
# * Master-key → true (back-compat; existing operators have
|
|
1363
|
+
# only the env vars today, and adding a
|
|
1364
|
+
# false default would silently disable
|
|
1365
|
+
# writes for every existing master-key
|
|
1366
|
+
# agent).
|
|
1367
|
+
# When +parent:+ is supplied, the child cannot widen the parent's
|
|
1368
|
+
# gate: if parent.allow_mutations? is false, child must also be
|
|
1369
|
+
# false. Default-on-nil inherits the parent's value verbatim so
|
|
1370
|
+
# the safe path (omit kwarg) is trivially correct.
|
|
1371
|
+
if parent
|
|
1372
|
+
parent_allows = parent.respond_to?(:allow_mutations?) ? parent.allow_mutations? : true
|
|
1373
|
+
resolved_allow_mutations =
|
|
1374
|
+
if allow_mutations.nil?
|
|
1375
|
+
parent_allows
|
|
1376
|
+
else
|
|
1377
|
+
allow_mutations == true
|
|
1378
|
+
end
|
|
1379
|
+
if resolved_allow_mutations && !parent_allows
|
|
1380
|
+
raise ArgumentError,
|
|
1381
|
+
"sub-agent allow_mutations: true exceeds parent's " \
|
|
1382
|
+
"allow_mutations: false. A sub-agent cannot widen the " \
|
|
1383
|
+
"parent's mutation gate — drop the override (omit to inherit) " \
|
|
1384
|
+
"or pass allow_mutations: false explicitly."
|
|
1385
|
+
end
|
|
1386
|
+
@allow_mutations = resolved_allow_mutations
|
|
1387
|
+
else
|
|
1388
|
+
@allow_mutations =
|
|
1389
|
+
if allow_mutations.nil?
|
|
1390
|
+
!@client_mode
|
|
1391
|
+
else
|
|
1392
|
+
allow_mutations == true
|
|
1393
|
+
end
|
|
1394
|
+
end
|
|
1395
|
+
|
|
1274
1396
|
# Resolve the ACL scope ONCE at construction into a frozen
|
|
1275
1397
|
# Parse::ACLScope::Resolution. Three modes:
|
|
1276
1398
|
#
|
|
@@ -1561,12 +1683,24 @@ module Parse
|
|
|
1561
1683
|
#
|
|
1562
1684
|
# Resolution order is strict: builtin permission-tier tools are unioned
|
|
1563
1685
|
# with registered tools whose declared permission is <= the agent's
|
|
1564
|
-
# tier, then the per-instance filter narrows that set
|
|
1565
|
-
#
|
|
1686
|
+
# tier, then the per-instance filter narrows that set, then in client
|
|
1687
|
+
# mode the client-safe ceiling narrows it further. None of these
|
|
1688
|
+
# steps can elevate above its input — `tools: { only:
|
|
1566
1689
|
# [:delete_object] }` on a `:readonly` agent still excludes
|
|
1567
|
-
# `delete_object
|
|
1568
|
-
#
|
|
1569
|
-
#
|
|
1690
|
+
# `delete_object`, and `tools: { only: [:aggregate] }` on a
|
|
1691
|
+
# client-mode agent still excludes `aggregate`. This invariant is
|
|
1692
|
+
# the structural correctness of the layered design (mode ceiling ▷
|
|
1693
|
+
# env-gates ▷ permission tier ▷ per-instance filter) and must not
|
|
1694
|
+
# be violated by future changes.
|
|
1695
|
+
#
|
|
1696
|
+
# The client-mode intersection here is what makes the advertised
|
|
1697
|
+
# catalog (MCP `tools/list`, OpenAI function definitions, the
|
|
1698
|
+
# describe output) match the set the dispatch path will actually
|
|
1699
|
+
# dispatch. Without it, an LLM would see a refused tool in its
|
|
1700
|
+
# catalog, attempt it, and learn about the refusal only via an
|
|
1701
|
+
# access-denied error — wasting turns on tools it never could have
|
|
1702
|
+
# called. The dispatch-path gate in {#execute} remains as the
|
|
1703
|
+
# belt-and-suspenders enforcement point.
|
|
1570
1704
|
#
|
|
1571
1705
|
# @return [Array<Symbol>] list of allowed tool names
|
|
1572
1706
|
def allowed_tools
|
|
@@ -1575,6 +1709,14 @@ module Parse
|
|
|
1575
1709
|
|
|
1576
1710
|
permitted = permitted & @tool_filter_only.to_a if @tool_filter_only
|
|
1577
1711
|
permitted = permitted - @tool_filter_except.to_a if @tool_filter_except
|
|
1712
|
+
|
|
1713
|
+
if @client_mode
|
|
1714
|
+
permitted = permitted.select { |sym| Parse::Agent::Tools.client_safe?(sym) }
|
|
1715
|
+
unless @allow_mutations
|
|
1716
|
+
permitted -= Parse::Agent::CLIENT_SAFE_MUTATION_TOOLS
|
|
1717
|
+
end
|
|
1718
|
+
end
|
|
1719
|
+
|
|
1578
1720
|
permitted
|
|
1579
1721
|
end
|
|
1580
1722
|
|
|
@@ -1692,10 +1834,64 @@ module Parse
|
|
|
1692
1834
|
end
|
|
1693
1835
|
|
|
1694
1836
|
unless tool_allowed?(tool_name)
|
|
1695
|
-
# Distinguish
|
|
1696
|
-
#
|
|
1697
|
-
#
|
|
1698
|
-
#
|
|
1837
|
+
# Distinguish refusal reasons so the LLM (and SOC tooling) see
|
|
1838
|
+
# the meaningful diagnostic. Resolution order matters — the
|
|
1839
|
+
# client-mode ceiling and the per-agent mutation gate emit
|
|
1840
|
+
# specific :access_denied messages so an operator can tell
|
|
1841
|
+
# which knob refused the call. The generic "filter excluded
|
|
1842
|
+
# it" / "tier never allowed it" branches catch what's left.
|
|
1843
|
+
|
|
1844
|
+
# Operator-filter precedence: when the per-instance `tools:`
|
|
1845
|
+
# filter is the binding gate, prefer the filter message even
|
|
1846
|
+
# if the client-mode ceiling or mutation gate would also have
|
|
1847
|
+
# refused. Otherwise an operator who set
|
|
1848
|
+
# `tools: { except: [:create_object] }` AND `allow_mutations:
|
|
1849
|
+
# false` is told "set allow_mutations: true", which won't
|
|
1850
|
+
# actually help — the filter is the real blocker.
|
|
1851
|
+
operator_filter_excludes =
|
|
1852
|
+
(@tool_filter_except && @tool_filter_except.include?(tool_name)) ||
|
|
1853
|
+
(@tool_filter_only && !@tool_filter_only.include?(tool_name))
|
|
1854
|
+
if operator_filter_excludes && tier_permits_tool?(tool_name)
|
|
1855
|
+
return error_response(
|
|
1856
|
+
"Tool '#{tool_name}' is not enabled for this agent instance " \
|
|
1857
|
+
"(excluded by the configured tools: filter).",
|
|
1858
|
+
error_code: :tool_filtered,
|
|
1859
|
+
)
|
|
1860
|
+
end
|
|
1861
|
+
|
|
1862
|
+
if @client_mode &&
|
|
1863
|
+
Parse::Agent::CLIENT_SAFE_MUTATION_TOOLS.include?(tool_name) &&
|
|
1864
|
+
!@allow_mutations &&
|
|
1865
|
+
Parse::Agent::Tools.client_safe?(tool_name)
|
|
1866
|
+
# The tool is REST-safe (the mode ceiling would let it
|
|
1867
|
+
# through) but the per-agent mutation gate is closed.
|
|
1868
|
+
# Naming the gate specifically avoids sending operators to
|
|
1869
|
+
# the env-var rabbit hole when the real fix is the
|
|
1870
|
+
# constructor kwarg.
|
|
1871
|
+
return error_response(
|
|
1872
|
+
"Raw mutation tool '#{tool_name}' is disabled for this " \
|
|
1873
|
+
"client-mode agent. Construct the agent with " \
|
|
1874
|
+
"allow_mutations: true to enable write/delete dispatch. " \
|
|
1875
|
+
"The process-level PARSE_AGENT_ALLOW_WRITE_TOOLS / " \
|
|
1876
|
+
"PARSE_AGENT_ALLOW_RAW_CRUD env vars must additionally " \
|
|
1877
|
+
"be set on the deployment.",
|
|
1878
|
+
error_code: :access_denied,
|
|
1879
|
+
)
|
|
1880
|
+
end
|
|
1881
|
+
if @client_mode && !Parse::Agent::Tools.client_safe?(tool_name)
|
|
1882
|
+
# Mode ceiling. Tool requires either master-key REST or
|
|
1883
|
+
# mongo-direct, neither of which a client-mode agent has.
|
|
1884
|
+
# Refuse with a specific message so the LLM doesn't retry.
|
|
1885
|
+
return error_response(
|
|
1886
|
+
"Tool '#{tool_name}' is not available to client-mode agents. " \
|
|
1887
|
+
"Client mode (no master_key on the underlying Parse::Client) " \
|
|
1888
|
+
"restricts dispatch to session-token-authorized REST tools: " \
|
|
1889
|
+
"#{(CLIENT_SAFE_READ_TOOLS + CLIENT_SAFE_MUTATION_TOOLS).sort.join(", ")}, " \
|
|
1890
|
+
"plus any custom tool registered with client_safe: true. " \
|
|
1891
|
+
"Refused at the mode ceiling.",
|
|
1892
|
+
error_code: :access_denied,
|
|
1893
|
+
)
|
|
1894
|
+
end
|
|
1699
1895
|
if tier_permits_tool?(tool_name)
|
|
1700
1896
|
return error_response(
|
|
1701
1897
|
"Tool '#{tool_name}' is not enabled for this agent instance " \
|
|
@@ -1724,10 +1920,11 @@ module Parse
|
|
|
1724
1920
|
# also re-opening the generic create_object/update_object surface
|
|
1725
1921
|
# (which additionally requires RAW_CRUD=true).
|
|
1726
1922
|
if WRITE_GATED_TOOLS.include?(tool_name) &&
|
|
1727
|
-
!(Parse::Agent.write_tools_enabled? && Parse::Agent.raw_crud_enabled?)
|
|
1923
|
+
!(Parse::Agent.write_tools_enabled? && Parse::Agent.raw_crud_enabled? && @allow_mutations)
|
|
1728
1924
|
missing = []
|
|
1729
1925
|
missing << "PARSE_AGENT_ALLOW_WRITE_TOOLS=true" unless Parse::Agent.write_tools_enabled?
|
|
1730
1926
|
missing << "PARSE_AGENT_ALLOW_RAW_CRUD=true" unless Parse::Agent.raw_crud_enabled?
|
|
1927
|
+
missing << "allow_mutations: true (per-agent kwarg)" unless @allow_mutations
|
|
1731
1928
|
return error_response(
|
|
1732
1929
|
"Raw CRUD tool '#{tool_name}' is disabled. Required: #{missing.join(' AND ')}. " \
|
|
1733
1930
|
"Prefer declaring an agent_method on the target class for an intent-based " \
|
data/lib/parse/api/analytics.rb
CHANGED
|
@@ -6,12 +6,34 @@ module Parse
|
|
|
6
6
|
# Defines the Analytics interface for the Parse REST API
|
|
7
7
|
module Analytics
|
|
8
8
|
|
|
9
|
-
# Send analytics data.
|
|
10
|
-
#
|
|
11
|
-
#
|
|
9
|
+
# Send analytics data. Parse Server's default `analyticsAdapter`
|
|
10
|
+
# is a no-op: events POSTed here are accepted but not persisted
|
|
11
|
+
# and cannot be read back through Parse Server. Operators who wire
|
|
12
|
+
# in a custom adapter decide what (if anything) to do with each
|
|
13
|
+
# event, including whether to cap dimension count — the legacy
|
|
14
|
+
# parse.com eight-pair cap does NOT apply to Parse Server out of
|
|
15
|
+
# the box. If you need to read events back, persist them to a
|
|
16
|
+
# regular `Parse::Object` subclass instead.
|
|
17
|
+
#
|
|
18
|
+
# @param event_name [String] the name of the event. Restricted to
|
|
19
|
+
# word characters, hyphens, and dots so the value cannot escape
|
|
20
|
+
# the `/events/` path segment.
|
|
21
|
+
# @param metrics [Hash] dimension pairs to attach to the event.
|
|
22
|
+
# @param opts [Hash] additional options forwarded to {Parse::Client#request}
|
|
23
|
+
# (e.g. :session_token, :use_master_key). Analytics events are
|
|
24
|
+
# typically public-writable, but a session token can be threaded
|
|
25
|
+
# through for installations that require authentication on /events.
|
|
26
|
+
# @raise [ArgumentError] when `event_name` is empty or contains
|
|
27
|
+
# characters outside `[\w\-\.]`.
|
|
12
28
|
# @see http://docs.parseplatform.org/rest/guide/#analytics Parse Analytics
|
|
13
|
-
def send_analytics(event_name, metrics = {})
|
|
14
|
-
|
|
29
|
+
def send_analytics(event_name, metrics = {}, **opts)
|
|
30
|
+
safe = event_name.to_s
|
|
31
|
+
unless safe.match?(/\A[\w\-\.]+\z/)
|
|
32
|
+
raise ArgumentError,
|
|
33
|
+
"Parse::API::Analytics#send_analytics: event_name must contain only " \
|
|
34
|
+
"word characters, hyphens, or dots (got #{event_name.inspect})"
|
|
35
|
+
end
|
|
36
|
+
request :post, "events/#{safe}", body: metrics, opts: opts
|
|
15
37
|
end
|
|
16
38
|
end
|
|
17
39
|
end
|
data/lib/parse/api/files.rb
CHANGED
|
@@ -15,12 +15,16 @@ module Parse
|
|
|
15
15
|
# @param fileName [String] the basename of the file.
|
|
16
16
|
# @param data [Hash] the data related to this file.
|
|
17
17
|
# @param content_type [String] the mime-type of the file.
|
|
18
|
+
# @param opts [Hash] additional options forwarded to {Parse::Client#request}
|
|
19
|
+
# (e.g. :session_token, :use_master_key). When the SDK is running in
|
|
20
|
+
# client mode against a Parse Server with `fileUpload.enableForAuthenticatedUser`
|
|
21
|
+
# on, a session_token is required for the upload to be accepted.
|
|
18
22
|
# @return [Parse::Response]
|
|
19
|
-
def create_file(fileName, data = {}, content_type = nil)
|
|
23
|
+
def create_file(fileName, data = {}, content_type = nil, **opts)
|
|
20
24
|
safe = Parse::API::PathSegment.file!(fileName, kind: "file name")
|
|
21
25
|
headers = {}
|
|
22
26
|
headers.merge!({ Parse::Protocol::CONTENT_TYPE => content_type.to_s }) if content_type.present?
|
|
23
|
-
response = request :post, "#{FILES_PATH}/#{safe}", body: data, headers: headers
|
|
27
|
+
response = request :post, "#{FILES_PATH}/#{safe}", body: data, headers: headers, opts: opts
|
|
24
28
|
response.parse_class = Parse::Model::TYPE_FILE
|
|
25
29
|
response
|
|
26
30
|
end
|
data/lib/parse/api/push.rb
CHANGED
|
@@ -8,12 +8,29 @@ module Parse
|
|
|
8
8
|
# @!visibility private
|
|
9
9
|
PUSH_PATH = "push"
|
|
10
10
|
|
|
11
|
-
#
|
|
12
|
-
#
|
|
11
|
+
# Send a Push notification.
|
|
12
|
+
#
|
|
13
|
+
# Parse Server's `POST /parse/push` endpoint is master-key-only —
|
|
14
|
+
# there is no session-token authorization model for sending pushes,
|
|
15
|
+
# and a no-master-key client cannot use this method. Calling it
|
|
16
|
+
# without master-key credentials returns HTTP 403 from the server;
|
|
17
|
+
# this guard fails closed in the SDK so the deployment's
|
|
18
|
+
# configuration isn't the only line of defense.
|
|
19
|
+
#
|
|
20
|
+
# @param payload [Hash] the payload for the Push notification.
|
|
21
|
+
# @param headers [Hash] additional HTTP headers to send with the request.
|
|
22
|
+
# @param opts [Hash] additional options to pass to the {Parse::Client} request.
|
|
13
23
|
# @return [Parse::Response]
|
|
24
|
+
# @raise [Parse::Error::AuthenticationError] when the client has no master key configured.
|
|
14
25
|
# @see http://docs.parseplatform.org/rest/guide/#sending-pushes Sending Pushes
|
|
15
|
-
def push(payload = {})
|
|
16
|
-
|
|
26
|
+
def push(payload = {}, headers: {}, **opts)
|
|
27
|
+
unless master_key.is_a?(String) && !master_key.empty?
|
|
28
|
+
raise Parse::Error::AuthenticationError,
|
|
29
|
+
"Parse::API::Push#push requires a master key — push notifications " \
|
|
30
|
+
"have no session-token authorization model in Parse Server"
|
|
31
|
+
end
|
|
32
|
+
opts[:use_master_key] = true unless opts.key?(:use_master_key)
|
|
33
|
+
request :post, PUSH_PATH, body: payload.as_json, headers: headers, opts: opts
|
|
17
34
|
end
|
|
18
35
|
end
|
|
19
36
|
end
|
data/lib/parse/api/server.rb
CHANGED
|
@@ -14,6 +14,16 @@ module Parse
|
|
|
14
14
|
SERVER_INFO_PATH = "serverInfo"
|
|
15
15
|
# @!visibility private
|
|
16
16
|
SERVER_HEALTH_PATH = "health"
|
|
17
|
+
|
|
18
|
+
# Minimum supported Parse Server major version. Below this floor the
|
|
19
|
+
# SDK emits a one-shot deprecation warning per client instance the
|
|
20
|
+
# first time `server_info` resolves. The threshold tracks "current
|
|
21
|
+
# major minus two" against Parse Server's release cadence — Parse
|
|
22
|
+
# Server 9.x is current in 2026, so anything below 7.0 is flagged.
|
|
23
|
+
# Override with `PARSE_DEPRECATED_SERVER_VERSION_BELOW=6.0` or
|
|
24
|
+
# silence entirely with `PARSE_SUPPRESS_SERVER_VERSION_WARNING=true`.
|
|
25
|
+
DEPRECATED_SERVER_VERSION_BELOW = "7.0.0"
|
|
26
|
+
|
|
17
27
|
# Fetch and cache information about the Parse server configuration. This
|
|
18
28
|
# hash contains information specifically to the configuration of the running
|
|
19
29
|
# parse server.
|
|
@@ -23,6 +33,8 @@ module Parse
|
|
|
23
33
|
response = request :get, SERVER_INFO_PATH
|
|
24
34
|
@server_info = response.error? ? nil :
|
|
25
35
|
response.result.with_indifferent_access
|
|
36
|
+
warn_if_deprecated_server_version! if @server_info
|
|
37
|
+
@server_info
|
|
26
38
|
end
|
|
27
39
|
|
|
28
40
|
# Fetches the status of the server based on the health check.
|
|
@@ -37,6 +49,7 @@ module Parse
|
|
|
37
49
|
# @return [Hash] a hash containing server configuration if available.
|
|
38
50
|
def server_info!
|
|
39
51
|
@server_info = nil
|
|
52
|
+
@server_version_warned = false
|
|
40
53
|
server_info
|
|
41
54
|
end
|
|
42
55
|
|
|
@@ -45,6 +58,52 @@ module Parse
|
|
|
45
58
|
def server_version
|
|
46
59
|
server_info.present? ? @server_info[:parseServerVersion] : nil
|
|
47
60
|
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# One-shot deprecation warning. The check runs once per client
|
|
65
|
+
# instance (`@server_version_warned` latches), after `server_info`
|
|
66
|
+
# has actually resolved against the wire — so unit tests that
|
|
67
|
+
# never reach a real server don't pay the cost. Silenceable via
|
|
68
|
+
# ENV so operators on a known-old Parse Server pinned for an
|
|
69
|
+
# explicit reason can suppress the noise.
|
|
70
|
+
def warn_if_deprecated_server_version!
|
|
71
|
+
return if @server_version_warned
|
|
72
|
+
return if ENV["PARSE_SUPPRESS_SERVER_VERSION_WARNING"] == "true"
|
|
73
|
+
return unless defined?(Parse) && (!Parse.respond_to?(:suppress_server_version_warning?) || !Parse.suppress_server_version_warning?)
|
|
74
|
+
version_string = @server_info[:parseServerVersion].to_s
|
|
75
|
+
return if version_string.empty?
|
|
76
|
+
floor = ENV["PARSE_DEPRECATED_SERVER_VERSION_BELOW"].to_s
|
|
77
|
+
floor = DEPRECATED_SERVER_VERSION_BELOW if floor.empty?
|
|
78
|
+
return unless server_version_below?(version_string, floor)
|
|
79
|
+
@server_version_warned = true
|
|
80
|
+
message = "[Parse::Client] DEPRECATION: connected Parse Server version #{version_string} " \
|
|
81
|
+
"is below the supported floor #{floor}. Newer Parse Stack releases assume " \
|
|
82
|
+
"behaviors (CLP shape, aggregate envelope, $vectorSearch, schema endpoints) " \
|
|
83
|
+
"that may not be present on this server. Upgrade Parse Server, or silence " \
|
|
84
|
+
"with Parse.suppress_server_version_warning = true / " \
|
|
85
|
+
"PARSE_SUPPRESS_SERVER_VERSION_WARNING=true. Override the floor with " \
|
|
86
|
+
"PARSE_DEPRECATED_SERVER_VERSION_BELOW=#{floor.sub(/\.0\.0\z/, ".0")}."
|
|
87
|
+
if defined?(Parse) && Parse.respond_to?(:logger) && Parse.logger
|
|
88
|
+
Parse.logger.warn(message)
|
|
89
|
+
else
|
|
90
|
+
warn message
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Loose semver compare on major.minor. Parse Server publishes
|
|
95
|
+
# `parseServerVersion` as `"9.0.0"`, `"6.5.7"`, `"8.0.0-alpha.1"`,
|
|
96
|
+
# etc. We only care about the major (and minor as a tiebreak) for
|
|
97
|
+
# the deprecation gate. Falls back to "not below" on any
|
|
98
|
+
# unparseable input so a wire-format surprise never raises.
|
|
99
|
+
def server_version_below?(actual, floor)
|
|
100
|
+
actual_parts = actual.scan(/\d+/).first(2).map(&:to_i)
|
|
101
|
+
floor_parts = floor.scan(/\d+/).first(2).map(&:to_i)
|
|
102
|
+
return false if actual_parts.empty? || floor_parts.empty?
|
|
103
|
+
actual_parts << 0 while actual_parts.length < 2
|
|
104
|
+
floor_parts << 0 while floor_parts.length < 2
|
|
105
|
+
(actual_parts <=> floor_parts) < 0
|
|
106
|
+
end
|
|
48
107
|
end
|
|
49
108
|
end
|
|
50
109
|
end
|
data/lib/parse/api/users.rb
CHANGED
|
@@ -37,6 +37,7 @@ module Parse
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
# Find user matching this active session token.
|
|
40
|
+
# @param session_token [String] the Parse user session token to look up.
|
|
40
41
|
# @param opts [Hash] additional options to pass to the {Parse::Client} request.
|
|
41
42
|
# @param headers [Hash] additional HTTP headers to send with the request.
|
|
42
43
|
# @return [Parse::Response]
|
|
@@ -69,7 +70,7 @@ module Parse
|
|
|
69
70
|
# @param headers [Hash] additional HTTP headers to send with the request.
|
|
70
71
|
# @return [Parse::Response]
|
|
71
72
|
def update_user(id, body = {}, headers: {}, **opts)
|
|
72
|
-
response = request :put, "#{USER_PATH_PREFIX}/#{id}", body: body, opts: opts
|
|
73
|
+
response = request :put, "#{USER_PATH_PREFIX}/#{id}", body: body, headers: headers, opts: opts
|
|
73
74
|
response.parse_class = Parse::Model::CLASS_USER
|
|
74
75
|
response
|
|
75
76
|
end
|
|
@@ -97,13 +98,36 @@ module Parse
|
|
|
97
98
|
end
|
|
98
99
|
|
|
99
100
|
# Request a password reset for a registered email.
|
|
101
|
+
#
|
|
102
|
+
# Client-side rate limited on a per-email basis using the same
|
|
103
|
+
# tracker that backs {#login} (entries are namespaced under a
|
|
104
|
+
# +pwreset:+ prefix so the two limiters don't collide on usernames
|
|
105
|
+
# that happen to equal an email). Every request counts toward the
|
|
106
|
+
# backoff — Parse Server's +requestPasswordReset+ response does
|
|
107
|
+
# not differentiate "email exists" from "email does not exist"
|
|
108
|
+
# (and rightly so, to avoid account enumeration), so the SDK
|
|
109
|
+
# cannot distinguish a legitimate retry from an attacker probing
|
|
110
|
+
# for valid emails. The cap mirrors {LOGIN_MAX_FAILURES}: 5
|
|
111
|
+
# requests within the rolling window before exponential backoff
|
|
112
|
+
# kicks in and the limit clears via the same TTL-based cleanup.
|
|
113
|
+
#
|
|
100
114
|
# @param email [String] the Parse user email.
|
|
101
115
|
# @param opts [Hash] additional options to pass to the {Parse::Client} request.
|
|
102
116
|
# @param headers [Hash] additional HTTP headers to send with the request.
|
|
117
|
+
# @raise [RuntimeError] when the per-email request rate is exceeded.
|
|
103
118
|
# @return [Parse::Response]
|
|
104
119
|
def request_password_reset(email, headers: {}, **opts)
|
|
120
|
+
rate_key = "pwreset:#{email}"
|
|
121
|
+
check_login_rate_limit!(rate_key)
|
|
105
122
|
body = { email: email }
|
|
106
|
-
request :post, REQUEST_PASSWORD_RESET, body: body, opts: opts, headers: headers
|
|
123
|
+
response = request :post, REQUEST_PASSWORD_RESET, body: body, opts: opts, headers: headers
|
|
124
|
+
# Always count the attempt as a "failure" for backoff purposes:
|
|
125
|
+
# the response body is intentionally indistinguishable across
|
|
126
|
+
# found/not-found emails, so we cannot reset the counter on
|
|
127
|
+
# "success" without leaking that distinction to an attacker who
|
|
128
|
+
# is probing.
|
|
129
|
+
track_login_attempt(rate_key, false)
|
|
130
|
+
response
|
|
107
131
|
end
|
|
108
132
|
|
|
109
133
|
# Login a user. Implements client-side rate limiting with exponential
|
|
@@ -101,6 +101,74 @@ module Parse
|
|
|
101
101
|
indexes.find { |idx| idx["name"] == index_name }
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
+
# List only `type: "search"` indexes (lexical / BM25). Filters the
|
|
105
|
+
# raw {.list_indexes} output. `$listSearchIndexes` returns both
|
|
106
|
+
# search and vectorSearch index types on the same collection;
|
|
107
|
+
# callers building lexical pipelines should consume this view.
|
|
108
|
+
# Indexes with an absent `type` field default to `"search"` for
|
|
109
|
+
# compatibility with older Atlas Search releases that pre-date the
|
|
110
|
+
# vector-search index type.
|
|
111
|
+
#
|
|
112
|
+
# @param collection_name [String] the Parse collection name
|
|
113
|
+
# @return [Array<Hash>] search indexes only
|
|
114
|
+
def list_search_indexes(collection_name)
|
|
115
|
+
list_indexes(collection_name).select do |idx|
|
|
116
|
+
type = (idx["type"] || idx[:type] || "search").to_s
|
|
117
|
+
type == "search"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# List only `type: "vectorSearch"` indexes. Filters the raw
|
|
122
|
+
# {.list_indexes} output. Vector-search indexes are the only ones
|
|
123
|
+
# eligible for the `$vectorSearch` aggregation stage; routing a
|
|
124
|
+
# lexical-index name into vector search (or vice versa) fails at
|
|
125
|
+
# the Atlas Search node with a non-obvious error, so the SDK
|
|
126
|
+
# filters at lookup time.
|
|
127
|
+
#
|
|
128
|
+
# @param collection_name [String] the Parse collection name
|
|
129
|
+
# @return [Array<Hash>] vector-search indexes only
|
|
130
|
+
def list_vector_indexes(collection_name)
|
|
131
|
+
list_indexes(collection_name).select do |idx|
|
|
132
|
+
(idx["type"] || idx[:type]).to_s == "vectorSearch"
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Find the vector-search index that covers a given field path on a
|
|
137
|
+
# collection. Walks `latestDefinition.fields[]` looking for an
|
|
138
|
+
# entry whose `type` is `"vector"` and whose `path` matches
|
|
139
|
+
# `field`. Returns the first matching index, or nil if none.
|
|
140
|
+
#
|
|
141
|
+
# The shape this matches is the documented Atlas vectorSearch
|
|
142
|
+
# index definition:
|
|
143
|
+
#
|
|
144
|
+
# {
|
|
145
|
+
# "name" => "Movie_embedding_..._idx",
|
|
146
|
+
# "type" => "vectorSearch",
|
|
147
|
+
# "latestDefinition" => {
|
|
148
|
+
# "fields" => [
|
|
149
|
+
# { "type" => "vector", "path" => "embedding",
|
|
150
|
+
# "numDimensions" => 1536, "similarity" => "cosine" },
|
|
151
|
+
# { "type" => "filter", "path" => "wiki_id" }
|
|
152
|
+
# ]
|
|
153
|
+
# }
|
|
154
|
+
# }
|
|
155
|
+
#
|
|
156
|
+
# @param collection_name [String]
|
|
157
|
+
# @param field [String, Symbol] the vector field path
|
|
158
|
+
# @return [Hash, nil] the index definition or nil
|
|
159
|
+
def find_vector_index(collection_name, field:)
|
|
160
|
+
field_str = field.to_s
|
|
161
|
+
list_vector_indexes(collection_name).find do |idx|
|
|
162
|
+
defn = idx["latestDefinition"] || idx[:latestDefinition] || {}
|
|
163
|
+
fields = defn["fields"] || defn[:fields] || []
|
|
164
|
+
fields.any? do |f|
|
|
165
|
+
type = (f["type"] || f[:type]).to_s
|
|
166
|
+
path = (f["path"] || f[:path]).to_s
|
|
167
|
+
type == "vector" && path == field_str
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
104
172
|
# Validate that an index exists and is ready
|
|
105
173
|
# @param collection_name [String] the Parse collection name
|
|
106
174
|
# @param index_name [String] the index name to validate
|
|
@@ -349,5 +417,21 @@ module Parse
|
|
|
349
417
|
end
|
|
350
418
|
end
|
|
351
419
|
end
|
|
420
|
+
|
|
421
|
+
# Alias for {IndexManager} exposing the type-aware lookup surface
|
|
422
|
+
# ({IndexManager.list_search_indexes},
|
|
423
|
+
# {IndexManager.list_vector_indexes},
|
|
424
|
+
# {IndexManager.find_vector_index}). Backed by the same module, so
|
|
425
|
+
# the cache, mutex, and TTL are shared across both names — calling
|
|
426
|
+
# `IndexCatalog.clear_cache` and `IndexManager.clear_cache`
|
|
427
|
+
# invalidates the same store.
|
|
428
|
+
#
|
|
429
|
+
# @example Discover the vector index covering Movie.embedding
|
|
430
|
+
# idx = Parse::AtlasSearch::IndexCatalog.find_vector_index(
|
|
431
|
+
# "Movie", field: "embedding"
|
|
432
|
+
# )
|
|
433
|
+
# idx&.dig("name")
|
|
434
|
+
# # => "Movie_embedding_openai_text_embedding_ada_002_1536_idx"
|
|
435
|
+
IndexCatalog = IndexManager
|
|
352
436
|
end
|
|
353
437
|
end
|