Module: Parse::Agent::MetadataRegistry

Extended by:
MetadataRegistry
Included in:
MetadataRegistry
Defined in:
lib/parse/agent/metadata_registry.rb

Overview

Registry module that enriches server schemas with local model metadata. Merges class descriptions, property descriptions, and agent-allowed methods from registered Parse::Object models into the schema data returned by the agent.

Examples:

Enriching a schema

server_schema = { "className" => "Song", "fields" => { ... } }
enriched = MetadataRegistry.enriched_schema("Song", server_schema)
# enriched now includes :description and :agent_methods if defined

Constant Summary collapse

ALWAYS_KEEP_FIELDS =

Fields that always pass through the agent_fields allowlist filter. These carry semantic meaning the LLM needs even when not explicitly listed as analytics-relevant.

%w[objectId createdAt updatedAt].freeze
NOISY_FIELD_METADATA =

Per-field metadata keys that bloat the agent schema response without helping analytics queries. Dropped before the schema reaches the LLM.

%w[indexed].freeze

Instance Method Summary collapse

Instance Method Details

#accessible?(class_name) ⇒ Boolean

Check whether a class name is accessible to agent tools. Inverse of #hidden?. Use at tool-dispatch time to refuse access before any query hits Parse Server.

Parameters:

Returns:

  • (Boolean)


197
198
199
# File 'lib/parse/agent/metadata_registry.rb', line 197

def accessible?(class_name)
  !hidden?(class_name)
end

#agent_methods(class_name, agent_permission: :readonly) ⇒ Hash<Symbol, Hash>

Get agent methods for a Parse class filtered by permission.

Parameters:

  • class_name (String)

    the Parse class name

  • agent_permission (Symbol) (defaults to: :readonly)

    the agent’s permission level

Returns:



520
521
522
523
524
# File 'lib/parse/agent/metadata_registry.rb', line 520

def agent_methods(class_name, agent_permission: :readonly)
  klass = find_model_class(class_name)
  return {} unless klass&.respond_to?(:agent_methods_for)
  klass.agent_methods_for(agent_permission)
end

#allow_collscan?(class_name) ⇒ Boolean

Check whether COLLSCANs are explicitly permitted for the given class. Returns true when the model declares ‘agent_allow_collscan true`, false otherwise (including when no model class is registered).

Parameters:

  • class_name (String)

    the Parse class name

Returns:

  • (Boolean)


541
542
543
544
545
# File 'lib/parse/agent/metadata_registry.rb', line 541

def allow_collscan?(class_name)
  klass = find_model_class(class_name)
  return false unless klass&.respond_to?(:agent_allow_collscan?)
  klass.agent_allow_collscan?
end

#canonical_filter(class_name) ⇒ Hash?

Look up the canonical “valid state” filter declared via ‘agent_canonical_filter` on the model class. Returns nil when no filter is declared.

Parameters:

  • class_name (String)

    the Parse class name

Returns:

  • (Hash, nil)

    a String-keyed where-style hash, or nil



553
554
555
556
557
# File 'lib/parse/agent/metadata_registry.rb', line 553

def canonical_filter(class_name)
  klass = find_model_class(class_name)
  return nil unless klass&.respond_to?(:agent_canonical_filter_for_apply)
  klass.agent_canonical_filter_for_apply
end

#class_description(class_name) ⇒ String?

Get the class description for a Parse class if registered.

Parameters:

  • class_name (String)

    the Parse class name

Returns:

  • (String, nil)

    the description or nil



500
501
502
503
# File 'lib/parse/agent/metadata_registry.rb', line 500

def class_description(class_name)
  klass = find_model_class(class_name)
  klass&.respond_to?(:agent_description) ? klass.agent_description : nil
end

#enriched_schema(class_name, server_schema, agent_permission: :readonly, edges: nil) ⇒ Hash

Enrich a server schema with local model metadata.

Parameters:

  • class_name (String)

    the Parse class name

  • server_schema (Hash)

    the schema from Parse Server

  • agent_permission (Symbol) (defaults to: :readonly)

    the agent’s permission level for method filtering

  • edges (Array<Hash>, nil) (defaults to: nil)

    pre-built relation edges from RelationGraph#build. When omitted, edges are built on demand for this single class; pass a pre-built array when enriching many schemas in a row to avoid the N+1 traversal.

