@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/dist/domain/personal-connection-delegations.d.ts +10 -0
- package/dist/domain/resources.d.ts +1 -1
- package/dist/domain/sessions.d.ts +3 -3
- package/dist/index.js +86 -19
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
- package/src/application/new-session-drafts.ts +8 -3
- package/src/domain/personal-connection-delegations.ts +81 -7
- package/src/domain/resources.ts +13 -2
- package/src/domain/scheduled-tasks.ts +7 -1
- package/src/domain/sessions.ts +17 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/core",
|
|
3
|
-
"version": "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.
|
|
50
|
-
"@opengeni/config": "^0.16.
|
|
51
|
-
"@opengeni/contracts": "^0.
|
|
52
|
-
"@opengeni/db": "^0.
|
|
53
|
-
"@opengeni/documents": "^0.5.
|
|
54
|
-
"@opengeni/events": "^0.3.
|
|
55
|
-
"@opengeni/observability": "^0.7.
|
|
56
|
-
"@opengeni/runtime": "^0.
|
|
57
|
-
"@opengeni/storage": "^0.2.
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
:
|
|
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) =>
|
|
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:
|
|
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,
|
package/src/domain/resources.ts
CHANGED
|
@@ -17,7 +17,11 @@ import {
|
|
|
17
17
|
type ResourceRef,
|
|
18
18
|
type ToolRef,
|
|
19
19
|
} from "@opengeni/contracts";
|
|
20
|
-
import {
|
|
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
|
|
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(
|
|
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,
|
package/src/domain/sessions.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
2142
|
+
modelContext: input.modelContext ?? null,
|
|
2137
2143
|
resources: requestedResources,
|
|
2138
2144
|
model: input.model ?? null,
|
|
2139
2145
|
reasoningEffort: input.reasoningEffort ?? null,
|