parse-stack-next 5.5.6 → 5.7.0

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 (119) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +634 -0
  3. data/README.md +378 -6
  4. data/docs/caching.md +748 -0
  5. data/examples/basic_client.rb +3 -3
  6. data/examples/basic_server.rb +3 -3
  7. data/examples/live_query_listener.rb +12 -12
  8. data/examples/rag_chatbot.rb +14 -14
  9. data/examples/transaction_example.rb +44 -45
  10. data/examples/webhook_server.rb +3 -3
  11. data/lib/parse/access.rb +493 -0
  12. data/lib/parse/acl_scope.rb +125 -52
  13. data/lib/parse/agent/approval_gate.rb +0 -0
  14. data/lib/parse/agent/cancellation_token.rb +3 -3
  15. data/lib/parse/agent/constraint_translator.rb +4 -4
  16. data/lib/parse/agent/describe.rb +34 -34
  17. data/lib/parse/agent/errors.rb +9 -9
  18. data/lib/parse/agent/mcp_client.rb +61 -59
  19. data/lib/parse/agent/mcp_dispatcher.rb +89 -101
  20. data/lib/parse/agent/mcp_rack_app.rb +92 -93
  21. data/lib/parse/agent/mcp_server.rb +1 -1
  22. data/lib/parse/agent/mcp_subscriptions.rb +22 -22
  23. data/lib/parse/agent/metadata_audit.rb +1 -2
  24. data/lib/parse/agent/metadata_dsl.rb +17 -7
  25. data/lib/parse/agent/metadata_registry.rb +12 -12
  26. data/lib/parse/agent/prompt_hardening.rb +4 -4
  27. data/lib/parse/agent/prompts.rb +10 -10
  28. data/lib/parse/agent/result_formatter.rb +4 -4
  29. data/lib/parse/agent/tools.rb +493 -400
  30. data/lib/parse/agent.rb +157 -116
  31. data/lib/parse/api/hooks.rb +1 -1
  32. data/lib/parse/api/server.rb +2 -2
  33. data/lib/parse/api/users.rb +2 -2
  34. data/lib/parse/atlas_search/index_manager.rb +1 -1
  35. data/lib/parse/atlas_search/session.rb +40 -218
  36. data/lib/parse/atlas_search.rb +181 -60
  37. data/lib/parse/authorization.rb +466 -0
  38. data/lib/parse/cache/invalidation.rb +167 -0
  39. data/lib/parse/cache/keyspace.rb +306 -0
  40. data/lib/parse/cache/moneta_surface.rb +126 -0
  41. data/lib/parse/cache/pool.rb +48 -5
  42. data/lib/parse/cache/redis.rb +425 -10
  43. data/lib/parse/cache/scoped_view.rb +443 -0
  44. data/lib/parse/cache/sub_cache.rb +264 -0
  45. data/lib/parse/cache/upstream_roles.rb +230 -0
  46. data/lib/parse/client/authentication.rb +1 -1
  47. data/lib/parse/client/body_builder.rb +1 -2
  48. data/lib/parse/client/caching.rb +84 -5
  49. data/lib/parse/client.rb +327 -189
  50. data/lib/parse/clp_scope.rb +225 -28
  51. data/lib/parse/console.rb +3 -3
  52. data/lib/parse/embeddings/batch_embedder.rb +3 -3
  53. data/lib/parse/embeddings/binding_audit.rb +211 -0
  54. data/lib/parse/embeddings/cache.rb +17 -17
  55. data/lib/parse/embeddings/cohere.rb +21 -22
  56. data/lib/parse/embeddings/image_fetch.rb +22 -21
  57. data/lib/parse/embeddings/jina.rb +21 -21
  58. data/lib/parse/embeddings/local_http.rb +6 -7
  59. data/lib/parse/embeddings/media_file.rb +137 -0
  60. data/lib/parse/embeddings/openai.rb +5 -5
  61. data/lib/parse/embeddings/provider.rb +40 -1
  62. data/lib/parse/embeddings/qwen.rb +10 -10
  63. data/lib/parse/embeddings/spend_cap.rb +2 -3
  64. data/lib/parse/embeddings/streaming_body.rb +170 -0
  65. data/lib/parse/embeddings/video_source.rb +121 -0
  66. data/lib/parse/embeddings/voyage.rb +554 -143
  67. data/lib/parse/embeddings.rb +83 -27
  68. data/lib/parse/graphql/type_generator.rb +1 -1
  69. data/lib/parse/graphql.rb +5 -5
  70. data/lib/parse/live_query/client.rb +1 -1
  71. data/lib/parse/live_query.rb +1 -1
  72. data/lib/parse/lock.rb +15 -18
  73. data/lib/parse/lock_backend.rb +1 -1
  74. data/lib/parse/lookup_rewriter.rb +6 -6
  75. data/lib/parse/model/classes/role.rb +227 -28
  76. data/lib/parse/model/classes/user.rb +60 -1
  77. data/lib/parse/model/clp.rb +4 -4
  78. data/lib/parse/model/core/actions.rb +97 -15
  79. data/lib/parse/model/core/create_lock.rb +0 -2
  80. data/lib/parse/model/core/describe.rb +57 -57
  81. data/lib/parse/model/core/embed_managed.rb +43 -19
  82. data/lib/parse/model/core/fetching.rb +2 -0
  83. data/lib/parse/model/core/indexing.rb +14 -14
  84. data/lib/parse/model/core/parse_reference.rb +1 -1
  85. data/lib/parse/model/core/properties.rb +39 -3
  86. data/lib/parse/model/core/querying.rb +1 -1
  87. data/lib/parse/model/core/schema.rb +2 -2
  88. data/lib/parse/model/core/search_indexing.rb +2 -2
  89. data/lib/parse/model/core/vector_searchable.rb +34 -8
  90. data/lib/parse/model/file.rb +21 -15
  91. data/lib/parse/model/geojson.rb +2 -2
  92. data/lib/parse/model/geopoint.rb +1 -0
  93. data/lib/parse/model/object.rb +132 -66
  94. data/lib/parse/model/pointer.rb +2 -0
  95. data/lib/parse/model/polygon.rb +3 -6
  96. data/lib/parse/model/push.rb +2 -2
  97. data/lib/parse/model/vector.rb +3 -1
  98. data/lib/parse/mongodb.rb +512 -282
  99. data/lib/parse/pipeline_security.rb +3 -0
  100. data/lib/parse/query/constraints.rb +46 -46
  101. data/lib/parse/query.rb +146 -59
  102. data/lib/parse/retrieval/agent_tool.rb +34 -20
  103. data/lib/parse/retrieval/chunk.rb +1 -0
  104. data/lib/parse/retrieval/reranker/cohere.rb +11 -11
  105. data/lib/parse/retrieval/reranker.rb +3 -4
  106. data/lib/parse/retrieval/retriever.rb +4 -6
  107. data/lib/parse/schema/index_migrator.rb +26 -26
  108. data/lib/parse/schema/search_index_migrator.rb +19 -19
  109. data/lib/parse/stack/tasks.rb +6 -6
  110. data/lib/parse/stack/version.rb +1 -1
  111. data/lib/parse/stack.rb +10 -10
  112. data/lib/parse/vector_search/hybrid.rb +166 -33
  113. data/lib/parse/vector_search.rb +141 -13
  114. data/lib/parse/webhooks/payload.rb +1 -0
  115. data/lib/parse/webhooks/registration.rb +4 -4
  116. data/lib/parse/webhooks/trigger_audit.rb +50 -49
  117. data/lib/parse/webhooks.rb +22 -4
  118. data/parse-stack-next.gemspec +6 -6
  119. metadata +14 -1
