@opengeni/api-router 2.13.0-canary.0 → 2.13.0-canary.2

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.
Files changed (52) hide show
  1. package/dist/app.js +1 -1
  2. package/dist/{chunk-BJWHVMOL.js → chunk-K2NU4KRK.js} +3165 -3662
  3. package/dist/{chunk-BJWHVMOL.js.map → chunk-K2NU4KRK.js.map} +1 -1
  4. package/dist/index.js +5 -4
  5. package/dist/index.js.map +1 -1
  6. package/dist/integrations/github-lens-connect.d.ts +2 -2
  7. package/dist/integrations/slack-interactions.d.ts +1 -1
  8. package/dist/mcp/documents.d.ts +5 -13
  9. package/dist/mcp/files.d.ts +2 -2
  10. package/dist/mcp/first-party-tool-permissions.d.ts +36 -0
  11. package/dist/mcp/knowledge-entries.d.ts +5 -0
  12. package/dist/mcp/server.d.ts +4 -4
  13. package/dist/routes/host-mcp-resolvers.d.ts +3 -0
  14. package/dist/routes/knowledge.d.ts +3 -0
  15. package/dist/sandbox-file-artifacts.d.ts +6 -2
  16. package/dist/workspace-state-projection.d.ts +3 -5
  17. package/dist/workspace-tool-gateway.d.ts +2 -1
  18. package/package.json +19 -19
  19. package/src/access-grant-rls.ts +13 -1
  20. package/src/app.ts +4 -11
  21. package/src/codemode.ts +4 -8
  22. package/src/index.ts +4 -3
  23. package/src/integrations/atlassian.ts +31 -28
  24. package/src/integrations/google-drive.ts +33 -30
  25. package/src/integrations/marketplace-plugin.ts +1 -1
  26. package/src/integrations/provider-oauth.ts +11 -2
  27. package/src/mcp/documents.ts +57 -428
  28. package/src/mcp/files.ts +76 -58
  29. package/src/mcp/first-party-tool-permissions.ts +9 -0
  30. package/src/mcp/knowledge-entries.ts +276 -0
  31. package/src/mcp/server.ts +7 -361
  32. package/src/routes/company-brain.ts +58 -107
  33. package/src/routes/documents.ts +78 -294
  34. package/src/routes/files.ts +68 -3
  35. package/src/routes/host-mcp-resolvers.ts +41 -0
  36. package/src/routes/knowledge.ts +386 -0
  37. package/src/routes/organization-memberships.ts +33 -0
  38. package/src/routes/scheduled-tasks.ts +9 -1
  39. package/src/routes/sessions.ts +20 -3
  40. package/src/routes/skill-content.ts +8 -2
  41. package/src/routes/video-generation.ts +7 -5
  42. package/src/routes/workspace-learning.ts +12 -82
  43. package/src/routes/workspace-state.ts +34 -25
  44. package/src/routes/workspaces.ts +20 -10
  45. package/src/sandbox-file-artifacts.ts +54 -2
  46. package/src/scheduled-task-deletion.ts +3 -2
  47. package/src/workspace-state-projection.ts +18 -148
  48. package/src/workspace-tool-gateway.ts +14 -25
  49. package/dist/mcp/company-brain-governed-writes.d.ts +0 -26
  50. package/dist/mcp/remember.d.ts +0 -28
  51. package/src/mcp/company-brain-governed-writes.ts +0 -271
  52. package/src/mcp/remember.ts +0 -191
@@ -1,21 +1,24 @@
1
+ import { withAccessGrantSessionRlsContext } from "../access-grant-rls";
1
2
  import {
2
- WORKSPACE_STATE_MAX_BASES,
3
- WORKSPACE_STATE_MAX_TOPICS,
4
- WORKSPACE_STATE_TOPIC_MAX_CHARS,
3
+ WORKSPACE_STATE_KNOWLEDGE_SAMPLE_LIMIT,
5
4
  WorkspaceStateQuery,
6
5
  WorkspaceStateResponse,
7
- type AccessGrant,
8
6
  type WorkspaceStateQuery as WorkspaceStateQueryType,
9
7
  } from "@opengeni/contracts";
