parse-stack-next 5.6.0 → 5.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +489 -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 +7 -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 +219 -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/cache.rb +17 -17
  54. data/lib/parse/embeddings/cohere.rb +21 -22
  55. data/lib/parse/embeddings/image_fetch.rb +22 -21
  56. data/lib/parse/embeddings/jina.rb +21 -21
  57. data/lib/parse/embeddings/local_http.rb +6 -7
  58. data/lib/parse/embeddings/media_file.rb +5 -4
  59. data/lib/parse/embeddings/openai.rb +5 -5
  60. data/lib/parse/embeddings/provider.rb +1 -1
  61. data/lib/parse/embeddings/qwen.rb +10 -10
  62. data/lib/parse/embeddings/spend_cap.rb +2 -3
  63. data/lib/parse/embeddings/streaming_body.rb +11 -11
  64. data/lib/parse/embeddings/video_source.rb +7 -6
  65. data/lib/parse/embeddings/voyage.rb +59 -61
  66. data/lib/parse/embeddings.rb +28 -27
  67. data/lib/parse/graphql/type_generator.rb +1 -1
  68. data/lib/parse/graphql.rb +5 -5
  69. data/lib/parse/live_query/client.rb +1 -1
  70. data/lib/parse/live_query.rb +1 -1
  71. data/lib/parse/lock.rb +15 -18
  72. data/lib/parse/lock_backend.rb +1 -1
  73. data/lib/parse/lookup_rewriter.rb +6 -6
  74. data/lib/parse/model/associations/belongs_to.rb +4 -0
  75. data/lib/parse/model/associations/collection_proxy.rb +3 -0
  76. data/lib/parse/model/associations/has_many.rb +4 -0
  77. data/lib/parse/model/classes/role.rb +227 -28
  78. data/lib/parse/model/classes/user.rb +60 -1
  79. data/lib/parse/model/clp.rb +4 -4
  80. data/lib/parse/model/core/actions.rb +366 -113
  81. data/lib/parse/model/core/create_lock.rb +0 -2
  82. data/lib/parse/model/core/describe.rb +57 -57
  83. data/lib/parse/model/core/embed_managed.rb +21 -23
  84. data/lib/parse/model/core/fetching.rb +6 -0
  85. data/lib/parse/model/core/indexing.rb +14 -14
  86. data/lib/parse/model/core/parse_reference.rb +1 -1
  87. data/lib/parse/model/core/properties.rb +6 -2
  88. data/lib/parse/model/core/querying.rb +1 -1
  89. data/lib/parse/model/core/schema.rb +2 -2
  90. data/lib/parse/model/core/search_indexing.rb +2 -2
  91. data/lib/parse/model/core/vector_searchable.rb +4 -5
  92. data/lib/parse/model/file.rb +21 -15
  93. data/lib/parse/model/geojson.rb +2 -2
  94. data/lib/parse/model/geopoint.rb +1 -0
  95. data/lib/parse/model/object.rb +136 -66
  96. data/lib/parse/model/pointer.rb +2 -0
  97. data/lib/parse/model/polygon.rb +3 -6
  98. data/lib/parse/model/push.rb +2 -2
  99. data/lib/parse/model/vector.rb +3 -1
  100. data/lib/parse/mongodb.rb +512 -282
  101. data/lib/parse/pipeline_security.rb +3 -0
  102. data/lib/parse/query/constraints.rb +46 -46
  103. data/lib/parse/query.rb +146 -59
  104. data/lib/parse/retrieval/agent_tool.rb +34 -20
  105. data/lib/parse/retrieval/chunk.rb +1 -0
  106. data/lib/parse/retrieval/reranker/cohere.rb +11 -11
  107. data/lib/parse/retrieval/reranker.rb +3 -4
  108. data/lib/parse/retrieval/retriever.rb +4 -6
  109. data/lib/parse/schema/index_migrator.rb +26 -26
  110. data/lib/parse/schema/search_index_migrator.rb +19 -19
  111. data/lib/parse/stack/tasks.rb +6 -6
  112. data/lib/parse/stack/version.rb +1 -1
  113. data/lib/parse/stack.rb +10 -10
  114. data/lib/parse/vector_search/hybrid.rb +66 -41
  115. data/lib/parse/vector_search.rb +14 -11
  116. data/lib/parse/webhooks/payload.rb +1 -0
  117. data/lib/parse/webhooks/registration.rb +4 -4
  118. data/lib/parse/webhooks/trigger_audit.rb +50 -49
  119. data/lib/parse/webhooks.rb +120 -5
  120. data/parse-stack-next.gemspec +6 -6
  121. metadata +10 -1