data/lib/parse/agent.rb CHANGED
@@ -714,7 +714,7 @@ module Parse
714
714
  # only WRITE_TOOLS leaves the raw tools off, so a deployment can
715
715
  # permit "set_client_description" (an agent_method) while keeping
716
716
  # "create_object" disabled.
717
- WRITE_GATED_TOOLS = %i[create_object update_object delete_object].freeze
717
+ WRITE_GATED_TOOLS = %i[create_object update_object delete_object].freeze
718
718
  SCHEMA_GATED_TOOLS = %i[create_class delete_class].freeze
719
719
 
720
720
  # Built-in tools that are safe to dispatch when the agent runs on a
@@ -1024,6 +1024,7 @@ module Parse
1024
1024
  def approval_gate
1025
1025
  @approval_gate ||= Parse::Agent::NullGate.new
1026
1026
  end
1027
+
1027
1028
  attr_writer :approval_gate
1028
1029
 
1029
1030
  # @return [Boolean] true if the active cancellation token has been
@@ -1084,15 +1085,38 @@ module Parse
1084
1085
  #
1085
1086
  # @return [Hash]
1086
1087
  def acl_scope_kwargs
1087
- if @session_token && !@session_token.to_s.empty?
1088
- { session_token: @session_token }
1089
- elsif @acl_user_scope
1090
- { acl_user: @acl_user_scope }
1091
- elsif @acl_role_scope
1092
- { acl_role: @acl_role_scope }
1093
- else
1094
- { master: true }
1095
- end
1088
+ scope = if @session_token && !@session_token.to_s.empty?
1089
+ { session_token: @session_token }
1090
+ elsif @acl_user_scope
1091
+ { acl_user: @acl_user_scope }
1092
+ elsif @acl_role_scope
1093
+ { acl_role: @acl_role_scope }
1094
+ else
1095
+ { master: true }
1096
+ end
1097
+ scope
1098
+ end
1099
+
1100
+ # {#acl_scope_kwargs} plus the agent's own client.
1101
+ #
1102
+ # Separate from {#acl_scope_kwargs} rather than folded into it, because
1103
+ # that hash is public and documented for `agent_method` bodies to splat
1104
+ # into calls of their own. Those calls can be anything, including REST
1105
+ # paths and user-written helpers, and a stray `client:` would be an
1106
+ # unknown-keyword error there. Only the direct paths that understand
1107
+ # `client:` as an auth kwarg use this variant.
1108
+ #
1109
+ # It matters because an agent built on a secondary client would otherwise
1110
+ # resolve its token, walk its role graph, and read its rows against the
1111
+ # default application. Consumers are `Parse::MongoDB.aggregate`, the
1112
+ # `Parse::Query` direct methods, and `Parse::AtlasSearch`'s own scope
1113
+ # resolution, all of which consume `client:` before it reaches a driver.
1114
+ #
1115
+ # @return [Hash]
1116
+ def direct_auth_kwargs
1117
+ kwargs = acl_scope_kwargs
1118
+ kwargs = kwargs.merge(client: @client) if @client
1119
+ kwargs
1096
1120
  end
1097
1121
 
1098
1122
  # The agent's resolved identity claim set — the
@@ -1201,11 +1225,10 @@ module Parse
1201
1225
  def refresh_scope!
1202
1226
  return @acl_scope if @session_token
1203
1227
  return nil if @acl_user_scope.nil? && @acl_role_scope.nil?
1204
- resolved =
1205
- if @acl_user_scope
1206
- Parse::ACLScope.resolve_for_user(@acl_user_scope)
1228
+ resolved = if @acl_user_scope
1229
+ Parse::ACLScope.resolve_for_user(@acl_user_scope, client: @client)
1207
1230
  else
1208
- Parse::ACLScope.resolve_for_role(@acl_role_scope)
1231
+ Parse::ACLScope.resolve_for_role(@acl_role_scope, client: @client)
1209
1232
  end
1210
1233
  @acl_scope = resolved&.freeze
1211
1234
  @auth_context = nil # invalidate memoized auth_context — user_id may have changed
@@ -1232,16 +1255,16 @@ module Parse
1232
1255
  # @return [self]
1233
1256
  def impersonate(user, mint: false, label: nil)
1234
1257
  token = resolve_impersonation_token!(user, mint: mint)
1235
- @session_token = token
1258
+ @session_token = token
1236
1259
  @acl_user_scope = nil
1237
1260
  @acl_role_scope = nil
