@opengeni/core 0.28.0 → 1.0.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/core",
3
- "version": "0.28.0",
3
+ "version": "1.0.1",
4
4
  "description": "OpenGeni framework-agnostic core: the domain, access, and billing layers (neutral access, off-HTTP V2 surface). Behavior-preserving extraction from apps/api — keeps Hono's HTTPException for error throwing (typed-errors cleanup deferred).",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -46,15 +46,15 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "@modelcontextprotocol/sdk": "^1.29.0",
49
- "@opengeni/codex": "^0.2.16",
50
- "@opengeni/config": "^0.16.1",
51
- "@opengeni/contracts": "^0.50.0",
52
- "@opengeni/db": "^0.36.0",
53
- "@opengeni/documents": "^0.5.38",
54
- "@opengeni/events": "^0.3.109",
55
- "@opengeni/observability": "^0.7.5",
56
- "@opengeni/runtime": "^0.23.0",
57
- "@opengeni/storage": "^0.2.93",
49
+ "@opengeni/codex": "^0.2.17",
50
+ "@opengeni/config": "^0.16.4",
51
+ "@opengeni/contracts": "^1.0.1",
52
+ "@opengeni/db": "^1.0.1",
53
+ "@opengeni/documents": "^0.5.41",
54
+ "@opengeni/events": "^0.3.112",
55
+ "@opengeni/observability": "^0.7.7",
56
+ "@opengeni/runtime": "^1.0.1",
57
+ "@opengeni/storage": "^0.2.96",
58
58
  "hono": "^4.12.18"
59
59
  },
