parse-stack-next 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.bundle/config +2 -0
- data/.env.sample +112 -0
- data/.env.test +10 -0
- data/.github/workflows/ruby.yml +36 -0
- data/.gitignore +49 -0
- data/.ruby-version +1 -0
- data/.solargraph.yml +22 -0
- data/CHANGELOG.md +5816 -0
- data/Gemfile +30 -0
- data/Gemfile.lock +175 -0
- data/LICENSE.txt +23 -0
- data/Makefile +63 -0
- data/README.md +5655 -0
- data/Rakefile +573 -0
- data/bin/console +38 -0
- data/bin/parse-console +136 -0
- data/bin/server +17 -0
- data/bin/setup +7 -0
- data/config/parse-config.json +12 -0
- data/docs/TEST_SERVER.md +271 -0
- data/docs/_config.yml +1 -0
- data/docs/mcp_guide.md +3484 -0
- data/docs/mongodb_direct_guide.md +1348 -0
- data/docs/mongodb_index_optimization_guide.md +631 -0
- data/examples/transaction_example.rb +219 -0
- data/lib/parse/acl_scope.rb +728 -0
- data/lib/parse/agent/cancellation_token.rb +80 -0
- data/lib/parse/agent/constraint_translator.rb +480 -0
- data/lib/parse/agent/describe.rb +420 -0
- data/lib/parse/agent/errors.rb +133 -0
- data/lib/parse/agent/mcp_client.rb +557 -0
- data/lib/parse/agent/mcp_dispatcher.rb +1023 -0
- data/lib/parse/agent/mcp_rack_app.rb +1143 -0
- data/lib/parse/agent/mcp_server.rb +376 -0
- data/lib/parse/agent/metadata_audit.rb +259 -0
- data/lib/parse/agent/metadata_dsl.rb +733 -0
- data/lib/parse/agent/metadata_registry.rb +794 -0
- data/lib/parse/agent/pipeline_validator.rb +82 -0
- data/lib/parse/agent/prompts.rb +351 -0
- data/lib/parse/agent/rate_limiter.rb +158 -0
- data/lib/parse/agent/relation_graph.rb +162 -0
- data/lib/parse/agent/result_formatter.rb +453 -0
- data/lib/parse/agent/tools.rb +5489 -0
- data/lib/parse/agent.rb +3249 -0
- data/lib/parse/api/aggregate.rb +79 -0
- data/lib/parse/api/all.rb +26 -0
- data/lib/parse/api/analytics.rb +18 -0
- data/lib/parse/api/batch.rb +33 -0
- data/lib/parse/api/cloud_functions.rb +58 -0
- data/lib/parse/api/config.rb +125 -0
- data/lib/parse/api/files.rb +29 -0
- data/lib/parse/api/hooks.rb +117 -0
- data/lib/parse/api/objects.rb +146 -0
- data/lib/parse/api/path_segment.rb +75 -0
- data/lib/parse/api/push.rb +20 -0
- data/lib/parse/api/schema.rb +49 -0
- data/lib/parse/api/server.rb +50 -0
- data/lib/parse/api/sessions.rb +24 -0
- data/lib/parse/api/users.rb +250 -0
- data/lib/parse/atlas_search/index_manager.rb +353 -0
- data/lib/parse/atlas_search/result.rb +204 -0
- data/lib/parse/atlas_search/search_builder.rb +604 -0
- data/lib/parse/atlas_search/session.rb +253 -0
- data/lib/parse/atlas_search.rb +995 -0
- data/lib/parse/client/authentication.rb +97 -0
- data/lib/parse/client/batch.rb +234 -0
- data/lib/parse/client/body_builder.rb +240 -0
- data/lib/parse/client/caching.rb +203 -0
- data/lib/parse/client/logging.rb +293 -0
- data/lib/parse/client/profiling.rb +181 -0
- data/lib/parse/client/protocol.rb +91 -0
- data/lib/parse/client/request.rb +233 -0
- data/lib/parse/client/response.rb +208 -0
- data/lib/parse/client.rb +1104 -0
- data/lib/parse/clp_scope.rb +361 -0
- data/lib/parse/live_query/circuit_breaker.rb +256 -0
- data/lib/parse/live_query/client.rb +1001 -0
- data/lib/parse/live_query/configuration.rb +224 -0
- data/lib/parse/live_query/event.rb +115 -0
- data/lib/parse/live_query/event_queue.rb +272 -0
- data/lib/parse/live_query/health_monitor.rb +214 -0
- data/lib/parse/live_query/logging.rb +149 -0
- data/lib/parse/live_query/subscription.rb +294 -0
- data/lib/parse/live_query.rb +163 -0
- data/lib/parse/lookup_rewriter.rb +445 -0
- data/lib/parse/model/acl.rb +968 -0
- data/lib/parse/model/associations/belongs_to.rb +275 -0
- data/lib/parse/model/associations/collection_proxy.rb +435 -0
- data/lib/parse/model/associations/has_many.rb +597 -0
- data/lib/parse/model/associations/has_one.rb +158 -0
- data/lib/parse/model/associations/pointer_collection_proxy.rb +134 -0
- data/lib/parse/model/associations/relation_collection_proxy.rb +177 -0
- data/lib/parse/model/bytes.rb +62 -0
- data/lib/parse/model/classes/audience.rb +262 -0
- data/lib/parse/model/classes/installation.rb +363 -0
- data/lib/parse/model/classes/job_schedule.rb +153 -0
- data/lib/parse/model/classes/job_status.rb +264 -0
- data/lib/parse/model/classes/product.rb +75 -0
- data/lib/parse/model/classes/push_status.rb +263 -0
- data/lib/parse/model/classes/role.rb +751 -0
- data/lib/parse/model/classes/session.rb +201 -0
- data/lib/parse/model/classes/user.rb +943 -0
- data/lib/parse/model/clp.rb +544 -0
- data/lib/parse/model/core/actions.rb +1268 -0
- data/lib/parse/model/core/builder.rb +139 -0
- data/lib/parse/model/core/create_lock.rb +386 -0
- data/lib/parse/model/core/describe.rb +382 -0
- data/lib/parse/model/core/enhanced_change_tracking.rb +159 -0
- data/lib/parse/model/core/errors.rb +38 -0
- data/lib/parse/model/core/fetching.rb +566 -0
- data/lib/parse/model/core/field_guards.rb +220 -0
- data/lib/parse/model/core/indexing.rb +382 -0
- data/lib/parse/model/core/parse_reference.rb +407 -0
- data/lib/parse/model/core/properties.rb +809 -0
- data/lib/parse/model/core/querying.rb +491 -0
- data/lib/parse/model/core/schema.rb +202 -0
- data/lib/parse/model/core/search_indexing.rb +174 -0
- data/lib/parse/model/date.rb +88 -0
- data/lib/parse/model/email.rb +213 -0
- data/lib/parse/model/file.rb +527 -0
- data/lib/parse/model/geojson.rb +271 -0
- data/lib/parse/model/geopoint.rb +261 -0
- data/lib/parse/model/model.rb +260 -0
- data/lib/parse/model/object.rb +2068 -0
- data/lib/parse/model/phone.rb +520 -0
- data/lib/parse/model/pointer.rb +443 -0
- data/lib/parse/model/polygon.rb +406 -0
- data/lib/parse/model/push.rb +975 -0
- data/lib/parse/model/shortnames.rb +8 -0
- data/lib/parse/model/time_zone.rb +141 -0
- data/lib/parse/model/validations/uniqueness_validator.rb +97 -0
- data/lib/parse/model/validations.rb +96 -0
- data/lib/parse/mongodb.rb +2300 -0
- data/lib/parse/pipeline_security.rb +554 -0
- data/lib/parse/query/constraint.rb +198 -0
- data/lib/parse/query/constraints.rb +3279 -0
- data/lib/parse/query/cursor.rb +434 -0
- data/lib/parse/query/n_plus_one_detector.rb +445 -0
- data/lib/parse/query/operation.rb +104 -0
- data/lib/parse/query/ordering.rb +66 -0
- data/lib/parse/query.rb +7028 -0
- data/lib/parse/schema/index_migrator.rb +291 -0
- data/lib/parse/schema/search_index_migrator.rb +289 -0
- data/lib/parse/schema.rb +494 -0
- data/lib/parse/stack/generators/rails.rb +40 -0
- data/lib/parse/stack/generators/templates/model.erb +51 -0
- data/lib/parse/stack/generators/templates/model_installation.rb +4 -0
- data/lib/parse/stack/generators/templates/model_role.rb +4 -0
- data/lib/parse/stack/generators/templates/model_session.rb +4 -0
- data/lib/parse/stack/generators/templates/model_user.rb +11 -0
- data/lib/parse/stack/generators/templates/parse.rb +12 -0
- data/lib/parse/stack/generators/templates/webhooks.rb +10 -0
- data/lib/parse/stack/railtie.rb +18 -0
- data/lib/parse/stack/tasks.rb +563 -0
- data/lib/parse/stack/version.rb +11 -0
- data/lib/parse/stack.rb +455 -0
- data/lib/parse/two_factor_auth/user_extension.rb +449 -0
- data/lib/parse/two_factor_auth.rb +310 -0
- data/lib/parse/webhooks/payload.rb +360 -0
- data/lib/parse/webhooks/registration.rb +199 -0
- data/lib/parse/webhooks/replay_protection.rb +189 -0
- data/lib/parse/webhooks.rb +510 -0
- data/lib/parse-stack-next.rb +5 -0
- data/lib/parse-stack.rb +5 -0
- data/parse-stack-next.gemspec +82 -0
- data/parse-stack.png +0 -0
- data/scripts/debug-ips.js +35 -0
- data/scripts/docker/Dockerfile.parse +13 -0
- data/scripts/docker/atlas-init.js +284 -0
- data/scripts/docker/docker-compose.atlas.yml +76 -0
- data/scripts/docker/docker-compose.test.yml +106 -0
- data/scripts/docker/mongo-init.js +21 -0
- data/scripts/eval_mcp_with_lm_studio.rb +274 -0
- data/scripts/start-parse.sh +90 -0
- data/scripts/start_mcp_server.rb +78 -0
- data/scripts/test_server_connection.rb +82 -0
- metadata +377 -0
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Parse
|
|
5
|
+
class Agent
|
|
6
|
+
# DSL module that adds agent metadata capabilities to Parse::Object models.
|
|
7
|
+
# Allows models to self-document with descriptions and expose safe methods
|
|
8
|
+
# to the Parse Agent for LLM interaction.
|
|
9
|
+
#
|
|
10
|
+
# @example Define a model with agent metadata
|
|
11
|
+
# class Team < Parse::Object
|
|
12
|
+
# agent_description "A group of users contributing to a Project"
|
|
13
|
+
#
|
|
14
|
+
# property :name, :string, description: "The team's display name"
|
|
15
|
+
# property :member_count, :integer, description: "Number of active members"
|
|
16
|
+
#
|
|
17
|
+
# agent_method :active_projects, "Returns projects currently in progress"
|
|
18
|
+
# agent_method :member_names, "Returns array of member display names"
|
|
19
|
+
#
|
|
20
|
+
# def self.active_projects
|
|
21
|
+
# Project.query(status: "active")
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# def member_names
|
|
25
|
+
# members.map(&:display_name)
|
|
26
|
+
# end
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
module MetadataDSL
|
|
30
|
+
def self.included(base)
|
|
31
|
+
base.extend(ClassMethods)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
module ClassMethods
|
|
35
|
+
# Mark this class as visible to agents.
|
|
36
|
+
# Only classes marked with agent_visible will be included in schema listings.
|
|
37
|
+
# If no classes are marked, all classes are shown (backwards compatible).
|
|
38
|
+
#
|
|
39
|
+
# @example Mark a class as agent-visible
|
|
40
|
+
# class Song < Parse::Object
|
|
41
|
+
# agent_visible
|
|
42
|
+
# agent_description "A music track"
|
|
43
|
+
# end
|
|
44
|
+
#
|
|
45
|
+
# @return [Boolean] true
|
|
46
|
+
def agent_visible
|
|
47
|
+
@agent_visible = true
|
|
48
|
+
Parse::Agent::MetadataRegistry.register_visible_class(self)
|
|
49
|
+
true
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Check if this class is marked as visible to agents
|
|
53
|
+
# @return [Boolean]
|
|
54
|
+
def agent_visible?
|
|
55
|
+
@agent_visible == true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Mark this class as hidden from agent tools. Hidden classes are
|
|
59
|
+
# filtered out of `get_all_schemas`, refused by `query_class` /
|
|
60
|
+
# `count_objects` / `get_object` / `get_objects` / `get_sample_objects` /
|
|
61
|
+
# `aggregate` / `explain_query` / `get_schema` with a sanitized
|
|
62
|
+
# `:permission_denied` error response, and excluded from the
|
|
63
|
+
# `RelationGraph` prompt diagram.
|
|
64
|
+
#
|
|
65
|
+
# Unlike `agent_visible` (which is opt-in for diagram-walking only),
|
|
66
|
+
# `agent_hidden` is a hard access denial. Use it for classes that
|
|
67
|
+
# contain PII the agent must never touch — student SSN tables,
|
|
68
|
+
# internal billing records, password reset tokens, etc.
|
|
69
|
+
#
|
|
70
|
+
# Records still exist in the database; only the agent surface is
|
|
71
|
+
# blocked. Direct application code (Parse::Object#query, Parse::MongoDB)
|
|
72
|
+
# is unaffected.
|
|
73
|
+
#
|
|
74
|
+
# @example Hide a PII class from every agent surface
|
|
75
|
+
# class StudentSSN < Parse::Object
|
|
76
|
+
# parse_class "StudentSSN"
|
|
77
|
+
# property :student_name, :string
|
|
78
|
+
# property :ssn, :string
|
|
79
|
+
# agent_hidden
|
|
80
|
+
# end
|
|
81
|
+
#
|
|
82
|
+
# @param except [Symbol, nil] when set to `:master_key`, session-bound
|
|
83
|
+
# agents refuse this class but master-key agents are allowed through.
|
|
84
|
+
# This is the "internal admin tooling can see it, user-facing agents
|
|
85
|
+
# never can" tier — intended for collections like `_Session` where a
|
|
86
|
+
# dev-MCP / customer-support tool may legitimately need read access
|
|
87
|
+
# but no end-user-bound agent ever should. The field-level
|
|
88
|
+
# `INTERNAL_FIELDS_DENYLIST` floor (sessionToken, _hashed_password,
|
|
89
|
+
# etc.) still applies, so even master-key reads cannot exfiltrate
|
|
90
|
+
# credential columns.
|
|
91
|
+
# @return [Boolean] true
|
|
92
|
+
def agent_hidden(except: nil)
|
|
93
|
+
@agent_hidden = true
|
|
94
|
+
@agent_hidden_except = case except
|
|
95
|
+
when nil then nil
|
|
96
|
+
when :master_key, "master_key" then :master_key
|
|
97
|
+
else
|
|
98
|
+
raise ArgumentError,
|
|
99
|
+
"agent_hidden(except:) accepts only :master_key (got #{except.inspect})"
|
|
100
|
+
end
|
|
101
|
+
Parse::Agent::MetadataRegistry.register_hidden_class(self, except: @agent_hidden_except)
|
|
102
|
+
true
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Reverse a previous `agent_hidden` declaration on this class. Clears the
|
|
106
|
+
# per-class hidden flag and removes the class from the registry's hidden
|
|
107
|
+
# set so that every agent tool surface treats the class as visible again
|
|
108
|
+
# (subject to the per-tool `agent_fields` allowlist and other policy).
|
|
109
|
+
# The field-level `INTERNAL_FIELDS_DENYLIST` floor still strips
|
|
110
|
+
# credential columns from every response.
|
|
111
|
+
#
|
|
112
|
+
# The intended use is to opt back in to a built-in class that
|
|
113
|
+
# parse-stack marks hidden by default — for example `Parse::Product`,
|
|
114
|
+
# which is hidden in `lib/parse/agent.rb` because the `_Product`
|
|
115
|
+
# collection is a vestigial iOS IAP feature, but an application that
|
|
116
|
+
# actually does use the collection can call:
|
|
117
|
+
#
|
|
118
|
+
# Parse::Product.agent_unhidden
|
|
119
|
+
#
|
|
120
|
+
# at boot time (after `require 'parse/stack'`) to expose it. The same
|
|
121
|
+
# mechanism applies to any application-defined class that was marked
|
|
122
|
+
# `agent_hidden` and needs to be re-enabled for a specific deployment.
|
|
123
|
+
#
|
|
124
|
+
# @return [Boolean] true if a previous `agent_hidden` declaration was
|
|
125
|
+
# actually reversed; false when the class was not hidden to begin
|
|
126
|
+
# with (idempotent no-op). Matches `Hash#delete?`/`Set#delete?`
|
|
127
|
+
# "did anything change" semantics so callers can branch on the
|
|
128
|
+
# return value.
|
|
129
|
+
def agent_unhidden
|
|
130
|
+
was_hidden = @agent_hidden == true
|
|
131
|
+
@agent_hidden = false
|
|
132
|
+
@agent_hidden_except = nil
|
|
133
|
+
Parse::Agent::MetadataRegistry.unregister_hidden_class(self)
|
|
134
|
+
# Only audit on a real state flip — calling `agent_unhidden` on a
|
|
135
|
+
# class that was never hidden is a no-op and shouldn't emit a banner
|
|
136
|
+
# that trains operators to suppress the warning globally.
|
|
137
|
+
if was_hidden && !(defined?(Parse::Agent) && Parse::Agent.respond_to?(:suppress_master_key_warning?) && Parse::Agent.suppress_master_key_warning?)
|
|
138
|
+
warn "[Parse::Agent:SECURITY] #{name} (#{respond_to?(:parse_class) ? parse_class : name}) was marked agent_unhidden — " \
|
|
139
|
+
"this class is now reachable from every agent tool surface (query_class, aggregate, get_schema, etc.). " \
|
|
140
|
+
"Master-key agents bypass per-row ACL/CLP enforcement, so per-class agent_fields / agent_canonical_filter / " \
|
|
141
|
+
"tenant_id are the only remaining access boundary. Credential columns are still stripped by the " \
|
|
142
|
+
"INTERNAL_FIELDS_DENYLIST floor regardless of class visibility. Confirm this is intentional. " \
|
|
143
|
+
"Silence with Parse::Agent.suppress_master_key_warning = true."
|
|
144
|
+
end
|
|
145
|
+
was_hidden
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Check if this class is hidden from agent tools.
|
|
149
|
+
# @return [Boolean]
|
|
150
|
+
def agent_hidden?
|
|
151
|
+
@agent_hidden == true
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# The exception scope a previous `agent_hidden(except: ...)` declared,
|
|
155
|
+
# or nil when the class is unconditionally hidden / not hidden at all.
|
|
156
|
+
# Currently the only supported value is `:master_key`.
|
|
157
|
+
# @return [Symbol, nil]
|
|
158
|
+
def agent_hidden_except
|
|
159
|
+
@agent_hidden_except
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Set or get the class-level description for agent context.
|
|
163
|
+
# This description helps LLMs understand what this class represents.
|
|
164
|
+
#
|
|
165
|
+
# @example Set a description
|
|
166
|
+
# agent_description "A music track in the catalog"
|
|
167
|
+
#
|
|
168
|
+
# @example Get the description
|
|
169
|
+
# Song.agent_description # => "A music track in the catalog"
|
|
170
|
+
#
|
|
171
|
+
# @param text [String, nil] the description to set, or nil to get
|
|
172
|
+
# @return [String, nil] the current description
|
|
173
|
+
def agent_description(text = nil)
|
|
174
|
+
if text
|
|
175
|
+
@agent_description = text.to_s.freeze
|
|
176
|
+
else
|
|
177
|
+
@agent_description
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Property descriptions are stored in Parse::Properties module.
|
|
182
|
+
# This method is provided there via the `property` DSL with `_description:` option.
|
|
183
|
+
# @see Parse::Properties::ClassMethods#property_descriptions
|
|
184
|
+
|
|
185
|
+
# Declare which fields are surfaced to agent tools for this class.
|
|
186
|
+
# When set, agent schema enrichment trims the field list down to this
|
|
187
|
+
# allowlist (plus the always-on `objectId`/`createdAt`/`updatedAt`), and
|
|
188
|
+
# agent query/fetch tools push the allowlist into the server-side `keys`
|
|
189
|
+
# projection unless the caller passed an explicit `keys:` override.
|
|
190
|
+
# Called without arguments, returns the current allowlist.
|
|
191
|
+
#
|
|
192
|
+
# @example Limit agent visibility to analytics-relevant fields
|
|
193
|
+
# class Team < Parse::Object
|
|
194
|
+
# agent_fields :name, :status, :member_count, :owner
|
|
195
|
+
# end
|
|
196
|
+
#
|
|
197
|
+
# @param names [Array<Symbol, String>] field names to allow
|
|
198
|
+
# @return [Array<Symbol>] the resulting allowlist
|
|
199
|
+
def agent_fields(*names)
|
|
200
|
+
return @agent_field_allowlist ||= [] if names.empty?
|
|
201
|
+
@agent_field_allowlist = names.flatten.map(&:to_sym).freeze
|
|
202
|
+
# If agent_join_fields was declared earlier in the class body, the
|
|
203
|
+
# subset invariant must still hold once agent_fields lands. Re-check
|
|
204
|
+
# so declaration order doesn't matter.
|
|
205
|
+
assert_agent_join_fields_subset!
|
|
206
|
+
@agent_field_allowlist
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Read-only accessor for the agent field allowlist.
|
|
210
|
+
# @return [Array<Symbol>] the allowlist (empty if not declared)
|
|
211
|
+
def agent_field_allowlist
|
|
212
|
+
@agent_field_allowlist || []
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Declare a narrower projection used when this class shows up as an
|
|
216
|
+
# included pointer on another class's query (`query_class` /
|
|
217
|
+
# `get_object` / `get_objects` / `get_sample_objects` /
|
|
218
|
+
# `export_data` + `include:`). When the agent asks for
|
|
219
|
+
# `keys: ["user", ...] + include: ["user"]`, the SDK auto-rewrites
|
|
220
|
+
# `keys` to dotted paths (`user.firstName, user.email, ...`) so the
|
|
221
|
+
# joined record is projected to exactly the fields listed here.
|
|
222
|
+
#
|
|
223
|
+
# This sits one tier tighter than `agent_fields`. The direct-query
|
|
224
|
+
# allowlist is typically the full "what the agent may see" set;
|
|
225
|
+
# the join-projection list is the narrower "what's interesting when
|
|
226
|
+
# I'm a foreign key" set. Example: `_User` may surface 18 fields on
|
|
227
|
+
# a direct query, but when it's joined onto a `Membership` row the
|
|
228
|
+
# agent usually only needs `firstName`, `lastName`, `email`,
|
|
229
|
+
# `internalTag` — not the `teams[]` pointer array or the
|
|
230
|
+
# `iconImage` presigned URL.
|
|
231
|
+
#
|
|
232
|
+
# **Subset invariant**: when both `agent_fields` and
|
|
233
|
+
# `agent_join_fields` are declared, every entry in
|
|
234
|
+
# `agent_join_fields` MUST also appear in `agent_fields`. The
|
|
235
|
+
# direct-query allowlist is the upper bound on what the agent ever
|
|
236
|
+
# sees; the join list can only tighten that, never widen it.
|
|
237
|
+
# Violations raise `ArgumentError` at class load time. Declaring
|
|
238
|
+
# `agent_join_fields` without `agent_fields` is allowed — it means
|
|
239
|
+
# "no direct-query allowlist, but on a join project to these only."
|
|
240
|
+
#
|
|
241
|
+
# When `agent_join_fields` is NOT declared, the auto-projection
|
|
242
|
+
# falls back to `agent_fields - agent_large_fields` (or, when only
|
|
243
|
+
# `agent_large_fields` is declared, to `field_map.keys -
|
|
244
|
+
# agent_large_fields`). Callers can always opt out per call by
|
|
245
|
+
# passing dotted-path keys (`keys: ["user.iconImage"]`), which
|
|
246
|
+
# signals explicit intent and suppresses auto-expansion for that
|
|
247
|
+
# pointer.
|
|
248
|
+
#
|
|
249
|
+
# @example
|
|
250
|
+
# class Membership < Parse::Object
|
|
251
|
+
# belongs_to :user
|
|
252
|
+
# property :title, :string
|
|
253
|
+
# property :active, :boolean
|
|
254
|
+
# # …
|
|
255
|
+
# end
|
|
256
|
+
#
|
|
257
|
+
# # In the _User reopen / customization:
|
|
258
|
+
# class Parse::User
|
|
259
|
+
# agent_fields :first_name, :last_name, :email, :icon_image,
|
|
260
|
+
# :source_image, :teams, :organizations, :last_active_at,
|
|
261
|
+
# :internal_tag
|
|
262
|
+
# agent_large_fields :icon_image, :source_image
|
|
263
|
+
# agent_join_fields :first_name, :last_name, :email,
|
|
264
|
+
# :last_active_at, :internal_tag
|
|
265
|
+
# end
|
|
266
|
+
#
|
|
267
|
+
# @param names [Array<Symbol, String>] field names to project on join
|
|
268
|
+
# @return [Array<Symbol>] the resulting join-projection list
|
|
269
|
+
def agent_join_fields(*names)
|
|
270
|
+
return @agent_join_field_list ||= [] if names.empty?
|
|
271
|
+
@agent_join_field_list = names.flatten.map(&:to_sym).freeze
|
|
272
|
+
assert_agent_join_fields_subset!
|
|
273
|
+
@agent_join_field_list
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Read-only accessor for the agent join-projection list.
|
|
277
|
+
# @return [Array<Symbol>] the list (empty if not declared)
|
|
278
|
+
def agent_join_field_list
|
|
279
|
+
@agent_join_field_list || []
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Declare fields known to carry large payloads (full text, embedded
|
|
283
|
+
# documents, base64 blobs, long descriptions). Schema introspection
|
|
284
|
+
# annotates these with `large_field: true` so an LLM client can
|
|
285
|
+
# project them away proactively in its first `query_class` call
|
|
286
|
+
# rather than discovering the size by hitting the dispatcher's
|
|
287
|
+
# response cap. Has no effect on Pointer/Relation type fields —
|
|
288
|
+
# the stored value is a small reference; size only materializes
|
|
289
|
+
# via `include:` resolution, which is a query-time concern.
|
|
290
|
+
# Called without arguments, returns the current list.
|
|
291
|
+
#
|
|
292
|
+
# @example Flag the long-text fields up-front
|
|
293
|
+
# class Article < Parse::Object
|
|
294
|
+
# property :title, :string
|
|
295
|
+
# property :body, :string
|
|
296
|
+
# property :raw_html, :string
|
|
297
|
+
# agent_large_fields :body, :raw_html
|
|
298
|
+
# end
|
|
299
|
+
#
|
|
300
|
+
# @param names [Array<Symbol, String>] field names known to be large
|
|
301
|
+
# @return [Array<Symbol>] the resulting list
|
|
302
|
+
def agent_large_fields(*names)
|
|
303
|
+
return @agent_large_fields ||= [] if names.empty?
|
|
304
|
+
@agent_large_fields = names.flatten.map(&:to_sym).freeze
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Read-only accessor for the large-field list.
|
|
308
|
+
# @return [Array<Symbol>] the declared large fields (empty if none)
|
|
309
|
+
def agent_large_field_list
|
|
310
|
+
@agent_large_fields || []
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Declare a canonical "valid state" filter for this class that the
|
|
314
|
+
# agent's read tools (`query_class`, `count_objects`, `aggregate`)
|
|
315
|
+
# apply BY DEFAULT to every call. Closes the silently-suspect-
|
|
316
|
+
# counts gap: when a class soft-deletes via `isRemoved`, hides
|
|
317
|
+
# rows via `on_timeline: false`, or has any other always-applied
|
|
318
|
+
# validity predicate, the canonical filter ensures an LLM that
|
|
319
|
+
# drops to raw aggregate doesn't accidentally include the
|
|
320
|
+
# excluded rows.
|
|
321
|
+
#
|
|
322
|
+
# The filter is a MongoDB-style match expression (the same shape
|
|
323
|
+
# `query_class`'s `where:` argument accepts). When applied:
|
|
324
|
+
# - `query_class` / `count_objects`: merged with the caller's
|
|
325
|
+
# `where:` via top-level `$and` so caller constraints
|
|
326
|
+
# compose rather than override.
|
|
327
|
+
# - `aggregate`: prepended as a `$match` stage at index 0
|
|
328
|
+
# (after tenant-scope injection).
|
|
329
|
+
#
|
|
330
|
+
# Callers opt out per call with `apply_canonical_filter: false`.
|
|
331
|
+
# The filter is also surfaced via `get_schema` so an opt-out
|
|
332
|
+
# caller can reproduce it manually.
|
|
333
|
+
#
|
|
334
|
+
# @example
|
|
335
|
+
# class Capture < Parse::Object
|
|
336
|
+
# property :isRemoved, :boolean
|
|
337
|
+
# property :onTimeline, :boolean
|
|
338
|
+
# agent_canonical_filter "isRemoved" => { "$ne" => true },
|
|
339
|
+
# "onTimeline" => true
|
|
340
|
+
# end
|
|
341
|
+
#
|
|
342
|
+
# @param filter [Hash, nil] a where-style hash. Pass nil to
|
|
343
|
+
# read the current value.
|
|
344
|
+
# @return [Hash, nil] the filter, or nil when not declared.
|
|
345
|
+
def agent_canonical_filter(filter = nil)
|
|
346
|
+
return @agent_canonical_filter if filter.nil?
|
|
347
|
+
raise ArgumentError, "agent_canonical_filter expects a Hash, got #{filter.class}" unless filter.is_a?(Hash)
|
|
348
|
+
# Validate at registration time so a developer misconfiguration
|
|
349
|
+
# (e.g. `$where`, `$function`, or an internal-field key) fails at
|
|
350
|
+
# app boot rather than silently bypassing PipelineValidator at
|
|
351
|
+
# request time. The filter is treated like a permissive pipeline
|
|
352
|
+
# node: server-side JS operators and internal-field keys are refused;
|
|
353
|
+
# normal Mongo query operators ($ne, $gt, $exists, etc.) are allowed.
|
|
354
|
+
begin
|
|
355
|
+
Parse::PipelineSecurity.validate_filter!(filter)
|
|
356
|
+
rescue Parse::PipelineSecurity::Error => e
|
|
357
|
+
raise ArgumentError, "agent_canonical_filter rejected: #{e.message}"
|
|
358
|
+
end
|
|
359
|
+
@agent_canonical_filter = filter.transform_keys(&:to_s).freeze
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Read-only accessor for the canonical filter.
|
|
363
|
+
# @return [Hash, nil] the filter as String-keyed Hash, or nil
|
|
364
|
+
def agent_canonical_filter_for_apply
|
|
365
|
+
@agent_canonical_filter
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Opt this class out of the global COLLSCAN refusal check.
|
|
369
|
+
# Intended for small lookup tables (Roles, Config) where full scans
|
|
370
|
+
# are acceptable and an index is not needed.
|
|
371
|
+
#
|
|
372
|
+
# @example
|
|
373
|
+
# class AppConfig < Parse::Object
|
|
374
|
+
# agent_allow_collscan true
|
|
375
|
+
# end
|
|
376
|
+
#
|
|
377
|
+
# @param value [Boolean] true to allow COLLSCANs for this class
|
|
378
|
+
# @return [Boolean] the current setting
|
|
379
|
+
def agent_allow_collscan(value = nil)
|
|
380
|
+
return @agent_allow_collscan if value.nil?
|
|
381
|
+
@agent_allow_collscan = value == true
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# Check whether COLLSCANs are explicitly permitted for this class.
|
|
385
|
+
# @return [Boolean]
|
|
386
|
+
def agent_allow_collscan?
|
|
387
|
+
@agent_allow_collscan == true
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Class-level analytics usage hint, surfaced inside agent schema output.
|
|
391
|
+
# Distinct from `agent_description` (a short human summary): use this for
|
|
392
|
+
# specific guidance the LLM needs to query the class well — enum values,
|
|
393
|
+
# denormalization caveats, recommended aggregations, etc.
|
|
394
|
+
#
|
|
395
|
+
# @example
|
|
396
|
+
# agent_usage <<~USAGE
|
|
397
|
+
# `status` values: "active" | "archived" | "frozen".
|
|
398
|
+
# `member_count` is denormalized; recompute via _User pointer.
|
|
399
|
+
# USAGE
|
|
400
|
+
#
|
|
401
|
+
# @param text [String, nil] the usage text to set, or nil to read
|
|
402
|
+
# @return [String, nil] the current usage hint
|
|
403
|
+
def agent_usage(text = nil)
|
|
404
|
+
return @agent_usage unless text
|
|
405
|
+
@agent_usage = text.to_s.strip.freeze
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# Storage hash for agent-allowed methods.
|
|
409
|
+
# Maps method names (symbols) to their metadata hashes.
|
|
410
|
+
#
|
|
411
|
+
# @return [Hash<Symbol, Hash>]
|
|
412
|
+
def agent_methods
|
|
413
|
+
@agent_methods ||= {}
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# Permission levels for agent methods (matches Parse::Agent permission levels)
|
|
417
|
+
AGENT_METHOD_PERMISSIONS = %i[readonly write admin].freeze
|
|
418
|
+
|
|
419
|
+
# Patterns that suggest a method performs write operations
|
|
420
|
+
# Used to warn developers who may have misclassified a method as readonly
|
|
421
|
+
WRITE_METHOD_PATTERNS = [
|
|
422
|
+
/save/i, /update/i, /delete/i, /destroy/i, /create/i, /remove/i,
|
|
423
|
+
/insert/i, /upsert/i, /modify/i, /set/i, /clear/i, /reset/i,
|
|
424
|
+
/add/i, /append/i, /push/i, /increment/i, /decrement/i,
|
|
425
|
+
].freeze
|
|
426
|
+
|
|
427
|
+
# Mark a method as callable by the agent with an optional description.
|
|
428
|
+
# Only methods marked with this DSL can be invoked via the `call_method` tool.
|
|
429
|
+
#
|
|
430
|
+
# @example Mark a readonly class method (default)
|
|
431
|
+
# agent_method :find_popular, "Find songs with over 1000 plays"
|
|
432
|
+
#
|
|
433
|
+
# @example Mark an instance method requiring write permission
|
|
434
|
+
# agent_method :update_play_count, "Increment play count", permission: :write
|
|
435
|
+
#
|
|
436
|
+
# @example Mark a method requiring admin permission
|
|
437
|
+
# agent_method :reset_all_counts, "Reset all play counts to zero", permission: :admin
|
|
438
|
+
#
|
|
439
|
+
# @example Mark a write method that explicitly supports dry-run preview
|
|
440
|
+
# agent_method :archive, "Archive this record", permission: :admin, supports_dry_run: true
|
|
441
|
+
# def archive(dry_run: false)
|
|
442
|
+
# return { would_archive: id } if dry_run
|
|
443
|
+
# self.status = "archived"; save!
|
|
444
|
+
# end
|
|
445
|
+
#
|
|
446
|
+
# @param method_name [Symbol, String] the name of the method to expose
|
|
447
|
+
# @param description [String, nil] optional description for LLM context
|
|
448
|
+
# @param permission [Symbol] required permission level (:readonly, :write, :admin)
|
|
449
|
+
# @param supports_dry_run [Boolean] whether the method accepts dry_run: true for
|
|
450
|
+
# preview-only execution. When false (default), passing dry_run: true in
|
|
451
|
+
# arguments is refused at dispatch time with :invalid_argument.
|
|
452
|
+
# @param permitted_keys [Array<Symbol,String>, nil] when provided,
|
|
453
|
+
# +call_method+ refuses any +arguments+ key not in this list.
|
|
454
|
+
# Without this, an LLM (or a prompt-injection payload) can
|
|
455
|
+
# pass arbitrary keys through a method that splats with +**+,
|
|
456
|
+
# reaching protected columns like +_hashed_password+ or +ACL+.
|
|
457
|
+
# Highly recommended on any +agent_write+/+agent_admin+ method
|
|
458
|
+
# that takes a kwargs splat.
|
|
459
|
+
# @param parameters [Hash, nil] when provided, a JSON Schema (as a
|
|
460
|
+
# Ruby Hash) describing the +arguments+ object. Surfaced in
|
|
461
|
+
# +tools/list+ so the LLM submits properly-shaped inputs and
|
|
462
|
+
# stricter MCP clients can validate before dispatch.
|
|
463
|
+
# @return [Hash] the method metadata
|
|
464
|
+
def agent_method(method_name, description = nil, permission: :readonly,
|
|
465
|
+
supports_dry_run: false, permitted_keys: nil, parameters: nil)
|
|
466
|
+
method_sym = method_name.to_sym
|
|
467
|
+
|
|
468
|
+
unless AGENT_METHOD_PERMISSIONS.include?(permission)
|
|
469
|
+
raise ArgumentError, "Invalid permission level: #{permission}. Must be one of: #{AGENT_METHOD_PERMISSIONS.join(", ")}"
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
if permitted_keys && !permitted_keys.is_a?(Array)
|
|
473
|
+
raise ArgumentError, "permitted_keys must be an Array of Symbol/String, got #{permitted_keys.class}"
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# Determine if this is an instance or class method
|
|
477
|
+
# Note: method_defined? checks instance methods, respond_to? checks class methods
|
|
478
|
+
method_type = if method_defined?(method_sym)
|
|
479
|
+
:instance
|
|
480
|
+
elsif respond_to?(method_sym) || singleton_methods.include?(method_sym)
|
|
481
|
+
:class
|
|
482
|
+
else
|
|
483
|
+
# Method not yet defined - we'll check again at runtime
|
|
484
|
+
:unknown
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
agent_methods[method_sym] = {
|
|
488
|
+
description: description&.to_s&.freeze,
|
|
489
|
+
type: method_type,
|
|
490
|
+
permission: permission,
|
|
491
|
+
supports_dry_run: supports_dry_run == true,
|
|
492
|
+
permitted_keys: permitted_keys&.map(&:to_sym)&.freeze,
|
|
493
|
+
parameters: parameters,
|
|
494
|
+
}
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
# Convenience method: mark a method as readonly-accessible (default)
|
|
498
|
+
#
|
|
499
|
+
# WARNING: This method checks if the method name suggests write behavior
|
|
500
|
+
# (save, update, delete, etc.) and emits a warning. This helps developers
|
|
501
|
+
# catch potential security misconfigurations early.
|
|
502
|
+
#
|
|
503
|
+
# @example
|
|
504
|
+
# agent_readonly :find_popular, "Find songs with over 1000 plays"
|
|
505
|
+
#
|
|
506
|
+
# @param method_name [Symbol, String] the method to expose
|
|
507
|
+
# @param description [String, nil] optional description
|
|
508
|
+
# @return [Hash] the method metadata
|
|
509
|
+
def agent_readonly(method_name, description = nil)
|
|
510
|
+
method_str = method_name.to_s
|
|
511
|
+
|
|
512
|
+
# Warn if method name suggests it performs write operations
|
|
513
|
+
if WRITE_METHOD_PATTERNS.any? { |pattern| method_str.match?(pattern) }
|
|
514
|
+
warn "[Parse::Agent::MetadataDSL] WARNING: Method '#{method_name}' on #{name} " \
|
|
515
|
+
"is marked as agent_readonly but its name suggests it may perform writes. " \
|
|
516
|
+
"Consider using agent_write or agent_admin if this method modifies data."
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
agent_method(method_name, description, permission: :readonly)
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# Convenience method: mark a method as requiring write permission
|
|
523
|
+
#
|
|
524
|
+
# @example
|
|
525
|
+
# agent_write :update_play_count, "Increment the play count"
|
|
526
|
+
#
|
|
527
|
+
# @param method_name [Symbol, String] the method to expose
|
|
528
|
+
# @param description [String, nil] optional description
|
|
529
|
+
# @return [Hash] the method metadata
|
|
530
|
+
def agent_write(method_name, description = nil)
|
|
531
|
+
agent_method(method_name, description, permission: :write)
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
# Convenience method: mark a method as requiring admin permission
|
|
535
|
+
#
|
|
536
|
+
# @example
|
|
537
|
+
# agent_admin :reset_all_counts, "Reset all play counts to zero"
|
|
538
|
+
#
|
|
539
|
+
# @param method_name [Symbol, String] the method to expose
|
|
540
|
+
# @param description [String, nil] optional description
|
|
541
|
+
# @return [Hash] the method metadata
|
|
542
|
+
def agent_admin(method_name, description = nil)
|
|
543
|
+
agent_method(method_name, description, permission: :admin)
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
# Declare a tenant scope rule for this class.
|
|
547
|
+
#
|
|
548
|
+
# When declared, every agent read tool (query_class, count_objects,
|
|
549
|
+
# get_sample_objects, export_data query-mode, aggregate, get_object,
|
|
550
|
+
# get_objects) will enforce that data access is limited to the agent's
|
|
551
|
+
# bound tenant. An agent with no tenant binding (tenant_id: nil) hitting
|
|
552
|
+
# a scoped class is refused with :access_denied unless the bypass
|
|
553
|
+
# condition is satisfied.
|
|
554
|
+
#
|
|
555
|
+
# @param field [Symbol, String] the Parse field to scope on (e.g. :org_id)
|
|
556
|
+
# @param from [Proc] callable receiving the agent, returning the scope value
|
|
557
|
+
# (return nil to mean "this agent has no tenant binding")
|
|
558
|
+
#
|
|
559
|
+
# @example
|
|
560
|
+
# class Order < Parse::Object
|
|
561
|
+
# property :org_id, :string
|
|
562
|
+
# agent_tenant_scope :org_id, from: ->(agent) { agent.tenant_id }
|
|
563
|
+
# end
|
|
564
|
+
#
|
|
565
|
+
def agent_tenant_scope(field, from:)
|
|
566
|
+
unless from.respond_to?(:call)
|
|
567
|
+
raise ArgumentError, "agent_tenant_scope :from must be a callable (Proc/lambda)"
|
|
568
|
+
end
|
|
569
|
+
parse_class_name = respond_to?(:parse_class) ? parse_class : name
|
|
570
|
+
Parse::Agent::MetadataRegistry.register_tenant_scope(parse_class_name, field, from: from)
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
# Declare a bypass condition for this class's tenant scope.
|
|
574
|
+
#
|
|
575
|
+
# When the block returns truthy for the given agent, tenant scope
|
|
576
|
+
# enforcement is skipped entirely for that agent on this class.
|
|
577
|
+
# A bypass block that raises is treated as not-bypassed (fail closed).
|
|
578
|
+
#
|
|
579
|
+
# Without a bypass declaration, any agent whose tenant_id is nil
|
|
580
|
+
# hitting a scoped class is refused.
|
|
581
|
+
#
|
|
582
|
+
# @yield [agent] the agent instance
|
|
583
|
+
# @yieldreturn [Boolean] truthy to bypass, falsy to enforce
|
|
584
|
+
#
|
|
585
|
+
# @example Allow admin agents to read across tenants
|
|
586
|
+
# class Order < Parse::Object
|
|
587
|
+
# agent_tenant_scope :org_id, from: ->(agent) { agent.tenant_id }
|
|
588
|
+
# agent_tenant_scope_bypass { |agent| agent.permissions == :admin }
|
|
589
|
+
# end
|
|
590
|
+
#
|
|
591
|
+
def agent_tenant_scope_bypass(&block)
|
|
592
|
+
raise ArgumentError, "agent_tenant_scope_bypass requires a block" unless block_given?
|
|
593
|
+
parse_class_name = respond_to?(:parse_class) ? parse_class : name
|
|
594
|
+
Parse::Agent::MetadataRegistry.register_tenant_scope_bypass(parse_class_name, block)
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
# Check if this model has any agent metadata defined.
|
|
598
|
+
#
|
|
599
|
+
# @return [Boolean] true if any metadata is present
|
|
600
|
+
def has_agent_metadata?
|
|
601
|
+
!agent_description.nil? ||
|
|
602
|
+
!agent_usage.nil? ||
|
|
603
|
+
!property_descriptions.empty? ||
|
|
604
|
+
!property_enum_descriptions.empty? ||
|
|
605
|
+
!agent_methods.empty? ||
|
|
606
|
+
!agent_field_allowlist.empty? ||
|
|
607
|
+
!agent_join_field_list.empty?
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
# Get all agent metadata as a hash for serialization.
|
|
611
|
+
#
|
|
612
|
+
# @return [Hash] all agent metadata
|
|
613
|
+
def agent_metadata
|
|
614
|
+
{
|
|
615
|
+
description: agent_description,
|
|
616
|
+
usage: agent_usage,
|
|
617
|
+
property_descriptions: property_descriptions.dup,
|
|
618
|
+
property_enum_descriptions: property_enum_descriptions.dup,
|
|
619
|
+
methods: agent_methods.dup,
|
|
620
|
+
field_allowlist: agent_field_allowlist.dup,
|
|
621
|
+
join_field_list: agent_join_field_list.dup,
|
|
622
|
+
}
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
private
|
|
626
|
+
|
|
627
|
+
# @api private
|
|
628
|
+
# Subset invariant: agent_join_fields entries must all appear in
|
|
629
|
+
# agent_fields when both are declared. The direct-query allowlist
|
|
630
|
+
# is the upper bound on what the agent sees; the join list can only
|
|
631
|
+
# tighten that, never widen it. Raises ArgumentError when violated,
|
|
632
|
+
# at class-load time, so the error surfaces immediately rather than
|
|
633
|
+
# at the first agent query.
|
|
634
|
+
def assert_agent_join_fields_subset!
|
|
635
|
+
return unless @agent_join_field_list&.any?
|
|
636
|
+
return unless @agent_field_allowlist&.any?
|
|
637
|
+
extras = @agent_join_field_list - @agent_field_allowlist
|
|
638
|
+
return if extras.empty?
|
|
639
|
+
raise ArgumentError,
|
|
640
|
+
"agent_join_fields must be a subset of agent_fields on #{self}; " \
|
|
641
|
+
"#{extras.inspect} appears in agent_join_fields but not in agent_fields. " \
|
|
642
|
+
"The direct-query allowlist is the upper bound; the join-projection list " \
|
|
643
|
+
"can only tighten it."
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
public
|
|
647
|
+
|
|
648
|
+
# Check if a specific method is allowed for agent invocation.
|
|
649
|
+
#
|
|
650
|
+
# @param method_name [Symbol, String] the method name to check
|
|
651
|
+
# @return [Boolean] true if the method is agent-allowed
|
|
652
|
+
def agent_method_allowed?(method_name)
|
|
653
|
+
agent_methods.key?(method_name.to_sym)
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
# Get metadata for a specific agent-allowed method.
|
|
657
|
+
#
|
|
658
|
+
# @param method_name [Symbol, String] the method name
|
|
659
|
+
# @return [Hash, nil] the method metadata or nil if not allowed
|
|
660
|
+
def agent_method_info(method_name)
|
|
661
|
+
agent_methods[method_name.to_sym]
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
# Check if an agent with given permission can call a specific method.
|
|
665
|
+
# Permission hierarchy: admin > write > readonly
|
|
666
|
+
#
|
|
667
|
+
# @param method_name [Symbol, String] the method to check
|
|
668
|
+
# @param agent_permission [Symbol] the agent's permission level
|
|
669
|
+
# @return [Boolean] true if the agent can call this method
|
|
670
|
+
def agent_can_call?(method_name, agent_permission)
|
|
671
|
+
method_info = agent_methods[method_name.to_sym]
|
|
672
|
+
return false unless method_info
|
|
673
|
+
|
|
674
|
+
required_permission = method_info[:permission] || :readonly
|
|
675
|
+
permission_allows?(agent_permission, required_permission)
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
# Get all methods available to an agent with given permission level.
|
|
679
|
+
#
|
|
680
|
+
# @param agent_permission [Symbol] the agent's permission level
|
|
681
|
+
# @return [Hash<Symbol, Hash>] methods the agent can call
|
|
682
|
+
def agent_methods_for(agent_permission)
|
|
683
|
+
agent_methods.select do |_name, info|
|
|
684
|
+
permission_allows?(agent_permission, info[:permission] || :readonly)
|
|
685
|
+
end
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
private
|
|
689
|
+
|
|
690
|
+
# Check if agent_permission level can access required_permission level.
|
|
691
|
+
# Permission hierarchy: admin > write > readonly
|
|
692
|
+
#
|
|
693
|
+
# @param agent_permission [Symbol] what the agent has
|
|
694
|
+
# @param required_permission [Symbol] what the method requires
|
|
695
|
+
# @return [Boolean]
|
|
696
|
+
def permission_allows?(agent_permission, required_permission)
|
|
697
|
+
hierarchy = { readonly: 0, write: 1, admin: 2 }
|
|
698
|
+
agent_level = hierarchy[agent_permission] || 0
|
|
699
|
+
required_level = hierarchy[required_permission] || 0
|
|
700
|
+
agent_level >= required_level
|
|
701
|
+
end
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
# Instance method to access class-level agent description
|
|
705
|
+
#
|
|
706
|
+
# @return [String, nil]
|
|
707
|
+
def agent_description
|
|
708
|
+
self.class.agent_description
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
# Instance method to access class-level property descriptions
|
|
712
|
+
#
|
|
713
|
+
# @return [Hash<Symbol, String>]
|
|
714
|
+
def property_descriptions
|
|
715
|
+
self.class.property_descriptions
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
# Instance method to access class-level per-value enum descriptions
|
|
719
|
+
#
|
|
720
|
+
# @return [Hash<Symbol, Hash{String => String}>]
|
|
721
|
+
def property_enum_descriptions
|
|
722
|
+
self.class.property_enum_descriptions
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
# Instance method to access class-level agent methods
|
|
726
|
+
#
|
|
727
|
+
# @return [Hash<Symbol, Hash>]
|
|
728
|
+
def agent_methods
|
|
729
|
+
self.class.agent_methods
|
|
730
|
+
end
|
|
731
|
+
end
|
|
732
|
+
end
|
|
733
|
+
end
|