1238
1261
  @impersonation_label = sanitize_impersonation_label(label) if label
1239
1262
  # Drop memoized scope/auth so the next call resolves under the new
1240
1263
  # token (session-token validity is checked per-call by Parse Server).
1241
- @acl_scope = nil
1264
+ @acl_scope = nil
1242
1265
  @auth_context = nil
1243
1266
  no_master_key = @client.respond_to?(:master_key) && @client.master_key.nil?
1244
- @client_mode = no_master_key && !@session_token.to_s.empty?
1267
+ @client_mode = no_master_key && !@session_token.to_s.empty?
1245
1268
  self
1246
1269
  end
1247
1270
 
@@ -1481,11 +1504,16 @@ module Parse
1481
1504
  # end,
1482
1505
  # )
1483
1506
  #
1507
+ # Default for the `client:` keyword, distinguishing "not specified" from
1508
+ # an explicit `client: :default`. A plain `:default` cannot do that job:
1509
+ # it is also a legitimate value a caller may pass on purpose.
1510
+ INHERIT_CLIENT = Object.new.freeze
1511
+
1484
1512
  def initialize(permissions: :readonly, session_token: nil,
1485
1513
  acl_user: nil, acl_role: nil,
1486
1514
  impersonate_user: nil, impersonate_mint: false,
1487
1515
  impersonation_label: nil,
1488
- client: :default,
1516
+ client: INHERIT_CLIENT,
1489
1517
  tenant_id: nil,
1490
1518
  rate_limit: DEFAULT_RATE_LIMIT, rate_window: DEFAULT_RATE_WINDOW,
1491
1519
  rate_limiter: nil,
@@ -1503,9 +1531,9 @@ module Parse
1503
1531
  permission: nil,
1504
1532
  impersonation_user: nil, impersonation_mint: nil,
1505
1533
  impersonate_label: nil)
1506
- permissions = permission unless permission.nil?
1507
- impersonate_user ||= impersonation_user
1508
- impersonate_mint = impersonation_mint unless impersonation_mint.nil?
1534
+ permissions = permission unless permission.nil?
1535
+ impersonate_user ||= impersonation_user
1536
+ impersonate_mint = impersonation_mint unless impersonation_mint.nil?
1509
1537
  impersonation_label ||= impersonate_label
1510
1538
  # SECURITY: Mutually exclusive identity inputs. `acl_user:` and
1511
1539
  # `acl_role:` are unverified constructor assertions (the SDK does
@@ -1552,6 +1580,20 @@ module Parse
1552
1580
  end
1553
1581
 
1554
1582
  @permissions = permissions
1583
+ # A sub-agent inherits its parent's client unless one was named
1584
+ # explicitly. Falling back to the default meant a child of an agent
1585
+ # bound to a secondary application silently resolved and read against
1586
+ # the default one, and Parse::MongoDB's binding guard would see the
1587
+ # default and wave it through.
1588
+ #
1589
+ # The check is on {INHERIT_CLIENT}, not on `:default`. Those are not
1590
+ # the same thing: `Parse::Agent.new(parent: b_agent, client: :default)`
1591
+ # is an explicit instruction to use the default client, and treating it
1592
+ # as "unspecified" would silently substitute the parent's and ignore
1593
+ # what the caller asked for.
1594
+ if client.equal?(INHERIT_CLIENT)
1595
+ client = (parent.client if parent.respond_to?(:client) && parent.client) || :default
1596
+ end
1555
1597
  @client = client.is_a?(Parse::Client) ? client : Parse::Client.client(client)
1556
1598
  @operation_log = []
1557
1599
  @max_log_size = max_log_size
@@ -1586,8 +1628,8 @@ module Parse
1586
1628
  raise RecursionLimitExceeded.new(depth: parent.recursion_depth)
1587
1629
  end
1588
1630
  @recursion_depth = inherited_depth
1589
- @agent_depth = parent.agent_depth + 1
1590
- rate_limiter ||= parent.rate_limiter
1631
+ @agent_depth = parent.agent_depth + 1
1632
+ rate_limiter ||= parent.rate_limiter
1591
1633
  @parent_agent_id = parent.agent_id
1592
1634
  @inherited_correlation_id = parent.correlation_id
1593
1635
 
@@ -1611,10 +1653,12 @@ module Parse
1611
1653
  # produces a `:readonly` sub-agent — the safe default. To
1612
1654
  # maintain parity at the call site, pass `permissions:
1613
1655
  # parent.permissions`; the clamp check below validates that the
1614
- # resolved tier does not exceed the parent's. `client:` is also
1615
- # not inherited; its constructor default `:default` resolves to
1616
- # the same client the parent uses in standard single-app
1617
- # deployments.
1656
+ # resolved tier does not exceed the parent's. `client:` IS
1657
+ # inherited, unlike permissions: a sub-agent that quietly resolved
1658
+ # and read against the default application while its parent was
1659
+ # bound to another is a correctness bug, not a safe default, and
1660
+ # there is no security argument for narrowing which application a
1661
+ # child addresses.
1618
1662
  # Inherit auth scope from the parent only when the child supplied
1619
1663
  # NO identity at all. Three reasons:
1620
1664
  #
@@ -1674,7 +1718,7 @@ module Parse
1674
1718
  # tools emit progress over the same SSE stream the parent's
1675
1719
  # client is observing.
1676
1720
  @cancellation_token = parent.cancellation_token
1677
- @progress_callback = parent.progress_callback
1721
+ @progress_callback = parent.progress_callback
1678
1722
 
1679
1723
  # Clamp the sub-agent's permission tier at the parent's. The
1680
1724
  # default :readonly is always ≤ any parent tier, so this fires
@@ -1684,7 +1728,7 @@ module Parse
1684
1728
  # permissions: :admin)` and silently elevate above what the
1685
1729
  # parent's session was scoped to do.
1686
1730
  parent_tier = PERMISSION_HIERARCHY[parent.permissions] || 0