@@ -4,6 +4,7 @@
4
4
  require "set"
5
5
  require_relative "model/acl"
6
6
  require_relative "clp_scope"
7
+ require_relative "authorization"
7
8
 
8
9
  module Parse
9
10
  # Shared identity-resolution helper for query paths that simulate
@@ -22,11 +23,13 @@ module Parse
22
23
  # userObjectId, "role:Admin", ...]`), so callers can prepend a
23
24
  # `$match` stage built via {Parse::ACL.read_predicate}.
24
25
  #
25
- # Atlas Search uses the same pattern through
26
- # `Parse::AtlasSearch::Session`; this module reuses that resolver
27
- # (token user_id role expansion + caching) and adds a
28
- # path-agnostic kwarg-popping front door so every mongo-direct entry
29
- # point can speak the same auth vocabulary.
26
+ # Identity and role resolution live in {Parse::Authorization}, which
27
+ # owns the token user_id → role-expansion pipeline and its caches.
28
+ # This module adds a path-agnostic kwarg-popping front door on top, so
29
+ # every mongo-direct entry point speaks the same auth vocabulary.
30
+ # Atlas Search is a peer consumer of the same resolver, not its home:
31
+ # `Parse::Query#results_direct` runs no `$search` at all and must not
32
+ # depend on the Atlas Search namespace to decide who the caller is.
30
33
  module ACLScope
31
34
  # Raised when a query path is configured to require an explicit
32
35
  # session-token or master mode and the caller supplied neither.
@@ -50,7 +53,7 @@ module Parse
50
53
  # @return [String, nil] the resolved user_id, or `nil` for
51
54
  # `:master` and `:public`.
52
55
  # @!attribute session
53
- # @return [Parse::AtlasSearch::Session::Resolved, nil] the
56
+ # @return [Parse::Authorization::Resolved, nil] the
54
57
  # underlying resolved-session struct (carries role-name set),
55
58
  # `nil` for `:master`.
56
59
  # @!attribute strict_role
@@ -64,13 +67,38 @@ module Parse
64
67
  # `strict_role: true`, rows with NO `_rperm` field still pass
65
68
  # (Parse-Server treats absent `_rperm` as public-default); the
66
69
  # knob only suppresses the `"*"` subscription in the `$in` set.
67
- Resolution = Struct.new(:mode, :permission_strings, :user_id, :session, :strict_role, keyword_init: true) do
70
+ # @!attribute client
71
+ # @return [Parse::Client, nil] the client whose authorization context
72
+ # produced this resolution. Carried so a caller about to run the
73
+ # query against a process-global resource can check that the two
74
+ # agree; see {Parse::MongoDB.verify_client!}. Per-client
75
+ # authorization plus a process-global MongoDB connection is safe only
76
+ # when they belong to the same application.
77
+ Resolution = Struct.new(:mode, :permission_strings, :user_id, :session, :strict_role, :client, keyword_init: true) do
68
78
  def master?; mode == :master; end
69
79
  def session?; mode == :session; end
70
80
  def public?; mode == :public; end
71
81
  def strict_role?; strict_role == true; end
72
82
  end
73
83
 
