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_hiddendeclaration 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 archived, hides
rows via published: 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 Subscription row the
agent usually only needs firstName, lastName, email,
category — not the workspaces[] 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 |