1687
- child_tier = PERMISSION_HIERARCHY[permissions] || 0
1731
+ child_tier = PERMISSION_HIERARCHY[permissions] || 0
1688
1732
  if child_tier > parent_tier
1689
1733
  raise ArgumentError,
1690
1734
  "sub-agent permissions: #{permissions.inspect} exceeds parent's " \
@@ -1695,7 +1739,7 @@ module Parse
1695
1739
  end
1696
1740
  else
1697
1741
  @recursion_depth = (recursion_depth || Parse::Agent.default_recursion_depth).to_i
1698
- @agent_depth = 0
1742
+ @agent_depth = 0
1699
1743
  @parent_agent_id = nil
1700
1744
  @inherited_correlation_id = nil
1701
1745
  end
@@ -1707,7 +1751,7 @@ module Parse
1707
1751
  # session-token path (client-mode detection, eager scope
1708
1752
  # resolution, request routing) unchanged. Fail-closed: raises when
1709
1753
  # the client has no master key or no session can be resolved.
1710
- @impersonation_label = sanitize_impersonation_label(impersonation_label)
1754
+ @impersonation_label = sanitize_impersonation_label(impersonation_label)
1711
1755
  @impersonated_user_id = nil
1712
1756
  if impersonate_user
1713
1757
  session_token = resolve_impersonation_token!(impersonate_user, mint: impersonate_mint)
@@ -1717,11 +1761,11 @@ module Parse
1717
1761
  # above resolves before the ivars are set. Without this ordering,
1718
1762
  # `@session_token = session_token` would assign the constructor's
1719
1763
  # nil default, and the inheritance would be a no-op.
1720
- @session_token = session_token
1721
- @acl_user_scope = acl_user
1722
- @acl_role_scope = acl_role
1723
- @tenant_id = tenant_id
1724
- @master_atlas = master_atlas == true
1764
+ @session_token = session_token
1765
+ @acl_user_scope = acl_user
1766
+ @acl_role_scope = acl_role
1767
+ @tenant_id = tenant_id
1768
+ @master_atlas = master_atlas == true
1725
1769
 
1726
1770
  # Client-mode detection. An agent runs in CLIENT MODE when its
1727
1771
  # underlying Parse::Client has no master_key AND it was constructed
@@ -1772,8 +1816,7 @@ module Parse
1772
1816
  # the safe path (omit kwarg) is trivially correct.
1773
1817
  if parent
1774
1818
  parent_allows = parent.respond_to?(:allow_mutations?) ? parent.allow_mutations? : true
1775
- resolved_allow_mutations =
1776
- if allow_mutations.nil?
1819
+ resolved_allow_mutations = if allow_mutations.nil?
1777
1820
  parent_allows
1778
1821
  else
1779
1822
  allow_mutations == true
@@ -1787,8 +1830,7 @@ module Parse
1787
1830
  end
1788
1831
  @allow_mutations = resolved_allow_mutations
1789
1832
  else
1790
- @allow_mutations =
1791
- if allow_mutations.nil?
1833
+ @allow_mutations = if allow_mutations.nil?
1792
1834
  !@client_mode
1793
1835
  else
1794
1836
  allow_mutations == true
@@ -1812,8 +1854,7 @@ module Parse
1812
1854
  # than at first tool call, and makes the subset check below
1813
1855
  # uniform across modes. Long-lived agents can re-resolve via
1814
1856
  # {#refresh_scope!}.
1815
- @acl_scope =
1816
- if @session_token
1857
+ @acl_scope = if @session_token
1817
1858
  # Best-effort eager resolution. If Parse Server's /users/me is
1818
1859
  # unreachable at construction time (network blip, test env, MCP
1819
1860
  # bootstrap-before-server-ready), leave @acl_scope nil and let
@@ -1824,15 +1865,15 @@ module Parse
1824
1865
  # per-call validation will surface auth errors at the
1825
1866
  # actual usage site where the operator can act on them.
1826
1867
  begin
1827
- opts = { session_token: @session_token }
1868
+ opts = { session_token: @session_token, client: @client }.compact
1828
1869
  Parse::ACLScope.resolve!(opts, method_name: :agent_init)
1829
1870
  rescue StandardError
1830
1871
  nil
1831
1872
  end
1832
1873
  elsif @acl_user_scope
1833
- Parse::ACLScope.resolve_for_user(@acl_user_scope)
1874
+ Parse::ACLScope.resolve_for_user(@acl_user_scope, client: @client)
1834
1875
  elsif @acl_role_scope
1835
- Parse::ACLScope.resolve_for_role(@acl_role_scope)
1876
+ Parse::ACLScope.resolve_for_role(@acl_role_scope, client: @client)
1836
1877
  else
1837
1878
  nil
1838
1879
  end
@@ -1948,10 +1989,10 @@ module Parse
1948
1989
  # Normalize the `tools:`, `methods:`, and `classes:` filters. Errors
1949
1990
  # raise ArgumentError (bad shape) or, when strict mode is on,
1950
1991
  # ArgumentError (unknown tool / class name).
1951
- @tool_filter_only, @tool_filter_except = normalize_tool_filter(tools)
1992
+ @tool_filter_only, @tool_filter_except = normalize_tool_filter(tools)
1952
1993
  @method_filter_only, @method_filter_except = normalize_method_filter(methods)
1953
- @class_filter_only, @class_filter_except = normalize_class_filter(classes)
1954
- @filters = normalize_query_filters(filters)
1994
+ @class_filter_only, @class_filter_except = normalize_class_filter(classes)
1995
+ @filters = normalize_query_filters(filters)
1955
1996
 
1956
1997
  # Sub-agent class-filter inheritance. Unlike `tools:` (which overrides
1957
1998
  # outright), `classes:` clamps to the parent's effective set so a
@@ -1960,7 +2001,7 @@ module Parse
1960
2001
  # parent's effective set raises at construction — empty-onlyset means
1961
2002
  # "address no classes," which is almost certainly a typo, not intent.
1962
2003
  if parent
1963
- parent_only = parent.instance_variable_get(:@class_filter_only)
2004
+ parent_only = parent.instance_variable_get(:@class_filter_only)
1964
2005
  parent_except = parent.instance_variable_get(:@class_filter_except)