84
+ # The client a resolution was produced by, or nil when it cannot say.
85
+ #
86
+ # Deliberately tolerant of objects that are resolution-shaped without
87
+ # being a {Resolution}: test doubles and any caller-supplied stand-in.
88
+ # A resolution that cannot name its client IS an unidentified caller,
89
+ # which is a case {Parse::MongoDB.verify_client!} models explicitly (it
90
+ # allows the single-application case and refuses the ambiguous one).
91
+ # Crashing on the missing method instead would convert an unknown into
92
+ # a NoMethodError far from its cause.
93
+ #
94
+ # @param resolution [Object, nil]
95
+ # @return [Parse::Client, nil]
96
+ def self.client_of(resolution)
97
+ return nil if resolution.nil?
98
+ return nil unless resolution.respond_to?(:client)
99
+ resolution.client
100
+ end
101
+
74
102
  class << self
75
103
  # When `true`, every call to {.resolve!} that did NOT receive
76
104
  # `session_token:` or `master: true` raises {ACLRequired} instead
@@ -124,7 +152,7 @@ module Parse
124
152
  # Parse::Query#scope_to_user. Mirrors the session-token path
125
153
  # but skips the /users/me round-trip; role expansion still
126
154
  # runs via Parse::Role.all_for_user.
127
- return resolve_for_user(acl_user)
155
+ return resolve_for_user(acl_user, client: authorization_client(options))
128
156
  end
129
157
 
130
158
  if acl_role
@@ -137,22 +165,25 @@ module Parse
137
165
  # identity. Parent-role inheritance applies (passing
138
166
  # "scope:admin" includes any role "scope:admin" inherits
139
167
  # from).
140
- return resolve_for_role(acl_role, strict_role: strict_role)
168
+ return resolve_for_role(acl_role, strict_role: strict_role,
169
+ client: authorization_client(options))
141
170
  end
142
171
 
143
172
  if session_token
144
- require_atlas_session!
145
- resolved = Parse::AtlasSearch::Session.resolve(session_token)
173
+ context = authorization_for(options)
174
+ resolved = context.resolve(session_token)
146
175
  return Resolution.new(
147
- mode: :session,
148
- permission_strings: resolved.permission_strings,
149
- user_id: resolved.user_id,
150
- session: resolved,
151
- )
176
+ mode: :session,
177
+ permission_strings: resolved.permission_strings,
178
+ user_id: resolved.user_id,
179
+ session: resolved,
180
+ client: context.client,
181
+ )
152
182
  end
153
183
 
154
184
  if master == true
155
- return Resolution.new(mode: :master, permission_strings: nil, user_id: nil, session: nil)
185
+ return Resolution.new(mode: :master, permission_strings: nil, user_id: nil, session: nil,
186
+ client: authorization_client(options))
156
187
  end
157
188
 
158
189
  if @require_session_token == true
@@ -165,13 +196,13 @@ module Parse
165
196
  end
166
197
 
167
198
  warn_no_acl_context_once!(method_name)
168
- require_atlas_session!
169
- anonymous = Parse::AtlasSearch::Session::Resolved.new(nil, Set.new)
199
+ anonymous = Parse::Authorization::Resolved.new(nil, Set.new)
170
200
  Resolution.new(
171
201
  mode: :public,
172
202
  permission_strings: anonymous.permission_strings,
173
203
  user_id: nil,
174
204
  session: anonymous,
205
+ client: authorization_client(options),
175
206
  )
176
207
  end
177
208
 
@@ -384,8 +415,7 @@ module Parse
384
415
  # either shape so the CLP gate fires before the String→Hash
385
416
  # upgrade — denying access to the joined class BEFORE we go
386
417
  # to the trouble of building out an upgraded sub-pipeline.
387
- target =
388
- if spec.is_a?(String)
418
+ target = if spec.is_a?(String)
389
419
  spec
390
420
  elsif spec.is_a?(Hash)
391
421
  spec["coll"] || spec[:coll]
@@ -417,8 +447,7 @@ module Parse
417
447
  # `$and`.
418
448
  acl_predicate = acl_match["$match"]
419
449
  existing = spec["restrictSearchWithMatch"] || spec[:restrictSearchWithMatch]
