@cosmicdrift/kumiko-framework 0.385.0 → 0.386.0

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.
@@ -21,6 +21,7 @@ export type RequestContextData = {
21
21
  readonly risk: AgentRisk;
22
22
  };
23
23
  readonly chargedRateLimitBuckets?: Set<string>;
24
+ readonly agentToolCall?: true;
24
25
  };
25
26
  export declare const requestContext: {
26
27
  run<T>(data: RequestContextData, fn: () => T): T;
@@ -41,4 +42,5 @@ export declare function runWithOrigin<T>(origin: {
41
42
  };
42
43
  }, fn: () => T): T;
43
44
  export declare function runAsDirectCallEntry<T>(fn: () => T): T;
45
+ export declare function isAgentToolCallScope(): boolean;
44
46
  export declare function runWithWriteOrigin<T>(origin: WriteOrigin, fn: () => T): T;
@@ -48,15 +48,24 @@ export function runWithOrigin(origin, fn) {
48
48
  : new Set(),
49
49
  }, fn);
50
50
  }
51
- // Clears entryHandler so the next dispatch inside fn becomes the new entry —
52
- // used by dispatchToolCall so the invoked tool handler, not the surrounding
53
- // turn/approve handler, is what the irreversible-operation gate sees.
51
+ // Clears entryHandler so the next dispatch inside fn becomes the new entry, letting
52
+ // the irreversible-operation gate see the invoked tool handler instead of the
53
+ // surrounding turn/approve handler. Reserved for dispatchToolCall, so it also marks
54
+ // the scope as an agent tool call; it always mints a scope, because an agent loop
55
+ // running outside any request must still carry that marker.
54
56
  export function runAsDirectCallEntry(fn) {
55
57
  const current = requestContext.get();
56
- if (!current)
57
- return fn();
58
- const { entryHandler: _entryHandler, ...rest } = current;
59
- return requestContext.run(rest, fn);
58
+ const requestId = current?.requestId ?? requestContext.generateId();
59
+ const { entryHandler: _entryHandler, ...rest } = current ?? {};
60
+ return requestContext.run({
61
+ ...rest,
62
+ requestId,
63
+ correlationId: current?.correlationId ?? requestId,
64
+ agentToolCall: true,
65
+ }, fn);
66
+ }
67
+ export function isAgentToolCallScope() {
68
+ return requestContext.get()?.agentToolCall === true;
60
69
  }
61
70
  // An ungated origin never mints a scope: seeds and boot writes keep their missing context.