1965
2006
  if parent_only && @class_filter_only
1966
2007
  intersection = Set.new(@class_filter_only) & parent_only
@@ -2107,9 +2148,9 @@ module Parse
2107
2148
  # @return [Array<Symbol>] list of allowed tool names
2108
2149
  def allowed_tools
2109
2150
  registered = Parse::Agent::Tools.registered_tools_for(@permissions)
2110
- permitted = (tier_builtin_set + registered).uniq
2151
+ permitted = (tier_builtin_set + registered).uniq
2111
2152
 
2112
- permitted = permitted & @tool_filter_only.to_a if @tool_filter_only
2153
+ permitted = permitted & @tool_filter_only.to_a if @tool_filter_only
2113
2154
  permitted = permitted - @tool_filter_except.to_a if @tool_filter_except
2114
2155
 
2115
2156
  if @client_mode
@@ -2160,7 +2201,7 @@ module Parse
2160
2201
  return false if @method_filter_only.nil? && @method_filter_except.nil?
2161
2202
 
2162
2203
  method_sym = method_name.to_sym
2163
- qualified = "#{class_name}.#{method_name}"
2204
+ qualified = "#{class_name}.#{method_name}"
2164
2205
 
2165
2206
  if @method_filter_only
2166
2207
  permitted = @method_filter_only.include?(method_sym) ||
@@ -2230,7 +2271,7 @@ module Parse
2230
2271
  # limit"). Borrow the configured limit/window when the injected
2231
2272
  # limiter exposes them; otherwise fall back to non-zero defaults.
2232
2273
  retry_after = (1.0 + rand * 4.0).round(2)
2233
- l = @rate_limiter.respond_to?(:limit) ? @rate_limiter.limit : RateLimiter::DEFAULT_LIMIT
2274
+ l = @rate_limiter.respond_to?(:limit) ? @rate_limiter.limit : RateLimiter::DEFAULT_LIMIT
2234
2275
  w = @rate_limiter.respond_to?(:window) ? @rate_limiter.window : RateLimiter::DEFAULT_WINDOW
2235
2276
  raise RateLimitExceeded.new(retry_after: retry_after, limit: l, window: w)
2236
2277
  end
@@ -2325,10 +2366,10 @@ module Parse
2325
2366
  !(Parse::Agent.write_tools_enabled? && Parse::Agent.raw_crud_enabled? && @allow_mutations)
2326
2367
  missing = []
2327
2368
  missing << "PARSE_AGENT_ALLOW_WRITE_TOOLS=true" unless Parse::Agent.write_tools_enabled?
2328
- missing << "PARSE_AGENT_ALLOW_RAW_CRUD=true" unless Parse::Agent.raw_crud_enabled?
2369
+ missing << "PARSE_AGENT_ALLOW_RAW_CRUD=true" unless Parse::Agent.raw_crud_enabled?
2329
2370
  missing << "allow_mutations: true (per-agent kwarg)" unless @allow_mutations
2330
2371
  return error_response(
2331
- "Raw CRUD tool '#{tool_name}' is disabled. Required: #{missing.join(' AND ')}. " \
2372
+ "Raw CRUD tool '#{tool_name}' is disabled. Required: #{missing.join(" AND ")}. " \
2332
2373
  "Prefer declaring an agent_method on the target class for an intent-based " \
2333
2374
  "write path that requires only PARSE_AGENT_ALLOW_WRITE_TOOLS.",
2334
2375
  error_code: :access_denied,
@@ -2340,7 +2381,7 @@ module Parse
2340
2381
  missing << "PARSE_AGENT_ALLOW_SCHEMA_OPS=true" unless Parse::Agent.schema_ops_enabled?
2341
2382
  missing << "PARSE_AGENT_ALLOW_RAW_SCHEMA=true" unless Parse::Agent.raw_schema_enabled?
2342
2383
  return error_response(
2343
- "Raw schema-mutating tool '#{tool_name}' is disabled. Required: #{missing.join(' AND ')}. " \
2384
+ "Raw schema-mutating tool '#{tool_name}' is disabled. Required: #{missing.join(" AND ")}. " \
2344
2385
  "These tools mutate the entire Parse schema; consider whether an explicit operator " \
2345
2386
  "process is a better fit than agent access.",
2346
2387
  error_code: :access_denied,
@@ -2356,7 +2397,7 @@ module Parse
2356
2397
  unless approval_tiers.empty?
2357
2398
  eff_perm = effective_permission_for(tool_name, kwargs)
2358
2399
  if approval_tiers.include?(eff_perm)
2359
- preview = build_approval_preview(tool_name, kwargs)
2400
+ preview = build_approval_preview(tool_name, kwargs)
2360
2401
  decision = approval_gate.review(
2361
2402
  tool_name: tool_name,
2362
2403
  effective_permission: eff_perm,
@@ -2365,9 +2406,9 @@ module Parse
2365
2406
  )
2366
2407
  unless decision.approved?
2367
2408
  return error_response(
2368
- decision.reason || "Operation '#{tool_name}' requires approval and was not approved.",
2369
- error_code: :approval_denied,
2370
- )
2409
+ decision.reason || "Operation '#{tool_name}' requires approval and was not approved.",
2410
+ error_code: :approval_denied,
2411
+ )
2371
2412
  end
2372
2413
  end
2373
2414
  end
@@ -2388,9 +2429,9 @@ module Parse
2388
2429
  agent_id: agent_id,
2389
2430
  agent_depth: @agent_depth,
2390
2431
  }
2391
- payload[:correlation_id] = @correlation_id if @correlation_id
2392
- payload[:parent_agent_id] = @parent_agent_id if @parent_agent_id
2393
- payload[:impersonation_label] = @impersonation_label if @impersonation_label
2432
+ payload[:correlation_id] = @correlation_id if @correlation_id
2433
+ payload[:parent_agent_id] = @parent_agent_id if @parent_agent_id
2434
+ payload[:impersonation_label] = @impersonation_label if @impersonation_label
2394
2435
  payload[:impersonated_user_id] = @impersonated_user_id if @impersonated_user_id