10
- import { hasPermission, requireAccessGrant, type ApiRouteDeps } from "@opengeni/core";
8
+ import {
9
+ hasPermission,
10
+ requireAccessGrantAuthorization,
11
+ knowledgeContextForAccess,
12
+ type AccessGrantAuthorization,
13
+ type ApiRouteDeps,
14
+ } from "@opengeni/core";
11
15
  import {
12
16
  getWorkspace,
13
17
  getCurrentPreferenceRegistryGovernanceMetadata,
14
18
  getWorkspaceStateAcceptedAttemptGovernance,
15
- listWorkspaceStateMemoryRecords,
19
+ listKnowledgeEntries,
16
20
  listWorkspaceInstructionPolicyRevisions,
17
21
  } from "@opengeni/db";
18
- import { getDocumentInventory } from "@opengeni/documents";
19
22
  import type { Hono } from "hono";
20
23
  import { HTTPException } from "hono/http-exception";
21
24
 
@@ -24,9 +27,10 @@ import { projectWorkspaceState } from "../workspace-state-projection";
24
27
 
25
28
  async function readWorkspaceState(
26
29
  deps: ApiRouteDeps,
27
- input: { workspaceId: string; query: WorkspaceStateQueryType; grant: AccessGrant },
30
+ input: { workspaceId: string; query: WorkspaceStateQueryType; access: AccessGrantAuthorization },
28
31
  ) {
29
- const { workspaceId, query, grant } = input;
32
+ const { workspaceId, query, access } = input;
33
+ const { grant } = access;
30
34
  const generatedAt = new Date().toISOString();
31
35
  const canInspectKnowledge = hasPermission(grant.permissions, "documents:search");
32
36
 
@@ -34,18 +38,13 @@ async function readWorkspaceState(
34
38
  getWorkspace(deps.db, workspaceId),
35
39
  listWorkspaceInstructionPolicyRevisions(deps.db, workspaceId, { limit: 1 }),
36
40
  canInspectKnowledge
37
- ? (async () => {
38
- const [documents, memories] = await Promise.all([
39
- getDocumentInventory(deps.db, workspaceId, {
40
- baseLimit: WORKSPACE_STATE_MAX_BASES,
41
- topicLimit: WORKSPACE_STATE_MAX_TOPICS,
42
- topicMaxChars: WORKSPACE_STATE_TOPIC_MAX_CHARS,
43
- access: { viewerSubjectId: grant.subjectId },
44
- }),
45
- listWorkspaceStateMemoryRecords(deps.db, workspaceId),
46
- ]);
47
- return { documents, memories };
48
- })()
41
+ ? withAccessGrantSessionRlsContext(deps, grant, async () =>
42
+ listKnowledgeEntries(
43
+ deps.db,
44
+ await knowledgeContextForAccess(deps, access, "documents:search"),
45
+ { limit: WORKSPACE_STATE_KNOWLEDGE_SAMPLE_LIMIT, view: "published" },
46
+ ),
47
+ )
49
48
  : Promise.resolve(null),
50
49
  getCurrentPreferenceRegistryGovernanceMetadata(deps.db, {
51
50
  workspaceId,
@@ -109,8 +108,13 @@ export function registerWorkspaceStateRoutes(app: Hono, deps: ApiRouteDeps): voi
109
108
  app.get(base, async (context) => {
110
109
  const workspaceId = context.req.param("workspaceId");
111
110
  const query = WorkspaceStateQuery.parse(context.req.query());
112
- const grant = await requireAccessGrant(context, deps, workspaceId, "workspace:read");
113
- const state = await readWorkspaceState(deps, { workspaceId, query, grant });
111
+ const access = await requireAccessGrantAuthorization(
112
+ context,
113
+ deps,
114
+ workspaceId,
115
+ "workspace:read",
116
+ );
117
+ const state = await readWorkspaceState(deps, { workspaceId, query, access });
114
118
  context.header("cache-control", "private, no-store");
115
119
  return context.json(state);
116
120
  });
@@ -118,8 +122,13 @@ export function registerWorkspaceStateRoutes(app: Hono, deps: ApiRouteDeps): voi
118
122
  app.get(`${base}/export`, async (context) => {
119
123
  const workspaceId = context.req.param("workspaceId");
120
124
  const query = WorkspaceStateQuery.parse(context.req.query());
121
- const grant = await requireAccessGrant(context, deps, workspaceId, "workspace:read");
122
- const state = await readWorkspaceState(deps, { workspaceId, query, grant });
125
+ const access = await requireAccessGrantAuthorization(
126
+ context,
127
+ deps,
128
+ workspaceId,
129
+ "workspace:read",
130
+ );
131
+ const state = await readWorkspaceState(deps, { workspaceId, query, access });
123
132
  context.header("cache-control", "private, no-store");
124
133
  context.header("content-type", "application/json; charset=utf-8");
125
134
  context.header(
@@ -398,16 +398,26 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
398
398
  }
399
399
  // Request-scoped: bound the exclusive control-prefix wait so a busy
400
400
  // workspace yields the retryable 503 instead of parking this request.
401
- const workspace = await updateWorkspaceSettingsWithToolDefaults(
402
- deps.db,
403
- workspaceId,
404
- parsed.data,
405
- { requireWorkspace, updateWorkspaceSettings },
406
- {
407
- controlLockTimeoutMs: workspaceControlRequestLockTimeoutMs(),
408
- },
409
- );
410
- return c.json(Workspace.parse(workspace));
401
+ try {
402
+ const workspace = await updateWorkspaceSettingsWithToolDefaults(
403
+ deps.db,
404
+ workspaceId,
405
+ parsed.data,
406
+ { requireWorkspace, updateWorkspaceSettings },
407
+ {
408
+ controlLockTimeoutMs: workspaceControlRequestLockTimeoutMs(),
409
+ },
410
+ );
411
+ return c.json(Workspace.parse(workspace));
412
+ } catch (error) {
413
+ if (nestedPostgresSqlState(error) === "0A000") {
414
+ throw new HTTPException(409, {
415
+ message:
416
+ "Memory settings moved to Settings > Agent learning. Refresh the app to change them.",
417
+ });
418
+ }
419
+ throw error;
420
+ }
411
421
  });
412
422
 
413
423
  // Per-workspace model/provider availability policy (the HARD blocker over
@@ -8,8 +8,22 @@ import {
8
8
  type AccessGrant,
9
9
  type Session,
10
10
  } from "@opengeni/contracts";
11
- import { recordWorkspaceUsage, requireLimit, type ApiRouteDeps } from "@opengeni/core";
12
- import { completeFileUpload, getFile, prepareGeneratedWorkspaceFile } from "@opengeni/db";
11
+ import {
12
+ fileOwnerContextForAccess,
13
+ fileOwnerContextForAgent,
14
+ recordWorkspaceUsage,
15
+ requireLimit,
16
+ type ApiRouteDeps,
17
+ type AccessGrantAuthorization,
18
+ } from "@opengeni/core";
19
+ import {
20
+ completeFileUpload,
21
+ getFile,
22
+ prepareGeneratedWorkspaceFile,
23
+ getSessionAuthorityProjection,
24
+ requireWorkspace,
25
+ withSessionRlsActorContext,
26
+ } from "@opengeni/db";
13
27
  import { retryWhileMissing, type ObjectHead, type ObjectStorage } from "@opengeni/storage";
14
28
  import { HTTPException } from "hono/http-exception";
15
29
 
@@ -21,10 +35,48 @@ const UPLOAD_INTENT_TTL_MS = 60 * 60_000;
21
35
  const SANDBOX_ARTIFACT_ABSOLUTE_PATH_MAX_CHARS = 4_096;
22
36
  const SANDBOX_ARTIFACT_SAFE_FILENAME_MAX_CHARS = 200;
23
37
 
38
+ /** Original bytes inherit the already-authorized source session's ownership. */
24
39
  export async function publishSandboxFileArtifact(
40
+ deps: ApiRouteDeps,
41
+ input: Parameters<typeof publishSandboxFileArtifactInScope>[1],
42
+ ): Promise<SandboxFileArtifactReceipt> {
43
+ const actor = input.authorization
44
+ ? await fileOwnerContextForAccess(deps, input.authorization, "files:upload")
45
+ : input.grant.principalKind === "agent_attempt"
46
+ ? await fileOwnerContextForAgent(deps, input.grant, "files:upload")
47
+ : { subjectId: input.grant.subjectId, privateFileOwnerSubjectId: null };
48
+ return withSessionRlsActorContext(actor, async () => {
49
+ const [authority, workspace] = await Promise.all([
50
+ getSessionAuthorityProjection(deps.db, input.grant.workspaceId, input.session.id),
51
+ requireWorkspace(deps.db, input.grant.workspaceId),
52
+ ]);
53
+ if (!authority) throw new HTTPException(404, { message: "Source session is unavailable" });
54
+ const personal =
55
+ authority.visibility === "user_private" ||
56
+ authority.memoryScope === "user" ||
57
+ workspace.kind === "personal";
58
+ const expectedOwner =
59
+ authority.visibility === "user_private"
60
+ ? authority.ownerSubjectId
61
+ : authority.memoryScope === "user"
62
+ ? authority.scopeSubjectId
63
+ : actor.privateFileOwnerSubjectId;
64
+ if (personal && (!expectedOwner || actor.privateFileOwnerSubjectId !== expectedOwner))
65
+ throw new HTTPException(403, {
66
+ message: "Personal file publication requires the session owner's authority",
67
+ });
68
+ return withSessionRlsActorContext(
69
+ { ...actor, privateFileOwnerSubjectId: personal ? (expectedOwner ?? null) : null },
70
+ () => publishSandboxFileArtifactInScope(deps, input),
71
+ );
72
+ });
73
+ }
74
+
75
+ async function publishSandboxFileArtifactInScope(
25
76
  deps: ApiRouteDeps,
26
77
  input: {
27
78
  grant: AccessGrant;
79
+ authorization?: AccessGrantAuthorization;
28
80
  session: Session;
29
81
  path: string;
30
82
  signal?: AbortSignal | undefined;
@@ -1,3 +1,4 @@
1
+ import { scheduledTaskKnowledgeSource } from "@opengeni/contracts";
1
2
  import type { ScheduledTask as ScheduledTaskValue } from "@opengeni/contracts";
2
3
  import { deleteScheduledTaskLifecycle, type ApiRouteDeps } from "@opengeni/core";
3
4
  import type { TemporalScheduleCleanupClaim } from "@opengeni/db";
@@ -35,7 +36,7 @@ async function preflightConnectorAuthorization(
35
36
  task: ScheduledTaskValue,
36
37
  subjectId: string,
37
38
  ): Promise<void> {
38
- if (task.action.kind !== "knowledge_source_sync") return;
39
+ if (!scheduledTaskKnowledgeSource(task)) return;
39
40
  if (task.metadata.connectorKind === "atlassian") {
40
41
  await preflightAtlassianScheduleAuthorization(deps, { task, subjectId });
41
42
  return;
@@ -54,7 +55,7 @@ async function revokeConnectorAuthorization(
54
55
  task: ScheduledTaskValue,
55
56
  subjectId: string,
56
57
  ): Promise<void> {
57
- if (task.action.kind !== "knowledge_source_sync") return;
58
+ if (!scheduledTaskKnowledgeSource(task)) return;
58
59
  if (task.metadata.connectorKind === "atlassian") {
59
60
  await revokeAtlassianScheduleAuthorization(deps, { task, subjectId });
60
61
  return;
@@ -1,29 +1,17 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import {
3
- KnowledgeMemoryKind,
4
- KnowledgeMemoryStatus,
5
- WORKSPACE_STATE_BASE_NAME_MAX_CHARS,
6
3
  WORKSPACE_STATE_MAX_ACTIVE_POLICY_HEADS,
7
- WORKSPACE_STATE_MAX_BASES,
8
- WORKSPACE_STATE_MAX_GAPS,
9
- WORKSPACE_STATE_MAX_TOPICS,
10
- WORKSPACE_STATE_MEMORY_SAMPLE_LIMIT,
11
- WORKSPACE_STATE_TOPIC_MAX_CHARS,
4
+ WORKSPACE_STATE_KNOWLEDGE_SAMPLE_LIMIT,
5
+ type KnowledgeEntrySummary,
12
6
  WorkspaceStateResponse,
13
7
  type WorkspaceInstructionPolicyListResponse,
14
8
  type WorkspaceInstructionPolicySnapshot,
15
9
  type WorkspaceStateGovernanceDriftStatus,
16
- type WorkspaceStateGap,
17
- type WorkspaceStateMemoryKindCounts,
18
- type WorkspaceStateMemoryStatusCounts,
19
10
  type WorkspaceStateResponse as WorkspaceStateResponseType,
20
11
  } from "@opengeni/contracts";
21
- import type { WorkspaceStateMemoryRecord } from "@opengeni/db";
22
- import type { DocumentInventory } from "@opengeni/documents";
23
-
24
12
  type KnowledgeProjectionInput = {
25
- documents: DocumentInventory;
26
- memories: WorkspaceStateMemoryRecord[];
13
+ entries: Pick<KnowledgeEntrySummary, "id" | "scope" | "revision" | "updatedAt">[];
14
+ nextCursor: string | null;
27
15
  };
28
16
 
29
17
  type PreferenceGovernanceIdentity = {
@@ -239,30 +227,6 @@ function attemptGovernanceProjection(input: WorkspaceStateProjectionInput) {
239
227
  };
240
228
  }
241
229
 
242
- function emptyMemoryStatusCounts(): WorkspaceStateMemoryStatusCounts {
243
- return Object.fromEntries(
244
- KnowledgeMemoryStatus.options.map((status) => [status, 0]),
245
- ) as WorkspaceStateMemoryStatusCounts;
246
- }
247
-
248
- function emptyMemoryKindCounts(): WorkspaceStateMemoryKindCounts {
249
- return Object.fromEntries(
250
- KnowledgeMemoryKind.options.map((kind) => [kind, 0]),
251
- ) as WorkspaceStateMemoryKindCounts;
252
- }
253
-
254
- function boundedLabel(value: string, maxChars: number, fallback: string): string {
255
- const normalized = value.normalize("NFKC").trim().replace(/\s+/gu, " ");
256
- return (normalized || fallback).slice(0, maxChars);
257
- }
258
-
259
- function newestTimestamp(values: Array<string | null>): string | null {
260
- return values.reduce<string | null>((latest, value) => {
261
- if (!value) return latest;
262
- return latest === null || value > latest ? value : latest;
263
- }, null);
264
- }
265
-
266
230
  function comparePolicyTargets(
267
231
  left: WorkspaceInstructionPolicyListResponse["activeHeads"][number],
268
232
  right: WorkspaceInstructionPolicyListResponse["activeHeads"][number],
@@ -330,116 +294,22 @@ function preferenceProjection(input: CurrentPreferenceProjectionInput) {
330
294
  }
331
295
 
332
296
  function availableKnowledgeProjection(knowledge: KnowledgeProjectionInput) {
333
- const inventory = knowledge.documents;
334
- const selectedBases = inventory.bases.slice(0, WORKSPACE_STATE_MAX_BASES);
335
- const aggregateStatuses = { ...inventory.statusCounts };
336
- const aggregateSources = { ...inventory.sourceKindCounts };
337
- const topicCounts = new Map<string, number>();
338
- const projectedBases = selectedBases.map((base) => {
339
- return {
340
- id: base.id,
341
- name: boundedLabel(base.name, WORKSPACE_STATE_BASE_NAME_MAX_CHARS, "Untitled base"),
342
- visibleDocumentCount: base.visibleDocumentCount,
343
- statusCounts: base.statusCounts,
344
- latestUpdatedAt: base.latestUpdatedAt,
345
- };
346
- });
347
-
348
- for (const topic of inventory.topics) {
349
- const name = boundedLabel(topic.name, WORKSPACE_STATE_TOPIC_MAX_CHARS, "Unlabeled topic");
350
- topicCounts.set(name, (topicCounts.get(name) ?? 0) + topic.documentCount);
351
- }
352
-
353
- const sortedTopics = [...topicCounts.entries()]
354
- .sort(
355
- ([leftName, leftCount], [rightName, rightCount]) =>
356
- rightCount - leftCount || leftName.localeCompare(rightName),
357
- )
358
- .map(([name, documentCount]) => ({ name, documentCount }));
359
- const topics = sortedTopics.slice(0, WORKSPACE_STATE_MAX_TOPICS);
360
- const memories = knowledge.memories.slice(0, WORKSPACE_STATE_MEMORY_SAMPLE_LIMIT);
361
- const memoryStatuses = emptyMemoryStatusCounts();
362
- const memoryKinds = emptyMemoryKindCounts();
363
- for (const memory of memories) {
364
- memoryStatuses[memory.status] += 1;
365
- memoryKinds[memory.kind] += 1;
366
- }
367
-
368
- const inspectedVisibleDocumentCount = inventory.visibleDocumentCount;
369
- const basesTruncated = inventory.baseCount > projectedBases.length;
370
- const memoryLimitReached = memories.length === WORKSPACE_STATE_MEMORY_SAMPLE_LIMIT;
371
- const inventoryPartial = basesTruncated || inventory.topicsTruncated || memoryLimitReached;
372
- const gaps: WorkspaceStateGap[] = [];
373
- const addGap = (gap: WorkspaceStateGap): void => {
374
- if (gaps.length < WORKSPACE_STATE_MAX_GAPS) gaps.push(gap);
375
- };
376
- if (inventory.baseCount === 0) {
377
- addGap({ code: "no_document_bases", severity: "info", relatedCount: 0 });
378
- } else if (inspectedVisibleDocumentCount === 0) {
379
- addGap({ code: "no_visible_documents", severity: "info", relatedCount: 0 });
380
- }
381
- if (aggregateStatuses.failed > 0) {
382
- addGap({
383
- code: "failed_documents",
384
- severity: "warning",
385
- relatedCount: aggregateStatuses.failed,
386
- });
387
- }
388
- const processingCount = aggregateStatuses.queued + aggregateStatuses.indexing;
389
- if (processingCount > 0) {
390
- addGap({
391
- code: "processing_documents",
392
- severity: "info",
393
- relatedCount: processingCount,
394
- });
395
- }
396
- if (aggregateStatuses.ready > 0 && sortedTopics.length === 0) {
397
- addGap({
398
- code: "missing_topic_coverage",
399
- severity: "info",
400
- relatedCount: aggregateStatuses.ready,
401
- });
402
- }
403
- if (memories.length === 0) {
404
- addGap({ code: "no_memory_records", severity: "info", relatedCount: 0 });
405
- }
406
- if (memoryStatuses.proposed > 0) {
407
- addGap({
408
- code: "pending_memory_review",
409
- severity: "info",
410
- relatedCount: memoryStatuses.proposed,
411
- });
412
- }
413
- if (inventoryPartial) {
414
- addGap({ code: "partial_inventory", severity: "info", relatedCount: null });
415
- }
416
-
417
297
  return {
418
298
  availability: "available" as const,
419
- coverage: inventoryPartial ? ("partial" as const) : ("complete" as const),
420
- baseCount: inventory.baseCount,
421
- bases: projectedBases,
422
- basesTruncated,
423
- inspectedVisibleDocumentCount,
424
- documentStatusCounts: aggregateStatuses,
425
- sourceKindCounts: aggregateSources,
426
- authorityKindCounts: { ...inventory.authorityKindCounts },
427
- topics,
428
- topicsTruncated: inventory.topicsTruncated || sortedTopics.length > topics.length,
429
- latestDocumentUpdatedAt: inventory.latestUpdatedAt,
430
- memorySample: {
431
- recordCount: memories.length,
432
- sampleLimit: WORKSPACE_STATE_MEMORY_SAMPLE_LIMIT,
433
- limitReached: memoryLimitReached,
434
- statusCounts: memoryStatuses,
435
- kindCounts: memoryKinds,
436
- preferenceAuthority: {
437
- kindCountSource: "knowledge_memories_legacy_observations" as const,
438
- activeAuthority: "structured_preference_registry" as const,
439
- },
440
- latestUpdatedAt: newestTimestamp(memories.map((memory) => memory.updatedAt)),
441
- },
442
- gaps,
299
+ authority: "knowledge_entries" as const,
300
+ coverage:
301
+ knowledge.nextCursor || knowledge.entries.length > WORKSPACE_STATE_KNOWLEDGE_SAMPLE_LIMIT
302
+ ? ("partial" as const)
303
+ : ("complete" as const),
304
+ sampleLimit: WORKSPACE_STATE_KNOWLEDGE_SAMPLE_LIMIT,
305
+ entries: knowledge.entries.slice(0, WORKSPACE_STATE_KNOWLEDGE_SAMPLE_LIMIT).map((entry) => ({
306
+ id: entry.id,
307
+ revisionId: entry.revision.id,
308
+ title: entry.revision.title,
309
+ kind: entry.revision.kind,
310
+ scope: entry.scope,
311
+ updatedAt: entry.updatedAt,
312
+ })),
443
313
  };
444
314
  }
445
315
 
@@ -1,3 +1,4 @@
1
+ import { knowledgeContextForGateway } from "@opengeni/core";
1
2
  import { createHash, randomBytes } from "node:crypto";
2
3
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
4
  import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
@@ -48,12 +49,11 @@ import {
48
49
  requireWorkspace,
49
50
  withCodexAppsRequestAuthorization,
50
51
  consumeToolGatewayApproval,
51
- getWorkspaceArtifact,
52
+ getWorkspaceArtifactContentRef,
52
53
  issueToolGatewayApproval,
53
54
  ToolGatewayApprovalOperationStartedError,
54
55
  ToolGatewayApprovalRateLimitError,
55
56
  WorkspaceArtifactNotFoundError,
56
- resolveSessionMemoryAgentScope,
57
57
  type ApiIntegrationRuntime,
58
58
  } from "@opengeni/db";
59
59
  import {
@@ -288,7 +288,7 @@ async function prepareWorkspaceToolGatewayForGrantInternal(
288
288
  ...(gatewayServerIds.has("files")
289
289
  ? [inMemoryMcpRegistration("files", buildFilesMcpServer(deps, grant))]
290
290
  : []),
291
- ...(gatewayServerIds.has("docs")
291
+ ...(gatewayServerIds.has("docs") && hasPermission(grant.permissions, "documents:search")
292
292
  ? [
293
293
  inMemoryMcpRegistration(
294
294
  "docs",
@@ -298,19 +298,7 @@ async function prepareWorkspaceToolGatewayForGrantInternal(
298
298
  grant.workspaceId,
299
299
  routeDeps.getDocumentServices(),
300
300
  {
301
- initiatingSubjectId: grant.subjectId,
302
- // A session-bound gateway caller reads Memory through that
303
- // session's frozen selector; a sessionless caller keeps the
304
- // workspace layer.
305
- memory:
306
- typeof grant.metadata?.["sessionId"] === "string"
307
- ? ((await resolveSessionMemoryAgentScope(
308
- routeDeps.db,
309
- grant.workspaceId,
310
- grant.metadata["sessionId"],
311
- grant.metadata,
312
- )) ?? { mode: "off" as const, userSubjectId: null, rootSessionId: null })
313
- : null,
301
+ knowledge: await knowledgeContextForGateway(routeDeps, grant),
314
302
  },
315
303
  ),
316
304
  ),
@@ -719,24 +707,25 @@ function throwWorkspaceToolGatewayHttpError(error: unknown): never {
719
707
  throw error;
720
708
  }
721
709
 
722
- async function requireWorkspaceSiteToolAuthorization(
710
+ export async function requireWorkspaceSiteToolAuthorization(
723
711
  db: ApiRouteDeps["db"],
724
712
  grant: AccessGrant,
725
713
  context: WorkspaceSiteToolContext,
714
+ resolveVersion = getWorkspaceArtifactContentRef,
726
715
  ): Promise<void> {
727
716
  try {
728
- const detail = await getWorkspaceArtifact(db, grant.workspaceId, context.siteArtifactId);
729
- const currentVersion = detail.artifact.currentVersion;
730
- const requested = currentVersion?.requestedTools.some(
717
+ const { status, version } = await resolveVersion(
718
+ db,
719
+ grant.workspaceId,
720
+ context.siteArtifactId,
721
+ context.siteVersionId,
722
+ );
723
+ const requested = version.requestedTools.some(
731
724
  (identity) =>
732
725
  identity.serverId === context.identity.serverId &&
733
726
  identity.toolName === context.identity.toolName,
734
727
  );
735
- if (
736
- detail.artifact.status !== "active" ||
737
- currentVersion?.id !== context.siteVersionId ||
738
- !requested
739
- ) {
728
+ if (status !== "active" || version.id !== context.siteVersionId || !requested) {
740
729
  throw new HTTPException(403, { message: "site_tool_not_authorized" });
741
730
  }
742
731
  } catch (error) {
@@ -1,26 +0,0 @@
1
- import { type CompanyBrainGovernedWriteAttempt } from "@opengeni/contracts";
2
- import { createCompanyBrainLearningPolicyRouter } from "@opengeni/core";
3
- import { type Database } from "@opengeni/db";
4
- import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
- type JsonResult = (value: unknown) => {
6
- content: {
7
- type: "text";
8
- text: string;
9
- }[];
10
- };
11
- export type RegisterCompanyBrainGovernedWriteToolsInput = {
12
- server: McpServer;
13
- db: Database;
14
- attempt: CompanyBrainGovernedWriteAttempt;
15
- authorize: () => Promise<void>;
16
- json: JsonResult;
17
- router?: Pick<ReturnType<typeof createCompanyBrainLearningPolicyRouter>, "write">;
18
- };
19
- /**
20
- * Register explicit proposal-only Company Brain tools. The signed host attempt
21
- * owns tenant/session authority and the exact evidence id owns the learning
22
- * policy source override. Tool input cannot select scope, active authority, or
23
- * another policy source.
24
- */
25
- export declare function registerCompanyBrainGovernedWriteTools(input: RegisterCompanyBrainGovernedWriteToolsInput): void;
26
- export {};
@@ -1,28 +0,0 @@
1
- import { type CompanyBrainGovernedWriteAttempt } from "@opengeni/contracts";
2
- import { createRememberRouter } from "@opengeni/core";
3
- import { type Database } from "@opengeni/db";
4
- import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
- type JsonResult = (value: unknown) => {
6
- content: {
7
- type: "text";
8
- text: string;
9
- }[];
10
- };
11
- export type RegisterRememberToolsInput = {
12
- server: McpServer;
13
- db: Database;
14
- attempt: CompanyBrainGovernedWriteAttempt;
15
- authorize: () => Promise<void>;
16
- json: JsonResult;
17
- router?: Pick<ReturnType<typeof createRememberRouter>, "remember" | "confirm">;
18
- };
19
- /**
20
- * Explicit user-directed remember. Content becomes exact task-note evidence,
21
- * promotion runs through the frozen workspace learning policy, and activation
22
- * is either automatic (instruction under Automatic mode) or requires exactly one
23
- * bound human confirmation through the built-in `request_human_input` tool.
24
- * Tool input cannot select scope beyond the workspace, active authority, or
25
- * another learning-policy source.
26
- */
27
- export declare function registerRememberTools(input: RegisterRememberToolsInput): void;
28
- export {};