420
- combined =
421
- if existing.nil? || (existing.respond_to?(:empty?) && existing.empty?)
450
+ combined = if existing.nil? || (existing.respond_to?(:empty?) && existing.empty?)
422
451
  acl_predicate
423
452
  else
424
453
  { "$and" => [existing, acl_predicate] }
@@ -431,8 +460,7 @@ module Parse
431
460
  def rewrite_facet(spec, acl_match, perms)
432
461
  return spec unless spec.is_a?(Hash)
433
462
  spec.each_with_object({}) do |(branch_name, branch_pipeline), out|
434
- out[branch_name] =
435
- if branch_pipeline.is_a?(Array)
463
+ out[branch_name] = if branch_pipeline.is_a?(Array)
436
464
  # Recurse with the same perms — facet branches are
437
465
  # evaluated in the requesting session's authority, not
438
466
  # elevated.
@@ -565,7 +593,7 @@ module Parse
565
593
  # through a session token.
566
594
  # @param user [Parse::User, Parse::Pointer]
567
595
  # @return [Resolution]
568
- def resolve_for_user(user)
596
+ def resolve_for_user(user, client: nil)
569
597
  # SECURITY: className must be `_User` (or the legacy `User`
570
598
  # alias). Without this check, any duck-typed object exposing
571
599
  # `#id` — including a `Parse::Pointer` to a foreign class
@@ -596,10 +624,13 @@ module Parse
596
624
  "User Pointer with a non-empty objectId."
597
625
  end
598
626
 
599
- role_names =
600
- begin
627
+ role_names = begin
601
628
  require_relative "model/classes/role"
602
- Parse::Role.all_for_user(user, max_depth: 10)
629
+ # `client:` matters here for the same reason it does on the
630
+ # session-token path: resolving the user against one application
631
+ # and then walking the role graph on another would grant that
632
+ # user the other application's roles by name.
633
+ Parse::Role.all_for_user(user, max_depth: 10, client: client)
603
634
  rescue StandardError
604
635
  Set.new
605
636
  end
@@ -608,12 +639,12 @@ module Parse
608
639
  role_names.each { |name| perms << "role:#{name}" if name && !name.empty? }
609
640
  perms.uniq!
610
641
 
611
- require_atlas_session!
612
642
  Resolution.new(
613
643
  mode: :session,
614
644
  permission_strings: perms,
615
645
  user_id: user.id,
616
- session: Parse::AtlasSearch::Session::Resolved.new(user.id, role_names),
646
+ session: Parse::Authorization::Resolved.new(user.id, role_names),
647
+ client: client,
617
648
  )
618
649
  end
619
650
 
@@ -646,24 +677,22 @@ module Parse
646
677
  # {.match_stage_for} for the precise predicate shape.
647
678
  # @return [Resolution]
648
679
  # @raise [ArgumentError] when the role cannot be resolved.
649
- def resolve_for_role(role, strict_role: false)
680
+ def resolve_for_role(role, strict_role: false, client: nil)
650
681
  require_relative "model/classes/role"
651
- role_obj =
652
- case role
682
+ role_obj = case role
653
683
  when Parse::Role then role
654
684
  when String, Symbol
655
685
  name = role.to_s.sub(/\Arole:/, "")
656
686
  raise ArgumentError, "[Parse::ACLScope] role name must be non-empty." if name.empty?
657
- found = Parse::Role.first(name: name)
687
+ found = role_lookup_by_name(name, client: client)
658
688
  raise ArgumentError, "[Parse::ACLScope] no _Role found with name #{name.inspect}." if found.nil?
659
689
  found
660
690
  else
661
691
  raise ArgumentError, "[Parse::ACLScope] resolve_for_role expects Parse::Role or String."
662
692
  end
663
693
 
664
- names =
665
- begin
666
- role_obj.all_parent_role_names(max_depth: 10)
694
+ names = begin
695
+ role_obj.all_parent_role_names(max_depth: 10, client: client)
667
696
  rescue StandardError
668
697
  Set.new([role_obj.name].compact)
669
698
  end
