From 0a630d7b9ae74bb5fdd6c6dabf9cfe85d6bd4f2f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 15:58:39 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix(spec):=20ToolExecutionContext=20fails?= =?UTF-8?q?=20closed=20on=20missing=20actor=20=E2=80=94=20system=20is=20an?= =?UTF-8?q?=20explicit=20isSystem=20opt-in=20(#2991)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_019rao3tuTpJ8HPQV5fyxBoz --- .../ai-toolexecutioncontext-fail-closed.md | 5 +++ content/docs/ai/actions-as-tools.mdx | 10 +++-- packages/spec/src/contracts/ai-service.ts | 44 +++++++++++++++++-- 3 files changed, 52 insertions(+), 7 deletions(-) create mode 100644 .changeset/ai-toolexecutioncontext-fail-closed.md diff --git a/.changeset/ai-toolexecutioncontext-fail-closed.md b/.changeset/ai-toolexecutioncontext-fail-closed.md new file mode 100644 index 0000000000..7934833d27 --- /dev/null +++ b/.changeset/ai-toolexecutioncontext-fail-closed.md @@ -0,0 +1,5 @@ +--- +'@objectstack/spec': minor +--- + +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. diff --git a/content/docs/ai/actions-as-tools.mdx b/content/docs/ai/actions-as-tools.mdx index 68cbe6c13b..6c402f9c05 100644 --- a/content/docs/ai/actions-as-tools.mdx +++ b/content/docs/ai/actions-as-tools.mdx @@ -228,9 +228,13 @@ authenticated principal from `req.user` — both cookie session `aiService.chatWithTools(...)` as `toolExecutionContext: { actor, conversationId, environmentId }`. That threads the same RLS context through the runtime's built-in data tools and its -`action_` tools. From a custom server route you opt in by passing the actor -explicitly; omit `toolExecutionContext` to keep system-level behaviour (cron -jobs, internal callers). +`action_` tools. From a custom server route you pass the actor explicitly. +Omitting `toolExecutionContext` (or its `actor`) is **not** a system fallback: +the contract fails closed and data tools run as an unauthenticated, RLS-on +principal that sees nothing (#2991). Trusted internal callers (cron jobs, +migrations) that genuinely need full authority opt in explicitly with +`toolExecutionContext: { isSystem: true }` — a deliberate, greppable elevation, +never the consequence of a forgotten field. ```typescript await aiService.chatWithTools(messages, tools, { diff --git a/packages/spec/src/contracts/ai-service.ts b/packages/spec/src/contracts/ai-service.ts index 3210ff085f..8feec2fe49 100644 --- a/packages/spec/src/contracts/ai-service.ts +++ b/packages/spec/src/contracts/ai-service.ts @@ -366,14 +366,20 @@ export interface ChatWithToolsOptions extends AIRequestOptions { onToolError?: (toolCall: ToolCallPart, error: string) => 'continue' | 'abort'; /** * Per-call execution context threaded into every tool handler the - * loop dispatches. The HTTP route should populate this from + * loop dispatches. The HTTP route MUST populate this from * `req.user` (and any conversation/environment headers) so that * built-in data tools forward the actor into ObjectQL's * `ExecutionContext` and row-level security automatically scopes * what the LLM can see or change. * - * Optional for backward compatibility — when omitted, tools fall - * back to system-level behaviour. + * Optional at the type level only — a missing context is NOT a + * grant of authority (#2991). When omitted, executors MUST run + * data-touching tools as an unauthenticated (RLS-on, sees-nothing) + * principal, exactly as if an empty context had been passed; they + * MUST NOT fall back to system-level behaviour. Trusted internal + * callers (cron, migrations, server jobs) that genuinely need full + * authority opt in explicitly via + * {@link ToolExecutionContext.isSystem}. */ toolExecutionContext?: ToolExecutionContext; } @@ -383,12 +389,30 @@ export interface ChatWithToolsOptions extends AIRequestOptions { * by {@link ChatWithToolsOptions}. Mirrors {@link ExecutionContext} * but is tailored to the AI tool boundary (no transaction handle, no * raw access token — those live on the engine call site). + * + * ## Identity semantics (fail-closed, #2991) + * + * "No identity" is never a grant of authority. Executors MUST derive + * the ObjectQL `ExecutionContext` for data-touching tools as: + * + * - `actor` present → that user's context (RLS scopes reads/writes). + * - `isSystem: true` → system context (RLS bypass) — an explicit, + * greppable elevation, same convention as `IDataEngine` / + * `IKnowledgeService`. + * - neither → an anonymous, unauthenticated context (RLS on, sees + * nothing). NEVER system. */ export interface ToolExecutionContext { /** * Authenticated end user on whose behalf the LLM is acting. * Built-in tools promote this into the ObjectQL `ExecutionContext` - * so RLS engages. Omit for internal/system invocations. + * so RLS engages. + * + * A missing actor means an UNAUTHENTICATED caller (#2991): + * executors MUST run data-touching tools with an anonymous + * (RLS-on, sees-nothing) context — never as system. System + * execution is only ever the explicit {@link isSystem} opt-in, + * not the consequence of a forgotten field. */ actor?: { id: string; @@ -396,6 +420,18 @@ export interface ToolExecutionContext { positions?: string[]; permissions?: string[]; }; + /** + * Explicit, deliberate elevation: run tool handlers with a + * system-level (RLS-bypassing) `ExecutionContext` — the same + * convention as `IDataEngine` / `IKnowledgeService` + * (`isSystem: true`). Reserved for trusted server-side invocations + * (cron, migrations, internal jobs); MUST be set by trusted server + * code only, never derived from request input, and ignored when an + * {@link actor} is present. This flag is the ONLY way to obtain + * system behaviour from the tool loop — a merely absent actor + * fails closed to anonymous instead (#2991). + */ + isSystem?: boolean; /** Conversation id for trace/HITL correlation. */ conversationId?: string; /** From 6fe1a88037eb69c891fecf0381b46f4b69840855 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 16:10:48 +0000 Subject: [PATCH 2/2] chore: retrigger CI (Vercel deployment infra failure) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019rao3tuTpJ8HPQV5fyxBoz