Returns:

  • (Hash)

    the enriched schema



252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
# File 'lib/parse/agent/metadata_registry.rb', line 252

def enriched_schema(class_name, server_schema, agent_permission: :readonly, edges: nil)
  klass = find_model_class(class_name)
  return server_schema unless klass&.respond_to?(:has_agent_metadata?) && klass.has_agent_metadata?

  schema = deep_dup(server_schema)

  # Add class description
  if klass.agent_description
    schema["description"] = klass.agent_description
  end

  # Add class-level analytics usage hint (distinct from description)
  if klass.respond_to?(:agent_usage) && klass.agent_usage
    schema["usage"] = klass.agent_usage
  end

  # Enrich fields with property descriptions
  if schema["fields"] && klass.property_descriptions.any?
    schema["fields"] = enrich_fields(schema["fields"], klass)
  end

  # Filter fields to the declared allowlist (plus always-on system fields).
  # When no allowlist is declared, leave the field set alone.
  # Delegates to field_allowlist so allowlist symbols declared as Ruby
  # property names (snake_case) are normalized to the wire-format column
  # names (camelCase or explicit `field:` alias) before comparing against
  # Parse Server's schema keys. Without this normalization a model with
  # `agent_fields :device_type` filters against `"device_type"`, but the
  # server schema carries `"deviceType"` and the field is silently
  # stripped.
  if schema["fields"] && (allowed = field_allowlist(class_name))
    schema["fields"] = schema["fields"].select { |name, _| allowed.include?(name) }
  end

  # Strip noisy per-field metadata regardless of allowlist
  if schema["fields"]
    schema["fields"] = schema["fields"].transform_values do |config|
      next config unless config.is_a?(Hash)
      cleaned = config.reject { |k, _| NOISY_FIELD_METADATA.include?(k) }
      # Drop defaultValue if it's effectively empty (nil/empty string carry no signal)
      cleaned = cleaned.reject { |k, v| k == "defaultValue" && (v.nil? || v == "") }
      cleaned
    end
  end

  # Add agent-allowed methods (filtered by permission)
  available_methods = klass.agent_methods_for(agent_permission)
  if available_methods.any?
    schema["agent_methods"] = format_methods(available_methods)
  end

  # Surface the canonical "valid state" filter so an LLM that opts
  # out via `apply_canonical_filter: false` on a query can
  # reproduce the same predicate manually. The filter is applied
  # BY DEFAULT on `query_class`/`count_objects`/`aggregate`.
  canonical = klass.respond_to?(:agent_canonical_filter_for_apply) ?
    klass.agent_canonical_filter_for_apply : nil
  if canonical && canonical.any?
    schema["canonical_filter"] = canonical.dup
  end

  # Echo the wire-format `agent_fields` allowlist explicitly. The
  # registry already enforces the allowlist by stripping non-allowed
  # fields from `schema["fields"]`, but enforcement-by-omission left
  # an LLM guessing what it could write in `keys:` and led to
  # repeated refusals on storage-form column names (`_p_author`,
  # etc.). Listing the wire names alongside the trimmed fields hash
  # closes that gap. `ALWAYS_KEEP_FIELDS` (objectId/createdAt/
  # updatedAt) is filtered out — those are always available and
  # would only noise up the echo.
  allowed = field_allowlist(class_name)
  if allowed && (allowed - ALWAYS_KEEP_FIELDS).any?
    schema["agent_fields"] = (allowed - ALWAYS_KEEP_FIELDS)
  end

  # Echo the narrower join projection (wire-format) when declared.
  # Tells the LLM "when I'm included as a pointer on another class's
  # query, you'll see these fields and nothing else" so it can plan
  # the include path without a follow-up `get_schema`.
  join_proj = join_projection_fields(class_name)
  if join_proj && (join_proj[:project] - ALWAYS_KEEP_FIELDS).any?
    schema["agent_join_fields"] = (join_proj[:project] - ALWAYS_KEEP_FIELDS)
  end

  # Embed this class's relationship edges (incoming/outgoing) so the LLM
  # sees pointer/relation context alongside fields. Keeps each schema
  # response self-contained without the cost of the full graph.
  per_class = Parse::Agent::RelationGraph.edges_for(class_name, edges)
  if per_class[:outgoing].any? || per_class[:incoming].any?
    schema["relations"] = {
      "outgoing" => per_class[:outgoing].map { |e| edge_summary(e) },
      "incoming" => per_class[:incoming].map { |e| edge_summary(e) },
    }
  end

  schema