@@ -678,13 +707,13 @@ module Parse
678
707
  names.each { |n| perms << "role:#{n}" if n && !n.empty? }
679
708
  perms.uniq!
680
709
 
681
- require_atlas_session!
682
710
  Resolution.new(
683
711
  mode: :session,
684
712
  permission_strings: perms,
685
713
  user_id: nil,
686
- session: Parse::AtlasSearch::Session::Resolved.new(nil, names),
714
+ session: Parse::Authorization::Resolved.new(nil, names),
687
715
  strict_role: strict_role,
716
+ client: client,
688
717
  )
689
718
  end
690
719
 
@@ -717,18 +746,62 @@ module Parse
717
746
  "= true to make this misuse an error instead of a warning."
718
747
  end
719
748
 
720
- # Lazily load the Atlas Search session resolver — it carries the
721
- # token-cache / role-cache plumbing this module reuses. Loads
722
- # `atlas_search.rb` (not just `atlas_search/session.rb`) so the
723
- # parent module's session_cache / role_cache are initialized.
724
- # Loading session.rb in isolation leaves Parse::AtlasSearch
725
- # without its memory caches and Session.lookup_user_id crashes
726
- # with NoMethodError. Keeping the require lazy means apps that
727
- # never call an auth-resolving path don't pay the load cost.
728
- def require_atlas_session!
729
- return if defined?(Parse::AtlasSearch) && Parse::AtlasSearch.respond_to?(:session_cache) &&
730
- !Parse::AtlasSearch.session_cache.nil?
731
- require_relative "atlas_search"
749
+ # The authorization context that should resolve this call.
750
+ #
751
+ # **Mutates `options`** by `delete`-ing `:client`, consistent with the
752
+ # other auth kwargs, so the remaining hash can be forwarded to the
753
+ # transport without leaking it.
754
+ #
755
+ # Defaults to `Parse.client` at this boundary. Below it,
756
+ # {Parse::Authorization.resolve} requires `client:` with no default: a
757
+ # token belonging to application B must never be validated against
758
+ # application A, which is exactly what a global resolver did.
759
+ #
760
+ # This used to lazily `require "atlas_search"` and call
761
+ # `Parse::AtlasSearch::Session.resolve`, so a plain mongo-direct query
762
+ # with no `$search` anywhere in it pulled in the Atlas Search namespace
763
+ # to decide who the caller was.
764
+ def authorization_for(options)
765
+ client = authorization_client(options)
766
+ if client.nil?
767
+ raise ACLRequired,
768
+ "Parse::ACLScope could not determine a Parse client to resolve this " \
769
+ "session token against. Pass client: explicitly, or configure a default " \
770
+ "client with Parse.setup."
771
+ end
772
+ client.authorization
773
+ end
774
+
775
+ # Look up a `_Role` by name against a SPECIFIC client.
776
+ #
777
+ # `Parse::Role.first` resolves its client through the class, which is
778
+ # the default. On the `acl_role:` path that would find a role in the
779
+ # default application and then use its name to grant access in another,
780
+ # which is the same cross-application mixing the session-token path
781
+ # guards against. A nil client keeps the historical behavior.
782
+ def role_lookup_by_name(name, client: nil)
783
+ return Parse::Role.first(name: name) if client.nil?
784
+ query = Parse::Role.query(name: name)
785
+ query.client = client
786
+ query.first
787
+ end
788
+
789
+ # The client for this call, or `nil` when none can be determined.
790
+ #
791
+ # **Mutates `options`** by `delete`-ing `:client`, so calling this more
792
+ # than once per call is safe: the first call consumes the kwarg and
793
+ # later ones fall back to the default.
794
+ #
795
+ # Never raises. The master and public paths run no identity resolution
796
+ # and historically worked in a process that had never called
797
+ # `Parse.setup`, so making them raise `ConnectionError` just to label
798
+ # the resolution would be a regression. They still want the label when
799
+ # one is available, because {Parse::MongoDB.verify_client!} checks it
800
+ # against the application the global connection is bound to.
801
+ def authorization_client(options)
802
+ options.delete(:client) || Parse.client
803
+ rescue Parse::Error::ConnectionError
804
+ nil
732
805
  end