2395
2436
 
2396
2437
  # Audit surface — narrowing filters in effect for this call. SOC and
@@ -2400,12 +2441,12 @@ module Parse
2400
2441
  # the underlying frozen Sets) for stable JSON serialization. Omitted
2401
2442
  # entirely when no filter was declared so the payload stays minimal
2402
2443
  # for the common unscoped-agent case.
2403
- payload[:classes_only] = @class_filter_only.to_a.sort if @class_filter_only
2404
- payload[:classes_except] = @class_filter_except.to_a.sort if @class_filter_except
2405
- payload[:tools_only] = @tool_filter_only.to_a.sort if @tool_filter_only
2406
- payload[:tools_except] = @tool_filter_except.to_a.sort if @tool_filter_except
2407
- payload[:methods_only] = @method_filter_only.to_a.map(&:to_s).sort if @method_filter_only
2408
- payload[:methods_except] = @method_filter_except.to_a.map(&:to_s).sort if @method_filter_except
2444
+ payload[:classes_only] = @class_filter_only.to_a.sort if @class_filter_only
2445
+ payload[:classes_except] = @class_filter_except.to_a.sort if @class_filter_except
2446
+ payload[:tools_only] = @tool_filter_only.to_a.sort if @tool_filter_only
2447
+ payload[:tools_except] = @tool_filter_except.to_a.sort if @tool_filter_except
2448
+ payload[:methods_only] = @method_filter_only.to_a.map(&:to_s).sort if @method_filter_only
2449
+ payload[:methods_except] = @method_filter_except.to_a.map(&:to_s).sort if @method_filter_except
2409
2450
  # Per-agent per-class filters — emit class-name → field-name list,
2410
2451
  # NOT the constraint values. Filter values can contain user-identifying
2411
2452
  # data (`{ user_id: "abc123" }`, `{ org_id: tenant_uuid }`) that
@@ -2428,7 +2469,7 @@ module Parse
2428
2469
  # Checkpoint #2, which runs after the tool has executed, DOES
2429
2470
  # fire the notification with success: false, error_code: :cancelled.
2430
2471
  if cancelled?
2431
- payload[:success] = false
2472
+ payload[:success] = false
2432
2473
  payload[:error_code] = :cancelled
2433
2474
  return cancelled_response
2434
2475
  end
@@ -2476,7 +2517,7 @@ module Parse
2476
2517
  # of `agent.execute` and then crashes the dispatcher when it
2477
2518
  # inspects `result[:cancelled]`.
2478
2519
  if cancelled?
2479
- payload[:success] = false
2520
+ payload[:success] = false
2480
2521
  payload[:error_code] = :cancelled
2481
2522
  response = cancelled_response
2482
2523
  trigger_callbacks(:after_tool_call, tool_name, kwargs, response)
@@ -2506,9 +2547,9 @@ module Parse
2506
2547
  Parse::Agent::PromptInjectionDetected => e
2507
2548
  log_security_event(tool_name, kwargs, e)
2508
2549
  trigger_callbacks(:on_error, e, { tool: tool_name, args: kwargs })
2509
- payload[:success] = false
2550
+ payload[:success] = false
2510
2551
  payload[:error_class] = e.class.name
2511
- payload[:error_code] = :security_blocked
2552
+ payload[:error_code] = :security_blocked
2512
2553
  raise # Re-raise security errors to caller
2513
2554
 
2514
2555
  # Method excluded by the agent instance's `methods:` filter.
@@ -2520,9 +2561,9 @@ module Parse
2520
2561
  # filter denial path.
2521
2562
  rescue Parse::Agent::MethodFiltered => e
2522
2563
  trigger_callbacks(:on_error, e, { tool: tool_name, args: kwargs })
2523
- payload[:success] = false
2564
+ payload[:success] = false
2524
2565
  payload[:error_class] = e.class.name
2525
- payload[:error_code] = :tool_filtered
2566
+ payload[:error_code] = :tool_filtered
2526
2567
  response = error_response(e.message, error_code: :tool_filtered)
2527
2568
 
2528
2569
  # Access-denied errors raised by Tools.assert_class_accessible! when
@@ -2532,9 +2573,9 @@ module Parse
2532
2573
  # internal state.
2533
2574
  rescue Parse::Agent::AccessDenied => e
2534
2575
  trigger_callbacks(:on_error, e, { tool: tool_name, args: kwargs })
2535
- payload[:success] = false
2576
+ payload[:success] = false
2536
2577
  payload[:error_class] = e.class.name
2537
- payload[:error_code] = :access_denied
2578
+ payload[:error_code] = :access_denied
2538
2579
  # Surface the AccessDenied subcode (`:hidden_class`,
2539
2580
  # `:class_filter`, `:field_denied`, `:storage_form_field_ref`)
2540
2581
  # in the audit payload so SOC tooling can distinguish operator
@@ -2550,58 +2591,58 @@ module Parse
2550
2591
  # branch on "tool doesn't exist here" vs a real failure.
2551
2592
  rescue Parse::Agent::NotImplemented => e
2552
2593
  trigger_callbacks(:on_error, e, { tool: tool_name, args: kwargs })
2553
- payload[:success] = false
2594
+ payload[:success] = false
2554
2595
  payload[:error_class] = e.class.name
2555
- payload[:error_code] = :not_implemented
2596
+ payload[:error_code] = :not_implemented
2556
2597
  response = error_response(e.message, error_code: :not_implemented)
2557
2598
 
2558
2599
  # Validation errors (e.g. from registered tool handlers or get_objects)
2559
2600
  rescue Parse::Agent::ValidationError => e
2560
2601
  trigger_callbacks(:on_error, e, { tool: tool_name, args: kwargs })
2561
- payload[:success] = false
2602
+ payload[:success] = false
2562
2603
  payload[:error_class] = e.class.name
2563
- payload[:error_code] = :invalid_argument
2604
+ payload[:error_code] = :invalid_argument
2564
2605
  response = error_response("Invalid arguments: #{e.message}", error_code: :invalid_argument)
