Skip to content

Commit 0a630d7

Browse files
committed
fix(spec): ToolExecutionContext fails closed on missing actor — system is an explicit isSystem opt-in (#2991)
The AI tool-execution contract documented system-level (RLS-bypassing) execution as the default for a missing actor / toolExecutionContext — a contract-level fall-open across all built-in data tools (ADR-0096 E4, D1 invariant: no identity is never a grant of authority). - ChatWithToolsOptions.toolExecutionContext: omission now means an unauthenticated (RLS-on, sees-nothing) principal; executors MUST NOT fall back to system-level behaviour. - ToolExecutionContext: documents the fail-closed identity derivation (actor → user context; isSystem → system; neither → anonymous) and adds an explicit isSystem?: boolean opt-in mirroring the IDataEngine / IKnowledgeService convention, so 'run as system' is a deliberate, greppable choice — not the consequence of a forgotten field. - content/docs/ai/actions-as-tools.mdx: docs no longer advertise omission as the way to keep system-level behaviour for cron/internal callers; they now point at the explicit isSystem opt-in. Mirror of the #2981 knowledge/RAG fail-closed fix, applied at the contract layer. Cloud-side executor work is tracked cross-repo. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019rao3tuTpJ8HPQV5fyxBoz
1 parent ca30acd commit 0a630d7

3 files changed

Lines changed: 52 additions & 7 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
Security (#2991): the AI `ToolExecutionContext` contract no longer documents system-level execution as the missing-actor default. A missing `toolExecutionContext` / `actor` now means an unauthenticated (RLS-on, sees-nothing) principal — executors MUST fail closed to anonymous, never fall open to system. System execution becomes an explicit, greppable opt-in via the new `ToolExecutionContext.isSystem?: boolean` field (same convention as `IDataEngine` / `IKnowledgeService`), reserved for trusted server-side invocations and ignored when an `actor` is present. Migration for internal callers that relied on the old omission default (cron, migrations, server jobs): pass `toolExecutionContext: { isSystem: true }` explicitly.

content/docs/ai/actions-as-tools.mdx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -228,9 +228,13 @@ authenticated principal from `req.user` — both cookie session
228228
`aiService.chatWithTools(...)` as
229229
`toolExecutionContext: { actor, conversationId, environmentId }`. That threads the
230230
same RLS context through the runtime's built-in data tools and its
231-
`action_<name>` tools. From a custom server route you opt in by passing the actor
232-
explicitly; omit `toolExecutionContext` to keep system-level behaviour (cron
233-
jobs, internal callers).
231+
`action_<name>` tools. From a custom server route you pass the actor explicitly.
232+
Omitting `toolExecutionContext` (or its `actor`) is **not** a system fallback:
233+
the contract fails closed and data tools run as an unauthenticated, RLS-on
234+
principal that sees nothing (#2991). Trusted internal callers (cron jobs,
235+
migrations) that genuinely need full authority opt in explicitly with
236+
`toolExecutionContext: { isSystem: true }` — a deliberate, greppable elevation,
237+
never the consequence of a forgotten field.
234238

235239
```typescript
236240
await aiService.chatWithTools(messages, tools, {

packages/spec/src/contracts/ai-service.ts

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -366,14 +366,20 @@ export interface ChatWithToolsOptions extends AIRequestOptions {
366366
onToolError?: (toolCall: ToolCallPart, error: string) => 'continue' | 'abort';
367367
/**
368368
* Per-call execution context threaded into every tool handler the
369-
* loop dispatches. The HTTP route should populate this from
369+
* loop dispatches. The HTTP route MUST populate this from
370370
* `req.user` (and any conversation/environment headers) so that
371371
* built-in data tools forward the actor into ObjectQL's
372372
* `ExecutionContext` and row-level security automatically scopes
373373
* what the LLM can see or change.
374374
*
375-
* Optional for backward compatibility — when omitted, tools fall
376-
* back to system-level behaviour.
375+
* Optional at the type level only — a missing context is NOT a
376+
* grant of authority (#2991). When omitted, executors MUST run
377+
* data-touching tools as an unauthenticated (RLS-on, sees-nothing)
378+
* principal, exactly as if an empty context had been passed; they
379+
* MUST NOT fall back to system-level behaviour. Trusted internal
380+
* callers (cron, migrations, server jobs) that genuinely need full
381+
* authority opt in explicitly via
382+
* {@link ToolExecutionContext.isSystem}.
377383
*/
378384
toolExecutionContext?: ToolExecutionContext;
379385
}
@@ -383,19 +389,49 @@ export interface ChatWithToolsOptions extends AIRequestOptions {
383389
* by {@link ChatWithToolsOptions}. Mirrors {@link ExecutionContext}
384390
* but is tailored to the AI tool boundary (no transaction handle, no
385391
* raw access token — those live on the engine call site).
392+
*
393+
* ## Identity semantics (fail-closed, #2991)
394+
*
395+
* "No identity" is never a grant of authority. Executors MUST derive
396+
* the ObjectQL `ExecutionContext` for data-touching tools as:
397+
*
398+
* - `actor` present → that user's context (RLS scopes reads/writes).
399+
* - `isSystem: true` → system context (RLS bypass) — an explicit,
400+
* greppable elevation, same convention as `IDataEngine` /
401+
* `IKnowledgeService`.
402+
* - neither → an anonymous, unauthenticated context (RLS on, sees
403+
* nothing). NEVER system.
386404
*/
387405
export interface ToolExecutionContext {
388406
/**
389407
* Authenticated end user on whose behalf the LLM is acting.
390408
* Built-in tools promote this into the ObjectQL `ExecutionContext`
391-
* so RLS engages. Omit for internal/system invocations.
409+
* so RLS engages.
410+
*
411+
* A missing actor means an UNAUTHENTICATED caller (#2991):
412+
* executors MUST run data-touching tools with an anonymous
413+
* (RLS-on, sees-nothing) context — never as system. System
414+
* execution is only ever the explicit {@link isSystem} opt-in,
415+
* not the consequence of a forgotten field.
392416
*/
393417
actor?: {
394418
id: string;
395419
name?: string;
396420
positions?: string[];
397421
permissions?: string[];
398422
};
423+
/**
424+
* Explicit, deliberate elevation: run tool handlers with a
425+
* system-level (RLS-bypassing) `ExecutionContext` — the same
426+
* convention as `IDataEngine` / `IKnowledgeService`
427+
* (`isSystem: true`). Reserved for trusted server-side invocations
428+
* (cron, migrations, internal jobs); MUST be set by trusted server
429+
* code only, never derived from request input, and ignored when an
430+
* {@link actor} is present. This flag is the ONLY way to obtain
431+
* system behaviour from the tool loop — a merely absent actor
432+
* fails closed to anonymous instead (#2991).
433+
*/
434+
isSystem?: boolean;
399435
/** Conversation id for trace/HITL correlation. */
400436
conversationId?: string;
401437
/**

0 commit comments

Comments
 (0)