733
806
  end
734
807
 
Binary file
@@ -44,14 +44,14 @@ module Parse
44
44
 
45
45
  def initialize
46
46
  @cancelled = false
47
- @reason = nil
47
+ @reason = nil
48
48
  # Mutex protects the read-modify-write in {#cancel!} so a
49
49
  # concurrent cancel from notifications/cancelled and client
50
50
  # disconnect cannot lose a reason or partially update state.
51
51
  # The hot poll path (#cancelled?) reads the boolean ivar
52
52
  # directly — atomic on MRI and on each major Ruby
53
53
  # implementation we ship against.
54
- @mutex = Mutex.new
54
+ @mutex = Mutex.new
55
55
  end
56
56
 
57
57
  # @return [Boolean] true once {#cancel!} has been called at least once.
@@ -71,7 +71,7 @@ module Parse
71
71
  @mutex.synchronize do
72
72
  return false if @cancelled
73
73
  @cancelled = true
74
- @reason = reason
74
+ @reason = reason
75
75
  true
76
76
  end
77
77
  end
@@ -171,10 +171,10 @@ module Parse
171
171
  # count deltas even when operators are otherwise clean.
172
172
  assert_where_key_permitted!(key)
173
173
  result[columnize(key)] = if key == "$relatedTo"
174
- translate_related_to_value(value, depth: 0, agent: agent)
175
- else
176
- translate_value(value, depth: 0, agent: agent)
177
- end
174
+ translate_related_to_value(value, depth: 0, agent: agent)
175
+ else
176
+ translate_value(value, depth: 0, agent: agent)
177
+ end
178
178
  end
179
179
  end
180
180
 
@@ -51,14 +51,14 @@ module Parse
51
51
  class_name.to_s
52
52
  end
53
53
  {
54
- class_name: cn,
55
- accessible: describe_class_accessibility(cn),
56
- agent_fields: class_field_allowlist(cn),
57
- agent_canonical_filter: Parse::Agent::MetadataRegistry.canonical_filter(cn),
58
- per_agent_filter: respond_to?(:filter_for) ? filter_for(cn) : nil,
59
- tenant_scope: class_tenant_scope(cn),
60
- large_fields: class_large_fields(cn),
61
- agent_methods: class_agent_method_names(cn),
54
+ class_name: cn,
55
+ accessible: describe_class_accessibility(cn),
56
+ agent_fields: class_field_allowlist(cn),
57
+ agent_canonical_filter: Parse::Agent::MetadataRegistry.canonical_filter(cn),
58
+ per_agent_filter: respond_to?(:filter_for) ? filter_for(cn) : nil,
59
+ tenant_scope: class_tenant_scope(cn),
60
+ large_fields: class_large_fields(cn),
61
+ agent_methods: class_agent_method_names(cn),
62
62
  }
63
63
  end
64
64
 
@@ -136,7 +136,7 @@ module Parse
136
136
  # mirroring assert_class_accessible!'s signature.
137
137
  if class_name
138
138
  cn = class_name.is_a?(Class) && class_name.respond_to?(:parse_class) ?
139
- class_name.parse_class : class_name.to_s
139
+ class_name.parse_class : class_name.to_s
140
140
  begin
141
141
  Parse::Agent::Tools.assert_class_accessible!(cn, agent: self, op: op)
142
142
  rescue Parse::Agent::AccessDenied => e
@@ -152,7 +152,7 @@ module Parse
152
152
  # tool is call_method (the method-filter only narrows that tool).
153
153
  if tool_sym == :call_method && method_name && class_name
154
154
  cn = class_name.is_a?(Class) && class_name.respond_to?(:parse_class) ?
155
- class_name.parse_class : class_name.to_s
155
+ class_name.parse_class : class_name.to_s
156
156
  if respond_to?(:method_filtered?) &&