2565
2606
 
2566
2607
  # Validation errors - return structured error response
2567
2608
  rescue ConstraintTranslator::InvalidOperatorError => e
2568
2609
  trigger_callbacks(:on_error, e, { tool: tool_name, args: kwargs })
2569
- payload[:success] = false
2610
+ payload[:success] = false
2570
2611
  payload[:error_class] = e.class.name
2571
- payload[:error_code] = :invalid_query
2612
+ payload[:error_code] = :invalid_query
2572
2613
  response = error_response(e.message, error_code: :invalid_query)
2573
2614
 
2574
2615
  # Timeout errors
2575
2616
  rescue ToolTimeoutError => e
2576
2617
  trigger_callbacks(:on_error, e, { tool: tool_name, args: kwargs })
2577
- payload[:success] = false
2618
+ payload[:success] = false
2578
2619
  payload[:error_class] = e.class.name
2579
- payload[:error_code] = :timeout
2620
+ payload[:error_code] = :timeout
2580
2621
  response = error_response(e.message, error_code: :timeout)
2581
2622
 
2582
2623
  # Rate limit errors (raised by the built-in limiter or by external
2583
2624
  # injected limiters that re-raise the same constant).
2584
2625
  rescue RateLimitExceeded => e
2585
2626
  trigger_callbacks(:on_error, e, { tool: tool_name, args: kwargs })
2586
- payload[:success] = false
2627
+ payload[:success] = false
2587
2628
  payload[:error_class] = e.class.name
2588
- payload[:error_code] = :rate_limited
2629
+ payload[:error_code] = :rate_limited
2589
2630
  response = error_response(e.message, error_code: :rate_limited, retry_after: e.retry_after)
2590
2631
 
2591
2632
  # Invalid arguments
2592
2633
  rescue ArgumentError => e
2593
2634
  trigger_callbacks(:on_error, e, { tool: tool_name, args: kwargs })
2594
- payload[:success] = false
2635
+ payload[:success] = false
2595
2636
  payload[:error_class] = e.class.name
2596
- payload[:error_code] = :invalid_argument
2637
+ payload[:error_code] = :invalid_argument
2597
2638
  response = error_response("Invalid arguments: #{e.message}", error_code: :invalid_argument)
2598
2639
 
2599
2640
  # Parse API errors
2600
2641
  rescue Parse::Error => e
2601
2642
  trigger_callbacks(:on_error, e, { tool: tool_name, args: kwargs })
2602
- payload[:success] = false
2643
+ payload[:success] = false
2603
2644
  payload[:error_class] = e.class.name
2604
- payload[:error_code] = :parse_error
2645
+ payload[:error_code] = :parse_error
2605
2646
  response = error_response("Parse error: #{e.message}", error_code: :parse_error)
2606
2647
 
2607
2648
  # Pointer-shape mismatch in `$in`/`$nin` array against a pointer
@@ -2615,9 +2656,9 @@ module Parse
2615
2656
  # to "internal error".
2616
2657
  rescue Parse::Query::PointerShapeError => e
2617
2658
  trigger_callbacks(:on_error, e, { tool: tool_name, args: kwargs })
2618
- payload[:success] = false
2659
+ payload[:success] = false
2619
2660
  payload[:error_class] = e.class.name
2620
- payload[:error_code] = :pointer_shape_mismatch
2661
+ payload[:error_code] = :pointer_shape_mismatch
2621
2662
  response = error_response(e.message, error_code: :pointer_shape_mismatch)
2622
2663
 
2623
2664
  # MongoDB-level query timeout (maxTimeMS exceeded, code 50).
@@ -2633,9 +2674,9 @@ module Parse
2633
2674
  # response is returned rather than the opaque internal_error path.
2634
2675
  rescue Parse::MongoDB::ExecutionTimeout => e
2635
2676
  trigger_callbacks(:on_error, e, { tool: tool_name, args: kwargs })
2636
- payload[:success] = false
2677
+ payload[:success] = false
2637
2678
  payload[:error_class] = e.class.name