60
60
  "engines": {
@@ -13,7 +13,7 @@ import {
13
13
  NewSessionDraftAccessError,
14
14
  newSessionDraftToolsProvided,
15
15
  publicNewSessionDraftOptions,
16
- requireFile,
16
+ requireFileForSubject,
17
17
  saveNewSessionDraftInTransaction,
18
18
  withWorkspaceSubjectRls,
19
19
  } from "@opengeni/db";
@@ -88,7 +88,12 @@ async function hydrateNewSessionDraft(
88
88
  continue;
89
89
  }
90
90
  try {
91
- const file = await requireFile(deps.db, workspaceId, resource.fileId);
91
+ const file = await requireFileForSubject(deps.db, {
92
+ accountId: grant.accountId,
93
+ workspaceId,
94
+ subjectId: grant.subjectId,
95
+ fileId: resource.fileId,
96
+ });
92
97
  if (file.status === "ready") resources.push(resource);
93
98
  } catch {
94
99
  // Missing, foreign, failed, and pending files are stale draft state.
@@ -209,7 +214,7 @@ export async function saveActorNewSessionDraft(
209
214
  if (resources.some((resource) => resource.kind === "file") && !deps.objectStorage) {
210
215
  throw new HTTPException(503, { message: "object storage is not configured" });
211
216
  }
212
- await validateFileResources(deps.db, workspaceId, resources);
217
+ await validateFileResources(deps.db, grant.accountId, workspaceId, grant.subjectId, resources);
213
218
  assertConfiguredModel(deps.settings, input.model);
214
219
  await assertWorkspaceModelPolicyAllows(deps.db, deps.settings, workspaceId, input.model);
215
220
 
@@ -7,6 +7,12 @@ import type {
7
7
  SocialConnection,
8
8
  ToolRef,
9
9
  } from "@opengeni/contracts";
10
+ import {
11
+ GOOGLE_DRIVE_PROVIDER_DOMAIN,
12
+ GOOGLE_DRIVE_PUBLICATION_SERVER_ID,
13
+ GoogleDriveConnectionMetadata,
14
+ googleDriveScopesAllowCapability,
15
+ } from "@opengeni/contracts/google-drive";
10
16
  import {
11
17
  getSessionTurnPersonalConnectionDelegations,
12
18
  getConnectionMetadata,
@@ -250,11 +256,55 @@ export function personalConnectionDelegationsFromParent(input: {
250
256
  return [
251
257
  ...mcp,
252
258
  ...input.parentDelegations
253
- .filter((item) => item.connectionType === "social" || item.connectionType === "atlassian")
259
+ .filter(
260
+ (item) =>
261
+ item.connectionType === "social" ||
262
+ item.connectionType === "atlassian" ||
263
+ item.serverId === GOOGLE_DRIVE_PUBLICATION_SERVER_ID,
264
+ )
254
265
  .map((item) => ({ ...item })),
255
266
  ];
256
267
  }
257
268
 
269
+ /**
270
+ * Freezes Google Drive publishing only when one exact subject-owned connection
271
+ * is eligible. Multiple writable Google accounts are intentionally ambiguous:
272
+ * callers must narrow the connection before a later turn can advertise or use
273
+ * the private publication tool.
274
+ */
275
+ export function googleDrivePublicationDelegationFromVisibleConnections(input: {
276
+ subjectId: string;
277
+ connections: ConnectionMetadata[];
278
+ }): McpPersonalConnectionDelegation | null {
279
+ const eligible = input.connections.filter((connection) => {
280
+ if (
281
+ connection.subjectId !== input.subjectId ||
282
+ connection.status !== "active" ||
283
+ connection.kind !== "oauth2" ||
284
+ !sameProviderDomain(connection.providerDomain, GOOGLE_DRIVE_PROVIDER_DOMAIN) ||
285
+ !googleDriveScopesAllowCapability(connection.grantedScopes, "publish_file")
286
+ ) {
287
+ return false;
288
+ }
289
+ const metadata = GoogleDriveConnectionMetadata.safeParse(connection.metadata);
290
+ return Boolean(
291
+ metadata.success &&
292
+ metadata.data.outputDestination &&
293
+ metadata.data.lifecycle?.state !== "paused" &&
294
+ (!metadata.data.lifecycle || metadata.data.lifecycle.state === "active"),
295
+ );
296
+ });
297
+ if (eligible.length !== 1) return null;
298
+ const connection = eligible[0]!;
299
+ return {
300
+ serverId: GOOGLE_DRIVE_PUBLICATION_SERVER_ID,
301
+ connectionId: connection.id,
302
+ ownerSubjectId: input.subjectId,
303
+ providerDomain: connection.providerDomain,
304
+ kind: connection.kind,
305
+ };
306
+ }
307
+
258
308
  export function personalConnectionDelegationsEqual(
259
309
  left: McpPersonalConnectionDelegation[],
260
310
  right: McpPersonalConnectionDelegation[],
@@ -324,9 +374,22 @@ export function withFrozenPersonalConnectionDelegations(input: {
324
374
  let effectiveRequest = request;
325
375
  if (request.connectionRef.subjectScope === "subject") {
326
376
  const config = input.settings.mcpServers.find((server) => server.id === request.serverId);
377
+ const publicationDelegations =
378
+ request.serverId === GOOGLE_DRIVE_PUBLICATION_SERVER_ID &&
379
+ sameProviderDomain(request.connectionRef.providerDomain, GOOGLE_DRIVE_PROVIDER_DOMAIN)
380
+ ? input.personalConnectionDelegations.filter(
381
+ (candidate) =>
382
+ candidate.serverId === GOOGLE_DRIVE_PUBLICATION_SERVER_ID &&
383
+ sameProviderDomain(candidate.providerDomain, GOOGLE_DRIVE_PROVIDER_DOMAIN) &&
384
+ candidate.kind === "oauth2" &&
385
+ request.connectionRef.kind === "oauth2",
386
+ )
387
+ : [];
327
388
  const delegation = config
328
389
  ? personalConnectionDelegationForServer(input.personalConnectionDelegations, config)
329
- : null;
390
+ : publicationDelegations.length === 1
391
+ ? publicationDelegations[0]!
392
+ : null;
330
393
  if (!delegation || !(await input.ownerHasWorkspaceMembership(delegation.ownerSubjectId))) {
331
394
  return personalAuthorityUnavailable(request);
332
395
  }
@@ -374,15 +437,23 @@ export async function freezePersonalConnectionDelegations(input: {
374
437
  return includeFirstPartyConnections
375
438
  ? inherited
376
439
  : inherited.filter(
377
- (item) => item.connectionType !== "social" && item.connectionType !== "atlassian",
440
+ (item) =>
441
+ item.connectionType !== "social" &&
442
+ item.connectionType !== "atlassian" &&
443
+ item.serverId !== GOOGLE_DRIVE_PUBLICATION_SERVER_ID,
378
444
  );
379
445
  }
380
446
  const membership = await getWorkspaceGrant(input.db, input.source.subjectId, input.workspaceId);
381
447
  if (!membership) return [];
448
+ const visibleConnections = await listConnectionsMetadata(
449
+ input.db,
450
+ input.workspaceId,
451
+ input.source.subjectId,
452
+ );
382
453
  const mcp = personalConnectionDelegationsFromVisibleConnections({
383
454
  servers,
384
455
  subjectId: input.source.subjectId,
385
- connections: await listConnectionsMetadata(input.db, input.workspaceId, input.source.subjectId),
456
+ connections: visibleConnections,
386
457
  });
387
458
  if (!includeFirstPartyConnections) return mcp;
388
459
  const ownerSubjectId = input.source.subjectId;
@@ -399,16 +470,19 @@ export async function freezePersonalConnectionDelegations(input: {
399
470
  if (!prior || connection.updatedAt > prior.updatedAt)
400
471
  latest.set(connection.provider, connection);
401
472
  }
402
- const personalAtlassian = (
403
- await listConnectionsMetadata(input.db, input.workspaceId, ownerSubjectId)
404
- ).filter(
473
+ const personalAtlassian = visibleConnections.filter(
405
474
  (connection) =>
406
475
  connection.subjectId === ownerSubjectId &&
407
476
  connection.status === "active" &&
408
477
  sameProviderDomain(connection.providerDomain, "api.atlassian.com"),
409
478
  );
479
+ const googleDrivePublication = googleDrivePublicationDelegationFromVisibleConnections({
480
+ subjectId: ownerSubjectId,
481
+ connections: visibleConnections,
482
+ });
410
483
  return [
411
484
  ...mcp,
485
+ ...(googleDrivePublication ? [googleDrivePublication] : []),
412
486
  ...[...latest.values()].map((connection) => ({
413
487
  serverId: `social:${connection.provider}`,
414
488
  connectionId: connection.id,
@@ -17,7 +17,11 @@ import {
17
17
  type ResourceRef,
18
18
  type ToolRef,
19
19
  } from "@opengeni/contracts";
20
- import { areGitHubRepositoriesAllowedForWorkspace, requireFile, type Database } from "@opengeni/db";
20
+ import {
21
+ areGitHubRepositoriesAllowedForWorkspace,
22
+ requireFileForSubject,
23
+ type Database,
24
+ } from "@opengeni/db";
21
25
  import { HTTPException } from "hono/http-exception";
22
26
 
23
27
  export function validateToolRefs(tools: ToolRef[], settings: McpSettings): ToolRef[] {
@@ -311,7 +315,9 @@ export function isAuthoritativeGitHubRepositorySelectionError(error: unknown): b
311
315
 
312
316
  export async function validateFileResources(
313
317
  db: Database,
318
+ accountId: string,
314
319
  workspaceId: string,
320
+ subjectId: string,
315
321
  resources: ResourceRef[],
316
322
  ): Promise<void> {
317
323
  const fileIds = new Set<string>();
@@ -323,7 +329,12 @@ export async function validateFileResources(
323
329
  throw new HTTPException(422, { message: `duplicate file resource: ${resource.fileId}` });
324
330
  }
325
331
  fileIds.add(resource.fileId);
326
- const file = await requireFile(db, workspaceId, resource.fileId).catch(() => null);
332
+ const file = await requireFileForSubject(db, {
333
+ accountId,
334
+ workspaceId,
335
+ subjectId,
336
+ fileId: resource.fileId,
337
+ }).catch(() => null);
327
338
  if (!file) {
328
339
  throw new HTTPException(422, { message: `unknown file resource: ${resource.fileId}` });
329
340
  }
@@ -813,7 +813,13 @@ async function validateScheduledTaskAgentConfig(input: {
813
813
  if (resources.some((resource) => resource.kind === "file") && !input.objectStorage) {
814
814
  throw new HTTPException(503, { message: "object storage is not configured" });
815
815
  }
816
- await validateFileResources(input.db, input.workspaceId, resources);
816
+ await validateFileResources(
817
+ input.db,
818
+ input.grant.accountId,
819
+ input.workspaceId,
820
+ input.grant.subjectId,
821
+ resources,
822
+ );
817
823
  if (input.payload.agentConfig.slackBotConnectionId) {
818
824
  await validateOpenGeniSlackBotConnectionSelection(
819
825
  input.db,
@@ -565,7 +565,7 @@ export async function createAndStartSessionWithOutcome(input: {
565
565
  initialMessage: string;
566
566
  /** Create the session shell without an initial user event/agent turn. */
567
567
  deferInitialTurn?: boolean;
568
- turnInstructions?: string | null;
568
+ modelContext?: string | null;
569
569
  resources: ResourceRef[];
570
570
  skills?: SessionSkill[];
571
571
  tools: ToolRef[];
@@ -674,7 +674,7 @@ export async function createAndStartSessionWithOutcome(input: {
674
674
  accountId: input.accountId,
675
675
  workspaceId: input.workspaceId,
676
676
  initialMessage: input.initialMessage,
677
- initialTurnInstructions: input.turnInstructions ?? null,
677
+ initialModelContext: input.modelContext ?? null,
678
678
  resources: input.resources,
679
679
  skills: input.skills ?? [],
680
680
  tools: input.tools,
@@ -740,7 +740,7 @@ export async function createAndStartSessionWithOutcome(input: {
740
740
  accountId: input.accountId,
741
741
  workspaceId: input.workspaceId,
742
742
  initialMessage: input.initialMessage,
743
- initialTurnInstructions: input.turnInstructions ?? null,
743
+ initialModelContext: input.modelContext ?? null,
744
744
  resources: input.resources,
745
745
  skills: input.skills ?? [],
746
746
  tools: input.tools,
@@ -809,7 +809,7 @@ async function finishStartSession(
809
809
  workflowClient: Pick<SessionWorkflowClient, "wakeSessionWorkflow">;
810
810
  initialMessage: string;
811
811
  deferInitialTurn?: boolean;
812
- turnInstructions?: string | null;
812
+ modelContext?: string | null;
813
813
  resources: ResourceRef[];
814
814
  tools: ToolRef[];
815
815
  toolPolicy: SessionToolPolicy;
@@ -1103,7 +1103,7 @@ export async function postUserMessageTurn(input: {
1103
1103
  sessionId: string;
1104
1104
  text: string;
1105
1105
  annotations?: TimelineAnnotation[];
1106
- turnInstructions?: string | null;
1106
+ modelContext?: string | null;
1107
1107
  resources: ResourceRef[];
1108
1108
  model?: string | null;
1109
1109
  reasoningEffort?: Settings["openaiReasoningEffort"] | null;
@@ -1176,7 +1176,7 @@ export async function postUserMessageTurn(input: {
1176
1176
  expectedDraftRevision: input.expectedDraftRevision ?? null,
1177
1177
  text: input.text,
1178
1178
  annotations: input.annotations ?? [],
1179
- turnInstructions: input.turnInstructions ?? null,
1179
+ modelContext: input.modelContext ?? null,
1180
1180
  resources: input.resources,
1181
1181
  model: requestedModel,
1182
1182
  reasoningEffort: requestedReasoningEffort,
@@ -1451,7 +1451,7 @@ export async function createSessionForRequestWithOutcome(
1451
1451
  message: "object storage is not configured",
1452
1452
  });
1453
1453
  }
1454
- await validateFileResources(db, workspaceId, resources);
1454
+ await validateFileResources(db, grant.accountId, workspaceId, grant.subjectId, resources);
1455
1455
  // VariableSet attachment requires variable-sets:use on the calling grant
1456
1456
  // (validateVariableSetAttachment enforces it), preserving the invariant
1457
1457
  // that sandboxed agents cannot self-attach workspace secrets.
@@ -1883,7 +1883,7 @@ export async function createSessionForRequestWithOutcome(
1883
1883
  workspaceId,
1884
1884
  initialMessage: payload.initialMessage ?? "",
1885
1885
  deferInitialTurn: payload.startMode === "realtime",
1886
- turnInstructions: payload.turnInstructions ?? null,
1886
+ modelContext: payload.modelContext ?? null,
1887
1887
  resources,
1888
1888
  skills,
1889
1889
  tools,
@@ -2024,7 +2024,7 @@ export async function acceptSessionUserMessageWithOutcome(
2024
2024
  input: {
2025
2025
  text: string;
2026
2026
  annotations?: SubmittedTimelineAnnotation[];
2027
- turnInstructions?: string | null;
2027
+ modelContext?: string | null;
2028
2028
  resources?: ResourceRef[];
2029
2029
  model?: string | null;
2030
2030
  reasoningEffort?: ReasoningEffort | null;
@@ -2102,7 +2102,13 @@ export async function acceptSessionUserMessageWithOutcome(
2102
2102
  message: "object storage is not configured",
2103
2103
  });
2104
2104
  }
2105
- await validateFileResources(db, workspaceId, requestedResources);
2105
+ await validateFileResources(
2106
+ db,
2107
+ grant.accountId,
2108
+ workspaceId,
2109
+ grant.subjectId,
2110
+ requestedResources,
2111
+ );
2106
2112
  await validateGitHubRepositorySelection(db, workspaceId, [
2107
2113
  ...existingSession.resources,
2108
2114
  ...requestedResources,
@@ -2133,7 +2139,7 @@ export async function acceptSessionUserMessageWithOutcome(
2133
2139
  sessionId,
2134
2140
  text: input.text,
2135
2141
  annotations,
2136
- turnInstructions: input.turnInstructions ?? null,
2142
+ modelContext: input.modelContext ?? null,
2137
2143
  resources: requestedResources,
2138
2144
  model: input.model ?? null,
2139
2145
  reasoningEffort: input.reasoningEffort ?? null,