62
71
  export function runWithWriteOrigin(origin, fn) {
@@ -0,0 +1,8 @@
1
+ import type { CrossTenantReads } from "@cosmicdrift/kumiko-types/tenant-db-types";
2
+ import type { EscapeHatchReporter } from "../engine/types/index.ts";
3
+ import { type CreateTenantDbOptions, type TenantDb } from "./tenant-db.ts";
4
+ export declare function createCrossTenantReads(systemModeDb: TenantDb, reason: string, report: EscapeHatchReporter): CrossTenantReads;
5
+ export declare function createHandlerCrossTenantReads(options: Omit<CreateTenantDbOptions, "mode"> & {
6
+ readonly reason: string;
7
+ readonly report: EscapeHatchReporter;
8
+ }): CrossTenantReads;
@@ -0,0 +1,23 @@
1
+ import { createTenantDb } from "./tenant-db.js";
2
+ export function createCrossTenantReads(systemModeDb, reason, report) {
3
+ return {
4
+ selectMany(table, where, options) {
5
+ report("cross-tenant-read", reason);
6
+ return systemModeDb.selectMany(table, where, options);
7
+ },
8
+ fetchOne(table, where) {
9
+ report("cross-tenant-read", reason);
10
+ return systemModeDb.fetchOne(table, where);
11
+ },
12
+ count(table, where) {
13
+ report("cross-tenant-read", reason);
14
+ return systemModeDb.count(table, where);
15
+ },
16
+ };
17
+ }
18
+ // Own system-mode TenantDb without unsafeRaw/globalWrites: the handler gets the read surface only.
19
+ export function createHandlerCrossTenantReads(options) {
20
+ const { reason, report, ...dbOptions } = options;
21
+ const systemModeDb = createTenantDb({ ...dbOptions, mode: "system" });
22
+ return createCrossTenantReads(systemModeDb, reason, report);
23
+ }
@@ -2,5 +2,8 @@ import type { AnyDb } from "../query.ts";
2
2
  export { alterTableAddColumn, createIndexIfNotExists, executeDdlStatement } from "./ddl.ts";
3
3
  export declare function truncateTablesRestartIdentity(db: AnyDb, tableNames: readonly string[]): Promise<void>;
4
4
  export declare function databaseExists(db: AnyDb, dbName: string): Promise<boolean>;
5
- export declare function createDatabase(db: AnyDb, dbName: string): Promise<void>;
5
+ export declare function createDatabase(db: AnyDb, dbName: string, templateDbName?: string): Promise<void>;
6
+ export declare function renameDatabase(db: AnyDb, fromName: string, toName: string): Promise<void>;
7
+ export declare function listDatabaseNamesWithPrefix(db: AnyDb, prefix: string): Promise<readonly string[]>;
8
+ export declare function acquireSessionAdvisoryLock(db: AnyDb, key: string): Promise<void>;
6
9
  export declare function dropDatabaseIfExists(db: AnyDb, dbName: string): Promise<void>;
@@ -12,8 +12,21 @@ export async function databaseExists(db, dbName) {
12
12
  const rows = await asRawClient(db).unsafe(`SELECT EXISTS (SELECT 1 FROM pg_database WHERE datname = $1) AS exists`, [dbName]);
13
13
  return rows[0]?.exists === true;
14
14
  }
15
- export async function createDatabase(db, dbName) {
16
- await asRawClient(db).unsafe(`CREATE DATABASE ${quoteTableIdent(dbName)}`);
15
+ export async function createDatabase(db, dbName, templateDbName) {
16
+ const template = templateDbName === undefined ? "" : ` TEMPLATE ${quoteTableIdent(templateDbName)}`;
17
+ await asRawClient(db).unsafe(`CREATE DATABASE ${quoteTableIdent(dbName)}${template}`);
18
+ }
19
+ export async function renameDatabase(db, fromName, toName) {
20
+ await asRawClient(db).unsafe(`ALTER DATABASE ${quoteTableIdent(fromName)} RENAME TO ${quoteTableIdent(toName)}`);
21
+ }
22
+ export async function listDatabaseNamesWithPrefix(db, prefix) {
23
+ const rows = await asRawClient(db).unsafe(`SELECT datname FROM pg_database WHERE starts_with(datname, $1) ORDER BY datname`, [prefix]);
24
+ return rows.map((row) => row.datname);
25
+ }
26
+ // Session-scoped: CREATE DATABASE cannot run inside a transaction, so the
27
+ // xact-scoped variant cannot guard it. Released when the session closes.
28
+ export async function acquireSessionAdvisoryLock(db, key) {
29
+ await asRawClient(db).unsafe(`SELECT pg_advisory_lock(hashtext($1))`, [key]);
17
30
  }
18
31
  export async function dropDatabaseIfExists(db, dbName) {
19
32
  await asRawClient(db).unsafe(`DROP DATABASE IF EXISTS ${quoteTableIdent(dbName)}`);
@@ -233,6 +233,7 @@ export declare const PATTERN_SCHEMAS_BY_KIND: {
233
233
  escapeHatch: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
234
234
  reason: z.ZodString;
235
235
  grants: z.ZodOptional<z.ZodArray<z.ZodEnum<{
236
+ crossTenantReads: "crossTenantReads";
236
237
  globalWrites: "globalWrites";
237
238
  systemIdentity: "systemIdentity";
238
239
  unsafeRaw: "unsafeRaw";
@@ -314,6 +315,7 @@ export declare const PATTERN_SCHEMAS_BY_KIND: {
314
315
  escapeHatch: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
315
316
  reason: z.ZodString;
316
317
  grants: z.ZodOptional<z.ZodArray<z.ZodEnum<{
318
+ crossTenantReads: "crossTenantReads";
317
319
  globalWrites: "globalWrites";
318
320
  systemIdentity: "systemIdentity";
319
321
  unsafeRaw: "unsafeRaw";
@@ -384,6 +386,7 @@ export declare const PATTERN_SCHEMAS_BY_KIND: {
384
386
  escapeHatch: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
385
387
  reason: z.ZodString;
386
388
  grants: z.ZodOptional<z.ZodArray<z.ZodEnum<{
389
+ crossTenantReads: "crossTenantReads";
387
390
  globalWrites: "globalWrites";
388
391
  systemIdentity: "systemIdentity";
389
392
  unsafeRaw: "unsafeRaw";
@@ -420,6 +423,7 @@ export declare const PATTERN_SCHEMAS_BY_KIND: {
420
423
  escapeHatch: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
421
424
  reason: z.ZodString;
422
425
  grants: z.ZodOptional<z.ZodArray<z.ZodEnum<{
426
+ crossTenantReads: "crossTenantReads";
423
427
  globalWrites: "globalWrites";
424
428
  systemIdentity: "systemIdentity";
425
429
  unsafeRaw: "unsafeRaw";
@@ -828,6 +832,7 @@ export declare const patternSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
828
832
  escapeHatch: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
829
833
  reason: z.ZodString;
830
834
  grants: z.ZodOptional<z.ZodArray<z.ZodEnum<{
835
+ crossTenantReads: "crossTenantReads";
831
836
  globalWrites: "globalWrites";
832
837
  systemIdentity: "systemIdentity";
833
838
  unsafeRaw: "unsafeRaw";
@@ -908,6 +913,7 @@ export declare const patternSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
908
913
  escapeHatch: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
909
914
  reason: z.ZodString;
910
915
  grants: z.ZodOptional<z.ZodArray<z.ZodEnum<{
916
+ crossTenantReads: "crossTenantReads";
911
917
  globalWrites: "globalWrites";
912
918
  systemIdentity: "systemIdentity";
913
919
  unsafeRaw: "unsafeRaw";
@@ -977,6 +983,7 @@ export declare const patternSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
977
983
  escapeHatch: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
978
984
  reason: z.ZodString;
979
985
  grants: z.ZodOptional<z.ZodArray<z.ZodEnum<{
986
+ crossTenantReads: "crossTenantReads";
980
987
  globalWrites: "globalWrites";
981
988
  systemIdentity: "systemIdentity";
982
989
  unsafeRaw: "unsafeRaw";
@@ -1012,6 +1019,7 @@ export declare const patternSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
1012
1019
  escapeHatch: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
1013
1020
  reason: z.ZodString;
1014
1021
  grants: z.ZodOptional<z.ZodArray<z.ZodEnum<{
1022
+ crossTenantReads: "crossTenantReads";
1015
1023
  globalWrites: "globalWrites";
1016
1024
  systemIdentity: "systemIdentity";
1017
1025
  unsafeRaw: "unsafeRaw";
@@ -1,5 +1,5 @@
1
1
  export { createDbConnection } from "../db/connection.ts";
2
- export { createDatabase, databaseExists, dropDatabaseIfExists, truncateTablesRestartIdentity, } from "../db/queries/test-stack.ts";
2
+ export { acquireSessionAdvisoryLock, createDatabase, databaseExists, dropDatabaseIfExists, listDatabaseNamesWithPrefix, renameDatabase, truncateTablesRestartIdentity, } from "../db/queries/test-stack.ts";
3
3
  export { type AnyDb, isClosedConnectionError } from "../db/query.ts";
4
4
  export { columnNamesOf } from "../db/schema-inspection.ts";
5
5
  export { schemaTableName } from "../db/table-introspection.ts";
@@ -1,6 +1,6 @@
1
1
  // @runtime dev
2
2
  export { createDbConnection } from "../db/connection.js";
3
- export { createDatabase, databaseExists, dropDatabaseIfExists, truncateTablesRestartIdentity, } from "../db/queries/test-stack.js";
3
+ export { acquireSessionAdvisoryLock, createDatabase, databaseExists, dropDatabaseIfExists, listDatabaseNamesWithPrefix, renameDatabase, truncateTablesRestartIdentity, } from "../db/queries/test-stack.js";
4
4
  export { isClosedConnectionError } from "../db/query.js";
5
5
  export { columnNamesOf } from "../db/schema-inspection.js";
6
6
  export { schemaTableName } from "../db/table-introspection.js";
@@ -3,7 +3,7 @@ import type { ConfigAccessor } from "@cosmicdrift/kumiko-types/config";
3
3
  import type { TzContext } from "@cosmicdrift/kumiko-types/tz-context";
4
4
  import type { JobsOptions, Queue } from "bullmq";
5
5
  import type { TenantDb } from "../db/tenant-db.ts";
6
- import type { AppContext, EscapeHatchReporter, JobContext, JobDefinition, JobRunIn, JobTriggerEvent } from "../engine/types/index.ts";
6
+ import type { AppContext, JobDefinition, JobRunIn, JobTriggerEvent } from "../engine/types/index.ts";
7
7
  import type { Logger } from "../logging/types.ts";
8
8
  import type { Meter, SerializedTraceContext, Tracer } from "../observability/index.ts";
9
9
  import type { JobLogEntry, JobSubjectValue } from "./job-runner-types.ts";
@@ -14,7 +14,6 @@ export declare function serializeJobSubject(subject: Readonly<Record<string, Job
14
14
  export declare function jobSubjectKey(jobName: string, payload: Record<string, unknown>, fields: readonly string[] | undefined, declaration: "tenantVisibleFailure" | "tenantVisibleRun"): string | null;
15
15
  export declare function resolveJobTz(config: ConfigAccessor | undefined, context: Pick<AppContext, "geoTzProvider" | "clock">): Promise<TzContext>;
16
16
  export declare function missingDbProxy(jobName: string): TenantDb;
17
- export declare function createCrossTenantReads(systemModeDb: TenantDb, reason: string, report: EscapeHatchReporter): NonNullable<JobContext["crossTenantReads"]>;
18
17
  export declare function priorAttemptsOf(data: Record<string, unknown>): number;
19
18
  export declare function tenantFailureMessageKey(err: unknown, declaration: JobDefinition["tenantVisibleFailure"]): string | null;
20
19
  export declare const TRACE_CONTEXT_KEY = "_traceContext";
@@ -103,22 +103,6 @@ export function missingDbProxy(jobName) {
103
103
  },
104
104
  });
105
105
  }
106
- export function createCrossTenantReads(systemModeDb, reason, report) {
107
- return {
108
- selectMany(table, where, options) {
109
- report("cross-tenant-read", reason);
110
- return systemModeDb.selectMany(table, where, options);
111
- },
112
- fetchOne(table, where) {
113
- report("cross-tenant-read", reason);
114
- return systemModeDb.fetchOne(table, where);
115
- },
116
- count(table, where) {
117
- report("cross-tenant-read", reason);
118
- return systemModeDb.count(table, where);
119
- },
120
- };
121
- }
122
106
  export function priorAttemptsOf(data) {
123
107
  const value = data["_priorAttempts"];
124
108
  return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : 0;
@@ -1,8 +1,9 @@
1
1
  /// <reference types="temporal-polyfill/global" preserve="true" />
2
- import { escapeHatchFor } from "@cosmicdrift/kumiko-types/handlers";
2
+ import { crossTenantReadsGrantFor, escapeHatchFor } from "@cosmicdrift/kumiko-types/handlers";
3
3
  import { DelayedError, Queue, UnrecoverableError, Worker } from "bullmq";
4
4
  import { Redis } from "ioredis";
5
5
  import { requestContext } from "../api/request-context.js";
6
+ import { createCrossTenantReads } from "../db/cross-tenant-reads.js";
6
7
  import { createTenantDb, createUncheckedSystemDb } from "../db/tenant-db.js";
7
8
  import { createDerivativesContext } from "../derivatives/derivatives-context.js";
8
9
  import { bridgeStub } from "../engine/bridge-stub.js";
@@ -19,7 +20,7 @@ import { RedisKeys } from "../pipeline/redis-keys.js";
19
20
  import { createGatedIdentitySwitch } from "../pipeline/system-identity-switch.js";
20
21
  import { buildPersonalDataGate, parseWriteOrigin } from "../pipeline/write-origin.js";
21
22
  import { cronRepeatOptions } from "./cron-repeat-options.js";
22
- import { BOOT_GATE_SLOW_WARN_MS, BOOT_REDIS_TIMEOUT_MS, buildRetryBullOpts, COMPLETED_JOB_RETENTION_AGE_SEC, captureTraceContext, createCrossTenantReads, createJobLogger, DEFAULT_MAX_DEFERS, DEFER_BACKOFF_MAX_DOUBLINGS, DEFER_COUNT_KEY, DEFER_DELAY_CAP_MS, FAILED_JOB_RETENTION_AGE_SEC, HANDLER_DEFER_COUNT_KEY, isJobTriggerEvent, jobSubjectKey, maxRetryWindowMs, missingDbProxy, parseRedisOpts, payloadMatchesTriggerFilter, priorAttemptsOf, readTraceContext, resolveJobTz, STOP_QUEUE_READY_TIMEOUT_MS, stampGatedWriteOrigin, startQueueDepthPolling, TRACE_CONTEXT_KEY, tenantFailureMessageKey, timeoutReject, } from "./job-runner-helpers.js";
23
+ import { BOOT_GATE_SLOW_WARN_MS, BOOT_REDIS_TIMEOUT_MS, buildRetryBullOpts, COMPLETED_JOB_RETENTION_AGE_SEC, captureTraceContext, createJobLogger, DEFAULT_MAX_DEFERS, DEFER_BACKOFF_MAX_DOUBLINGS, DEFER_COUNT_KEY, DEFER_DELAY_CAP_MS, FAILED_JOB_RETENTION_AGE_SEC, HANDLER_DEFER_COUNT_KEY, isJobTriggerEvent, jobSubjectKey, maxRetryWindowMs, missingDbProxy, parseRedisOpts, payloadMatchesTriggerFilter, priorAttemptsOf, readTraceContext, resolveJobTz, STOP_QUEUE_READY_TIMEOUT_MS, stampGatedWriteOrigin, startQueueDepthPolling, TRACE_CONTEXT_KEY, tenantFailureMessageKey, timeoutReject, } from "./job-runner-helpers.js";
23
24
  import { ACTIVE_TENANT_IDS_QUERY_NAME, bootJobIdForJobName, DEFAULT_QUEUE_NAME_PREFIX, markSchedulersSeen, ORPHAN_SCHEDULER_GRACE_MS, perTenantChildJobId, queueNameFor, removeOrphanJobSchedulers, schedulerIdForJobName, } from "./job-runner-schedulers.js";
24
25
  import { resolveMaxPerTenant } from "./max-per-tenant.js";
25
26
  export { serializeJobSubject } from "./job-runner-helpers.js";
@@ -611,7 +612,7 @@ export function createJobRunner(options) {
611
612
  },
612
613
  })
613
614
  : undefined;
614
- const crossTenantReadsReason = escapeHatchFor(jobDef.escapeHatch, "unsafeRaw")?.reason;
615
+ const crossTenantReadsReason = crossTenantReadsGrantFor(jobDef.escapeHatch)?.reason;
615
616
  const crossTenantReads = tenantScopedDb && crossTenantReadsReason !== undefined
616
617
  ? createCrossTenantReads(tenantScopedDb, crossTenantReadsReason, reportEscapeHatch)
617
618
  : undefined;
@@ -1,5 +1,6 @@
1
- import { escapeHatchFor } from "@cosmicdrift/kumiko-types/handlers";
2
- import { requestContext, runWithOrigin } from "../api/request-context.js";
1
+ import { crossTenantReadsGrantFor, escapeHatchFor } from "@cosmicdrift/kumiko-types/handlers";
2
+ import { isAgentToolCallScope, requestContext, runWithOrigin } from "../api/request-context.js";
3
+ import { createHandlerCrossTenantReads } from "../db/cross-tenant-reads.js";
3
4
  import { runInSavepoint, selectMany } from "../db/query.js";
4
5
  import { createTenantDb, createUncheckedSystemDb, hasTenantColumn, } from "../db/tenant-db.js";
5
6
  import { createDerivativesContext } from "../derivatives/derivatives-context.js";
@@ -186,6 +187,7 @@ export function applyMemberResolutionReadOnly(handlerContext) {
186
187
  // A read as a member gets no system-scope DB, preSave pipeline or file storage.
187
188
  runPreSave: undefined,
188
189
  systemDb: undefined,
190
+ crossTenantReads: undefined,
189
191
  files: undefined,
190
192
  derivatives: undefined,
191
193
  ...(handlerContext.jobRunner && { jobRunner: denyingJobRunnerProxy() }),
@@ -326,6 +328,28 @@ readOnlyForTenant = false) {
326
328
  const systemDb = isSystem && db
327
329
  ? createUncheckedSystemDb(db, rawDbOutsideTransaction, reportEscapeHatch)
328
330
  : undefined;
331
+ // An agent tool call runs with the user's session but feeds the LLM context, so
332
+ // cross-tenant rows must never reach it, even when unsafeRaw implies the grant.
333
+ const crossTenantReadsReason = isAgentToolCallScope()
334
+ ? undefined
335
+ : crossTenantReadsGrantFor(handlerEscapeHatch)?.reason;
336
+ const crossTenantReads = dbSource && crossTenantReadsReason !== undefined
337
+ ? createHandlerCrossTenantReads({
338
+ db: dbSource,
339
+ tenantId: user.tenantId,
340
+ tracer: context.tracer,
341
+ meter: context.meter,
342
+ signal: reqCtx?.signal,
343
+ reason: crossTenantReadsReason,
344
+ report: reportEscapeHatch,
345
+ grants: {
346
+ report: reportEscapeHatch,
347
+ memberReadOnly: isMemberResolutionPrincipal(user),
348
+ personalDataGate: buildPersonalDataGate(registry, origin),
349
+ projectionRegistry: registry,
350
+ },
351
+ })
352
+ : undefined;
329
353
  // Exposed as ctx.db below — the internal `db` above stays the real,
330
354
  // working TenantDb for this function's own use (config/derivatives
331
355
  // accessors, systemDb construction).
@@ -855,6 +879,7 @@ readOnlyForTenant = false) {
855
879
  db: exposedDb,
856
880
  dbOutsideTransaction,
857
881
  ...(systemDb && { systemDb }),
882
+ ...(crossTenantReads && { crossTenantReads }),
858
883
  log,
859
884
  notify,
860
885
  ...(config && { config }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-framework",
3
- "version": "0.385.0",
3
+ "version": "0.386.0",
4
4
  "description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -199,7 +199,7 @@
199
199
  "./package.json": "./package.json"
200
200
  },
201
201
  "dependencies": {
202
- "@cosmicdrift/kumiko-types": "0.385.0",
202
+ "@cosmicdrift/kumiko-types": "0.386.0",
203
203
  "bullmq": "^5.76.7",
204
204
  "bun-types": "^1.3.13",
205
205
  "hono": "^4.13.1",
@@ -215,7 +215,7 @@
215
215
  "zod": "^4.4.3"
216
216
  },
217
217
  "devDependencies": {
218
- "@cosmicdrift/kumiko-testing": "0.385.0",
218
+ "@cosmicdrift/kumiko-testing": "0.386.0",
219
219
  "bun-types": "^1.3.13",
220
220
  "pino-pretty": "^13.1.3"
221
221
  },
package/src/changes.json CHANGED
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.386.0",
4
+ "type": "improvement",
5
+ "title": "ctx.crossTenantReads for handlers with the crossTenantReads grant",
6
+ "detail": "A handler whose `escapeHatch` lists `grants: [\"crossTenantReads\"]` (or `unsafeRaw`, which implies it) gets `ctx.crossTenantReads` with `selectMany`, `fetchOne` and `count`, unfiltered by tenant and including the events table. Every call is audited as `cross-tenant-read`; `ctx.db.unsafeRaw()` stays denied for the read-only grant, and neither a member-resolution read nor an agent tool call (`dispatchToolCall`, including handlers it reaches through nested `ctx.query`/`ctx.write`) ever receives the surface, so cross-tenant rows cannot reach an LLM context. Jobs use the same grant. The internal `runAsDirectCallEntry` (`/internal/request-context`) now also marks its scope as an agent tool call. Without the grant nothing changes."
7
+ },
2
8
  {
3
9
  "version": "0.385.0",
4
10
  "type": "improvement",