@exulu/backend 3.2.0 → 3.3.1

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.
@@ -1733,7 +1733,7 @@ var ExuluTool = class _ExuluTool {
1733
1733
  if (!agent) {
1734
1734
  throw new Error("Agent not found.");
1735
1735
  }
1736
- const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-QG7E6UX5.js");
1736
+ const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-WQWYMU7G.js");
1737
1737
  const tools = await convertExuluToolsToAiSdkTools2(
1738
1738
  [this],
1739
1739
  [],
@@ -3008,7 +3008,7 @@ function neutralResult(question, keywords, importantKeyword, steps = []) {
3008
3008
  return {
3009
3009
  memoryChunksForAnswer: [],
3010
3010
  memoryOverride: { active: false, chunks: [], reason: "" },
3011
- memoryPinnedItemIds: /* @__PURE__ */ new Set(),
3011
+ memoryPinnedItemIdsByContext: /* @__PURE__ */ new Map(),
3012
3012
  updatedQuestion: question,
3013
3013
  updatedKeywords: keywords,
3014
3014
  updatedImportantKeyword: importantKeyword,
@@ -3109,7 +3109,7 @@ async function runMemoryPhase({
3109
3109
  chunks: [],
3110
3110
  reason: ""
3111
3111
  };
3112
- let memoryPinnedItemIds = /* @__PURE__ */ new Set();
3112
+ const memoryPinnedItemIdsByContext = /* @__PURE__ */ new Map();
3113
3113
  let updatedQuestion = question;
3114
3114
  let updatedKeywords = keywords;
3115
3115
  let updatedImportantKeyword = importantKeyword;
@@ -3272,25 +3272,30 @@ ${glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n")}` : "";
3272
3272
  if (fileResult.output?.shouldPrioritizeFiles && fileResult.output?.fileNameHints?.length) {
3273
3273
  const hints = fileResult.output.fileNameHints;
3274
3274
  const pinResults = await Promise.all(
3275
- documentContexts.map(
3276
- (ctx) => fuzzyPrefilter({
3275
+ documentContexts.map(async (ctx) => ({
3276
+ ctxId: ctx.id,
3277
+ matches: await fuzzyPrefilter({
3277
3278
  cacheKey: `memory-pin:${ctx.id}`,
3278
3279
  relevantKeywords: hints,
3279
3280
  context: ctx,
3280
3281
  fields: ["name", "id", "external_id"],
3281
3282
  normalize: (item) => item.external_id ? normalizeFileName(item.external_id) : item.name
3282
3283
  }).catch(() => [])
3283
- )
3284
+ }))
3284
3285
  );
3285
- for (const results of pinResults) {
3286
- for (const r of results) {
3287
- memoryPinnedItemIds.add(r.id);
3286
+ const pinnedNames = [];
3287
+ for (const { ctxId, matches } of pinResults) {
3288
+ if (!matches.length) continue;
3289
+ const set = memoryPinnedItemIdsByContext.get(ctxId) ?? /* @__PURE__ */ new Set();
3290
+ for (const m of matches) {
3291
+ set.add(m.id);
3292
+ pinnedNames.push(m.name);
3288
3293
  }
3294
+ memoryPinnedItemIdsByContext.set(ctxId, set);
3289
3295
  }
3290
- if (memoryPinnedItemIds.size > 0) {
3291
- const names = pinResults.flat().map((i) => i.name).join(", ");
3296
+ if (pinnedNames.length > 0) {
3292
3297
  steps.push({
3293
- text: `Memory prioritizes specific document(s); pinning ${memoryPinnedItemIds.size} file(s) into the search: ${names}`
3298
+ text: `Memory prioritizes specific document(s); pinning ${pinnedNames.length} file(s) into the search: ${pinnedNames.join(", ")}`
3294
3299
  });
3295
3300
  }
3296
3301
  }
@@ -3313,7 +3318,7 @@ ${glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n")}` : "";
3313
3318
  return {
3314
3319
  memoryChunksForAnswer,
3315
3320
  memoryOverride,
3316
- memoryPinnedItemIds,
3321
+ memoryPinnedItemIdsByContext,
3317
3322
  updatedQuestion,
3318
3323
  updatedKeywords,
3319
3324
  updatedImportantKeyword,
@@ -3420,7 +3425,7 @@ async function searchContexts(opts) {
3420
3425
  preselectedItems,
3421
3426
  scopedItemsByContext,
3422
3427
  identifierPinsByContext,
3423
- memoryPinnedItemIds,
3428
+ memoryPinnedItemIdsByContext,
3424
3429
  userPinnedItemIdsByContext,
3425
3430
  rewrites,
3426
3431
  styleHint,
@@ -3451,7 +3456,8 @@ async function searchContexts(opts) {
3451
3456
  const identifierPins = identifierPinsByContext.get(ctxId) ?? /* @__PURE__ */ new Set();
3452
3457
  let pins = new Set(identifierPins);
3453
3458
  if (kind === "documents") {
3454
- for (const id of memoryPinnedItemIds) pins.add(id);
3459
+ const memPins = memoryPinnedItemIdsByContext.get(ctxId);
3460
+ if (memPins) for (const id of memPins) pins.add(id);
3455
3461
  }
3456
3462
  const userPins = userPinnedItemIdsByContext.get(ctxId);
3457
3463
  if (userPins && userPins.size > 0) {
@@ -3963,7 +3969,7 @@ ${projectScope.customInstructions}` : ""
3963
3969
  updatedQuestion,
3964
3970
  updatedKeywords,
3965
3971
  updatedImportantKeyword,
3966
- memoryPinnedItemIds,
3972
+ memoryPinnedItemIdsByContext,
3967
3973
  memoryOverride
3968
3974
  } = memResult;
3969
3975
  const { pinsByContext: identifierPinsByContext, exactPinsByContext, steps: pinSteps } = await resolveIdentifierPins({
@@ -3996,7 +4002,7 @@ ${projectScope.customInstructions}` : ""
3996
4002
  model: utilityModel,
3997
4003
  preselectedItems,
3998
4004
  identifierPinsByContext,
3999
- memoryPinnedItemIds,
4005
+ memoryPinnedItemIdsByContext,
4000
4006
  userPinnedItemIdsByContext,
4001
4007
  scopedItemsByContext: resolvedProject?.scopedItemsByContext,
4002
4008
  rewrites: cfg.vocabulary.rewrites,
@@ -4016,7 +4022,7 @@ ${projectScope.customInstructions}` : ""
4016
4022
  model: utilityModel,
4017
4023
  preselectedItems,
4018
4024
  identifierPinsByContext,
4019
- memoryPinnedItemIds,
4025
+ memoryPinnedItemIdsByContext,
4020
4026
  userPinnedItemIdsByContext,
4021
4027
  scopedItemsByContext: resolvedProject?.scopedItemsByContext,
4022
4028
  rewrites: cfg.vocabulary.rewrites,
@@ -4026,7 +4032,9 @@ ${projectScope.customInstructions}` : ""
4026
4032
  }) : Promise.resolve({ chunks: [] })
4027
4033
  ]);
4028
4034
  const pinnedItemIds = /* @__PURE__ */ new Set([
4029
- ...memoryPinnedItemIds,
4035
+ ...(function* () {
4036
+ for (const s of memoryPinnedItemIdsByContext.values()) yield* s;
4037
+ })(),
4030
4038
  ...(function* () {
4031
4039
  for (const s of exactPinsByContext.values()) yield* s;
4032
4040
  })(),
@@ -7483,6 +7491,7 @@ var buildToolCallEvent = async (ctx, opts) => {
7483
7491
  toolCallId: ctx.toolCallId
7484
7492
  },
7485
7493
  target: { kind: "tool", id: ctx.tool.id, name: ctx.tool.name, category: ctx.tool.category, builtin: ctx.builtin },
7494
+ ...ctx.client ? { client: ctx.client } : {},
7486
7495
  ...credential ? { credential } : {},
7487
7496
  status,
7488
7497
  ...status === "error" ? { error: { name: err?.name, message: String(err?.message ?? err ?? "unknown error") } } : {},
@@ -7599,6 +7608,25 @@ var emitToolCallAudit = async (logger, ctx) => {
7599
7608
  );
7600
7609
  };
7601
7610
 
7611
+ // src/exulu/audit/client-info.ts
7612
+ var firstHeader = (v) => Array.isArray(v) ? v[0] : v ?? void 0;
7613
+ function extractClientInfo(req) {
7614
+ if (!req) return void 0;
7615
+ const headers = req.headers ?? {};
7616
+ const forwardedFor = firstHeader(headers["x-forwarded-for"]);
7617
+ const ip = (forwardedFor ? forwardedFor.split(",")[0]?.trim() : void 0) || req.ip || req.socket?.remoteAddress || void 0;
7618
+ const userAgent = firstHeader(headers["user-agent"]);
7619
+ const referer = firstHeader(headers["referer"]);
7620
+ const origin = firstHeader(headers["origin"]);
7621
+ const client = {};
7622
+ if (ip) client.ip = ip;
7623
+ if (userAgent) client.userAgent = userAgent;
7624
+ if (referer) client.referer = referer;
7625
+ if (origin) client.origin = origin;
7626
+ if (forwardedFor) client.forwardedFor = forwardedFor;
7627
+ return Object.keys(client).length > 0 ? client : void 0;
7628
+ }
7629
+
7602
7630
  // src/templates/tools/convert-exulu-tools-to-ai-sdk-tools.ts
7603
7631
  var OUTPUT_OFFLOAD_EXEMPT_TOOL_IDS = /* @__PURE__ */ new Set(["agentic_context_search"]);
7604
7632
  var generateS3Key = (filename) => `${randomUUID5()}-${filename}`;
@@ -8080,7 +8108,8 @@ var convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approved
8080
8108
  input: inputs,
8081
8109
  output: __auditOutput,
8082
8110
  status: __auditStatus,
8083
- error: __auditError
8111
+ error: __auditError,
8112
+ client: extractClientInfo(req)
8084
8113
  });
8085
8114
  if (__auditLogger.failClosed) {
8086
8115
  await __emit;
@@ -2,7 +2,7 @@ import "dotenv/config";
2
2
  import {
3
3
  convertExuluToolsToAiSdkTools,
4
4
  hydrateVariables
5
- } from "./chunk-CVQTDG37.js";
5
+ } from "./chunk-5FTX543Z.js";
6
6
  import "./chunk-7CCMW3IW.js";
7
7
  export {
8
8
  convertExuluToolsToAiSdkTools,
package/dist/index.cjs CHANGED
@@ -5699,6 +5699,7 @@ var init_tool_call = __esm({
5699
5699
  toolCallId: ctx.toolCallId
5700
5700
  },
5701
5701
  target: { kind: "tool", id: ctx.tool.id, name: ctx.tool.name, category: ctx.tool.category, builtin: ctx.builtin },
5702
+ ...ctx.client ? { client: ctx.client } : {},
5702
5703
  ...credential ? { credential } : {},
5703
5704
  status,
5704
5705
  ...status === "error" ? { error: { name: err?.name, message: String(err?.message ?? err ?? "unknown error") } } : {},
@@ -5834,6 +5835,32 @@ var init_emit_tool_call = __esm({
5834
5835
  }
5835
5836
  });
5836
5837
 
5838
+ // src/exulu/audit/client-info.ts
5839
+ function extractClientInfo(req) {
5840
+ if (!req) return void 0;
5841
+ const headers = req.headers ?? {};
5842
+ const forwardedFor = firstHeader(headers["x-forwarded-for"]);
5843
+ const ip = (forwardedFor ? forwardedFor.split(",")[0]?.trim() : void 0) || req.ip || req.socket?.remoteAddress || void 0;
5844
+ const userAgent = firstHeader(headers["user-agent"]);
5845
+ const referer = firstHeader(headers["referer"]);
5846
+ const origin = firstHeader(headers["origin"]);
5847
+ const client2 = {};
5848
+ if (ip) client2.ip = ip;
5849
+ if (userAgent) client2.userAgent = userAgent;
5850
+ if (referer) client2.referer = referer;
5851
+ if (origin) client2.origin = origin;
5852
+ if (forwardedFor) client2.forwardedFor = forwardedFor;
5853
+ return Object.keys(client2).length > 0 ? client2 : void 0;
5854
+ }
5855
+ var firstHeader;
5856
+ var init_client_info = __esm({
5857
+ "src/exulu/audit/client-info.ts"() {
5858
+ "use strict";
5859
+ init_cjs_shims();
5860
+ firstHeader = (v) => Array.isArray(v) ? v[0] : v ?? void 0;
5861
+ }
5862
+ });
5863
+
5837
5864
  // src/templates/tools/convert-exulu-tools-to-ai-sdk-tools.ts
5838
5865
  var convert_exulu_tools_to_ai_sdk_tools_exports = {};
5839
5866
  __export(convert_exulu_tools_to_ai_sdk_tools_exports, {
@@ -5869,6 +5896,7 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
5869
5896
  init_context_budget();
5870
5897
  init_logger();
5871
5898
  init_emit_tool_call();
5899
+ init_client_info();
5872
5900
  OUTPUT_OFFLOAD_EXEMPT_TOOL_IDS = /* @__PURE__ */ new Set(["agentic_context_search"]);
5873
5901
  generateS3Key = (filename) => `${(0, import_node_crypto4.randomUUID)()}-${filename}`;
5874
5902
  getMimeType = (type) => {
@@ -6348,7 +6376,8 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
6348
6376
  input: inputs,
6349
6377
  output: __auditOutput,
6350
6378
  status: __auditStatus,
6351
- error: __auditError
6379
+ error: __auditError,
6380
+ client: extractClientInfo(req)
6352
6381
  });
6353
6382
  if (__auditLogger.failClosed) {
6354
6383
  await __emit;
@@ -7580,7 +7609,7 @@ function neutralResult(question, keywords, importantKeyword, steps = []) {
7580
7609
  return {
7581
7610
  memoryChunksForAnswer: [],
7582
7611
  memoryOverride: { active: false, chunks: [], reason: "" },
7583
- memoryPinnedItemIds: /* @__PURE__ */ new Set(),
7612
+ memoryPinnedItemIdsByContext: /* @__PURE__ */ new Map(),
7584
7613
  updatedQuestion: question,
7585
7614
  updatedKeywords: keywords,
7586
7615
  updatedImportantKeyword: importantKeyword,
@@ -7681,7 +7710,7 @@ async function runMemoryPhase({
7681
7710
  chunks: [],
7682
7711
  reason: ""
7683
7712
  };
7684
- let memoryPinnedItemIds = /* @__PURE__ */ new Set();
7713
+ const memoryPinnedItemIdsByContext = /* @__PURE__ */ new Map();
7685
7714
  let updatedQuestion = question;
7686
7715
  let updatedKeywords = keywords;
7687
7716
  let updatedImportantKeyword = importantKeyword;
@@ -7844,25 +7873,30 @@ ${glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n")}` : "";
7844
7873
  if (fileResult.output?.shouldPrioritizeFiles && fileResult.output?.fileNameHints?.length) {
7845
7874
  const hints = fileResult.output.fileNameHints;
7846
7875
  const pinResults = await Promise.all(
7847
- documentContexts.map(
7848
- (ctx) => fuzzyPrefilter({
7876
+ documentContexts.map(async (ctx) => ({
7877
+ ctxId: ctx.id,
7878
+ matches: await fuzzyPrefilter({
7849
7879
  cacheKey: `memory-pin:${ctx.id}`,
7850
7880
  relevantKeywords: hints,
7851
7881
  context: ctx,
7852
7882
  fields: ["name", "id", "external_id"],
7853
7883
  normalize: (item) => item.external_id ? normalizeFileName(item.external_id) : item.name
7854
7884
  }).catch(() => [])
7855
- )
7885
+ }))
7856
7886
  );
7857
- for (const results of pinResults) {
7858
- for (const r of results) {
7859
- memoryPinnedItemIds.add(r.id);
7887
+ const pinnedNames = [];
7888
+ for (const { ctxId, matches } of pinResults) {
7889
+ if (!matches.length) continue;
7890
+ const set = memoryPinnedItemIdsByContext.get(ctxId) ?? /* @__PURE__ */ new Set();
7891
+ for (const m of matches) {
7892
+ set.add(m.id);
7893
+ pinnedNames.push(m.name);
7860
7894
  }
7895
+ memoryPinnedItemIdsByContext.set(ctxId, set);
7861
7896
  }
7862
- if (memoryPinnedItemIds.size > 0) {
7863
- const names = pinResults.flat().map((i) => i.name).join(", ");
7897
+ if (pinnedNames.length > 0) {
7864
7898
  steps.push({
7865
- text: `Memory prioritizes specific document(s); pinning ${memoryPinnedItemIds.size} file(s) into the search: ${names}`
7899
+ text: `Memory prioritizes specific document(s); pinning ${pinnedNames.length} file(s) into the search: ${pinnedNames.join(", ")}`
7866
7900
  });
7867
7901
  }
7868
7902
  }
@@ -7885,7 +7919,7 @@ ${glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n")}` : "";
7885
7919
  return {
7886
7920
  memoryChunksForAnswer,
7887
7921
  memoryOverride,
7888
- memoryPinnedItemIds,
7922
+ memoryPinnedItemIdsByContext,
7889
7923
  updatedQuestion,
7890
7924
  updatedKeywords,
7891
7925
  updatedImportantKeyword,
@@ -8016,7 +8050,7 @@ async function searchContexts(opts) {
8016
8050
  preselectedItems,
8017
8051
  scopedItemsByContext,
8018
8052
  identifierPinsByContext,
8019
- memoryPinnedItemIds,
8053
+ memoryPinnedItemIdsByContext,
8020
8054
  userPinnedItemIdsByContext,
8021
8055
  rewrites,
8022
8056
  styleHint,
@@ -8047,7 +8081,8 @@ async function searchContexts(opts) {
8047
8081
  const identifierPins = identifierPinsByContext.get(ctxId) ?? /* @__PURE__ */ new Set();
8048
8082
  let pins = new Set(identifierPins);
8049
8083
  if (kind === "documents") {
8050
- for (const id of memoryPinnedItemIds) pins.add(id);
8084
+ const memPins = memoryPinnedItemIdsByContext.get(ctxId);
8085
+ if (memPins) for (const id of memPins) pins.add(id);
8051
8086
  }
8052
8087
  const userPins = userPinnedItemIdsByContext.get(ctxId);
8053
8088
  if (userPins && userPins.size > 0) {
@@ -8580,7 +8615,7 @@ ${projectScope.customInstructions}` : ""
8580
8615
  updatedQuestion,
8581
8616
  updatedKeywords,
8582
8617
  updatedImportantKeyword,
8583
- memoryPinnedItemIds,
8618
+ memoryPinnedItemIdsByContext,
8584
8619
  memoryOverride
8585
8620
  } = memResult;
8586
8621
  const { pinsByContext: identifierPinsByContext, exactPinsByContext, steps: pinSteps } = await resolveIdentifierPins({
@@ -8613,7 +8648,7 @@ ${projectScope.customInstructions}` : ""
8613
8648
  model: utilityModel,
8614
8649
  preselectedItems,
8615
8650
  identifierPinsByContext,
8616
- memoryPinnedItemIds,
8651
+ memoryPinnedItemIdsByContext,
8617
8652
  userPinnedItemIdsByContext,
8618
8653
  scopedItemsByContext: resolvedProject?.scopedItemsByContext,
8619
8654
  rewrites: cfg.vocabulary.rewrites,
@@ -8633,7 +8668,7 @@ ${projectScope.customInstructions}` : ""
8633
8668
  model: utilityModel,
8634
8669
  preselectedItems,
8635
8670
  identifierPinsByContext,
8636
- memoryPinnedItemIds,
8671
+ memoryPinnedItemIdsByContext,
8637
8672
  userPinnedItemIdsByContext,
8638
8673
  scopedItemsByContext: resolvedProject?.scopedItemsByContext,
8639
8674
  rewrites: cfg.vocabulary.rewrites,
@@ -8643,7 +8678,9 @@ ${projectScope.customInstructions}` : ""
8643
8678
  }) : Promise.resolve({ chunks: [] })
8644
8679
  ]);
8645
8680
  const pinnedItemIds = /* @__PURE__ */ new Set([
8646
- ...memoryPinnedItemIds,
8681
+ ...(function* () {
8682
+ for (const s of memoryPinnedItemIdsByContext.values()) yield* s;
8683
+ })(),
8647
8684
  ...(function* () {
8648
8685
  for (const s of exactPinsByContext.values()) yield* s;
8649
8686
  })(),
@@ -14452,8 +14489,6 @@ var addProviderFields = async (args, requestedFields, result, tools, user, conte
14452
14489
  let litellmEntry;
14453
14490
  if (isLiteLLMEnabled() && result?.model) {
14454
14491
  litellmEntry = await findLiteLLMModel(result.model);
14455
- } else {
14456
- throw new Error("Could not load modal for: " + result?.model);
14457
14492
  }
14458
14493
  if (requestedFields.includes("providerName")) {
14459
14494
  result.providerName = isLiteLLMEnabled() ? "LiteLLM" : "";
@@ -16421,6 +16456,40 @@ function createMutations(table, contexts, tools, config) {
16421
16456
  }
16422
16457
  return await ctx.entityLayer.detachItem(args.item);
16423
16458
  };
16459
+ if (table.RBAC) {
16460
+ mutations[`${tableNamePlural}BulkUpdateRBAC`] = async (_, args, context) => {
16461
+ const { db: db2 } = context;
16462
+ const { ids, rights_mode, RBAC } = args;
16463
+ if (!Array.isArray(ids) || ids.length === 0) {
16464
+ throw new Error("ids is required and must be a non-empty array.");
16465
+ }
16466
+ if (!VALID_RIGHTS_MODES.includes(rights_mode)) {
16467
+ throw new Error(
16468
+ `Invalid rights_mode "${rights_mode}" \u2014 expected one of: ${VALID_RIGHTS_MODES.join(", ")}`
16469
+ );
16470
+ }
16471
+ for (const id of ids) {
16472
+ await validateWriteAccess(id, context);
16473
+ }
16474
+ await db2.transaction(async (trx) => {
16475
+ await trx(tableNamePlural).whereIn("id", ids).update({ rights_mode, updatedAt: /* @__PURE__ */ new Date() });
16476
+ for (const id of ids) {
16477
+ const existingRbacRecords = await trx.from("rbac").where({ entity: table.name.singular, target_resource_id: id }).select("*");
16478
+ await handleRBACUpdate(
16479
+ trx,
16480
+ table.name.singular,
16481
+ id,
16482
+ RBAC ?? {},
16483
+ existingRbacRecords
16484
+ );
16485
+ }
16486
+ });
16487
+ return {
16488
+ message: `Access updated for ${ids.length} item${ids.length === 1 ? "" : "s"}.`,
16489
+ itemCount: ids.length
16490
+ };
16491
+ };
16492
+ }
16424
16493
  }
16425
16494
  return mutations;
16426
16495
  }
@@ -21392,6 +21461,17 @@ function createSDL(tables, contexts, tools, config, evals) {
21392
21461
  ${tableNameSingular}ExtractEntities(item: ID!): ${tableNameSingular}EntityExtractPayload
21393
21462
  ${tableNameSingular}DetachEntities(item: ID!): ${tableNameSingular}EntityDetachPayload
21394
21463
  `;
21464
+ if (table.RBAC) {
21465
+ mutationDefs += `
21466
+ ${tableNamePlural}BulkUpdateRBAC(ids: [ID!]!, rights_mode: String!, RBAC: RBACInput): ${tableNameSingular}BulkUpdateRBACPayload
21467
+ `;
21468
+ modelDefs += `
21469
+ type ${tableNameSingular}BulkUpdateRBACPayload {
21470
+ message: String!
21471
+ itemCount: Int!
21472
+ }
21473
+ `;
21474
+ }
21395
21475
  if (table.processor) {
21396
21476
  mutationDefs += `
21397
21477
  ${tableNameSingular}ProcessItem(item: ID!): ${tableNameSingular}ProcessItemFieldReturnPayload
package/dist/index.d.cts CHANGED
@@ -986,6 +986,13 @@ type AuditConfig = {
986
986
  };
987
987
  };
988
988
 
989
+ type AuditClient = {
990
+ ip?: string;
991
+ userAgent?: string;
992
+ referer?: string;
993
+ origin?: string;
994
+ forwardedFor?: string;
995
+ };
989
996
  type AuditEvent = {
990
997
  v: 1;
991
998
  ts: string;
@@ -1025,6 +1032,7 @@ type AuditEvent = {
1025
1032
  data?: Record<string, unknown>;
1026
1033
  durationMs?: number;
1027
1034
  truncated?: Record<string, boolean>;
1035
+ client?: AuditClient;
1028
1036
  };
1029
1037
  type AuditToolCallInput = {
1030
1038
  durationMs: number;
@@ -1054,6 +1062,7 @@ type AuditToolCallInput = {
1054
1062
  output: unknown;
1055
1063
  status: "ok" | "error" | "auth_required";
1056
1064
  error?: unknown;
1065
+ client?: AuditClient;
1057
1066
  };
1058
1067
 
1059
1068
  interface AuditLogger {
package/dist/index.d.ts CHANGED
@@ -986,6 +986,13 @@ type AuditConfig = {
986
986
  };
987
987
  };
988
988
 
989
+ type AuditClient = {
990
+ ip?: string;
991
+ userAgent?: string;
992
+ referer?: string;
993
+ origin?: string;
994
+ forwardedFor?: string;
995
+ };
989
996
  type AuditEvent = {
990
997
  v: 1;
991
998
  ts: string;
@@ -1025,6 +1032,7 @@ type AuditEvent = {
1025
1032
  data?: Record<string, unknown>;
1026
1033
  durationMs?: number;
1027
1034
  truncated?: Record<string, boolean>;
1035
+ client?: AuditClient;
1028
1036
  };
1029
1037
  type AuditToolCallInput = {
1030
1038
  durationMs: number;
@@ -1054,6 +1062,7 @@ type AuditToolCallInput = {
1054
1062
  output: unknown;
1055
1063
  status: "ok" | "error" | "auth_required";
1056
1064
  error?: unknown;
1065
+ client?: AuditClient;
1057
1066
  };
1058
1067
 
1059
1068
  interface AuditLogger {
package/dist/index.js CHANGED
@@ -88,7 +88,7 @@ import {
88
88
  verifyCredentialNonce,
89
89
  waitForLiteLLMReady,
90
90
  withRetry
91
- } from "./chunk-CVQTDG37.js";
91
+ } from "./chunk-5FTX543Z.js";
92
92
  import {
93
93
  findLiteLLMModel
94
94
  } from "./chunk-7CCMW3IW.js";
@@ -5576,8 +5576,6 @@ var addProviderFields = async (args, requestedFields, result, tools, user, conte
5576
5576
  let litellmEntry;
5577
5577
  if (isLiteLLMEnabled() && result?.model) {
5578
5578
  litellmEntry = await findLiteLLMModel(result.model);
5579
- } else {
5580
- throw new Error("Could not load modal for: " + result?.model);
5581
5579
  }
5582
5580
  if (requestedFields.includes("providerName")) {
5583
5581
  result.providerName = isLiteLLMEnabled() ? "LiteLLM" : "";
@@ -7523,6 +7521,40 @@ function createMutations(table, contexts, tools, config) {
7523
7521
  }
7524
7522
  return await ctx.entityLayer.detachItem(args.item);
7525
7523
  };
7524
+ if (table.RBAC) {
7525
+ mutations[`${tableNamePlural}BulkUpdateRBAC`] = async (_, args, context) => {
7526
+ const { db } = context;
7527
+ const { ids, rights_mode, RBAC } = args;
7528
+ if (!Array.isArray(ids) || ids.length === 0) {
7529
+ throw new Error("ids is required and must be a non-empty array.");
7530
+ }
7531
+ if (!VALID_RIGHTS_MODES.includes(rights_mode)) {
7532
+ throw new Error(
7533
+ `Invalid rights_mode "${rights_mode}" \u2014 expected one of: ${VALID_RIGHTS_MODES.join(", ")}`
7534
+ );
7535
+ }
7536
+ for (const id of ids) {
7537
+ await validateWriteAccess(id, context);
7538
+ }
7539
+ await db.transaction(async (trx) => {
7540
+ await trx(tableNamePlural).whereIn("id", ids).update({ rights_mode, updatedAt: /* @__PURE__ */ new Date() });
7541
+ for (const id of ids) {
7542
+ const existingRbacRecords = await trx.from("rbac").where({ entity: table.name.singular, target_resource_id: id }).select("*");
7543
+ await handleRBACUpdate(
7544
+ trx,
7545
+ table.name.singular,
7546
+ id,
7547
+ RBAC ?? {},
7548
+ existingRbacRecords
7549
+ );
7550
+ }
7551
+ });
7552
+ return {
7553
+ message: `Access updated for ${ids.length} item${ids.length === 1 ? "" : "s"}.`,
7554
+ itemCount: ids.length
7555
+ };
7556
+ };
7557
+ }
7526
7558
  }
7527
7559
  return mutations;
7528
7560
  }
@@ -12412,6 +12444,17 @@ function createSDL(tables, contexts, tools, config, evals) {
12412
12444
  ${tableNameSingular}ExtractEntities(item: ID!): ${tableNameSingular}EntityExtractPayload
12413
12445
  ${tableNameSingular}DetachEntities(item: ID!): ${tableNameSingular}EntityDetachPayload
12414
12446
  `;
12447
+ if (table.RBAC) {
12448
+ mutationDefs += `
12449
+ ${tableNamePlural}BulkUpdateRBAC(ids: [ID!]!, rights_mode: String!, RBAC: RBACInput): ${tableNameSingular}BulkUpdateRBACPayload
12450
+ `;
12451
+ modelDefs += `
12452
+ type ${tableNameSingular}BulkUpdateRBACPayload {
12453
+ message: String!
12454
+ itemCount: Int!
12455
+ }
12456
+ `;
12457
+ }
12415
12458
  if (table.processor) {
12416
12459
  mutationDefs += `
12417
12460
  ${tableNameSingular}ProcessItem(item: ID!): ${tableNameSingular}ProcessItemFieldReturnPayload
@@ -4,13 +4,13 @@ import { createAgenticRetrievalTool, parsePreselectedItems } from "./index";
4
4
  jest.mock("@EE/entitlements", () => ({ checkLicense: () => ({ "agentic-retrieval": true }) }));
5
5
  jest.mock("@SRC/exulu/resolve-reranker", () => ({ resolveReranker: jest.fn(async () => ({ model: "m", rerank: async (_q: any, c: any) => c })) }));
6
6
  jest.mock("@SRC/exulu/resolve-model", () => ({ resolveModel: jest.fn() }));
7
- jest.mock("@SRC/exulu/app/singleton", () => ({ exuluApp: { get: () => () } }));
7
+ jest.mock("@SRC/exulu/app/singleton", () => ({ exuluApp: { get: () => ({}) } }));
8
8
  jest.mock("./routing", () => ({ runRoutingPhase: jest.fn(async () => ({
9
9
  mainContexts: ["docs"], fallbackContexts: [], userPinnedItemIdsByContext: new Map(),
10
10
  userRequestedPage: null, hasExplicitDocAndPage: false, steps: [{ text: "routed" }] })) }));
11
11
  jest.mock("./memory", () => ({ runMemoryPhase: jest.fn(async () => ({
12
12
  memoryChunksForAnswer: [], memoryOverride: { active: false, chunks: [], reason: "" },
13
- memoryPinnedItemIds: new Set(), updatedQuestion: "q", updatedKeywords: ["k"],
13
+ memoryPinnedItemIdsByContext: new Map(), updatedQuestion: "q", updatedKeywords: ["k"],
14
14
  updatedImportantKeyword: "k", steps: [] })) }));
15
15
  jest.mock("./prefilter", () => ({ resolveIdentifierPins: jest.fn(async () => ({
16
16
  pinsByContext: new Map(), exactPinsByContext: new Map(), steps: [] })) }));
@@ -75,7 +75,7 @@ describe("createAgenticRetrievalTool", () => {
75
75
  runMemoryPhase.mockResolvedValueOnce({
76
76
  memoryChunksForAnswer: [{ chunk_id: "m1" }],
77
77
  memoryOverride: { active: false, chunks: [], reason: "" },
78
- memoryPinnedItemIds: new Set(),
78
+ memoryPinnedItemIdsByContext: new Map(),
79
79
  updatedQuestion: "q",
80
80
  updatedKeywords: ["k"],
81
81
  updatedImportantKeyword: "k",
@@ -114,7 +114,7 @@ describe("payload deduplication", () => {
114
114
  runMemoryPhase.mockResolvedValueOnce({
115
115
  memoryChunksForAnswer: [memChunk],
116
116
  memoryOverride: { active: false, chunks: [], reason: "" },
117
- memoryPinnedItemIds: new Set(), updatedQuestion: "q", updatedKeywords: ["k"],
117
+ memoryPinnedItemIdsByContext: new Map(), updatedQuestion: "q", updatedKeywords: ["k"],
118
118
  updatedImportantKeyword: "k",
119
119
  steps: [{ text: "memory step", chunks: [memChunk] }],
120
120
  });
@@ -425,7 +425,7 @@ export function createAgenticRetrievalTool(opts: {
425
425
  updatedQuestion,
426
426
  updatedKeywords,
427
427
  updatedImportantKeyword,
428
- memoryPinnedItemIds,
428
+ memoryPinnedItemIdsByContext,
429
429
  memoryOverride,
430
430
  } = memResult;
431
431
 
@@ -464,7 +464,7 @@ export function createAgenticRetrievalTool(opts: {
464
464
  model: utilityModel,
465
465
  preselectedItems,
466
466
  identifierPinsByContext,
467
- memoryPinnedItemIds,
467
+ memoryPinnedItemIdsByContext,
468
468
  userPinnedItemIdsByContext,
469
469
  scopedItemsByContext: resolvedProject?.scopedItemsByContext,
470
470
  rewrites: cfg.vocabulary.rewrites,
@@ -485,7 +485,7 @@ export function createAgenticRetrievalTool(opts: {
485
485
  model: utilityModel,
486
486
  preselectedItems,
487
487
  identifierPinsByContext,
488
- memoryPinnedItemIds,
488
+ memoryPinnedItemIdsByContext,
489
489
  userPinnedItemIdsByContext,
490
490
  scopedItemsByContext: resolvedProject?.scopedItemsByContext,
491
491
  rewrites: cfg.vocabulary.rewrites,
@@ -499,7 +499,9 @@ export function createAgenticRetrievalTool(opts: {
499
499
  // ── Build rerank state ────────────────────────────────────────────────
500
500
  // pinnedItemIds = memory ∪ exact identifier pins ∪ user pins ∪ project pins
501
501
  const pinnedItemIds = new Set<string>([
502
- ...memoryPinnedItemIds,
502
+ ...(function* () {
503
+ for (const s of memoryPinnedItemIdsByContext.values()) yield* s;
504
+ })(),
503
505
  ...(function* () {
504
506
  for (const s of exactPinsByContext.values()) yield* s;
505
507
  })(),
@@ -74,7 +74,7 @@ describe("runMemoryPhase", () => {
74
74
  expect(r.updatedImportantKeyword).toBe("FST-2XT");
75
75
  });
76
76
 
77
- it("resolves file-prioritization pins across all document contexts", async () => {
77
+ it("resolves file-prioritization pins keyed by their document context", async () => {
78
78
  (generateText as jest.Mock)
79
79
  .mockResolvedValueOnce({ output: { relevantChunkIds: ["1"] } })
80
80
  .mockResolvedValueOnce({ output: { shouldPrioritizeFiles: true, fileNameHints: ["PROJECT_NOTES"] } });
@@ -82,7 +82,9 @@ describe("runMemoryPhase", () => {
82
82
  const r = await runMemoryPhase({ ...baseOpts, memoryChunks: [memChunk("1", "always check PROJECT_NOTES")],
83
83
  memoryContext: undefined, documentContexts: [{ id: "docs" }],
84
84
  memoryConfig: { enabled: true, override: false, filePrioritization: true, queryAugmentation: false } });
85
- expect([...r.memoryPinnedItemIds]).toEqual(["d1"]);
85
+ // Pins are keyed by the context they were resolved in, so a consumer can apply them
86
+ // only to that context (no cross-context leak). See search.ts rule 2b.
87
+ expect([...(r.memoryPinnedItemIdsByContext.get("docs") ?? [])]).toEqual(["d1"]);
86
88
  });
87
89
 
88
90
  it("never throws even when post-Promise.all processing encounters runtime errors", async () => {
@@ -129,7 +129,7 @@ function neutralResult(
129
129
  return {
130
130
  memoryChunksForAnswer: [],
131
131
  memoryOverride: { active: false, chunks: [], reason: "" },
132
- memoryPinnedItemIds: new Set(),
132
+ memoryPinnedItemIdsByContext: new Map(),
133
133
  updatedQuestion: question,
134
134
  updatedKeywords: keywords,
135
135
  updatedImportantKeyword: importantKeyword,
@@ -272,7 +272,7 @@ export async function runMemoryPhase({
272
272
  chunks: [],
273
273
  reason: "",
274
274
  };
275
- let memoryPinnedItemIds = new Set<string>();
275
+ const memoryPinnedItemIdsByContext = new Map<string, Set<string>>();
276
276
  let updatedQuestion = question;
277
277
  let updatedKeywords = keywords;
278
278
  let updatedImportantKeyword = importantKeyword;
@@ -466,12 +466,15 @@ export async function runMemoryPhase({
466
466
  };
467
467
  }
468
468
 
469
- // File prioritization: resolve hints via fuzzyPrefilter against EVERY documentContexts entry
469
+ // File prioritization: resolve hints via fuzzyPrefilter against EVERY documentContexts entry.
470
+ // Pins are kept keyed by the context they were resolved in, so the search phase can apply a
471
+ // pin only to its home context — a tech_doc file must never filter a software-docs search to 0.
470
472
  if (fileResult.output?.shouldPrioritizeFiles && fileResult.output?.fileNameHints?.length) {
471
473
  const hints = fileResult.output.fileNameHints;
472
474
  const pinResults = await Promise.all(
473
- documentContexts.map((ctx) =>
474
- fuzzyPrefilter({
475
+ documentContexts.map(async (ctx) => ({
476
+ ctxId: ctx.id as string,
477
+ matches: await fuzzyPrefilter({
475
478
  cacheKey: `memory-pin:${ctx.id}`,
476
479
  relevantKeywords: hints,
477
480
  context: ctx,
@@ -479,17 +482,21 @@ export async function runMemoryPhase({
479
482
  normalize: (item: any) =>
480
483
  item.external_id ? normalizeFileName(item.external_id) : item.name,
481
484
  }).catch(() => []),
482
- ),
485
+ })),
483
486
  );
484
- for (const results of pinResults) {
485
- for (const r of results) {
486
- memoryPinnedItemIds.add(r.id);
487
+ const pinnedNames: string[] = [];
488
+ for (const { ctxId, matches } of pinResults) {
489
+ if (!matches.length) continue;
490
+ const set = memoryPinnedItemIdsByContext.get(ctxId) ?? new Set<string>();
491
+ for (const m of matches) {
492
+ set.add(m.id);
493
+ pinnedNames.push(m.name);
487
494
  }
495
+ memoryPinnedItemIdsByContext.set(ctxId, set);
488
496
  }
489
- if (memoryPinnedItemIds.size > 0) {
490
- const names = pinResults.flat().map((i) => i.name).join(", ");
497
+ if (pinnedNames.length > 0) {
491
498
  steps.push({
492
- text: `Memory prioritizes specific document(s); pinning ${memoryPinnedItemIds.size} file(s) into the search: ${names}`,
499
+ text: `Memory prioritizes specific document(s); pinning ${pinnedNames.length} file(s) into the search: ${pinnedNames.join(", ")}`,
493
500
  });
494
501
  }
495
502
  }
@@ -520,7 +527,7 @@ export async function runMemoryPhase({
520
527
  return {
521
528
  memoryChunksForAnswer,
522
529
  memoryOverride,
523
- memoryPinnedItemIds,
530
+ memoryPinnedItemIdsByContext,
524
531
  updatedQuestion,
525
532
  updatedKeywords,
526
533
  updatedImportantKeyword,
@@ -20,7 +20,7 @@ const base = {
20
20
  } as any,
21
21
  question: "how to fix door error E42", keywords: ["door", "E42"], importantKeyword: "E42",
22
22
  user: {}, role: "r", model: {},
23
- preselectedItems: new Map(), identifierPinsByContext: new Map(), memoryPinnedItemIds: new Set<string>(),
23
+ preselectedItems: new Map(), identifierPinsByContext: new Map(), memoryPinnedItemIdsByContext: new Map<string, Set<string>>(),
24
24
  userPinnedItemIdsByContext: new Map(), rewrites: [{ find: "fix", replace: "repair" }],
25
25
  styleHint: "", maxQueries: 5, skipPrefilter: false,
26
26
  };
@@ -55,7 +55,7 @@ describe("searchContexts", () => {
55
55
  await searchContexts({
56
56
  ...base, contextIds: ["docs"],
57
57
  identifierPinsByContext: new Map([["docs", new Set(["i1"])]]),
58
- memoryPinnedItemIds: new Set(["m1"]),
58
+ memoryPinnedItemIdsByContext: new Map([["docs", new Set(["m1"])]]),
59
59
  });
60
60
  expect(new Set((multiQuerySearch as jest.Mock).mock.calls[0][0].pinnedItemIds)).toEqual(new Set(["i1", "m1"]));
61
61
 
@@ -63,12 +63,27 @@ describe("searchContexts", () => {
63
63
  await searchContexts({
64
64
  ...base, contextIds: ["docs"],
65
65
  identifierPinsByContext: new Map([["docs", new Set(["i1"])]]),
66
- memoryPinnedItemIds: new Set(["m1"]),
66
+ memoryPinnedItemIdsByContext: new Map([["docs", new Set(["m1"])]]),
67
67
  userPinnedItemIdsByContext: new Map([["docs", new Set(["u1"])]]),
68
68
  });
69
69
  expect((multiQuerySearch as jest.Mock).mock.calls[0][0].pinnedItemIds).toEqual(["u1"]);
70
70
  });
71
71
 
72
+ it("memory pins apply ONLY to their own context (no cross-context leak)", async () => {
73
+ // Regression: a memory pin resolved in another context (e.g. tech_doc's FST-Miscel-Secrets)
74
+ // must not become a hard id-whitelist on THIS context, which would filter it down to 0 chunks.
75
+ await searchContexts({
76
+ ...base, contextIds: ["docs"],
77
+ memoryPinnedItemIdsByContext: new Map([
78
+ ["docs", new Set(["m_docs"])],
79
+ ["other", new Set(["m_other"])],
80
+ ]),
81
+ });
82
+ const pins = (multiQuerySearch as jest.Mock).mock.calls[0][0].pinnedItemIds;
83
+ expect(pins).toEqual(["m_docs"]); // own-context pin applied
84
+ expect(pins).not.toContain("m_other"); // foreign-context pin did NOT leak in
85
+ });
86
+
72
87
  it("preselected items win over everything and skip prefilters", async () => {
73
88
  await searchContexts({
74
89
  ...base, contextIds: ["docs"],
@@ -95,7 +110,7 @@ describe("searchContexts", () => {
95
110
  await searchContexts({
96
111
  ...base, contextIds: ["docs"], skipPrefilter: true,
97
112
  identifierPinsByContext: new Map([["docs", new Set(["i1"])]]),
98
- memoryPinnedItemIds: new Set(["m1"]),
113
+ memoryPinnedItemIdsByContext: new Map([["docs", new Set(["m1"])]]),
99
114
  });
100
115
  expect((multiQuerySearch as jest.Mock).mock.calls[0][0].pinnedItemIds).toEqual([]);
101
116
  });
@@ -25,7 +25,7 @@ export async function searchContexts(opts: {
25
25
  preselectedItems: Map<string, string[] | null>;
26
26
  scopedItemsByContext?: Map<string, string[] | null>; // Project-added sources: hard item filter per context (null = whole context).
27
27
  identifierPinsByContext: Map<string, Set<string>>; // from resolveIdentifierPins
28
- memoryPinnedItemIds: Set<string>; // from memory phase (documents kind only)
28
+ memoryPinnedItemIdsByContext: Map<string, Set<string>>; // from memory phase (documents kind only), keyed by home context
29
29
  userPinnedItemIdsByContext: Map<string, Set<string>>; // from routing phase
30
30
  rewrites: { find: string; replace: string }[];
31
31
  styleHint: string;
@@ -45,7 +45,7 @@ export async function searchContexts(opts: {
45
45
  preselectedItems,
46
46
  scopedItemsByContext,
47
47
  identifierPinsByContext,
48
- memoryPinnedItemIds,
48
+ memoryPinnedItemIdsByContext,
49
49
  userPinnedItemIdsByContext,
50
50
  rewrites,
51
51
  styleHint,
@@ -94,9 +94,13 @@ export async function searchContexts(opts: {
94
94
  const identifierPins = identifierPinsByContext.get(ctxId) ?? new Set<string>();
95
95
  let pins = new Set<string>(identifierPins);
96
96
 
97
- // 2b: documents kind only — UNION with memoryPinnedItemIds
97
+ // 2b: documents kind only — UNION with memory pins that belong to THIS context.
98
+ // Memory pins are keyed by their home context so a pin resolved elsewhere (e.g. a
99
+ // tech_doc file) never becomes a hard id-whitelist on a context that lacks it, which
100
+ // would prefilter that context down to 0 chunks.
98
101
  if (kind === "documents") {
99
- for (const id of memoryPinnedItemIds) pins.add(id);
102
+ const memPins = memoryPinnedItemIdsByContext.get(ctxId);
103
+ if (memPins) for (const id of memPins) pins.add(id);
100
104
  }
101
105
 
102
106
  // 2c: user pins REPLACE everything (authoritative)
@@ -21,7 +21,7 @@ export type RoutingPhaseResult = {
21
21
  export type MemoryPhaseResult = {
22
22
  memoryChunksForAnswer: ChunkWithScore[];
23
23
  memoryOverride: { active: boolean; chunks: ChunkWithScore[]; reason: string };
24
- memoryPinnedItemIds: Set<string>;
24
+ memoryPinnedItemIdsByContext: Map<string, Set<string>>;
25
25
  updatedQuestion: string;
26
26
  updatedKeywords: string[];
27
27
  updatedImportantKeyword: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@exulu/backend",
3
3
  "author": "Qventu Bv.",
4
- "version": "3.2.0",
4
+ "version": "3.3.1",
5
5
  "main": "./dist/index.js",
6
6
  "private": false,
7
7
  "publishConfig": {