end

#enriched_schemas(server_schemas, agent_permission: :readonly) ⇒ Array<Hash>

Enrich multiple schemas at once. Builds the relation graph exactly once and threads it through each per-schema enrichment so the combined call is O(classes) rather than O(classes^2).

Parameters:

  • server_schemas (Array<Hash>)

    schemas from Parse Server

  • agent_permission (Symbol) (defaults to: :readonly)

    the agent’s permission level

Returns:



489
490
491
492
493
494
# File 'lib/parse/agent/metadata_registry.rb', line 489

def enriched_schemas(server_schemas, agent_permission: :readonly)
  edges = Parse::Agent::RelationGraph.build
  server_schemas.map do |schema|
    enriched_schema(schema["className"], schema, agent_permission: agent_permission, edges: edges)
  end
end

#field_allowlist(class_name) ⇒ Array<String>?

Resolve the agent_fields allowlist for a Parse class name. Returns an array of field-name strings including the always-keep system fields, or nil when the model has no allowlist declared (callers should treat nil as “no filtering — return everything”).

Parameters:

  • class_name (String)

    the Parse class name

Returns:



357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
# File 'lib/parse/agent/metadata_registry.rb', line 357

def field_allowlist(class_name)
  klass = find_model_class(class_name)
  return nil unless klass&.respond_to?(:agent_field_allowlist)
  allowlist = klass.agent_field_allowlist
  return nil if allowlist.empty?
  # Translate each allowlist entry to its wire-format column name.
  # Priority: the class's field_map (Ruby symbol -> wire symbol) so
  # explicit `field:` aliases (`property :external_id, field: "ExtId"`)
  # resolve to the actual column. Fallback: `String#columnize` so plain
  # snake_case Ruby names (`:device_type` -> `"deviceType"`) match
  # Parse Server's lowerCamelCase wire format. Without this translation
  # the allowlist filter was case-sensitive against snake_case strings
  # and silently stripped legitimate camelCase columns from schema
  # enrichment, `keys:` projection, and pipeline policy enforcement.
  fmap = klass.respond_to?(:field_map) ? klass.field_map : {}
  resolved = allowlist.map do |name|
    mapped = fmap[name.to_sym]
    # When field_map carries an explicit wire name (e.g. a `property
    # :external_id, field: :ExternalReferenceCode` alias), use it
    # verbatim — columnize would lowercase the first character and
    # break the alias. Without a mapping, columnize the Ruby symbol
    # to convert snake_case to lowerCamelCase wire format.
    mapped ? mapped.to_s : name.to_s.columnize
  end
  # Defense-in-depth: refuse to surface Parse Server internal columns
  # (`_hashed_password`, `_session_token`, `_rperm`/`_wperm`, etc.) on
  # the agent surface, regardless of whether a developer accidentally
  # mapped a `property :pw, field: :_hashed_password` and listed it in
  # `agent_fields`. The columnize fallback already strips the leading
  # underscore for snake_case entries; this drop targets the wire-name
  # path that bypasses columnize.
  resolved.reject! { |wire| Parse::PipelineSecurity::INTERNAL_FIELDS_DENYLIST.include?(wire) }
  resolved | ALWAYS_KEEP_FIELDS
end

#filter_visible_schemas(schemas) ⇒ Array<Hash>

Filter schemas to only include visible classes. If no classes are marked visible, returns all schemas.

Parameters:

  • schemas (Array<Hash>)

    schemas from Parse Server

Returns:



226
227
228
229
230
231
# File 'lib/parse/agent/metadata_registry.rb', line 226

def filter_visible_schemas(schemas)
  return schemas unless has_visible_classes?

  visible_names = visible_class_names
  schemas.select { |s| visible_names.include?(s["className"]) }
end

#finalize_join_projection(project, dropped, source) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



475
476
477
478
479
480
# File 'lib/parse/agent/metadata_registry.rb', line 475

def finalize_join_projection(project, dropped, source)
  project = (project | ALWAYS_KEEP_FIELDS)
  project.reject! { |wire| Parse::PipelineSecurity::INTERNAL_FIELDS_DENYLIST.include?(wire) }
  dropped = dropped.reject { |wire| Parse::PipelineSecurity::INTERNAL_FIELDS_DENYLIST.include?(wire) }
  { project: project, dropped: dropped, source: source }
end

#has_metadata?(class_name) ⇒ Boolean

Check if a model class has agent metadata.

Parameters:

  • class_name (String)

    the Parse class name

Returns:

  • (Boolean)


530
531
532
533
# File 'lib/parse/agent/metadata_registry.rb', line 530

def has_metadata?(class_name)
  klass = find_model_class(class_name)
  klass&.respond_to?(:has_agent_metadata?) && klass.has_agent_metadata?
end

#has_visible_classes?Boolean

Check if any classes are registered as visible.

Returns:

  • (Boolean)


217
218
219
# File 'lib/parse/agent/metadata_registry.rb', line 217

def has_visible_classes?
  @visible_mutex.synchronize { @visible_classes.any? }
end

#hidden?(class_name) ⇒ Boolean

Check whether a class name is denied to agent tools.