157
157
  method_filtered?(method_name.to_sym, class_name: cn)
158
158
  return { allowed: false, reason: :method_filtered,
@@ -169,23 +169,23 @@ module Parse
169
169
  # paths share the same data source.
170
170
  def describe_hash
171
171
  {
172
- agent_id: agent_id,
173
- agent_depth: agent_depth,
174
- permissions: @permissions,
175
- auth: auth_descriptor,
176
- tenant_id: tenant_id,
177
- classes: filter_descriptor(@class_filter_only, @class_filter_except),
178
- tools: tools_descriptor,
179
- methods: filter_descriptor(@method_filter_only, @method_filter_except, transform: ->(s) { s.to_s }),
180
- filters: per_agent_filters_summary,
172
+ agent_id: agent_id,
173
+ agent_depth: agent_depth,
174
+ permissions: @permissions,
175
+ auth: auth_descriptor,
176
+ tenant_id: tenant_id,
177
+ classes: filter_descriptor(@class_filter_only, @class_filter_except),
178
+ tools: tools_descriptor,
179
+ methods: filter_descriptor(@method_filter_only, @method_filter_except, transform: ->(s) { s.to_s }),
180
+ filters: per_agent_filters_summary,
181
181
  hidden_classes: Parse::Agent::MetadataRegistry.hidden_class_names,
182
- per_class: per_class_descriptor,
183
- strict_modes: {
184
- tool_filter: strict_tool_filter?,
182
+ per_class: per_class_descriptor,
183
+ strict_modes: {
184
+ tool_filter: strict_tool_filter?,
185
185
  class_filter: strict_class_filter?,
186
186
  },
187
187
  correlation_id: @correlation_id,
188
- prompt: { version: Parse::Agent::PROMPT_VERSION },
188
+ prompt: { version: Parse::Agent::PROMPT_VERSION },
189
189
  }
190
190
  end
191
191
 
@@ -230,8 +230,8 @@ module Parse
230
230
 
231
231
  def tools_descriptor
232
232
  {
233
- only: @tool_filter_only && @tool_filter_only.to_a.sort,
234
- except: @tool_filter_except && @tool_filter_except.to_a.sort,
233
+ only: @tool_filter_only && @tool_filter_only.to_a.sort,
234
+ except: @tool_filter_except && @tool_filter_except.to_a.sort,
235
235
  effective: allowed_tools.sort,
236
236
  }
237
237
  end
@@ -250,7 +250,7 @@ module Parse
250
250
  # class.
251
251
  def per_class_descriptor
252
252
  names = Set.new
253
- names.merge(@class_filter_only.to_a) if @class_filter_only
253
+ names.merge(@class_filter_only.to_a) if @class_filter_only
254
254
  names.merge(@class_filter_except.to_a) if @class_filter_except
255
255
  if @filters
256
256
  names.merge(@filters.keys.reject { |k| k == :default }.map(&:to_s))
@@ -280,7 +280,7 @@ module Parse
280
280
  def describe_class_accessibility(class_name)
281
281
  if Parse::Agent::MetadataRegistry.hidden?(class_name)
282
282
  except = Parse::Agent::MetadataRegistry.respond_to?(:hidden_exception_for) ?
283
- Parse::Agent::MetadataRegistry.hidden_exception_for(class_name) : nil
283
+ Parse::Agent::MetadataRegistry.hidden_exception_for(class_name) : nil
284
284
  if except == :master_key && auth_context[:using_master_key] == true
285
285
  # Hidden from session-bound / acl_user / acl_role agents but
286
286
  # reachable by this master-key agent.
@@ -308,7 +308,7 @@ module Parse
308
308
  def class_tenant_scope(class_name)
309
309
  return nil if tenant_id.nil?
310
310
  rule = Parse::Agent::MetadataRegistry.respond_to?(:tenant_scope_rule) ?
311
- Parse::Agent::MetadataRegistry.tenant_scope_rule(class_name) : nil
311
+ Parse::Agent::MetadataRegistry.tenant_scope_rule(class_name) : nil
312
312
  return nil unless rule
313
313
  { field: rule[:field], value: tenant_id }
314
314
  end
@@ -361,8 +361,8 @@ module Parse
361
361
  lines = []
362
362
  auth = data[:auth]
363
363
  auth_line = auth[:mode] == :session_token ?
364
- "#{auth[:mode]} (fingerprint=#{auth[:fingerprint]})" :
365
- auth[:mode].to_s
364
+ "#{auth[:mode]} (fingerprint=#{auth[:fingerprint]})" :
365
+ auth[:mode].to_s
366
366
  lines << "Parse::Agent #{data[:agent_id]} (depth=#{data[:agent_depth]}, correlation=#{data[:correlation_id] || "—"})"
367
367
  lines << " auth: #{auth_line}"
368
368
  lines << " permissions: #{data[:permissions]}"
@@ -370,20 +370,20 @@ module Parse
370
370
 
371
371
  if data[:classes][:only] || data[:classes][:except]
372
372
  lines << " classes:"
373
- lines << " only: #{data[:classes][:only].inspect}" if data[:classes][:only]
373
+ lines << " only: #{data[:classes][:only].inspect}" if data[:classes][:only]
374
374
  lines << " except: #{data[:classes][:except].inspect}" if data[:classes][:except]
375
375
  else
376
376
  lines << " classes: (no filter — every visible class reachable)"
377
377
  end
378
378
 
379
379
  lines << " tools:"
380
- lines << " only: #{data[:tools][:only].inspect}" if data[:tools][:only]
381
- lines << " except: #{data[:tools][:except].inspect}" if data[:tools][:except]
380
+ lines << " only: #{data[:tools][:only].inspect}" if data[:tools][:only]
381
+ lines << " except: #{data[:tools][:except].inspect}" if data[:tools][:except]
382
382
  lines << " effective: #{data[:tools][:effective].inspect}"
383
383
 
384
384
  if data[:methods][:only] || data[:methods][:except]
385
385
  lines << " methods:"
386
- lines << " only: #{data[:methods][:only].inspect}" if data[:methods][:only]
386
+ lines << " only: #{data[:methods][:only].inspect}" if data[:methods][:only]
387
387
  lines << " except: #{data[:methods][:except].inspect}" if data[:methods][:except]
388
388
  end
389
389
 
@@ -68,10 +68,10 @@ module Parse
68
68
  def initialize(class_name = nil, message = nil,
69
69
  kind: nil, denied_field: nil, allowed_fields: nil,
70
70
  suggested_rewrite: nil)
71
- @class_name = class_name.to_s
72
- @kind = kind
73
- @denied_field = denied_field
74
- @allowed_fields = allowed_fields&.map(&:to_s)
71
+ @class_name = class_name.to_s
72
+ @kind = kind
73
+ @denied_field = denied_field
74
+ @allowed_fields = allowed_fields&.map(&:to_s)
75
75
  @suggested_rewrite = suggested_rewrite
76
76
  super(message || "Class '#{@class_name}' is not accessible to this agent")
77
77
  end
@@ -81,9 +81,9 @@ module Parse
81
81
  # unused nil fields.
82
82
  def to_details
83
83
  {
84
- kind: kind,
85
- denied_field: denied_field,
86
- allowed_fields: allowed_fields,
84
+ kind: kind,
85
+ denied_field: denied_field,
86
+ allowed_fields: allowed_fields,
87
87
  suggested_rewrite: suggested_rewrite,
88
88
  }.compact
89
89
  end
@@ -116,8 +116,8 @@ module Parse
116
116
  def initialize(message = nil, depth: nil)
117
117
  @depth = depth
118
118
  super(message || "Parse::Agent recursion depth exhausted (depth=#{depth.inspect}). " \
119
- "A sub-agent attempted to construct another sub-agent past the " \
120
- "configured recursion_depth: cap.")
119
+ "A sub-agent attempted to construct another sub-agent past the " \
120
+ "configured recursion_depth: cap.")
121
121
  end
122
122
  end
123
123