Module: Parse::Agent::MetadataDSL::ClassMethods
- Defined in:
- lib/parse/agent/metadata_dsl.rb
Constant Summary collapse
- AGENT_METHOD_PERMISSIONS =
Permission levels for agent methods (matches Parse::Agent permission levels)
%i[readonly write admin].freeze
- WRITE_METHOD_PATTERNS =
Patterns that suggest a method performs write operations Used to warn developers who may have misclassified a method as readonly
[ /save/i, /update/i, /delete/i, /destroy/i, /create/i, /remove/i, /insert/i, /upsert/i, /modify/i, /set/i, /clear/i, /reset/i, /add/i, /append/i, /push/i, /increment/i, /decrement/i, ].freeze
Instance Method Summary collapse
-
#agent_admin(method_name, description = nil) ⇒ Hash
Convenience method: mark a method as requiring admin permission.
-
#agent_allow_collscan(value = nil) ⇒ Boolean
Opt this class out of the global COLLSCAN refusal check.
-
#agent_allow_collscan? ⇒ Boolean
Check whether COLLSCANs are explicitly permitted for this class.
-
#agent_can_call?(method_name, agent_permission) ⇒ Boolean
Check if an agent with given permission can call a specific method.
-
#agent_canonical_filter(filter = nil) ⇒ Hash?
Declare a canonical “valid state” filter for this class that the agent’s read tools (‘query_class`, `count_objects`, `aggregate`) apply BY DEFAULT to every call.
-
#agent_canonical_filter_for_apply ⇒ Hash?
Read-only accessor for the canonical filter.
-
#agent_description(text = nil) ⇒ String?
Set or get the class-level description for agent context.
-
#agent_field_allowlist ⇒ Array<Symbol>
Read-only accessor for the agent field allowlist.
-
#agent_fields(*names) ⇒ Array<Symbol>
Declare which fields are surfaced to agent tools for this class.
-
#agent_hidden(except: nil) ⇒ Boolean
Mark this class as hidden from agent tools.
-
#agent_hidden? ⇒ Boolean
Check if this class is hidden from agent tools.
-
#agent_hidden_except ⇒ Symbol?
The exception scope a previous ‘agent_hidden(except: …)` declared, or nil when the class is unconditionally hidden / not hidden at all.
-
#agent_join_field_list ⇒ Array<Symbol>
Read-only accessor for the agent join-projection list.
-
#agent_join_fields(*names) ⇒ Array<Symbol>
Declare a narrower projection used when this class shows up as an included pointer on another class’s query (‘query_class` / `get_object` / `get_objects` / `get_sample_objects` / `export_data` + `include:`).
-
#agent_large_field_list ⇒ Array<Symbol>
Read-only accessor for the large-field list.
-
#agent_large_fields(*names) ⇒ Array<Symbol>
Declare fields known to carry large payloads (full text, embedded documents, base64 blobs, long descriptions).
-
#agent_metadata ⇒ Hash
Get all agent metadata as a hash for serialization.
-
#agent_method(method_name, description = nil, permission: :readonly, supports_dry_run: false, permitted_keys: nil, parameters: nil) ⇒ Hash
Mark a method as callable by the agent with an optional description.
-
#agent_method_allowed?(method_name) ⇒ Boolean
Check if a specific method is allowed for agent invocation.
-
#agent_method_info(method_name) ⇒ Hash?
Get metadata for a specific agent-allowed method.
-
#agent_methods ⇒ Hash<Symbol, Hash>
Storage hash for agent-allowed methods.
-
#agent_methods_for(agent_permission) ⇒ Hash<Symbol, Hash>
Get all methods available to an agent with given permission level.
-
#agent_readonly(method_name, description = nil) ⇒ Hash
Convenience method: mark a method as readonly-accessible (default).
-
#agent_tenant_scope(field, from:) ⇒ Object
Declare a tenant scope rule for this class.
-
#agent_tenant_scope_bypass {|agent| ... } ⇒ Object
Declare a bypass condition for this class’s tenant scope.
-
#agent_unhidden ⇒ Boolean
Reverse a previous ‘agent_hidden` declaration on this class.
-
#agent_usage(text = nil) ⇒ String?
Class-level analytics usage hint, surfaced inside agent schema output.
-
#agent_visible ⇒ Boolean
Mark this class as visible to agents.
-
#agent_visible? ⇒ Boolean
Check if this class is marked as visible to agents.
-
#agent_write(method_name, description = nil) ⇒ Hash
Convenience method: mark a method as requiring write permission.
-
#has_agent_metadata? ⇒ Boolean
Check if this model has any agent metadata defined.
Instance Method Details
#agent_admin(method_name, description = nil) ⇒ Hash
Convenience method: mark a method as requiring admin permission
542 543 544 |
# File 'lib/parse/agent/metadata_dsl.rb', line 542 def agent_admin(method_name, description = nil) agent_method(method_name, description, permission: :admin) end |
#agent_allow_collscan(value = nil) ⇒ Boolean
Opt this class out of the global COLLSCAN refusal check. Intended for small lookup tables (Roles, Config) where full scans are acceptable and an index is not needed.
379 380 381 382 |
# File 'lib/parse/agent/metadata_dsl.rb', line 379 def agent_allow_collscan(value = nil) return @agent_allow_collscan if value.nil? @agent_allow_collscan = value == true end |
#agent_allow_collscan? ⇒ Boolean
Check whether COLLSCANs are explicitly permitted for this class.
386 387 388 |
# File 'lib/parse/agent/metadata_dsl.rb', line 386 def agent_allow_collscan? @agent_allow_collscan == true end |
#agent_can_call?(method_name, agent_permission) ⇒ Boolean
Check if an agent with given permission can call a specific method. Permission hierarchy: admin > write > readonly
670 671 672 673 674 675 676 |
# File 'lib/parse/agent/metadata_dsl.rb', line 670 def agent_can_call?(method_name, ) method_info = agent_methods[method_name.to_sym] return false unless method_info = method_info[:permission] || :readonly (, ) end |
#agent_canonical_filter(filter = nil) ⇒ Hash?
Declare a canonical “valid state” filter for this class that the agent’s read tools (‘query_class`, `count_objects`, `aggregate`) apply BY DEFAULT to every call. Closes the silently-suspect- counts gap: when a class soft-deletes via `isRemoved`, hides rows via `on_timeline: false`, or has any other always-applied validity predicate, the canonical filter ensures an LLM that drops to raw aggregate doesn’t accidentally include the excluded rows.
The filter is a MongoDB-style match expression (the same shape ‘query_class`’s ‘where:` argument accepts). When applied:
- `query_class` / `count_objects`: merged with the caller's
`where:` via top-level `$and` so caller constraints
compose rather than override.
- `aggregate`: prepended as a `$match` stage at index 0
(after tenant-scope injection).
Callers opt out per call with ‘apply_canonical_filter: false`. The filter is also surfaced via `get_schema` so an opt-out caller can reproduce it manually.
345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 |
# File 'lib/parse/agent/metadata_dsl.rb', line 345 def agent_canonical_filter(filter = nil) return @agent_canonical_filter if filter.nil? raise ArgumentError, "agent_canonical_filter expects a Hash, got #{filter.class}" unless filter.is_a?(Hash) # Validate at registration time so a developer misconfiguration # (e.g. `$where`, `$function`, or an internal-field key) fails at # app boot rather than silently bypassing PipelineValidator at # request time. The filter is treated like a permissive pipeline # node: server-side JS operators and internal-field keys are refused; # normal Mongo query operators ($ne, $gt, $exists, etc.) are allowed. begin Parse::PipelineSecurity.validate_filter!(filter) rescue Parse::PipelineSecurity::Error => e raise ArgumentError, "agent_canonical_filter rejected: #{e.}" end @agent_canonical_filter = filter.transform_keys(&:to_s).freeze end |
#agent_canonical_filter_for_apply ⇒ Hash?
Read-only accessor for the canonical filter.
364 365 366 |
# File 'lib/parse/agent/metadata_dsl.rb', line 364 def agent_canonical_filter_for_apply @agent_canonical_filter end |
#agent_description(text = nil) ⇒ String?
Set or get the class-level description for agent context. This description helps LLMs understand what this class represents.
173 174 175 176 177 178 179 |
# File 'lib/parse/agent/metadata_dsl.rb', line 173 def agent_description(text = nil) if text @agent_description = text.to_s.freeze else @agent_description end end |
#agent_field_allowlist ⇒ Array<Symbol>
Read-only accessor for the agent field allowlist.
211 212 213 |
# File 'lib/parse/agent/metadata_dsl.rb', line 211 def agent_field_allowlist @agent_field_allowlist || [] end |
#agent_fields(*names) ⇒ Array<Symbol>
Declare which fields are surfaced to agent tools for this class. When set, agent schema enrichment trims the field list down to this allowlist (plus the always-on ‘objectId`/`createdAt`/`updatedAt`), and agent query/fetch tools push the allowlist into the server-side `keys` projection unless the caller passed an explicit `keys:` override. Called without arguments, returns the current allowlist.
199 200 201 202 203 204 205 206 207 |
# File 'lib/parse/agent/metadata_dsl.rb', line 199 def agent_fields(*names) return @agent_field_allowlist ||= [] if names.empty? @agent_field_allowlist = names.flatten.map(&:to_sym).freeze # If agent_join_fields was declared earlier in the class body, the # subset invariant must still hold once agent_fields lands. Re-check # so declaration order doesn't matter. assert_agent_join_fields_subset! @agent_field_allowlist end |
#agent_hidden(except: nil) ⇒ Boolean
Mark this class as hidden from agent tools. Hidden classes are filtered out of ‘get_all_schemas`, refused by `query_class` / `count_objects` / `get_object` / `get_objects` / `get_sample_objects` / `aggregate` / `explain_query` / `get_schema` with a sanitized `:permission_denied` error response, and excluded from the `RelationGraph` prompt diagram.
Unlike ‘agent_visible` (which is opt-in for diagram-walking only), `agent_hidden` is a hard access denial. Use it for classes that contain PII the agent must never touch — student SSN tables, internal billing records, password reset tokens, etc.
Records still exist in the database; only the agent surface is blocked. Direct application code (Parse::Object#query, Parse::MongoDB) is unaffected.
92 93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/parse/agent/metadata_dsl.rb', line 92 def agent_hidden(except: nil) @agent_hidden = true @agent_hidden_except = case except when nil then nil when :master_key, "master_key" then :master_key else raise ArgumentError, "agent_hidden(except:) accepts only :master_key (got #{except.inspect})" end Parse::Agent::MetadataRegistry.register_hidden_class(self, except: @agent_hidden_except) true end |
#agent_hidden? ⇒ Boolean
Check if this class is hidden from agent tools.
150 151 152 |
# File 'lib/parse/agent/metadata_dsl.rb', line 150 def agent_hidden? @agent_hidden == true end |
#agent_hidden_except ⇒ Symbol?
The exception scope a previous ‘agent_hidden(except: …)` declared, or nil when the class is unconditionally hidden / not hidden at all. Currently the only supported value is `:master_key`.
158 159 160 |
# File 'lib/parse/agent/metadata_dsl.rb', line 158 def agent_hidden_except @agent_hidden_except end |
#agent_join_field_list ⇒ Array<Symbol>
Read-only accessor for the agent join-projection list.
278 279 280 |
# File 'lib/parse/agent/metadata_dsl.rb', line 278 def agent_join_field_list @agent_join_field_list || [] end |
#agent_join_fields(*names) ⇒ Array<Symbol>
Declare a narrower projection used when this class shows up as an included pointer on another class’s query (‘query_class` / `get_object` / `get_objects` / `get_sample_objects` / `export_data` + `include:`). When the agent asks for `keys: [“user”, …] + include: [“user”]`, the SDK auto-rewrites `keys` to dotted paths (`user.firstName, user.email, …`) so the joined record is projected to exactly the fields listed here.
This sits one tier tighter than ‘agent_fields`. The direct-query allowlist is typically the full “what the agent may see” set; the join-projection list is the narrower “what’s interesting when I’m a foreign key” set. Example: ‘_User` may surface 18 fields on a direct query, but when it’s joined onto a ‘Membership` row the agent usually only needs `firstName`, `lastName`, `email`, `internalTag` — not the `teams[]` pointer array or the `iconImage` presigned URL.
**Subset invariant**: when both ‘agent_fields` and `agent_join_fields` are declared, every entry in `agent_join_fields` MUST also appear in `agent_fields`. The direct-query allowlist is the upper bound on what the agent ever sees; the join list can only tighten that, never widen it. Violations raise `ArgumentError` at class load time. Declaring `agent_join_fields` without `agent_fields` is allowed — it means “no direct-query allowlist, but on a join project to these only.”
When ‘agent_join_fields` is NOT declared, the auto-projection falls back to `agent_fields - agent_large_fields` (or, when only `agent_large_fields` is declared, to `field_map.keys - agent_large_fields`). Callers can always opt out per call by passing dotted-path keys (`keys: [“user.iconImage”]`), which signals explicit intent and suppresses auto-expansion for that pointer.
269 270 271 272 273 274 |
# File 'lib/parse/agent/metadata_dsl.rb', line 269 def agent_join_fields(*names) return @agent_join_field_list ||= [] if names.empty? @agent_join_field_list = names.flatten.map(&:to_sym).freeze assert_agent_join_fields_subset! @agent_join_field_list end |
#agent_large_field_list ⇒ Array<Symbol>
Read-only accessor for the large-field list.
309 310 311 |
# File 'lib/parse/agent/metadata_dsl.rb', line 309 def agent_large_field_list @agent_large_fields || [] end |
#agent_large_fields(*names) ⇒ Array<Symbol>
Declare fields known to carry large payloads (full text, embedded documents, base64 blobs, long descriptions). Schema introspection annotates these with ‘large_field: true` so an LLM client can project them away proactively in its first `query_class` call rather than discovering the size by hitting the dispatcher’s response cap. Has no effect on Pointer/Relation type fields —the stored value is a small reference; size only materializes via ‘include:` resolution, which is a query-time concern. Called without arguments, returns the current list.
302 303 304 305 |
# File 'lib/parse/agent/metadata_dsl.rb', line 302 def agent_large_fields(*names) return @agent_large_fields ||= [] if names.empty? @agent_large_fields = names.flatten.map(&:to_sym).freeze end |
#agent_metadata ⇒ Hash
Get all agent metadata as a hash for serialization.
613 614 615 616 617 618 619 620 621 622 623 |
# File 'lib/parse/agent/metadata_dsl.rb', line 613 def { description: agent_description, usage: agent_usage, property_descriptions: property_descriptions.dup, property_enum_descriptions: property_enum_descriptions.dup, methods: agent_methods.dup, field_allowlist: agent_field_allowlist.dup, join_field_list: agent_join_field_list.dup, } end |
#agent_method(method_name, description = nil, permission: :readonly, supports_dry_run: false, permitted_keys: nil, parameters: nil) ⇒ Hash
Mark a method as callable by the agent with an optional description. Only methods marked with this DSL can be invoked via the ‘call_method` tool.
464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 |
# File 'lib/parse/agent/metadata_dsl.rb', line 464 def agent_method(method_name, description = nil, permission: :readonly, supports_dry_run: false, permitted_keys: nil, parameters: nil) method_sym = method_name.to_sym unless AGENT_METHOD_PERMISSIONS.include?() raise ArgumentError, "Invalid permission level: #{}. Must be one of: #{AGENT_METHOD_PERMISSIONS.join(", ")}" end if permitted_keys && !permitted_keys.is_a?(Array) raise ArgumentError, "permitted_keys must be an Array of Symbol/String, got #{permitted_keys.class}" end # Determine if this is an instance or class method # Note: method_defined? checks instance methods, respond_to? checks class methods method_type = if method_defined?(method_sym) :instance elsif respond_to?(method_sym) || singleton_methods.include?(method_sym) :class else # Method not yet defined - we'll check again at runtime :unknown end agent_methods[method_sym] = { description: description&.to_s&.freeze, type: method_type, permission: , supports_dry_run: supports_dry_run == true, permitted_keys: permitted_keys&.map(&:to_sym)&.freeze, parameters: parameters, } end |
#agent_method_allowed?(method_name) ⇒ Boolean
Check if a specific method is allowed for agent invocation.
652 653 654 |
# File 'lib/parse/agent/metadata_dsl.rb', line 652 def agent_method_allowed?(method_name) agent_methods.key?(method_name.to_sym) end |
#agent_method_info(method_name) ⇒ Hash?
Get metadata for a specific agent-allowed method.
660 661 662 |
# File 'lib/parse/agent/metadata_dsl.rb', line 660 def agent_method_info(method_name) agent_methods[method_name.to_sym] end |
#agent_methods ⇒ Hash<Symbol, Hash>
Storage hash for agent-allowed methods. Maps method names (symbols) to their metadata hashes.
412 413 414 |
# File 'lib/parse/agent/metadata_dsl.rb', line 412 def agent_methods @agent_methods ||= {} end |
#agent_methods_for(agent_permission) ⇒ Hash<Symbol, Hash>
Get all methods available to an agent with given permission level.
682 683 684 685 686 |
# File 'lib/parse/agent/metadata_dsl.rb', line 682 def agent_methods_for() agent_methods.select do |_name, info| (, info[:permission] || :readonly) end end |
#agent_readonly(method_name, description = nil) ⇒ Hash
Convenience method: mark a method as readonly-accessible (default)
WARNING: This method checks if the method name suggests write behavior (save, update, delete, etc.) and emits a warning. This helps developers catch potential security misconfigurations early.
509 510 511 512 513 514 515 516 517 518 519 520 |
# File 'lib/parse/agent/metadata_dsl.rb', line 509 def agent_readonly(method_name, description = nil) method_str = method_name.to_s # Warn if method name suggests it performs write operations if WRITE_METHOD_PATTERNS.any? { |pattern| method_str.match?(pattern) } warn "[Parse::Agent::MetadataDSL] WARNING: Method '#{method_name}' on #{name} " \ "is marked as agent_readonly but its name suggests it may perform writes. " \ "Consider using agent_write or agent_admin if this method modifies data." end agent_method(method_name, description, permission: :readonly) end |
#agent_tenant_scope(field, from:) ⇒ Object
Declare a tenant scope rule for this class.
When declared, every agent read tool (query_class, count_objects, get_sample_objects, export_data query-mode, aggregate, get_object, get_objects) will enforce that data access is limited to the agent’s bound tenant. An agent with no tenant binding (tenant_id: nil) hitting a scoped class is refused with :access_denied unless the bypass condition is satisfied.
565 566 567 568 569 570 571 |
# File 'lib/parse/agent/metadata_dsl.rb', line 565 def agent_tenant_scope(field, from:) unless from.respond_to?(:call) raise ArgumentError, "agent_tenant_scope :from must be a callable (Proc/lambda)" end parse_class_name = respond_to?(:parse_class) ? parse_class : name Parse::Agent::MetadataRegistry.register_tenant_scope(parse_class_name, field, from: from) end |
#agent_tenant_scope_bypass {|agent| ... } ⇒ Object
Declare a bypass condition for this class’s tenant scope.
When the block returns truthy for the given agent, tenant scope enforcement is skipped entirely for that agent on this class. A bypass block that raises is treated as not-bypassed (fail closed).
Without a bypass declaration, any agent whose tenant_id is nil hitting a scoped class is refused.
591 592 593 594 595 |
# File 'lib/parse/agent/metadata_dsl.rb', line 591 def agent_tenant_scope_bypass(&block) raise ArgumentError, "agent_tenant_scope_bypass requires a block" unless block_given? parse_class_name = respond_to?(:parse_class) ? parse_class : name Parse::Agent::MetadataRegistry.register_tenant_scope_bypass(parse_class_name, block) end |
#agent_unhidden ⇒ Boolean
Reverse a previous ‘agent_hidden` declaration on this class. Clears the per-class hidden flag and removes the class from the registry’s hidden set so that every agent tool surface treats the class as visible again (subject to the per-tool ‘agent_fields` allowlist and other policy). The field-level `INTERNAL_FIELDS_DENYLIST` floor still strips credential columns from every response.
The intended use is to opt back in to a built-in class that parse-stack marks hidden by default — for example ‘Parse::Product`, which is hidden in `lib/parse/agent.rb` because the `_Product` collection is a vestigial iOS IAP feature, but an application that actually does use the collection can call:
Parse::Product.agent_unhidden
at boot time (after ‘require ’parse/stack’‘) to expose it. The same mechanism applies to any application-defined class that was marked `agent_hidden` and needs to be re-enabled for a specific deployment.
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
# File 'lib/parse/agent/metadata_dsl.rb', line 129 def agent_unhidden was_hidden = @agent_hidden == true @agent_hidden = false @agent_hidden_except = nil Parse::Agent::MetadataRegistry.unregister_hidden_class(self) # Only audit on a real state flip — calling `agent_unhidden` on a # class that was never hidden is a no-op and shouldn't emit a banner # that trains operators to suppress the warning globally. if was_hidden && !(defined?(Parse::Agent) && Parse::Agent.respond_to?(:suppress_master_key_warning?) && Parse::Agent.suppress_master_key_warning?) warn "[Parse::Agent:SECURITY] #{name} (#{respond_to?(:parse_class) ? parse_class : name}) was marked agent_unhidden — " \ "this class is now reachable from every agent tool surface (query_class, aggregate, get_schema, etc.). " \ "Master-key agents bypass per-row ACL/CLP enforcement, so per-class agent_fields / agent_canonical_filter / " \ "tenant_id are the only remaining access boundary. Credential columns are still stripped by the " \ "INTERNAL_FIELDS_DENYLIST floor regardless of class visibility. Confirm this is intentional. " \ "Silence with Parse::Agent.suppress_master_key_warning = true." end was_hidden end |
#agent_usage(text = nil) ⇒ String?
Class-level analytics usage hint, surfaced inside agent schema output. Distinct from ‘agent_description` (a short human summary): use this for specific guidance the LLM needs to query the class well — enum values, denormalization caveats, recommended aggregations, etc.
403 404 405 406 |
# File 'lib/parse/agent/metadata_dsl.rb', line 403 def agent_usage(text = nil) return @agent_usage unless text @agent_usage = text.to_s.strip.freeze end |
#agent_visible ⇒ Boolean
Mark this class as visible to agents. Only classes marked with agent_visible will be included in schema listings. If no classes are marked, all classes are shown (backwards compatible).
46 47 48 49 50 |
# File 'lib/parse/agent/metadata_dsl.rb', line 46 def agent_visible @agent_visible = true Parse::Agent::MetadataRegistry.register_visible_class(self) true end |
#agent_visible? ⇒ Boolean
Check if this class is marked as visible to agents
54 55 56 |
# File 'lib/parse/agent/metadata_dsl.rb', line 54 def agent_visible? @agent_visible == true end |
#agent_write(method_name, description = nil) ⇒ Hash
Convenience method: mark a method as requiring write permission
530 531 532 |
# File 'lib/parse/agent/metadata_dsl.rb', line 530 def agent_write(method_name, description = nil) agent_method(method_name, description, permission: :write) end |
#has_agent_metadata? ⇒ Boolean
Check if this model has any agent metadata defined.
600 601 602 603 604 605 606 607 608 |
# File 'lib/parse/agent/metadata_dsl.rb', line 600 def !agent_description.nil? || !agent_usage.nil? || !property_descriptions.empty? || !property_enum_descriptions.empty? || !agent_methods.empty? || !agent_field_allowlist.empty? || !agent_join_field_list.empty? end |