Surfaced by the ADR-0096 E4 verification. The framework owns the AI tool-execution contract but not its executor (which lives in cloud). The contract's documented default for a missing actor is system-level (RLS-bypassing) execution, spanning all built-in data tools — a fall-open baked into the type's documentation.
The contract
packages/spec/src/contracts/ai-service.ts:
ToolExecutionContext.actor is optional (:393). Docstring (:388-397): "Built-in tools promote this into the ObjectQL ExecutionContext so RLS engages. Omit for internal/system invocations."
ChatWithToolsOptions.toolExecutionContext (:367-378): the route "should populate this from req.user … so RLS automatically scopes what the LLM can see or change. Optional for backward compatibility — when omitted, tools fall back to system-level behaviour." (:376-377)
So a missing actor ⇒ system-level, for "built-in data tools" generically (query/get/create/update/delete/run_action, not just knowledge). Any cloud or custom route that forgets to populate actor from req.user runs the LLM's tools with full authority.
Why framework-side, even though the executor is cloud
Fix direction
- Change the documented default from "system-level behaviour" to anonymous/deny for data-touching tools (
ai-service.ts:376-377, :388-397): a missing actor must mean an unauthenticated (RLS-on, sees nothing) principal, never isSystem. This turns the fall-open into a fail-closed.
- Optionally make
actor non-optional for data tools, or add an explicit system: true opt-in (mirroring IKnowledgeService's isSystem convention) so "run as system" is a deliberate, greppable choice — not the consequence of a forgotten field.
- Cloud side (cross-repo): the tool loop must (a) populate
actor from req.user on every AI route, and (b) default a missing actor to anonymous/deny ExecutionContext, never isSystem.
Relationship to ADR-0096
E4 "unknown seam" resolved to a contract-level fall-open. This is the D1 invariant ("no identity is never a grant of authority") applied at the contract layer, and the mirror of the #2981 knowledge/RAG fix (which closed the same shape framework-side where an executor existed). Refs: ADR-0096 (#2975 / e07645c), #2981, #2849.
Surfaced by the ADR-0096 E4 verification. The framework owns the AI tool-execution contract but not its executor (which lives in cloud). The contract's documented default for a missing
actoris system-level (RLS-bypassing) execution, spanning all built-in data tools — a fall-open baked into the type's documentation.The contract
packages/spec/src/contracts/ai-service.ts:ToolExecutionContext.actoris optional (:393). Docstring (:388-397): "Built-in tools promote this into the ObjectQLExecutionContextso RLS engages. Omit for internal/system invocations."ChatWithToolsOptions.toolExecutionContext(:367-378): the route "should populate this fromreq.user… so RLS automatically scopes what the LLM can see or change. Optional for backward compatibility — when omitted, tools fall back to system-level behaviour." (:376-377)So a missing
actor⇒ system-level, for "built-in data tools" generically (query/get/create/update/delete/run_action, not just knowledge). Any cloud or custom route that forgets to populateactorfromreq.userruns the LLM's tools with full authority.Why framework-side, even though the executor is cloud
chatWithTools|toolExecutionContext|ToolExecutionContextacrosspackages/**matches onlyai-service.ts. Noimplements IAIService, no tool loop.IKnowledgeService, whose executor does live in framework and now fails closed — see Security: Knowledge/RAG retrieval fall-opens to unfiltered corpus when ExecutionContext is missing or system — agent data ceiling escape #2981).systemas the omission default — which is what invites the bypass. The honest, greppable fix belongs in the contract.Fix direction
ai-service.ts:376-377,:388-397): a missingactormust mean an unauthenticated (RLS-on, sees nothing) principal, neverisSystem. This turns the fall-open into a fail-closed.actornon-optional for data tools, or add an explicitsystem: trueopt-in (mirroringIKnowledgeService'sisSystemconvention) so "run as system" is a deliberate, greppable choice — not the consequence of a forgotten field.actorfromreq.useron every AI route, and (b) default a missing actor to anonymous/denyExecutionContext, neverisSystem.Relationship to ADR-0096
E4 "unknown seam" resolved to a contract-level fall-open. This is the D1 invariant ("no identity is never a grant of authority") applied at the contract layer, and the mirror of the #2981 knowledge/RAG fix (which closed the same shape framework-side where an executor existed). Refs: ADR-0096 (#2975 / e07645c), #2981, #2849.