2638
- payload[:error_code] = :timeout
2679
+ payload[:error_code] = :timeout
2639
2680
  response = error_response(
2640
2681
  "Query timed out at the database (max_time_ms=#{e.max_time_ms}ms). " \
2641
2682
  "Narrow the filter, add an index, or call explain_query to inspect the plan.",
@@ -2658,9 +2699,9 @@ module Parse
2658
2699
  warn "[Parse::Agent] Unexpected error in #{tool_name}: #{e.class} - #{e.message}"
2659
2700
  warn e.backtrace.first(5).join("\n") if e.backtrace
2660
2701
  trigger_callbacks(:on_error, e, { tool: tool_name, args: kwargs })
2661
- payload[:success] = false
2702
+ payload[:success] = false
2662
2703
  payload[:error_class] = e.class.name
2663
- payload[:error_code] = :internal_error
2704
+ payload[:error_code] = :internal_error
2664
2705
  response = error_response("#{tool_name} failed: internal error", error_code: :internal_error)
2665
2706
  ensure
2666
2707
  # Attribute embedding cost to this tool span and restore the
@@ -2668,7 +2709,7 @@ module Parse
2668
2709
  # when no embed happened, matching the minimal-payload discipline.
2669
2710
  embed_frame = Parse::Agent.embed_accumulator_end!(embed_frame_saved)
2670
2711
  if embed_frame && embed_frame[:calls] > 0
2671
- payload[:embed_calls] = embed_frame[:calls]
2712
+ payload[:embed_calls] = embed_frame[:calls]
2672
2713
  payload[:embed_tokens] = embed_frame[:tokens]
2673
2714
  cost = Parse::Agent.embed_cost_usd(embed_frame[:tokens])
2674
2715
  payload[:embed_cost_usd] = cost if cost
@@ -3212,7 +3253,7 @@ module Parse
3212
3253
 
3213
3254
  tools = expand_tool_profile(tools)
3214
3255
  only_list, except_list = extract_filter_lists(:tools, tools)
3215
- only_set = only_list && Set.new(Array(only_list).map(&:to_sym)).freeze
3256
+ only_set = only_list && Set.new(Array(only_list).map(&:to_sym)).freeze
3216
3257
  except_set = except_list && Set.new(Array(except_list).map(&:to_sym)).freeze
3217
3258
 
3218
3259
  # "Known" tools include the global registry plus every tool in
@@ -3281,7 +3322,7 @@ module Parse
3281
3322
  return [nil, nil] if methods.nil?
3282
3323
 
3283
3324
  only_list, except_list = extract_filter_lists(:methods, methods)
3284
- only_set = only_list && Set.new(Array(only_list).map(&method(:normalize_method_filter_entry))).freeze
3325
+ only_set = only_list && Set.new(Array(only_list).map(&method(:normalize_method_filter_entry))).freeze
3285
3326
  except_set = except_list && Set.new(Array(except_list).map(&method(:normalize_method_filter_entry))).freeze
3286
3327
  [only_set, except_set]
3287
3328
  end
@@ -3321,10 +3362,10 @@ module Parse
3321
3362
 
3322
3363
  only_list, except_list = extract_filter_lists(:classes, classes)
3323
3364
 
3324
- only_entries = only_list && resolve_class_filter_entries(only_list, validate: true)
3365
+ only_entries = only_list && resolve_class_filter_entries(only_list, validate: true)
3325
3366
  except_entries = except_list && resolve_class_filter_entries(except_list, validate: false)
3326
3367
 
3327
- only_set = only_entries && Set.new(only_entries).freeze
3368
+ only_set = only_entries && Set.new(only_entries).freeze
3328
3369
  except_set = except_entries && Set.new(except_entries).freeze
3329
3370
  [only_set, except_set]
3330
3371
  end
@@ -3590,9 +3631,9 @@ module Parse
3590
3631
  "#{kwarg_name}: accepts only :only and :except keys " \
3591
3632
  "(got unexpected #{bad_keys.inspect})"
3592
3633
  end
3593
- only = value[:only] || value["only"]
3634
+ only = value[:only] || value["only"]
3594
3635
  except = value[:except] || value["except"]
3595
- unless only.nil? || only.is_a?(Array)
3636
+ unless only.nil? || only.is_a?(Array)
3596
3637
  raise ArgumentError, "#{kwarg_name}: :only must be an Array (got #{only.class})"
3597
3638
  end
3598
3639
  unless except.nil? || except.is_a?(Array)
@@ -3787,8 +3828,8 @@ module Parse
3787
3828
  identity: @acl_scope&.user_id }
3788
3829
  elsif @acl_user_scope
3789
3830
  { type: :acl_user, using_master_key: false,
3790
- identity: (@acl_scope&.user_id ||
3791
- (@acl_user_scope.respond_to?(:id) ? @acl_user_scope.id : nil)) }
3831
+ identity: (@acl_scope&.user_id ||
3832
+ (@acl_user_scope.respond_to?(:id) ? @acl_user_scope.id : nil)) }
3792
3833
  elsif @acl_role_scope
3793
3834
  role_name = case @acl_role_scope
3794
3835
  when Parse::Role then @acl_role_scope.name
@@ -3910,8 +3951,8 @@ module Parse
3910
3951
  # multi-client / multi-tenant setup those can point at different apps,
3911
3952
  # and the existing-session lookup must hit the same app we minted into.
3912
3953
  existing = Parse::Session.for_user(pointer)
3913
- .where(:expires_at.gte => Time.now)
3914
- .order(:updated_at.desc)
3954
+ .where(:expires_at.gte => Time.now)
3955
+ .order(:updated_at.desc)
3915
3956
  existing.client = @client
3916
3957
  existing = existing.first
3917
3958
  token = existing&.session_token
@@ -3949,8 +3990,8 @@ module Parse
3949
3990
  minted = resp.result["sessionToken"] || resp.result[:sessionToken]
3950
3991
  if minted.nil? || minted.to_s.empty?
3951
3992
  refreshed = Parse::Session.for_user(pointer)
3952
- .where(:expires_at.gte => Time.now)
3953
- .order(:updated_at.desc)
3993
+ .where(:expires_at.gte => Time.now)
3994
+ .order(:updated_at.desc)
3954
3995
  refreshed.client = @client
3955
3996
  refreshed = refreshed.first
3956
3997
  minted = refreshed&.session_token
@@ -4008,7 +4049,7 @@ module Parse
4008
4049
  # @return [Symbol] :readonly / :write / :admin (/ :unknown)
4009
4050
  def effective_permission_for(tool_name, kwargs)
4010
4051
  if tool_name.to_sym == :call_method
4011
- class_name = kwargs[:class_name] || kwargs["class_name"]
4052
+ class_name = kwargs[:class_name] || kwargs["class_name"]
4012
4053
  method_name = kwargs[:method_name] || kwargs["method_name"]
4013
4054
  if class_name && method_name
4014
4055
  klass = (Parse::Model.find_class(class_name.to_s) rescue nil)
@@ -4060,9 +4101,9 @@ module Parse
4060
4101
  append_log(entry)
4061
4102
 
4062
4103
  response = { success: false, error: message }
4063
- response[:error_code] = error_code if error_code
4104
+ response[:error_code] = error_code if error_code
4064
4105
  response[:retry_after] = retry_after if retry_after
4065
- response[:details] = details if details.is_a?(Hash) && details.any?
4106
+ response[:details] = details if details.is_a?(Hash) && details.any?
4066
4107
  response
4067
4108
  end
4068
4109
 
@@ -4074,10 +4115,10 @@ module Parse
4074
4115
  reason = @cancellation_token&.reason
4075
4116
  message = reason ? "Cancelled by client (#{reason})" : "Cancelled by client"
4076
4117
  {
4077
- success: false,
4078
- error: message,
4118
+ success: false,
4119
+ error: message,
4079
4120
  error_code: :cancelled,
4080
- cancelled: true,
4121
+ cancelled: true,
4081
4122
  }
4082
4123
  end
4083
4124
  end