parse-stack-next 5.8.0 → 5.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +318 -0
- data/README.md +25 -19
- data/docs/caching.md +50 -0
- data/docs/mcp_guide.md +15 -3
- data/docs/webhooks_guide.md +59 -7
- data/lib/parse/acl_scope.rb +71 -9
- data/lib/parse/agent/describe.rb +8 -2
- data/lib/parse/agent/mcp_dispatcher.rb +8 -1
- data/lib/parse/agent.rb +205 -20
- data/lib/parse/atlas_search/index_manager.rb +5 -1
- data/lib/parse/atlas_search.rb +49 -1
- data/lib/parse/authorization.rb +235 -3
- data/lib/parse/cache/sub_cache.rb +26 -0
- data/lib/parse/client/authentication.rb +19 -1
- data/lib/parse/client/batch.rb +35 -2
- data/lib/parse/client.rb +26 -2
- data/lib/parse/clp_scope.rb +37 -3
- data/lib/parse/live_query/client.rb +72 -2
- data/lib/parse/lock_backend.rb +4 -3
- data/lib/parse/model/associations/collection_proxy.rb +19 -11
- data/lib/parse/model/associations/has_many.rb +11 -0
- data/lib/parse/model/associations/pointer_collection_proxy.rb +36 -0
- data/lib/parse/model/associations/relation_collection_proxy.rb +97 -12
- data/lib/parse/model/classes/role.rb +23 -0
- data/lib/parse/model/classes/session.rb +279 -19
- data/lib/parse/model/classes/user.rb +33 -12
- data/lib/parse/model/core/actions.rb +19 -4
- data/lib/parse/model/core/fetching.rb +29 -2
- data/lib/parse/model/core/field_guards.rb +16 -8
- data/lib/parse/model/object.rb +52 -1
- data/lib/parse/model/push.rb +11 -1
- data/lib/parse/mongodb.rb +19 -1
- data/lib/parse/query/constraint.rb +1 -1
- data/lib/parse/query/constraints.rb +31 -27
- data/lib/parse/query.rb +793 -55
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +19 -0
- data/lib/parse/vector_search/hybrid.rb +74 -15
- data/lib/parse/vector_search.rb +214 -7
- data/lib/parse/webhooks/payload.rb +43 -0
- data/lib/parse/webhooks.rb +312 -9
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: de8e9c2d24346754146f36a8c24b272840456010e08bf86f71b11d38741abad8
|
|
4
|
+
data.tar.gz: 83df07a458c9e00cae15f63fe12d4e8c19bc6913f60b297e1fa402985b2073ce
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ead735d8d48a495293b810f2b6100327869ae68051d9e1aaee87b43d51aeb51b1323e6f29a9b823a40ec0ac15b52bfc91eb838009524239bd7f1f42a63d7e02d
|
|
7
|
+
data.tar.gz: 0666b77620a76d2b19a81a5a43f3a9cd61f94661568c269ac07e612c71ff25642c4759d93f0e0b7d7370bb3930447f1fa39ed3eb986fa9bf73a63a072009cbb8
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,323 @@
|
|
|
1
1
|
## parse-stack-next Changelog
|
|
2
2
|
|
|
3
|
+
### 5.8.1
|
|
4
|
+
|
|
5
|
+
A correctness release for webhooks, sessions, associations, queries, search,
|
|
6
|
+
and agent policies. No new defaults and no API changes; every corrected
|
|
7
|
+
outcome is listed under Behavior Notes.
|
|
8
|
+
|
|
9
|
+
#### Webhooks
|
|
10
|
+
|
|
11
|
+
- **FIXED**: A client create through a `before_save` webhook whose handler
|
|
12
|
+
returns `parse_object` now gets the ACL its class policy resolves, exactly
|
|
13
|
+
as an SDK-side create would. Owner-based policies (`:owner_else_private`,
|
|
14
|
+
the default, plus `:owner_else_public` and `:owner_but_public_read`) take the
|
|
15
|
+
owner from the declared `owner:` field, or from the requesting user when that
|
|
16
|
+
field is empty. Before, a signed-in client got the policy's no-owner
|
|
17
|
+
fallback (`{}` under `:owner_else_private`) and could not read its own
|
|
18
|
+
record. A request without a user, or a master-key request, gets the policy
|
|
19
|
+
fallback. A `_User` class declared with `owner: :self` is owned by the new
|
|
20
|
+
user, never the requester. An ACL the client sent is kept, and an ACL the
|
|
21
|
+
handler sets always wins (through `acl=` or mass assignment), even when it
|
|
22
|
+
equals the class default. A `guard :acl, :master_only` revert on a create
|
|
23
|
+
returns the ACL to the class policy instead of `{}`. A handler-assigned ACL
|
|
24
|
+
is written whatever the handler returns (`parse_object`, `true`, `nil`, or
|
|
25
|
+
a Hash that does not set `ACL` itself).
|
|
26
|
+
- **FIXED**: When a `before_save` handler changes a record, a sub-document the
|
|
27
|
+
client changed is written back as one-level dotted paths (`"meta.count":
|
|
28
|
+
{...}`) instead of the whole field, so concurrent writes to other sub-keys
|
|
29
|
+
survive. A removed sub-key is written as a `Delete`. Dotted keys in a
|
|
30
|
+
returned Hash are folded into the field or `field.sub` write they belong to,
|
|
31
|
+
with operators (`Increment`, `Add`, `AddUnique`, `Remove`, `Delete`) applied
|
|
32
|
+
rather than stored. Hash keys may use Ruby names.
|
|
33
|
+
|
|
34
|
+
#### Sessions and master-key scope
|
|
35
|
+
|
|
36
|
+
- **FIXED**: Deleting `Parse::Session` or `Parse::User` objects, one at a time
|
|
37
|
+
or with a batch `Array#destroy`, drops their identity-cache entries when the
|
|
38
|
+
delete succeeds or reports the row already gone. A denied delete leaves the
|
|
39
|
+
cache alone. A session that does not carry its token or owner (built from
|
|
40
|
+
its objectId alone, fetched without the master key, or fetched with `keys:`
|
|
41
|
+
leaving them out) has them looked up first, in one query per batch: with the
|
|
42
|
+
master key when the client has one, otherwise with the delete's `session:`.
|
|
43
|
+
A lookup that finds no row leaves nothing to forget; only a lookup that
|
|
44
|
+
fails resets the client's identity cache, at most once every 5 seconds per
|
|
45
|
+
client. When the lookup finds no row, the delete uses the owner recorded
|
|
46
|
+
when a `_Session` query or `Parse::Session.session` last fetched that
|
|
47
|
+
session through the same client (only the user id, never the token, in a
|
|
48
|
+
store separate from the identity plane,
|
|
49
|
+
`client.authorization.session_owner_cache`). Without one, a batch delete
|
|
50
|
+
that applied or reported the row gone falls back to the rate-limited reset;
|
|
51
|
+
a denied delete never resets, and a single `destroy` never resets. The
|
|
52
|
+
looked-up token is used only for the delete, is never kept on the object,
|
|
53
|
+
and never enters the response cache. A token resolution in flight when an
|
|
54
|
+
invalidation lands, in this process or, on the Redis identity plane, in any
|
|
55
|
+
other, does not cache its answer: the resolver checks a shared invalidation
|
|
56
|
+
marker before and after writing and evicts its own write when the marker
|
|
57
|
+
moved.
|
|
58
|
+
- **FIXED**: `Parse::User#signup!`, signup on save, and `upgrade_anonymous!`
|
|
59
|
+
store `created_at` / `updated_at` as `Parse::Date` values. Reading
|
|
60
|
+
`updated_at` on a newly signed-up user no longer marks it dirty.
|
|
61
|
+
- **FIXED**: Inside `Parse.without_master_key`, an explicit `master: true`
|
|
62
|
+
passed to `Parse::MongoDB.aggregate`, `Parse::ACLScope`, `Parse::Query`
|
|
63
|
+
direct reads, vector and hybrid search, Atlas Search, or agent tools for
|
|
64
|
+
master-key agents no longer runs as master. The call runs in the public
|
|
65
|
+
scope, as the REST request does once the block strips its master key, and
|
|
66
|
+
the `parse.mongodb.aggregate` notification reports `master_dropped: true`.
|
|
67
|
+
The SDK's own metadata reads keep the master key without lifting the block:
|
|
68
|
+
the class schema and role graph read to enforce a scope, index statistics,
|
|
69
|
+
and the Atlas Search index listing. Before, a schema read refused inside the
|
|
70
|
+
block was cached and denied every scoped mongo-direct read of that class,
|
|
71
|
+
for all callers, for 5 seconds. `Parse::AtlasSearch.faceted_search` raises
|
|
72
|
+
`FacetedSearchNotACLSafe` inside the block, because its bucket counts are
|
|
73
|
+
not ACL-filtered. The metadata marker rides only on the outgoing request
|
|
74
|
+
copy, never on `response.request` or an error's request. A schema read that
|
|
75
|
+
fails inside the block is retried at most every 2 seconds there. A LiveQuery
|
|
76
|
+
admin client that first connected inside the block keeps withholding its
|
|
77
|
+
master key on every reconnect until `allow_master_key_connection!`.
|
|
78
|
+
|
|
79
|
+
#### Agent policies
|
|
80
|
+
|
|
81
|
+
- **FIXED**: A sub-agent's `tools:` and `methods:` filters now only narrow its
|
|
82
|
+
parent's, as `classes:` already did. A child `tools: { only: }` is
|
|
83
|
+
intersected with the parent's allowlist and `except:` sets are unioned; for
|
|
84
|
+
`methods:` the parent's filters stay in force alongside the child's. Before,
|
|
85
|
+
the child's filter replaced the parent's, so a sub-agent could enable a tool
|
|
86
|
+
or `agent_method` within its tier that the parent was not given. A child
|
|
87
|
+
`only:` list with nothing its parent allows raises `ArgumentError` at
|
|
88
|
+
construction.
|
|
89
|
+
- **FIXED**: A sub-agent built with `parent:` uses its parent's approval gate,
|
|
90
|
+
read at call time, unless given its own. Before, it fell back to
|
|
91
|
+
`NullGate`, so a sub-agent built inside an MCP `tools/call` ran tools in
|
|
92
|
+
`require_approval_for` tiers without approval.
|
|
93
|
+
- **FIXED**: Qualified `methods:` entries compare the class by its Parse class
|
|
94
|
+
name, so `"User.x"` and `"_User.x"` (and the `Role`, `Session`, and
|
|
95
|
+
`Installation` aliases) match whichever spelling the entry or the
|
|
96
|
+
`call_method` caller uses. Before, `methods: { except: ["_User.x"] }` could
|
|
97
|
+
be bypassed by calling the method on `"User"`.
|
|
98
|
+
- **FIXED**: A sub-agent can no longer turn on `master_atlas` when its parent
|
|
99
|
+
does not have it. `Parse::Agent.new(parent: scoped_parent, master_atlas:
|
|
100
|
+
true)` raises `ArgumentError`; before, the child's `atlas_faceted_search` ran
|
|
101
|
+
with master-key semantics and returned every row under the parent's scoped
|
|
102
|
+
identity. Omitting `master_atlas` still inherits, and `false` still drops
|
|
103
|
+
it.
|
|
104
|
+
- **FIXED**: The MCP dispatcher snapshots and restores an agent's own approval
|
|
105
|
+
gate, so driving a sub-agent through `tools/call` no longer pins its
|
|
106
|
+
parent's gate onto it. Assigning `approval_gate = nil` makes a sub-agent use
|
|
107
|
+
its parent's gate again.
|
|
108
|
+
- **FIXED**: Qualified `methods:` entries may name a model by its Ruby
|
|
109
|
+
constant (`"Artist.purge"` for a class whose `parse_class` is `"Musician"`)
|
|
110
|
+
and are compared by Parse class name, so such an `except:` entry is no
|
|
111
|
+
longer bypassed through the Parse class name. An entry whose class resolves
|
|
112
|
+
to no loaded model warns at construction.
|
|
113
|
+
- **IMPROVED**: The `parse.agent.tool_call` audit payload adds
|
|
114
|
+
`methods_layers` whenever a sub-agent inherits a `methods:` filter
|
|
115
|
+
(including a single parent layer), and `describe[:methods][:layers]` lists
|
|
116
|
+
every `methods:` filter in force, parent first.
|
|
117
|
+
|
|
118
|
+
#### Associations
|
|
119
|
+
|
|
120
|
+
- **FIXED**: `add!`, `add_unique!`, and `remove!` on a
|
|
121
|
+
`has_many through: :array` collection take the array Parse Server returns,
|
|
122
|
+
as plain array properties already did. A stale local copy is corrected,
|
|
123
|
+
objects already fetched locally are kept, and the field is not marked
|
|
124
|
+
changed. The array is adopted only when every entry is a pointer to the
|
|
125
|
+
declared class; otherwise the operation is applied locally.
|
|
126
|
+
- **FIXED**: Saving a relation collection on its own (`owner.likes.save`)
|
|
127
|
+
sends the staged additions and removals and returns whether they were
|
|
128
|
+
applied. Before, it silently sent nothing.
|
|
129
|
+
- **FIXED**: `Parse::Object#clear_changes!` (also called by `reload!`) drops
|
|
130
|
+
a relation's staged additions and removals and clears array collection
|
|
131
|
+
dirty state. Before, discarded relation changes were sent with the next
|
|
132
|
+
change to that relation. A fetch (`fetch!`, partial fetch, autofetch, with
|
|
133
|
+
or without `preserve_changes:`) keeps a relation's staged additions and
|
|
134
|
+
removals and leaves the relation marked changed, including when the response
|
|
135
|
+
carries the relation's descriptor, so the next save sends them.
|
|
136
|
+
`rollback!` drops a relation's staged additions and removals too, including
|
|
137
|
+
after a fetch.
|
|
138
|
+
- **IMPROVED**: `RelationCollectionProxy#save` accepts `session:` to send the
|
|
139
|
+
staged operations as a given user (`nil` for no session token). Relation
|
|
140
|
+
writes on an owner whose objectId was assigned client-side during a create
|
|
141
|
+
are staged rather than sent until the create returns.
|
|
142
|
+
- **FIXED**: Atomic relation operations (`add!` / `remove!` on a
|
|
143
|
+
`through: :relation` collection) write to the relation's Parse column when
|
|
144
|
+
its name differs from the Ruby name (`has_many :liked_items` maps to
|
|
145
|
+
`likedItems`, or a relation declared with `field:`). Before, they targeted
|
|
146
|
+
the Ruby name.
|
|
147
|
+
|
|
148
|
+
#### Queries
|
|
149
|
+
|
|
150
|
+
- **FIXED**: An OR whose branch has no constraints now matches every row.
|
|
151
|
+
Previously `query | Model.query` and `Parse::Query.or(query, Model.query)`
|
|
152
|
+
dropped that branch and returned only the other side. An empty receiver of
|
|
153
|
+
`|` still starts a new OR, as `or_where` does, so
|
|
154
|
+
`[q1, q2].reduce(Model.query, :|)` builds `q1 OR q2`.
|
|
155
|
+
- **FIXED**: `Model.query(:or => [{...}, {...}])` builds a `$or` group ANDed
|
|
156
|
+
with the query's other constraints, instead of a constraint on a literal
|
|
157
|
+
field named `or`. Branches can be constraint hashes, `Parse::Query` objects
|
|
158
|
+
of the same class, or arrays of constraints.
|
|
159
|
+
- **FIXED**: A constraint that needs the aggregation pipeline or mongo-direct
|
|
160
|
+
(`readable_by`, `:ACL.readable_by`, `:field.array_size`, the mongo-direct geo
|
|
161
|
+
operators) raises `ArgumentError` inside an OR (`:or`, `or_where`, `|`,
|
|
162
|
+
`Parse::Query.or`). Before, the branch was emitted as `{}` and matched every
|
|
163
|
+
row, or lost part of its conditions, including in push targeting, which
|
|
164
|
+
could reach every installation.
|
|
165
|
+
- **FIXED**: `Parse::Query#clone`, `|`, `or_where`, `Parse::Query.or`, `:or`
|
|
166
|
+
branches (including Hash branches with `session:` / `use_master_key:` keys
|
|
167
|
+
or nested scoped queries), and subqueries (`$inQuery`, `$notInQuery`,
|
|
168
|
+
`$select`, `$dontSelect`) keep the query's authority and application:
|
|
169
|
+
session token, a session-bound client from `Parse::Client#become`, scoped
|
|
170
|
+
user or role, master-key flag, read preference, and the Parse application
|
|
171
|
+
the query targets. Queries combined this way must run under the same
|
|
172
|
+
authority against the same application. An unscoped query takes the
|
|
173
|
+
other's authority, and different authorities or applications raise
|
|
174
|
+
`ArgumentError`. Before, a session combined with `scope_to_user` ran as the
|
|
175
|
+
scoped user, a `become`-client query composed into an OR ran as the master
|
|
176
|
+
key, a scoped subquery ran under the outer query's master key, and a clone
|
|
177
|
+
could send one application's session token to the default application.
|
|
178
|
+
Changing the authority or client of a combined query raises and leaves the
|
|
179
|
+
query unchanged. A combined query re-checks its authority before it runs,
|
|
180
|
+
and explicit auth passed to `results_direct`, `count_direct`,
|
|
181
|
+
`distinct_direct`, or `atlas_search` must match it.
|
|
182
|
+
- **FIXED**: LiveQuery subscriptions and push targeting refuse constraints that
|
|
183
|
+
only the aggregation pipeline or mongo-direct can run (`array_size`,
|
|
184
|
+
`set_equals`, `not_empty`, `elem_match`, `readable_by`, mongo-direct geo
|
|
185
|
+
operators). Before, those constraints were dropped and the subscription or
|
|
186
|
+
push reached a wider audience. A subquery with such a constraint is also
|
|
187
|
+
refused, where it used to match every row. `Parse::Query#subscribe` and
|
|
188
|
+
`client.subscribe(query)` run under the query's own session or `become`
|
|
189
|
+
client, and such a subscription is refused with `ArgumentError` when the
|
|
190
|
+
LiveQuery client targets a different Parse application id than the query,
|
|
191
|
+
so one application's session token is never sent to another application's
|
|
192
|
+
LiveQuery server.
|
|
193
|
+
- **FIXED**: A field repeated in `order` is sent once, at its first position
|
|
194
|
+
with the last direction given, matching what MongoDB applies on direct
|
|
195
|
+
reads. String forms such as `"-title"` are parsed as descending, so they
|
|
196
|
+
deduplicate too, and the direct `$sort` stage sorts on the real field.
|
|
197
|
+
`order("title,-plays")` is parsed as two fields.
|
|
198
|
+
- **FIXED**: A bare objectId compared to a declared pointer field
|
|
199
|
+
(`Song.query(:artist => "abc123")`, `:artist.ne`, `:artist.in`) now matches
|
|
200
|
+
on REST, in `Parse::Push` targeting, and in LiveQuery subscriptions from
|
|
201
|
+
`Parse::Query#subscribe`, `Model.subscribe`, and `client.subscribe(query)`.
|
|
202
|
+
Equality and `$in` used to return no rows and `$ne` every row, while direct
|
|
203
|
+
and aggregate reads already matched. The new
|
|
204
|
+
`Parse::Query#compile_rest_where` returns the where clause in that form.
|
|
205
|
+
|
|
206
|
+
#### Search
|
|
207
|
+
|
|
208
|
+
- **FIXED**: Atlas Search (`Parse::AtlasSearch.search`, `search_with_stage`,
|
|
209
|
+
`autocomplete`, `Parse::Query#atlas_search`) and native hybrid search apply
|
|
210
|
+
the CLP `pointerFields` / `readUserFields` ownership constraint before the
|
|
211
|
+
page is cut. For native hybrid, the lexical input filters before its
|
|
212
|
+
per-input `$limit`, and the fused result is filtered again before the final
|
|
213
|
+
`$limit`. Rows used to be filtered after the page was cut, so when the
|
|
214
|
+
top-ranked hits belonged to other users a scoped caller got a short or empty
|
|
215
|
+
page. Inaccessible rows are still never returned.
|
|
216
|
+
- **IMPROVED**: `Parse::VectorSearch.search` and native hybrid search enforce
|
|
217
|
+
the `pointerFields` ownership constraint server-side across the whole
|
|
218
|
+
candidate window. When every owner field is declared as a scalar pointer
|
|
219
|
+
(`belongs_to`) and its storage path (`_p_<field>`) is a `type: "filter"`
|
|
220
|
+
field of the vectorSearch index, the constraint also goes
|
|
221
|
+
into `$vectorSearch.filter`, so other users' rows no longer use up the
|
|
222
|
+
candidate window. The prefilter is used only while the index is READY and
|
|
223
|
+
serving its latest definition. If Atlas refuses it (for example mid-rebuild),
|
|
224
|
+
the search reruns once without it and skips it for the index cache TTL. A
|
|
225
|
+
failed index lookup is also remembered for that TTL.
|
|
226
|
+
|
|
227
|
+
- **FIXED**: A `$vectorSearch` refusal caused by the caller's own
|
|
228
|
+
`vector_filter` on an unindexed path no longer switches off the owner
|
|
229
|
+
prefilter for every caller on that index or clears the index cache; only a
|
|
230
|
+
refusal naming an owner `_p_<field>` path does. Native hybrid search uses
|
|
231
|
+
`Parse::VectorSearch.default_index` when the vector branch names no index.
|
|
232
|
+
|
|
233
|
+
#### Documentation
|
|
234
|
+
|
|
235
|
+
- **FIXED**: README and query YARD examples use `:field.pointer_id` and
|
|
236
|
+
`:field.array_size`, the names the DSL has used since 5.8.0.
|
|
237
|
+
|
|
238
|
+
#### Behavior Notes
|
|
239
|
+
|
|
240
|
+
- An OR that includes an unconstrained branch now returns every row. The one
|
|
241
|
+
exception is an empty receiver of `|`, which starts the OR. Code that passed
|
|
242
|
+
an empty query for "no filter" should leave that branch out. `:or => []`
|
|
243
|
+
matches no rows.
|
|
244
|
+
- A pipeline-only constraint inside an OR now raises `ArgumentError`. Apply it
|
|
245
|
+
outside the OR, or run the branches as separate queries.
|
|
246
|
+
- Combining queries that run under different authorities or against
|
|
247
|
+
different Parse applications now raises `ArgumentError`. That covers OR
|
|
248
|
+
composition, `:or` Hash branches, and subqueries, and includes different
|
|
249
|
+
session tokens, a session against `scope_to_user` / `scope_to_role` / the
|
|
250
|
+
master key, and different scoped users or roles. Setting a new session
|
|
251
|
+
token, master-key flag, scope, or client on a query that took its authority
|
|
252
|
+
from another query also raises, as does passing different explicit auth to
|
|
253
|
+
a direct or Atlas terminal. Query-shaping options (`limit`, `order`, `keys`,
|
|
254
|
+
`skip`, `include`) inside an `:or` branch raise.
|
|
255
|
+
- LiveQuery subscriptions, push targeting, and subqueries with pipeline-only
|
|
256
|
+
constraints raise `ArgumentError`. Subscribing to a `scope_to_user` /
|
|
257
|
+
`scope_to_role` query, or passing a `session_token:` that differs from the
|
|
258
|
+
query's session, raises.
|
|
259
|
+
- REST queries, LiveQuery subscriptions, and push targeting that compare a
|
|
260
|
+
bare objectId to a declared pointer field now return the matching rows.
|
|
261
|
+
Undeclared fields are unchanged. If a model declares `belongs_to` for a
|
|
262
|
+
field whose server column holds a plain String, a bare string compared to
|
|
263
|
+
that field is now sent as a Pointer and stops matching those values;
|
|
264
|
+
declare such a field as a `:string` property instead.
|
|
265
|
+
- Code that relied on `master: true` keeping master authority inside
|
|
266
|
+
`Parse.without_master_key` should wrap that call in `Parse.with_master_key`.
|
|
267
|
+
A dropped `master: true` raises `ACLRequired` under either
|
|
268
|
+
`Parse::ACLScope.require_session_token` or
|
|
269
|
+
`Parse::AtlasSearch.require_session_token`, with a message naming the block,
|
|
270
|
+
and no longer emits the no-ACL banner. The role-graph helpers
|
|
271
|
+
(`users_in_role_subtree` and related) raise `ACLRequired` for
|
|
272
|
+
`master: true` inside the block; pass `as:` instead.
|
|
273
|
+
- A LiveQuery connection that already connected with the master key outside
|
|
274
|
+
`Parse.without_master_key` stays elevated for subscriptions made inside the
|
|
275
|
+
block, because Parse Server authorizes per connection; the SDK warns.
|
|
276
|
+
- `Parse.without_master_key` guards against accidental master-key use and is
|
|
277
|
+
not an isolation boundary.
|
|
278
|
+
- `Parse::Role.all_for_user` and `Parse::Role#all_parent_role_names` without
|
|
279
|
+
`master:` or `as:` return the full role closure inside
|
|
280
|
+
`Parse.without_master_key`, because the role graph is read as SDK metadata.
|
|
281
|
+
- `Parse::Session._preload_identity_for_destroy!` and the
|
|
282
|
+
`_after_batch_destroy` hooks are now private.
|
|
283
|
+
- A session deleted elsewhere whose owner this process never recorded keeps
|
|
284
|
+
resolving until the next rate-limited reset or `identity_cache_ttl`.
|
|
285
|
+
- A custom identity plane gets the cross-process revocation-race guarantee by
|
|
286
|
+
implementing `invalidation_nonce` and `bump_invalidation_nonce`, as the
|
|
287
|
+
built-in Redis plane does. One with generation counters only is covered for
|
|
288
|
+
single-token and per-user invalidations, but a lookup racing another
|
|
289
|
+
process's full reset is bounded by `identity_cache_ttl` when the plane's
|
|
290
|
+
`clear` also deletes its counters.
|
|
291
|
+
- A sub-agent's `tools:` or `methods:` `only:` list that leaves nothing once
|
|
292
|
+
the parent's `only:` and `except:` lists apply now raises `ArgumentError`.
|
|
293
|
+
An explicitly empty `only: []` is accepted. A sub-agent passing
|
|
294
|
+
`master_atlas: true` under a parent without it raises `ArgumentError`.
|
|
295
|
+
- Parse Server sends a `before_save` webhook only the resulting value of a
|
|
296
|
+
dotted operator. When the handler changes the record, the SDK writes back
|
|
297
|
+
changed sub-keys one level deep and deletes removed ones by path. Two
|
|
298
|
+
concurrent writes inside the same sub-key (including deeper paths under it)
|
|
299
|
+
can still overwrite each other, an array is written whole, and a changed
|
|
300
|
+
sub-key carrying a typed value (Date, Bytes, Pointer) makes the whole field
|
|
301
|
+
be written. A client's whole replace of a sub-document becomes a merge. The
|
|
302
|
+
save response reports the dotted keys; the JavaScript SDK applies them, and
|
|
303
|
+
other clients may need a fetch. A handler operator that cannot be folded,
|
|
304
|
+
or two conflicting dotted keys from the handler, fail the save with an
|
|
305
|
+
error.
|
|
306
|
+
- A `before_save` handler that returns `true`, `nil`, or a Hash keeps the
|
|
307
|
+
client's write as sent, so a client create without an ACL through such a
|
|
308
|
+
handler is stored public read and write, as before, unless the handler
|
|
309
|
+
assigns an ACL on `parse_object`.
|
|
310
|
+
- Saving a relation collection on its own sends removals and additions as two
|
|
311
|
+
requests. If the second fails, the first stays applied and the unsent
|
|
312
|
+
operations stay staged.
|
|
313
|
+
- Owner fields stored as arrays of pointers, undeclared, or not filter-indexed
|
|
314
|
+
get no `$vectorSearch` prefilter. Vector search then fills results only from
|
|
315
|
+
its existing candidate window.
|
|
316
|
+
- A custom identity cache with neither `invalidate_value` nor generation
|
|
317
|
+
support (`generation`, `generation_current?`, `bump_generation`) only
|
|
318
|
+
expires a user's tokens at `identity_cache_ttl` after a password change,
|
|
319
|
+
account deletion, `logout_all!`, or a batch destroy of sessions.
|
|
320
|
+
|
|
3
321
|
### 5.8.0
|
|
4
322
|
|
|
5
323
|
#### Breaking Changes
|
data/README.md
CHANGED
|
@@ -3873,8 +3873,8 @@ If you want to see if a particular field contains a specific Parse::Object (poin
|
|
|
3873
3873
|
q.where :field => Parse::Pointer.new("_User", "anObjectId")
|
|
3874
3874
|
# alias using subclass helper
|
|
3875
3875
|
q.where :field => Parse::User.pointer("anObjectId")
|
|
3876
|
-
# alias using
|
|
3877
|
-
q.where :user.
|
|
3876
|
+
# alias using the `pointer_id` constraint. We will infer :user maps to class "_User" (Parse::User)
|
|
3877
|
+
q.where :user.pointer_id => "anObjectId"
|
|
3878
3878
|
```
|
|
3879
3879
|
|
|
3880
3880
|
#### Less Than
|
|
@@ -3976,15 +3976,15 @@ Match arrays by their length:
|
|
|
3976
3976
|
|
|
3977
3977
|
```ruby
|
|
3978
3978
|
# Exact size
|
|
3979
|
-
q.where :tags.
|
|
3979
|
+
q.where :tags.array_size => 2 # arrays with exactly 2 elements
|
|
3980
3980
|
|
|
3981
3981
|
# Size comparisons
|
|
3982
|
-
q.where :tags.
|
|
3983
|
-
q.where :tags.
|
|
3984
|
-
q.where :tags.
|
|
3985
|
-
q.where :tags.
|
|
3986
|
-
q.where :tags.
|
|
3987
|
-
q.where :tags.
|
|
3982
|
+
q.where :tags.array_size => { gt: 3 } # size > 3
|
|
3983
|
+
q.where :tags.array_size => { gte: 2 } # size >= 2
|
|
3984
|
+
q.where :tags.array_size => { lt: 5 } # size < 5
|
|
3985
|
+
q.where :tags.array_size => { lte: 4 } # size <= 4
|
|
3986
|
+
q.where :tags.array_size => { ne: 0 } # size != 0
|
|
3987
|
+
q.where :tags.array_size => { gte: 2, lt: 10 } # range: 2 <= size < 10
|
|
3988
3988
|
|
|
3989
3989
|
# Empty/non-empty shortcuts (index-friendly)
|
|
3990
3990
|
q.where :tags.arr_empty => true # empty arrays (uses { field: [] })
|
|
@@ -4033,7 +4033,7 @@ All array constraints work with `has_many :through => :array` relations:
|
|
|
4033
4033
|
Product.query(:categories.set_equals => [cat1, cat2])
|
|
4034
4034
|
|
|
4035
4035
|
# Find products with more than 3 categories
|
|
4036
|
-
Product.query(:categories.
|
|
4036
|
+
Product.query(:categories.array_size => { gt: 3 })
|
|
4037
4037
|
```
|
|
4038
4038
|
|
|
4039
4039
|
**Note:** Array constraints using aggregation pipelines require MongoDB 3.6+.
|
|
@@ -4213,7 +4213,7 @@ Event.where(:updated_at.between_dates => [1.week.ago, Time.now])
|
|
|
4213
4213
|
```
|
|
4214
4214
|
|
|
4215
4215
|
#### Matches Object Id
|
|
4216
|
-
Sometimes you want to find rows where a particular Parse object exists. You can do so by passing a the Parse::Object subclass or a Parse::Pointer. In some cases you may only have the "objectId" of the record you are looking for. For convenience, you can also use the `
|
|
4216
|
+
Sometimes you want to find rows where a particular Parse object exists. You can do so by passing a the Parse::Object subclass or a Parse::Pointer. In some cases you may only have the "objectId" of the record you are looking for. For convenience, you can also use the `pointer_id` constraint. This will assume that the name of the field matches a particular Parse class you have defined. Assume the following:
|
|
4217
4217
|
|
|
4218
4218
|
```ruby
|
|
4219
4219
|
# where this Parse object equals the object in the column `field`.
|
|
@@ -4221,18 +4221,18 @@ q.where :field => Parse::Pointer("Field", "someObjectId")
|
|
|
4221
4221
|
# => "field":{"__type":"Pointer","className":"Field","objectId":"someObjectId"}}
|
|
4222
4222
|
|
|
4223
4223
|
# alias, shorthand when we infer `:field` maps to `Field` parse class.
|
|
4224
|
-
q.where :field.
|
|
4224
|
+
q.where :field.pointer_id => "someObjectId"
|
|
4225
4225
|
# => "field":{"__type":"Pointer","className":"Field","objectId":"someObjectId"}}
|
|
4226
4226
|
|
|
4227
4227
|
```
|
|
4228
4228
|
It is always important to be thoughtful in naming column names in associations as
|
|
4229
4229
|
close to their foreign Parse class names. This enables more expressive syntax while reducing
|
|
4230
|
-
code.
|
|
4230
|
+
code. `pointer_id` also accepts any object or pointer object. These are all equivalent:
|
|
4231
4231
|
|
|
4232
4232
|
```ruby
|
|
4233
4233
|
q.where :user => User.pointer("xyx123")
|
|
4234
|
-
q.where :user.
|
|
4235
|
-
q.where :user.
|
|
4234
|
+
q.where :user.pointer_id => "xyx123"
|
|
4235
|
+
q.where :user.pointer_id => User.pointer("xyx123")
|
|
4236
4236
|
# All produce
|
|
4237
4237
|
# => "user":{"__type":"Pointer","className":"_User","objectId":"xyx123"}}
|
|
4238
4238
|
```
|
|
@@ -4260,15 +4260,15 @@ In some cases, you do not have the Parse object, but you have its `objectId`. Yo
|
|
|
4260
4260
|
|
|
4261
4261
|
```ruby
|
|
4262
4262
|
# shorthand if you are using convention. Will infer class `Artist`
|
|
4263
|
-
Song.all :artist.
|
|
4263
|
+
Song.all :artist.pointer_id => artist_id
|
|
4264
4264
|
|
|
4265
4265
|
# other approaches, same result
|
|
4266
4266
|
Song.all :artist => Artist.pointer(artist_id)
|
|
4267
4267
|
Song.all :artist => Parse::Pointer.new("Artist", artist_id)
|
|
4268
4268
|
|
|
4269
|
-
#
|
|
4269
|
+
# `pointer_id` safely accepts pointers and strings for these kinds of API patterns
|
|
4270
4270
|
def find_songs(artist)
|
|
4271
|
-
Song.all :artist.
|
|
4271
|
+
Song.all :artist.pointer_id => artist
|
|
4272
4272
|
end
|
|
4273
4273
|
|
|
4274
4274
|
# all ok
|
|
@@ -5813,7 +5813,7 @@ Event.query(:event_date.gt => Time.now).results_direct
|
|
|
5813
5813
|
Event.query(:event_date.gte => start_date, :event_date.lte => end_date).results_direct
|
|
5814
5814
|
|
|
5815
5815
|
# Array operators
|
|
5816
|
-
Song.query(:tags.
|
|
5816
|
+
Song.query(:tags.array_size => 3).results_direct
|
|
5817
5817
|
Song.query(:tags.contains_all => ["rock", "classic"]).results_direct
|
|
5818
5818
|
Song.query(:tags.empty_or_nil => true).results_direct
|
|
5819
5819
|
|
|
@@ -6101,6 +6101,12 @@ Parse.client.authorization.invalidate(token)
|
|
|
6101
6101
|
Parse.client.authorization.invalidate_user_roles(user_id)
|
|
6102
6102
|
```
|
|
6103
6103
|
|
|
6104
|
+
A custom identity plane that implements neither `invalidate_value` nor
|
|
6105
|
+
generation support (`generation`, `generation_current?`, `bump_generation`)
|
|
6106
|
+
can only forget one token at a time. Revoking every session
|
|
6107
|
+
of a user (password change, account deletion, `logout_all!`, a batch destroy of
|
|
6108
|
+
sessions) is then bounded by `identity_cache_ttl` rather than immediate.
|
|
6109
|
+
|
|
6104
6110
|
Both caches default to per-process memory. With a keyspaced
|
|
6105
6111
|
`Parse::Cache::Redis` you can move them to the shared backend so every worker
|
|
6106
6112
|
resolves against the same view, and let webhook triggers invalidate them
|
data/docs/caching.md
CHANGED
|
@@ -358,6 +358,56 @@ Two behaviors to know before you rely on these:
|
|
|
358
358
|
triggers depend on. The deprecated `Parse::AtlasSearch::Session.invalidate` /
|
|
359
359
|
`.invalidate_user_roles` forms still work through 5.x: they delegate to the
|
|
360
360
|
default client's context, so they can only ever address `Parse.client`.
|
|
361
|
+
* A custom identity plane you write yourself needs `get`, `set`, and
|
|
362
|
+
`invalidate`. Revoking one token (logout, `Parse::Session#destroy` on a
|
|
363
|
+
session that carries its token) works with those alone. Revoking every token
|
|
364
|
+
of a user (password change, account deletion, `logout_all!`, destroying a
|
|
365
|
+
session fetched without its token, or a batch `Array#destroy` of sessions or
|
|
366
|
+
users) also needs either `invalidate_value(user_id)` or generation support
|
|
367
|
+
(`generation`, `generation_current?`, and `bump_generation`), because the
|
|
368
|
+
plane is keyed by token. A plane with neither keeps resolving
|
|
369
|
+
those tokens until their cached entries expire, so revocation there is
|
|
370
|
+
bounded by `identity_cache_ttl`, not immediate. Lower the TTL if that window
|
|
371
|
+
is too long.
|
|
372
|
+
* A session or user delete drops its cached identity when the delete succeeds
|
|
373
|
+
or reports the row already gone ("object not found"); a denied delete leaves
|
|
374
|
+
the cache alone. A session fetched without its token is looked up first
|
|
375
|
+
(with the master key when the client has one, otherwise with the delete's
|
|
376
|
+
`session:`). When the lookup finds no row (the session is already gone or
|
|
377
|
+
not visible), the delete uses the owner recorded when a `_Session` query or
|
|
378
|
+
`Parse::Session.session` last fetched that session through the same client.
|
|
379
|
+
Only the owner's user id is recorded, never the token, for as long as an
|
|
380
|
+
identity entry lives. The records sit in their own store
|
|
381
|
+
(`client.authorization.session_owner_cache`, process-local by default, set
|
|
382
|
+
a shared store to share them), never in the identity plane, so no session
|
|
383
|
+
token can read one. With no
|
|
384
|
+
recorded owner, a batch delete that reports the row deleted or "object not
|
|
385
|
+
found" clears the client's identity and role cache, and so does a lookup
|
|
386
|
+
that fails outright. Either reset happens at most once every 5 seconds per
|
|
387
|
+
client, so an endpoint that deletes caller-supplied session ids cannot flush
|
|
388
|
+
a shared plane on every request. A single `destroy` cannot tell "already
|
|
389
|
+
gone" from "denied", so it uses the recorded owner but never resets. The
|
|
390
|
+
residual window is a session deleted elsewhere whose owner this process
|
|
391
|
+
never recorded: its cached token resolves until the next reset or until
|
|
392
|
+
`identity_cache_ttl`.
|
|
393
|
+
* A token resolution that is in flight when an invalidation lands does not
|
|
394
|
+
cache its answer, so it cannot put a just-revoked token back. Every
|
|
395
|
+
invalidation moves a marker before it drops entries, and the resolver
|
|
396
|
+
checks the marker before writing and again after, evicting its own write
|
|
397
|
+
when it moved. On the Redis identity plane the marker is a random nonce
|
|
398
|
+
shared by every process, replaced on each invalidation, so this also holds
|
|
399
|
+
for invalidations made by other processes, including a full reset. A custom
|
|
400
|
+
plane gets the same cross-process guarantee by implementing
|
|
401
|
+
`invalidation_nonce` (return the current marker String, or nil when none is
|
|
402
|
+
set) and `bump_invalidation_nonce` (store and return a new random String).
|
|
403
|
+
A custom plane with generation support but no nonce shares a generation
|
|
404
|
+
counter instead. That covers single-token and per-user invalidations made
|
|
405
|
+
by other processes, but not a full reset if the plane's `clear` also deletes
|
|
406
|
+
its generation counters: the counter restarts at a value an in-flight
|
|
407
|
+
lookup may already hold, so a lookup racing another process's reset can
|
|
408
|
+
keep its answer until `identity_cache_ttl`. A plane with neither gets the
|
|
409
|
+
guarantee within one process only, and across processes it is bounded by
|
|
410
|
+
`identity_cache_ttl`.
|
|
361
411
|
|
|
362
412
|
On a Redis outage these planes behave differently from the response cache.
|
|
363
413
|
The response cache degrades to a passthrough request; the identity and role
|
data/docs/mcp_guide.md
CHANGED
|
@@ -880,6 +880,15 @@ The approval gate is a pluggable `agent.approval_gate` consulted inside
|
|
|
880
880
|
unit-testable with a fake approver. `Parse::Agent::MCPElicitationGate` is the
|
|
881
881
|
spec-native implementation; `Parse::Agent::NullGate` (the default) approves.
|
|
882
882
|
|
|
883
|
+
A sub-agent built with `parent:` and no gate of its own uses its parent's gate,
|
|
884
|
+
read at call time, so the gate the dispatcher installs on the parent for a
|
|
885
|
+
`tools/call` also covers any sub-agent a tool builds during that call. Before
|
|
886
|
+
5.8.1 a sub-agent fell back to `NullGate` and skipped approval. Assigning a
|
|
887
|
+
gate on the sub-agent itself overrides this, and assigning `nil` removes the
|
|
888
|
+
override so the sub-agent uses its parent's gate again. The dispatcher
|
|
889
|
+
snapshots and restores the agent's own gate (`approval_gate_override`), so
|
|
890
|
+
driving a sub-agent through `tools/call` never pins its parent's gate onto it.
|
|
891
|
+
|
|
883
892
|
Round-trip over the streaming transport:
|
|
884
893
|
|
|
885
894
|
1. A `tools/call` for a gated tier pauses before execution. The server builds an
|
|
@@ -1646,7 +1655,7 @@ agent = Parse::Agent.new(methods: [:archive, "Project.set_client_description"])
|
|
|
1646
1655
|
agent = Parse::Agent.new(methods: { except: ["Account.delete_account"] })
|
|
1647
1656
|
```
|
|
1648
1657
|
|
|
1649
|
-
Entries are bare method names (`:archive
|
|
1658
|
+
Entries are bare method names (`:archive`, which matches the method on any class) or qualified names (`"Project.archive"`, which matches only on that class). Both forms coexist in the same Set; matching is an OR. The class part is compared by its Parse class name, so `"User.reset"` and `"_User.reset"` name the same method (likewise `Role`, `Session`, and `Installation`), whichever spelling the entry or the `call_method` caller uses. An entry may also name the model by its Ruby constant (`"Artist.purge"` for `class Artist < Parse::Object; parse_class "Musician"; end`), which is compared by its Parse class name too. A qualified entry whose class resolves to no loaded model warns at construction and is matched by its class part as written. Before 5.8.1 an entry was matched against the raw string the caller passed, so `methods: { except: ["_User.reset"] }` could be bypassed by calling it on `"User"`.
|
|
1650
1659
|
|
|
1651
1660
|
The filter **narrows declared methods** — it cannot expose a method that was not declared via the `agent_method` DSL, and it cannot bypass tier checks (`agent_can_call?`) or env-gates (`PARSE_AGENT_ALLOW_WRITE_TOOLS`, `PARSE_AGENT_ALLOW_SCHEMA_OPS`). A filtered-out invocation returns `error_code: :tool_filtered`.
|
|
1652
1661
|
|
|
@@ -1714,7 +1723,7 @@ Parse::Agent.new(classes: { only: [Pots] }, strict_class_filter: true) # per-in
|
|
|
1714
1723
|
|
|
1715
1724
|
`except:` is never validated — an operator may proactively block a class not yet loaded.
|
|
1716
1725
|
|
|
1717
|
-
**Sub-agent inheritance: intersect, never widen.**
|
|
1726
|
+
**Sub-agent inheritance: intersect, never widen.** Like `tools:` and `methods:`, `classes:` is **intersected** with the parent's effective set so a sub-agent can NEVER widen the parent's data reach. A child `only:` that has no overlap with the parent's `only:` raises `ArgumentError` at construction. A child that omits `classes:` inherits the parent's filter verbatim. `except:` sets are unioned (a sub-agent cannot un-deny a class the parent denied).
|
|
1718
1727
|
|
|
1719
1728
|
**Schema-catalog filtering.** `get_all_schemas` omits classes outside the per-agent allowlist from the catalog response so the LLM doesn't waste a tool call discovering classes it would be refused on.
|
|
1720
1729
|
|
|
@@ -1793,6 +1802,7 @@ Parse::Agent::Tools.register(
|
|
|
1793
1802
|
| `session_token` | Yes (security-critical) | Without it, a session-token parent silently produces a master-key sub-agent — the constructor default is `nil`, which means master-key mode. This was the v4.2 advisor-flagged blocker; do not undo. |
|
|
1794
1803
|
| `acl_user` (v4.4.0) | Yes (security-critical) | When the parent was constructed with `acl_user:` and the child supplies none of `session_token:` / `acl_user:` / `acl_role:`, the parent's identity inherits verbatim. Inheritance is conditional on the child supplying NO identity at all — explicit overrides on the child resolve normally and then face the subset check below. |
|
|
1795
1804
|
| `acl_role` (v4.4.0) | Yes (security-critical) | Same rule as `acl_user`. A child that omits identity inherits the parent's role scope; one that supplies its own identity falls through to the subset check. |
|
|
1805
|
+
| `master_atlas` | Yes (security-critical) | `nil` inherits the parent's setting and `false` drops it. `true` is accepted only when the parent has it: since 5.8.1 a sub-agent of a parent without `master_atlas` raises `ArgumentError`, because master-key faceting would return every row's buckets and results under the parent's scoped identity. |
|
|
1796
1806
|
| `tenant_id` | Yes (security-critical) | Without it, a tenant-bound parent produces an unbound sub-agent that escapes `agent_tenant_scope` rules. |
|
|
1797
1807
|
| `recursion_depth` | Always (decremented) | The parent's budget is authoritative — the explicit `recursion_depth:` kwarg is ignored on inherited construction. |
|
|
1798
1808
|
|
|
@@ -1802,7 +1812,8 @@ Parse::Agent::Tools.register(
|
|
|
1802
1812
|
|-------|---------|
|
|
1803
1813
|
| `permissions` | The default of `:readonly` means `Parse::Agent.new(parent: write_agent)` produces a `:readonly` sub-agent. A sub-agent is at most as privileged as the parent by tier; this is enforced by a clamp check at construction, not by inheritance. An explicit override is accepted only if `≤ parent.permissions` — `Parse::Agent.new(parent: readonly_parent, permissions: :admin)` raises `ArgumentError`. Pass `permissions: parent.permissions` to maintain parity intentionally. |
|
|
1804
1814
|
| `client` | The constructor default `:default` resolves to the same client in standard single-app deployments. Explicit passes through. |
|
|
1805
|
-
|
|
1815
|
+
|
|
1816
|
+
**`tools:` and `methods:` narrow only (5.8.1).** A sub-agent's `tools:` and `methods:` filters can only remove from what its parent allows. A child `tools: { only: }` is intersected with the parent's allowlist and `except:` sets are unioned; a child that omits `tools:` inherits the parent's filter. For `methods:`, the parent's filters stay in force alongside the child's, so a call must pass every one (entries may be bare or `Class.method`, which plain set intersection would match wrongly). A child `only:` list with nothing its parent allows (after the parent's allowlist and `except:` lists apply) raises `ArgumentError` at construction; an explicitly empty `only: []` is the strictest narrowing and is accepted. Before 5.8.1 a child's filter replaced the parent's, so a sub-agent could enable a tool or agent method within its tier that the parent was not given.
|
|
1806
1817
|
|
|
1807
1818
|
**The clamp invariant:** `sub.permissions ≤ parent.permissions` always holds. The default `:readonly` is always safe regardless of parent tier; only explicit overrides hit the clamp check, and overrides that exceed the parent's tier raise at construction. This is the structural guarantee that a `delegate_to_subagent` chain cannot escape the parent's tier through sub-agent construction — the only path to a more-privileged agent is at the MCP factory, where the explicit elevation is auditable.
|
|
1808
1819
|
|
|
@@ -2785,6 +2796,7 @@ Every tool call dispatched through `Agent#execute` fires the `"parse.agent.tool_
|
|
|
2785
2796
|
| `:tools_except` | Array<Symbol> | v4.3.0+ — when the agent was constructed with `tools: { except: [...] }`. |
|
|
2786
2797
|
| `:methods_only` | Array<String> | v4.3.0+ — when the agent was constructed with `methods: { only: [...] }`. Bare names and `"Class.method"` qualified names mix. |
|
|
2787
2798
|
| `:methods_except` | Array<String> | v4.3.0+ — when the agent was constructed with `methods: { except: [...] }`. |
|
|
2799
|
+
| `:methods_layers` | Array<Hash> | 5.8.1+, sub-agents that inherit at least one `methods:` filter (including a single parent layer when the sub-agent has none of its own). Every `methods:` filter in force, parent layers first and the agent's own last, each `{only:, except:}` with sorted name strings (nil when absent). `:methods_only` / `:methods_except` describe only the agent's own layer. `describe[:methods][:layers]` carries the same list. |
|
|
2788
2800
|
| `:filters` | Hash<String,Array<String>> | v4.4.0+ — when the agent was constructed with `filters: {...}`. Maps each filtered class name (or `"default"`) to the list of FIELD NAMES the filter constrains. Filter VALUES are intentionally NOT echoed — `filters: { Account => { user_id: "abc123" } }` would otherwise emit the user-identifying value on every audit-log line. Subscribers that need the actual constraint can call `agent.filter_for(class_name)` directly. |
|
|
2789
2801
|
| `:denial_kind` | Symbol | v4.3.0+, AccessDenied failure path only — one of `:hidden_class` (global `agent_hidden`), `:class_filter` (per-agent `classes:` narrowing), `:field_denied` (outside `agent_fields`), or `:storage_form_field_ref` (referenced `_p_*` pointer-storage column). Lets SOC tooling distinguish operator narrowing from policy-level denials without parsing the message prose. |
|
|
2790
2802
|
|