@opengeni/core 0.11.8 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +35 -9
- package/dist/index.js +233 -120
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/dependencies.ts +2 -0
- package/src/domain/scheduled-tasks.ts +31 -1
- package/src/domain/sessions.ts +7 -0
- package/src/domain/slack-bot.ts +130 -0
- package/src/index.ts +1 -0
- package/src/sandbox/routing.ts +284 -243
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
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": {
|
|
@@ -35,14 +35,14 @@
|
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
37
37
|
"@opengeni/codex": "^0.2.7",
|
|
38
|
-
"@opengeni/config": "^0.7.
|
|
39
|
-
"@opengeni/contracts": "^0.
|
|
40
|
-
"@opengeni/db": "^0.
|
|
41
|
-
"@opengeni/documents": "^0.2.
|
|
42
|
-
"@opengeni/events": "^0.3.
|
|
38
|
+
"@opengeni/config": "^0.7.7",
|
|
39
|
+
"@opengeni/contracts": "^0.20.0",
|
|
40
|
+
"@opengeni/db": "^0.13.0",
|
|
41
|
+
"@opengeni/documents": "^0.2.37",
|
|
42
|
+
"@opengeni/events": "^0.3.28",
|
|
43
43
|
"@opengeni/observability": "^0.3.0",
|
|
44
|
-
"@opengeni/runtime": "^0.13.
|
|
45
|
-
"@opengeni/storage": "^0.2.
|
|
44
|
+
"@opengeni/runtime": "^0.13.11",
|
|
45
|
+
"@opengeni/storage": "^0.2.31",
|
|
46
46
|
"hono": "^4.12.18"
|
|
47
47
|
},
|
|
48
48
|
"engines": {
|
package/src/dependencies.ts
CHANGED
|
@@ -106,6 +106,8 @@ export type AppDependencies = {
|
|
|
106
106
|
managedAuth?: ManagedAuth | null;
|
|
107
107
|
/** Injectable Codex HTTP transport for deterministic API/provider tests. */
|
|
108
108
|
codexFetch?: typeof fetch;
|
|
109
|
+
/** Injectable Slack Web API transport for deterministic bot-connection tests. */
|
|
110
|
+
slackFetch?: typeof fetch;
|
|
109
111
|
// The API process's OWN agent-loop-free sandbox client (constructed from
|
|
110
112
|
// settings via @opengeni/runtime/sandbox). Undefined when sandboxBackend=none.
|
|
111
113
|
// This is the foundation of the API-direct control plane: the API resumes
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
CreateScheduledTaskRequest as CreateScheduledTaskPayload,
|
|
7
7
|
UpdateScheduledTaskRequest as UpdateScheduledTaskPayload,
|
|
8
8
|
} from "@opengeni/contracts";
|
|
9
|
+
import { OPENGENI_SLACK_BOT_SESSION_METADATA_KEY } from "@opengeni/contracts";
|
|
9
10
|
import {
|
|
10
11
|
createScheduledTask,
|
|
11
12
|
deleteScheduledTask,
|
|
@@ -24,6 +25,10 @@ import type { ObjectStorageDependency } from "../dependencies";
|
|
|
24
25
|
import { settingsWithEnabledCapabilityMcpServers } from "./capabilities";
|
|
25
26
|
import { validateVariableSetAttachment } from "./environments";
|
|
26
27
|
import { assertWorkspaceModelPolicyAllows, canonicalConfiguredModel } from "./sessions";
|
|
28
|
+
import {
|
|
29
|
+
hasReservedOpenGeniSlackBotSessionMetadata,
|
|
30
|
+
validateOpenGeniSlackBotConnectionSelection,
|
|
31
|
+
} from "./slack-bot";
|
|
27
32
|
import {
|
|
28
33
|
normalizeResources,
|
|
29
34
|
validateFileResources,
|
|
@@ -197,7 +202,7 @@ export async function validatedScheduledTaskUpdate(input: {
|
|
|
197
202
|
if (willHaveVariableSet) {
|
|
198
203
|
requirePermission(input.grant, "variable-sets:use");
|
|
199
204
|
}
|
|
200
|
-
|
|
205
|
+
const nextAgentConfig = await validateScheduledTaskAgentConfig({
|
|
201
206
|
settings: input.settings,
|
|
202
207
|
db: input.db,
|
|
203
208
|
objectStorage: input.objectStorage,
|
|
@@ -206,6 +211,18 @@ export async function validatedScheduledTaskUpdate(input: {
|
|
|
206
211
|
payload: { agentConfig: input.payload.agentConfig },
|
|
207
212
|
...(input.toolsProvided !== undefined ? { toolsProvided: input.toolsProvided } : {}),
|
|
208
213
|
});
|
|
214
|
+
if (
|
|
215
|
+
input.existing.reusableSessionId &&
|
|
216
|
+
input.existing.runMode === "reusable_session" &&
|
|
217
|
+
(input.existing.agentConfig.slackBotConnectionId ?? null) !==
|
|
218
|
+
(nextAgentConfig.slackBotConnectionId ?? null)
|
|
219
|
+
) {
|
|
220
|
+
throw new HTTPException(409, {
|
|
221
|
+
message:
|
|
222
|
+
"cannot change the OpenGeni Slack bot connection of a task with a live reusable session; recreate the task",
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
update.agentConfig = nextAgentConfig;
|
|
209
226
|
}
|
|
210
227
|
return update;
|
|
211
228
|
}
|
|
@@ -355,11 +372,24 @@ async function validateScheduledTaskAgentConfig(input: {
|
|
|
355
372
|
if (!prompt) {
|
|
356
373
|
throw new HTTPException(422, { message: "scheduled task prompt is required" });
|
|
357
374
|
}
|
|
375
|
+
if (hasReservedOpenGeniSlackBotSessionMetadata(input.payload.agentConfig.metadata)) {
|
|
376
|
+
throw new HTTPException(422, {
|
|
377
|
+
message: `${OPENGENI_SLACK_BOT_SESSION_METADATA_KEY} is reserved for scheduler routing`,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
358
380
|
await validateGitHubRepositorySelection(input.db, input.workspaceId, resources);
|
|
359
381
|
if (resources.some((resource) => resource.kind === "file") && !input.objectStorage) {
|
|
360
382
|
throw new HTTPException(503, { message: "object storage is not configured" });
|
|
361
383
|
}
|
|
362
384
|
await validateFileResources(input.db, input.workspaceId, resources);
|
|
385
|
+
if (input.payload.agentConfig.slackBotConnectionId) {
|
|
386
|
+
await validateOpenGeniSlackBotConnectionSelection(
|
|
387
|
+
input.db,
|
|
388
|
+
input.grant,
|
|
389
|
+
input.workspaceId,
|
|
390
|
+
input.payload.agentConfig.slackBotConnectionId,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
363
393
|
const requestedMaxDepth = input.payload.agentConfig.maxNestedAgentDepth;
|
|
364
394
|
if (requestedMaxDepth !== undefined) {
|
|
365
395
|
const workspace = await requireWorkspace(input.db, input.workspaceId);
|
package/src/domain/sessions.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
CreateSessionRequest,
|
|
11
11
|
DEFAULT_FIRST_PARTY_MCP_PERMISSIONS,
|
|
12
|
+
OPENGENI_SLACK_BOT_SESSION_METADATA_KEY,
|
|
12
13
|
SessionSpawnDenial,
|
|
13
14
|
ServiceTurnInitiator,
|
|
14
15
|
ServiceTurnInitiatorContext,
|
|
@@ -92,6 +93,7 @@ import { requireSessionAuthorization } from "../session-authorization";
|
|
|
92
93
|
import { swapActiveSandbox, type FleetContext } from "../sandbox/fleet";
|
|
93
94
|
import { settingsWithEnabledCapabilityMcpServers } from "./capabilities";
|
|
94
95
|
import { requireVariableSetEncryption, validateVariableSetAttachment } from "./environments";
|
|
96
|
+
import { hasReservedOpenGeniSlackBotSessionMetadata } from "./slack-bot";
|
|
95
97
|
import {
|
|
96
98
|
assertToolRefsSubset,
|
|
97
99
|
availableToolRefs,
|
|
@@ -1027,6 +1029,11 @@ export async function createSessionForRequest(
|
|
|
1027
1029
|
): Promise<Session> {
|
|
1028
1030
|
const { settings, db, bus, workflowClient, objectStorage } = deps;
|
|
1029
1031
|
const payload = CreateSessionRequest.parse(rawPayload);
|
|
1032
|
+
if (hasReservedOpenGeniSlackBotSessionMetadata(payload.metadata)) {
|
|
1033
|
+
throw new HTTPException(422, {
|
|
1034
|
+
message: `${OPENGENI_SLACK_BOT_SESSION_METADATA_KEY} is reserved for scheduler routing`,
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1030
1037
|
// A committed keyed denial is the idempotent outcome even if mutable
|
|
1031
1038
|
// resources, policy, authorization, or budget have changed since the first
|
|
1032
1039
|
// attempt. Replay it before any of those checks, just as a keyed successful
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import {
|
|
2
|
+
OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
|
|
3
|
+
OPENGENI_SLACK_BOT_REQUIRED_SCOPES,
|
|
4
|
+
OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
|
|
5
|
+
OPENGENI_SLACK_BOT_SESSION_METADATA_KEY,
|
|
6
|
+
OpenGeniSlackBotConnectionMetadata,
|
|
7
|
+
type AccessGrant,
|
|
8
|
+
type ConnectionMetadata,
|
|
9
|
+
type OpenGeniSlackBotConnectionMetadata as OpenGeniSlackBotMetadata,
|
|
10
|
+
type Session,
|
|
11
|
+
} from "@opengeni/contracts";
|
|
12
|
+
import {
|
|
13
|
+
getConnectionMetadata,
|
|
14
|
+
type ConnectionMetadataWithVerification,
|
|
15
|
+
type Database,
|
|
16
|
+
} from "@opengeni/db";
|
|
17
|
+
import { HTTPException } from "hono/http-exception";
|
|
18
|
+
import { requirePermission } from "../access";
|
|
19
|
+
|
|
20
|
+
export function openGeniSlackBotMetadata(
|
|
21
|
+
metadata: Record<string, unknown>,
|
|
22
|
+
): OpenGeniSlackBotMetadata | null {
|
|
23
|
+
const parsed = OpenGeniSlackBotConnectionMetadata.safeParse(metadata);
|
|
24
|
+
return parsed.success ? parsed.data : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isOpenGeniSlackBotConnection(
|
|
28
|
+
connection: ConnectionMetadata &
|
|
29
|
+
Partial<
|
|
30
|
+
Pick<ConnectionMetadataWithVerification, "verifiedInstallAt" | "verifiedInstallVersion">
|
|
31
|
+
>,
|
|
32
|
+
): boolean {
|
|
33
|
+
const granted = new Set(connection.grantedScopes);
|
|
34
|
+
return (
|
|
35
|
+
connection.verifiedInstallAt != null &&
|
|
36
|
+
connection.verifiedInstallVersion === connection.version &&
|
|
37
|
+
connection.subjectId === null &&
|
|
38
|
+
connection.providerDomain === "slack.com" &&
|
|
39
|
+
connection.kind === "app_install" &&
|
|
40
|
+
granted.size === OPENGENI_SLACK_BOT_REQUIRED_SCOPES.length &&
|
|
41
|
+
OPENGENI_SLACK_BOT_REQUIRED_SCOPES.every((scope) => granted.has(scope)) &&
|
|
42
|
+
openGeniSlackBotMetadata(connection.metadata)?.credentialRole ===
|
|
43
|
+
OPENGENI_SLACK_BOT_CREDENTIAL_ROLE
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function hasReservedOpenGeniSlackBotMetadata(
|
|
48
|
+
metadata: Record<string, unknown> | null | undefined,
|
|
49
|
+
): boolean {
|
|
50
|
+
return (
|
|
51
|
+
metadata?.credentialRole === OPENGENI_SLACK_BOT_CREDENTIAL_ROLE ||
|
|
52
|
+
metadata?.credentialLabel === OPENGENI_SLACK_BOT_CREDENTIAL_LABEL
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function hasReservedOpenGeniSlackBotSessionMetadata(
|
|
57
|
+
metadata: Record<string, unknown> | null | undefined,
|
|
58
|
+
): boolean {
|
|
59
|
+
return Boolean(
|
|
60
|
+
metadata &&
|
|
61
|
+
Object.prototype.hasOwnProperty.call(metadata, OPENGENI_SLACK_BOT_SESSION_METADATA_KEY),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Internal, pre-authorized lookup used by the scheduler and Slack tool adapter.
|
|
67
|
+
* Public callers must perform their permission check before calling this helper.
|
|
68
|
+
*/
|
|
69
|
+
export async function requireOpenGeniSlackBotConnection(
|
|
70
|
+
db: Database,
|
|
71
|
+
workspaceId: string,
|
|
72
|
+
connectionId: string,
|
|
73
|
+
): Promise<ConnectionMetadata> {
|
|
74
|
+
const connection = await getConnectionMetadata(db, workspaceId, connectionId, null);
|
|
75
|
+
if (!connection || !isOpenGeniSlackBotConnection(connection)) {
|
|
76
|
+
throw new HTTPException(422, {
|
|
77
|
+
message: "slackBotConnectionId must reference an OpenGeni Slack bot connection",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (connection.status !== "active") {
|
|
81
|
+
throw new HTTPException(422, {
|
|
82
|
+
message: `OpenGeni Slack bot connection is not active (${connection.status})`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return connection;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function validateOpenGeniSlackBotConnectionSelection(
|
|
89
|
+
db: Database,
|
|
90
|
+
grant: AccessGrant,
|
|
91
|
+
workspaceId: string,
|
|
92
|
+
connectionId: string,
|
|
93
|
+
): Promise<ConnectionMetadata> {
|
|
94
|
+
requirePermission(grant, "connections:read");
|
|
95
|
+
return await requireOpenGeniSlackBotConnection(db, workspaceId, connectionId);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function scheduledSlackBotConnectionId(
|
|
99
|
+
metadata: Record<string, unknown> | null | undefined,
|
|
100
|
+
): string | null {
|
|
101
|
+
const value = metadata?.[OPENGENI_SLACK_BOT_SESSION_METADATA_KEY];
|
|
102
|
+
return typeof value === "string" && zUuid(value) ? value : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The scheduler writes the connection pointer together with immutable creator
|
|
107
|
+
* provenance. Requiring both prevents ordinary session metadata from becoming
|
|
108
|
+
* an authorization mechanism for a workspace-shared bot credential.
|
|
109
|
+
*/
|
|
110
|
+
export function isTrustedScheduledSlackBotSession(
|
|
111
|
+
session: Pick<Session, "createdBy" | "createdByContext" | "metadata">,
|
|
112
|
+
): boolean {
|
|
113
|
+
const scheduledTaskId = session.metadata.scheduledTaskId;
|
|
114
|
+
const scheduledTaskRunId = session.metadata.scheduledTaskRunId;
|
|
115
|
+
return (
|
|
116
|
+
session.createdBy?.kind === "service" &&
|
|
117
|
+
session.createdBy.subjectId === "scheduler" &&
|
|
118
|
+
typeof scheduledTaskId === "string" &&
|
|
119
|
+
zUuid(scheduledTaskId) &&
|
|
120
|
+
typeof scheduledTaskRunId === "string" &&
|
|
121
|
+
zUuid(scheduledTaskRunId) &&
|
|
122
|
+
session.createdByContext.scheduledTaskId === scheduledTaskId &&
|
|
123
|
+
session.createdByContext.scheduledTaskRunId === scheduledTaskRunId &&
|
|
124
|
+
scheduledSlackBotConnectionId(session.metadata) !== null
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function zUuid(value: string): boolean {
|
|
129
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
130
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -60,6 +60,7 @@ export * from "./domain/resources";
|
|
|
60
60
|
export * from "./domain/session-tool-policy";
|
|
61
61
|
export * from "./domain/scheduled-tasks";
|
|
62
62
|
export * from "./domain/sessions";
|
|
63
|
+
export * from "./domain/slack-bot";
|
|
63
64
|
export * from "./domain/workspace-members";
|
|
64
65
|
export * from "./application/new-session-drafts";
|
|
65
66
|
export * from "./application/session-commands";
|