@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.
- package/dist/api/request-context.d.ts +2 -0
- package/dist/api/request-context.js +16 -7
- package/dist/db/cross-tenant-reads.d.ts +8 -0
- package/dist/db/cross-tenant-reads.js +23 -0
- package/dist/db/queries/test-stack.d.ts +4 -1
- package/dist/db/queries/test-stack.js +15 -2
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +8 -0
- package/dist/internal/stack-support.d.ts +1 -1
- package/dist/internal/stack-support.js +1 -1
- package/dist/jobs/job-runner-helpers.d.ts +1 -2
- package/dist/jobs/job-runner-helpers.js +0 -16
- package/dist/jobs/job-runner.js +4 -3
- package/dist/pipeline/dispatch-shared.js +27 -2
- package/package.json +3 -3
- package/src/changes.json +6 -0
|
@@ -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
|
-
//
|
|
53
|
-
// turn/approve handler
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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,
|
|
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;
|
package/dist/jobs/job-runner.js
CHANGED
|
@@ -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,
|
|
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 =
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|