Found by the ADR-0090 D10 adversarial security review.
Two related gaps on the analytics read path
1. getReadFilter has no D10 delegator intersection.
SecurityPlugin.getReadFilter (packages/plugins/plugin-security/src/security-plugin.ts:~1387-1415) resolves the caller's own permission sets and RLS but does not handle onBehalfOf / call resolveDelegatorContext — the only onBehalfOf handling in the plugin is the engine middleware. The analytics service binds this as its read-scope provider (service-analytics/src/plugin.ts:~303). So an analytics/report query run under an agent context is scoped only by the agent's own ceiling, never intersected with the delegator's RLS — the confused-deputy widening D10 prevents on the CRUD path.
(Note: an OAuth agent only reaches /mcp, which does not expose analytics today, so this is latent for the current agent surface — but it's a real invariant gap if analytics is ever driven by an agent ec.)
2. /analytics/query passes no execution context → unscoped.
http-dispatcher.ts:~2052 calls analyticsService.query(body) with no _context.executionContext, so getReadFilter(object, undefined) returns undefined and the query runs with no RLS scoping for any authenticated caller. This is a general (non-agent) RLS hole on the analytics route — worth confirming and closing.
Fix direction
- Thread the request
executionContext into /analytics/query (and any other analytics route) so getReadFilter scopes it.
- Extend
getReadFilter (and computeRlsFilter) to apply the D10 delegator intersection when context.onBehalfOf is set — mirroring the middleware — so the analytics/raw-SQL path stays in lock-step with the find path for agent principals.
Refs
packages/plugins/plugin-security/src/security-plugin.ts:~1387-1415 (getReadFilter), middleware D10 at ~572/~1000
packages/runtime/src/http-dispatcher.ts:~2052 (/analytics/query)
packages/services/service-analytics/src/plugin.ts:~303, analytics-service.ts:~302-316
Found by the ADR-0090 D10 adversarial security review.
Two related gaps on the analytics read path
1.
getReadFilterhas no D10 delegator intersection.SecurityPlugin.getReadFilter(packages/plugins/plugin-security/src/security-plugin.ts:~1387-1415) resolves the caller's own permission sets and RLS but does not handleonBehalfOf/ callresolveDelegatorContext— the onlyonBehalfOfhandling in the plugin is the engine middleware. The analytics service binds this as its read-scope provider (service-analytics/src/plugin.ts:~303). So an analytics/report query run under an agent context is scoped only by the agent's own ceiling, never intersected with the delegator's RLS — the confused-deputy widening D10 prevents on the CRUD path.(Note: an OAuth agent only reaches
/mcp, which does not expose analytics today, so this is latent for the current agent surface — but it's a real invariant gap if analytics is ever driven by an agent ec.)2.
/analytics/querypasses no execution context → unscoped.http-dispatcher.ts:~2052callsanalyticsService.query(body)with no_context.executionContext, sogetReadFilter(object, undefined)returnsundefinedand the query runs with no RLS scoping for any authenticated caller. This is a general (non-agent) RLS hole on the analytics route — worth confirming and closing.Fix direction
executionContextinto/analytics/query(and any other analytics route) sogetReadFilterscopes it.getReadFilter(andcomputeRlsFilter) to apply the D10 delegator intersection whencontext.onBehalfOfis set — mirroring the middleware — so the analytics/raw-SQL path stays in lock-step with the find path for agent principals.Refs
packages/plugins/plugin-security/src/security-plugin.ts:~1387-1415(getReadFilter), middleware D10 at ~572/~1000packages/runtime/src/http-dispatcher.ts:~2052(/analytics/query)packages/services/service-analytics/src/plugin.ts:~303,analytics-service.ts:~302-316