An LLM writing aggregations against Parse-on-Mongo will naturally type system classes by their alias form (‘“User”`, `“Role”`, `“Installation”`, `“Session”`) even though the canonical `parse_class` is the `_`-prefixed form (`“_User”`, etc.). Similarly, a class declared with `parse_class “Foo”` lives in the registry as `“Foo”` but a caller might pass the Ruby class name.

#hidden_name_variants_for expands each registered hidden class to every form a caller might submit; this predicate is a pure string match against that expanded set. Closes the oracle where an LLM could write ‘$lookup: { from: “User” }` and bypass an `agent_hidden`-on-`Parse::User` because the registry only knew `“_User”`.

Parameters:

Returns:

  • (Boolean)


134
135
136
137
# File 'lib/parse/agent/metadata_registry.rb', line 134

def hidden?(class_name)
  return false if class_name.nil?
  hidden_name_set.include?(class_name.to_s)
end

#hidden_class_namesArray<String>

Class names (Parse class names) that are hidden from every agent tool.

Returns:



110
111
112
113
114
# File 'lib/parse/agent/metadata_registry.rb', line 110

def hidden_class_names
  @hidden_mutex.synchronize { @hidden_classes.dup }.map do |klass|
    klass.respond_to?(:parse_class) ? klass.parse_class : klass.name
  end
end

#hidden_exception_for(class_name) ⇒ Symbol?

Look up the per-class hidden-exception scope (‘:master_key` or nil) for a Parse class name. Returns nil when the class is not hidden at all OR when it is hidden with no exception. Caller must compare against the agent’s auth context to decide whether the exception applies.

Parameters:

Returns:



96
97
98
99
100
101
102
103
104
105
106
# File 'lib/parse/agent/metadata_registry.rb', line 96

def hidden_exception_for(class_name)
  return nil if class_name.nil?
  target = class_name.to_s
  @hidden_mutex.synchronize do
    @hidden_classes.each do |klass|
      next unless hidden_name_variants_for(klass).include?(target)
      return @hidden_exceptions[klass]
    end
  end
  nil
end

#hidden_name_setArray<String>

All hidden-class name variants a caller might submit. Includes the canonical ‘parse_class`, the un-prefixed alias when `parse_class` starts with `_` (system-class form), and the Ruby class name when it differs from `parse_class` (`parse_class “Foo”` override). The `hidden_name_variants_for` helper MUST NOT take `@hidden_mutex` —it’s called from inside the synchronize block here, and recursive locking would deadlock.

Returns:



147
148
149
150
151
# File 'lib/parse/agent/metadata_registry.rb', line 147

def hidden_name_set
  @hidden_mutex.synchronize do
    @hidden_classes.flat_map { |klass| hidden_name_variants_for(klass) }.uniq
  end
end

#hidden_name_variants_for(klass) ⇒ Array<String>

Compute the set of names a caller might use to reference ‘klass`.

Variants emitted:

  • ‘parse_class` (canonical, always).

  • ‘parse_class` stripped of a leading `_` (system-class alias form; e.g. `_User` -> `User`).

  • Ruby class name when it differs from ‘parse_class`.

**Known limitation — collision direction is safe but technically over-broad.** If application code declares one class with ‘parse_class “_Foo”` and also a separate class with `parse_class “Foo”`, hiding the `_Foo` class implicitly causes `hidden?(“Foo”)` to return true as well, refusing reads on the un-prefixed sibling. The refusal direction is the safer one (false positive on the gate, not a leak), and the collision is contrived enough — `_`-prefixed parse_class names are reserved in practice for Parse’s own system classes — that we accept the trade-off. Applications that genuinely need both can either rename one, or call ‘agent_hidden` on both explicitly.

Parameters:

  • klass (Class)

Returns:



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/parse/agent/metadata_registry.rb', line 176

def hidden_name_variants_for(klass)
  variants = []
  if klass.respond_to?(:parse_class) && klass.parse_class
    pc = klass.parse_class.to_s
    variants << pc
    variants << pc.sub(/\A_/, "") if pc.start_with?("_")
  end
  if klass.respond_to?(:name) && klass.name && !klass.name.include?("::") && !variants.include?(klass.name)
    # Skip names containing `::` -- those are Ruby constant paths
    # (e.g. `"Parse::User"`) that no LLM would write in a `$lookup`,
    # and including them only adds noise to `hidden_name_set`.
    variants << klass.name
  end
  variants
end

#join_projection_fields(class_name) ⇒ Hash?

Resolve the wire-format projection set used when this class appears as an included pointer on another class’s query. Drives the auto-projection that turns ‘keys: [“user”] + include: [“user”]` into `keys: “user,user.firstName,user.email,…”` server-side.

Resolution order (first match wins):

1. `agent_join_fields` → those entries (wire-format).
2. `agent_fields` declared → `agent_fields - agent_large_fields`.
3. Only `agent_large_fields` declared → all `field_map` properties
   minus the large set.
4. None of the above → nil (no auto-projection; caller gets the
   full included record exactly as Parse Server returns it).

The returned array always includes ‘ALWAYS_KEEP_FIELDS` (objectId / createdAt / updatedAt). Internal Parse Server columns (`_hashed_password`, `_session_token`, `_rperm`, etc.) are filtered at the end as a defense-in-depth pass, identical to #field_allowlist, so an accidental `property :pw, field: :_hashed_password` cannot leak through the join surface.

Parameters:

  • class_name (String)

    the joined Parse class name

Returns:

  • (Hash, nil)

    Array<String>, dropped: Array<String>, source: Symbol or nil. ‘project` is the positive wire-format field list. `dropped` is the wire names this projection actively omits (used to populate the `truncated_include_fields` envelope). `source` is one of :join_fields, :allowlist_minus_large, :field_map_minus_large for diagnostics / testing.



420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
# File 'lib/parse/agent/metadata_registry.rb', line 420

def join_projection_fields(class_name)
  klass = find_model_class(class_name)
  return nil unless klass
  fmap = klass.respond_to?(:field_map) ? klass.field_map : {}
  to_wire = ->(sym) {
    mapped = fmap[sym.to_sym]
    mapped ? mapped.to_s : sym.to_s.columnize
  }
  large_wire = if klass.respond_to?(:agent_large_field_list)
      klass.agent_large_field_list.map(&to_wire)
    else
      []
    end

  join_list = klass.respond_to?(:agent_join_field_list) ? klass.agent_join_field_list : []
  if join_list.any?
    project = join_list.map(&to_wire)
    source = :join_fields
    # dropped: large fields that are NOT in the join projection.
    # The caller asked us to project to a narrow set; report large
    # fields they didn't include so they can re-ask explicitly.
    dropped = large_wire - project
    return finalize_join_projection(project, dropped, source)
  end

  allow_list = klass.respond_to?(:agent_field_allowlist) ? klass.agent_field_allowlist : []
  if allow_list.any?
    allow_wire = allow_list.map(&to_wire)
    project = allow_wire - large_wire
    # If everything in the allowlist is also large, fall through
    # rather than projecting to an empty set (would surface a useless
    # `{}` user object).
    unless project.empty?
      dropped = large_wire & allow_wire
      return finalize_join_projection(project, dropped, :allowlist_minus_large)
    end
  end

  if large_wire.any?
    # Strip mode: no positive allowlist, but we know which fields are
    # heavy. Project to (declared properties - large fields). Limited
    # to fields the Ruby model knows about — server-side columns not
    # declared as `property` won't come back, but that's an honest
    # trade-off (we can only project what we can name).
    known_wire = fmap.values.map(&:to_s)
    project = known_wire - large_wire
    return nil if project.empty?
    dropped = large_wire & known_wire
    return finalize_join_projection(project, dropped, :field_map_minus_large)
  end

  nil
end

#property_descriptions(class_name) ⇒ Hash<Symbol, String>

Get property descriptions for a Parse class if registered.

Parameters:

  • class_name (String)

    the Parse class name

Returns:



509
510
511
512
513
# File 'lib/parse/agent/metadata_registry.rb', line 509

def property_descriptions(class_name)
  klass = find_model_class(class_name)
  return {} unless klass&.respond_to?(:property_descriptions)
  klass.property_descriptions || {}
end

#register_hidden_class(klass, except: nil) ⇒ Object

Register a class as hidden from agent tools (opt-in PII denial).

Parameters:

  • klass (Class)

    the model class

  • except (Symbol, nil) (defaults to: nil)

    when ‘:master_key`, the class is still reachable by master-key agents but refused for session-bound agents. When nil (default), the class is hidden from every agent regardless of auth context. Re-calling `register_hidden_class` with a different `except:` value updates the scope (last-write-wins) — this is what lets an application re-mark `Parse::Session` with the relaxed scope after the parse-stack default marked it with the strict one.



64
65
66
67
68
69
70
71
72
73
# File 'lib/parse/agent/metadata_registry.rb', line 64

def register_hidden_class(klass, except: nil)
  @hidden_mutex.synchronize do
    @hidden_classes << klass unless @hidden_classes.include?(klass)
    if except.nil?
      @hidden_exceptions.delete(klass)
    else
      @hidden_exceptions[klass] = except
    end
  end
end

#register_tenant_scope(class_name, field, from:) ⇒ Object

Register a tenant scope rule for a class.

Parameters:

  • class_name (String)

    the Parse class name

  • field (Symbol)

    the field to scope on

  • from (Proc)

    callable receiving agent, returning the scope value



568
569
570
571
572
# File 'lib/parse/agent/metadata_registry.rb', line 568

def register_tenant_scope(class_name, field, from:)
  @tenant_scope_mutex.synchronize do
    @tenant_scope_rules[class_name.to_s] = { field: field.to_sym, from: from }
  end
end

#register_tenant_scope_bypass(class_name, bypass_proc) ⇒ Object

Register a bypass proc for a class’s tenant scope.

Parameters:

  • class_name (String)

    the Parse class name

  • bypass_proc (Proc)

    callable receiving agent, returning truthy to bypass



578
579
580
581
582
# File 'lib/parse/agent/metadata_registry.rb', line 578

def register_tenant_scope_bypass(class_name, bypass_proc)
  @tenant_scope_bypass_mutex.synchronize do
    @tenant_scope_bypasses[class_name.to_s] = bypass_proc
  end
end

#register_visible_class(klass) ⇒ Object

Register a class as visible to agents.

Parameters:

  • klass (Class)

    the model class



49
50
51
52
53
# File 'lib/parse/agent/metadata_registry.rb', line 49

def register_visible_class(klass)
  @visible_mutex.synchronize do
    @visible_classes << klass unless @visible_classes.include?(klass)
  end
end

#resolve_tenant_scope(class_name, agent) ⇒ Hash?

Resolve the effective tenant scope for a class and agent.

Returns nil when:

- No agent_tenant_scope is declared for this class (back-compat pass-through).
- The bypass condition is satisfied (admin agents, etc.).

Returns { field: Symbol, value: Object } when a scope should be enforced.

Raises Parse::Agent::AccessDenied when:

- A scope rule is declared and the bypass is not satisfied, but the
  agent's scope value (from: proc) returns nil — meaning the agent
  has no tenant binding and must not touch this class.

Parameters:

  • class_name (String)

    the Parse class name

  • agent (Parse::Agent)

    the agent instance

Returns:

  • (Hash, nil)

    { field: Symbol, value: Object } or nil

Raises:



626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
# File 'lib/parse/agent/metadata_registry.rb', line 626

def resolve_tenant_scope(class_name, agent)
  rule = tenant_scope_rule(class_name)
  return nil unless rule

  return nil if tenant_scope_bypassed?(class_name, agent)

  value = rule[:from].call(agent)
  if value.nil?
    raise Parse::Agent::AccessDenied.new(
      class_name,
      "Agent has no tenant binding for class '#{class_name}' which requires tenant scoping",
    )
  end

  { field: rule[:field], value: value }
end

#tenant_scope_bypassed?(class_name, agent) ⇒ Boolean

Check whether the given agent should bypass the tenant scope for a class. Returns false when no bypass is registered or when the bypass proc raises.

Parameters:

  • class_name (String)

    the Parse class name

  • agent (Parse::Agent)

    the agent instance

Returns:

  • (Boolean)


598
599
600
601
602
603
604
605
606
607
# File 'lib/parse/agent/metadata_registry.rb', line 598

def tenant_scope_bypassed?(class_name, agent)
  bypass = @tenant_scope_bypass_mutex.synchronize { @tenant_scope_bypasses[class_name.to_s] }
  return false unless bypass
  begin
    !!bypass.call(agent)
  rescue StandardError
    # A bypass proc that raises is treated as not-bypassed (fail closed).
    false
  end
end

#tenant_scope_rule(class_name) ⇒ Hash?

Return the tenant scope rule for a class name, or nil if none declared.

Parameters:

  • class_name (String)

    the Parse class name

Returns:

  • (Hash, nil)

    { field: Symbol, from: Proc } or nil



588
589
590
# File 'lib/parse/agent/metadata_registry.rb', line 588

def tenant_scope_rule(class_name)
  @tenant_scope_mutex.synchronize { @tenant_scope_rules[class_name.to_s] }
end

#unregister_hidden_class(klass) ⇒ Object

Reverse a prior ‘register_hidden_class` call. Used by `agent_unhidden` to re-expose a class that was marked hidden by an upstream declaration (typically a parse-stack built-in like `Parse::Product` or a base class in an application’s own model hierarchy). Removing the class from the registry is what actually allows ‘query_class` / `aggregate` / schema enumeration etc. to address it again — the per-class `@agent_hidden` ivar alone is not consulted by the tool surface.

Parameters:

  • klass (Class)

    the model class



83
84
85
86
87
88
# File 'lib/parse/agent/metadata_registry.rb', line 83

def unregister_hidden_class(klass)
  @hidden_mutex.synchronize do
    @hidden_classes.delete(klass)
    @hidden_exceptions.delete(klass)
  end
end

#visible_class_namesArray<String>

Get visible class names (Parse class names).

Returns:



209
210
211
212
213
# File 'lib/parse/agent/metadata_registry.rb', line 209

def visible_class_names
  visible_classes.map do |klass|
    klass.respond_to?(:parse_class) ? klass.parse_class : klass.name
  end
end

#visible_classesArray<Class>

Get all registered visible classes.

Returns:



203
204
205
# File 'lib/parse/agent/metadata_registry.rb', line 203

def visible_classes
  @visible_mutex.synchronize { @visible_classes.dup }
end