@opengeni/core 2.8.3 → 2.9.3-canary.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/access/external-actor-authority.d.ts +47 -0
- package/dist/access/index.d.ts +15 -1
- package/dist/application/connect-authority.d.ts +13 -0
- package/dist/application/connect-operation.d.ts +28 -0
- package/dist/application/external-continuation.d.ts +15 -0
- package/dist/application/external-identity-lifecycle.d.ts +20 -0
- package/dist/application/external-link-work-admission.d.ts +12 -0
- package/dist/application/external-workspace-members.d.ts +6 -0
- package/dist/application/host-mcp-owner.d.ts +6 -0
- package/dist/application/new-session-drafts.d.ts +2 -1
- package/dist/application/session-tenancy.d.ts +1 -0
- package/dist/dependencies.d.ts +2 -0
- package/dist/domain/capabilities.d.ts +19 -2
- package/dist/domain/external-creation-attribution.d.ts +6 -0
- package/dist/domain/host-mcp-task-admission.d.ts +18 -0
- package/dist/domain/product-integration-pack.d.ts +3 -9
- package/dist/domain/product-integration-skill.gen.d.ts +5 -0
- package/dist/domain/scheduled-tasks.d.ts +3 -0
- package/dist/domain/sessions.d.ts +15 -4
- package/dist/index.d.ts +7 -0
- package/dist/index.js +1231 -825
- package/dist/index.js.map +1 -1
- package/dist/remote-mcp-credentials.d.ts +8 -0
- package/dist/remote-mcp-credentials.js +219 -0
- package/dist/remote-mcp-credentials.js.map +1 -0
- package/dist/session-authorization.d.ts +5 -6
- package/package.json +17 -13
- package/src/access/external-actor-authority.ts +94 -0
- package/src/access/index.ts +255 -1
- package/src/application/connect-authority.ts +77 -0
- package/src/application/connect-operation.ts +51 -0
- package/src/application/external-continuation.ts +112 -0
- package/src/application/external-identity-lifecycle.ts +48 -0
- package/src/application/external-link-work-admission.ts +87 -0
- package/src/application/external-workspace-members.ts +95 -0
- package/src/application/host-mcp-owner.ts +41 -0
- package/src/application/new-session-drafts.ts +9 -2
- package/src/application/session-tenancy.ts +24 -3
- package/src/application/user-resource-grants.ts +2 -2
- package/src/dependencies.ts +2 -0
- package/src/domain/capabilities.ts +20 -4
- package/src/domain/external-creation-attribution.ts +22 -0
- package/src/domain/host-mcp-task-admission.ts +111 -0
- package/src/domain/product-integration-pack.ts +11 -464
- package/src/domain/product-integration-skill.gen.ts +52 -0
- package/src/domain/scheduled-tasks.ts +66 -5
- package/src/domain/sessions.ts +253 -23
- package/src/index.ts +7 -0
- package/src/remote-mcp-credentials.ts +293 -0
- package/src/session-authorization.ts +22 -17
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import { HTTPException } from "hono/http-exception";
|
|
3
|
+
import {
|
|
4
|
+
AddExternalWorkspaceMemberRequest,
|
|
5
|
+
type ExternalIdentity,
|
|
6
|
+
} from "@opengeni/contracts/external-identities";
|
|
7
|
+
import {
|
|
8
|
+
ensureExternalIdentity,
|
|
9
|
+
grantWorkspaceAccess,
|
|
10
|
+
listWorkspaceMembers,
|
|
11
|
+
lockExternalWorkspaceMembershipLifecycle,
|
|
12
|
+
requireWorkspace,
|
|
13
|
+
setRlsContext,
|
|
14
|
+
withWorkspaceSubjectRls,
|
|
15
|
+
} from "@opengeni/db";
|
|
16
|
+
import {
|
|
17
|
+
accountScopedApiKeyWorkspaceAuthority,
|
|
18
|
+
hasPermission,
|
|
19
|
+
requireAccessContext,
|
|
20
|
+
requireFreshAccessGrant,
|
|
21
|
+
type AccessDeps,
|
|
22
|
+
} from "../access";
|
|
23
|
+
|
|
24
|
+
/** Explicit host onboarding. Ordinary asUser reads never call this operation.
|
|
25
|
+
* Existing memberships are not overwritten, including reduced permissions. */
|
|
26
|
+
export async function addExternalWorkspaceMemberForRequest(
|
|
27
|
+
c: Context,
|
|
28
|
+
deps: AccessDeps,
|
|
29
|
+
workspaceId: string,
|
|
30
|
+
input: unknown,
|
|
31
|
+
): Promise<ExternalIdentity> {
|
|
32
|
+
const payload = AddExternalWorkspaceMemberRequest.parse(input);
|
|
33
|
+
const context = await requireAccessContext(c, deps);
|
|
34
|
+
const authority = accountScopedApiKeyWorkspaceAuthority(context);
|
|
35
|
+
if (!authority)
|
|
36
|
+
throw new HTTPException(403, {
|
|
37
|
+
message: "external onboarding requires an organization service key",
|
|
38
|
+
});
|
|
39
|
+
const grant = await requireFreshAccessGrant(c, deps, workspaceId, "members:manage");
|
|
40
|
+
if (
|
|
41
|
+
grant.accountId !== authority.accountId ||
|
|
42
|
+
payload.permissions.some((permission) => !hasPermission(grant.permissions, permission))
|
|
43
|
+
) {
|
|
44
|
+
throw new HTTPException(403, { message: "membership exceeds key authority" });
|
|
45
|
+
}
|
|
46
|
+
return withWorkspaceSubjectRls(deps.db, workspaceId, grant.subjectId, async (tx) => {
|
|
47
|
+
await lockExternalWorkspaceMembershipLifecycle(tx, grant.accountId);
|
|
48
|
+
const live = await requireFreshAccessGrant(
|
|
49
|
+
c,
|
|
50
|
+
{ ...deps, db: tx },
|
|
51
|
+
workspaceId,
|
|
52
|
+
"members:manage",
|
|
53
|
+
);
|
|
54
|
+
if (
|
|
55
|
+
live.accountId !== grant.accountId ||
|
|
56
|
+
live.subjectId !== grant.subjectId ||
|
|
57
|
+
payload.permissions.some((permission) => !hasPermission(live.permissions, permission))
|
|
58
|
+
) {
|
|
59
|
+
throw new HTTPException(403, { message: "membership authority changed" });
|
|
60
|
+
}
|
|
61
|
+
const workspace = await requireWorkspace(tx, workspaceId);
|
|
62
|
+
if (workspace.kind !== "shared" || workspace.accountId !== authority.accountId)
|
|
63
|
+
throw new HTTPException(403, {
|
|
64
|
+
message: "external onboarding requires a shared organization workspace",
|
|
65
|
+
});
|
|
66
|
+
const identity = await ensureExternalIdentity(tx, {
|
|
67
|
+
accountId: authority.accountId,
|
|
68
|
+
...payload.identity,
|
|
69
|
+
});
|
|
70
|
+
await setRlsContext(tx, { accountId: authority.accountId, workspaceId });
|
|
71
|
+
const existing = (await listWorkspaceMembers(tx, workspaceId)).find(
|
|
72
|
+
(member) => member.subjectId === identity.subjectId,
|
|
73
|
+
);
|
|
74
|
+
const permissions = [...new Set(payload.permissions)];
|
|
75
|
+
if (existing) {
|
|
76
|
+
if (
|
|
77
|
+
existing.permissions.length !== permissions.length ||
|
|
78
|
+
existing.permissions.some((permission) => !permissions.includes(permission))
|
|
79
|
+
) {
|
|
80
|
+
throw new HTTPException(409, {
|
|
81
|
+
message: "existing membership differs; onboarding does not overwrite permissions",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return identity;
|
|
85
|
+
}
|
|
86
|
+
await grantWorkspaceAccess(tx, {
|
|
87
|
+
accountId: authority.accountId,
|
|
88
|
+
workspaceId,
|
|
89
|
+
subjectId: identity.subjectId,
|
|
90
|
+
role: "member",
|
|
91
|
+
permissions,
|
|
92
|
+
});
|
|
93
|
+
return identity;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { HTTPException } from "hono/http-exception";
|
|
2
|
+
import { resolveHostMcpBindingOwner, type Database } from "@opengeni/db";
|
|
3
|
+
import {
|
|
4
|
+
externalActorContinuationForAuthorization,
|
|
5
|
+
hasPermission,
|
|
6
|
+
hasVerifiedOwningUserAuthorization,
|
|
7
|
+
requireResolvedAccessGrantAuthorization,
|
|
8
|
+
type AccessGrantAuthorization,
|
|
9
|
+
} from "../access";
|
|
10
|
+
import { requireConnectOwnerAuthority } from "./connect-authority";
|
|
11
|
+
|
|
12
|
+
/** Direct user admission only. Agents inherit exact accepted snapshots instead.
|
|
13
|
+
* Native and externally represented users share owner semantics; service keys
|
|
14
|
+
* cannot impersonate a human owner merely by naming a subject. */
|
|
15
|
+
export function prepareHostMcpOwnerAuthorization(
|
|
16
|
+
authorization: AccessGrantAuthorization,
|
|
17
|
+
workspaceId: string,
|
|
18
|
+
permission: "connections:read" | "connections:write",
|
|
19
|
+
) {
|
|
20
|
+
const grant = requireResolvedAccessGrantAuthorization(authorization, workspaceId);
|
|
21
|
+
if (
|
|
22
|
+
!hasVerifiedOwningUserAuthorization(authorization) ||
|
|
23
|
+
grant.metadata?.sessionId ||
|
|
24
|
+
!hasPermission(grant.permissions, permission)
|
|
25
|
+
)
|
|
26
|
+
throw new HTTPException(403, {
|
|
27
|
+
message: "Host binding requires verified owning-user authority",
|
|
28
|
+
});
|
|
29
|
+
const continuation = externalActorContinuationForAuthorization(authorization);
|
|
30
|
+
const scope = {
|
|
31
|
+
accountId: grant.accountId,
|
|
32
|
+
workspaceId,
|
|
33
|
+
subjectId: grant.subjectId,
|
|
34
|
+
personalOwnerVerified: authorization.canonicalManagedHumanSession,
|
|
35
|
+
...(continuation ? { externalContinuation: continuation } : {}),
|
|
36
|
+
};
|
|
37
|
+
return async (tx: Database) => {
|
|
38
|
+
await requireConnectOwnerAuthority(tx, scope, permission);
|
|
39
|
+
return resolveHostMcpBindingOwner(tx, scope);
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -29,7 +29,11 @@ import {
|
|
|
29
29
|
validateGitHubRepositorySelection,
|
|
30
30
|
validateToolRefs,
|
|
31
31
|
} from "../domain/resources";
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
hasPermission,
|
|
34
|
+
externalAttributionForAuthorization,
|
|
35
|
+
type AccessGrantAuthorization,
|
|
36
|
+
} from "../access";
|
|
33
37
|
import { assertConfiguredModel, assertWorkspaceModelPolicyAllows } from "../domain/sessions";
|
|
34
38
|
|
|
35
39
|
type NewSessionDraftDependencies = Pick<AppDependencies, "settings" | "db" | "objectStorage">;
|
|
@@ -227,6 +231,7 @@ export async function saveActorNewSessionDraft(
|
|
|
227
231
|
* the historical bare-membership fence.
|
|
228
232
|
*/
|
|
229
233
|
canonicalManagedHumanSession = false,
|
|
234
|
+
externalAuthorization?: AccessGrantAuthorization,
|
|
230
235
|
): Promise<NewSessionDraftValue> {
|
|
231
236
|
const input = SaveNewSessionDraftRequest.parse(rawInput);
|
|
232
237
|
// The pre-marker client contract required `tools` and had no
|
|
@@ -280,7 +285,9 @@ export async function saveActorNewSessionDraft(
|
|
|
280
285
|
// all, so the human-removal fence above must fall back to the
|
|
281
286
|
// organization-membership pointer for them — and only for the
|
|
282
287
|
// canonical managed-cookie session that owns it.
|
|
283
|
-
personalWorkspaceOwnerException:
|
|
288
|
+
personalWorkspaceOwnerException:
|
|
289
|
+
canonicalManagedHumanSession ||
|
|
290
|
+
externalAttributionForAuthorization(externalAuthorization, grant) !== null,
|
|
284
291
|
}),
|
|
285
292
|
),
|
|
286
293
|
);
|
|
@@ -26,10 +26,15 @@ import {
|
|
|
26
26
|
type ForkSessionContentResult,
|
|
27
27
|
} from "@opengeni/db";
|
|
28
28
|
import { publishDurableSessionEvents, type EventBus } from "@opengeni/events";
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
requirePermission,
|
|
31
|
+
hasVerifiedOwningUserAuthorization,
|
|
32
|
+
type AccessGrantAuthorization,
|
|
33
|
+
} from "../access";
|
|
30
34
|
import { requireSessionAuthorization } from "../session-authorization";
|
|
31
35
|
import type { AppDependencies } from "../dependencies";
|
|
32
36
|
import { validateVariableSetAttachment } from "../domain/environments";
|
|
37
|
+
import { externalContinuationCommitAuthorizer } from "./external-continuation";
|
|
33
38
|
|
|
34
39
|
type SessionTenancyDependencies = Pick<
|
|
35
40
|
AppDependencies,
|
|
@@ -64,6 +69,18 @@ export function requireCanonicalManagedHuman(
|
|
|
64
69
|
}
|
|
65
70
|
}
|
|
66
71
|
|
|
72
|
+
export function requireVerifiedOwningUser(
|
|
73
|
+
authorization: AccessGrantAuthorization,
|
|
74
|
+
workspaceId: string,
|
|
75
|
+
): void {
|
|
76
|
+
if (
|
|
77
|
+
!hasVerifiedOwningUserAuthorization(authorization) ||
|
|
78
|
+
authorization.grant.workspaceId !== workspaceId
|
|
79
|
+
) {
|
|
80
|
+
throw new SessionTenancyManagedHumanRequiredError();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
67
84
|
export async function getManagedHumanSessionCreateCapabilities(
|
|
68
85
|
deps: Pick<SessionTenancyDependencies, "db">,
|
|
69
86
|
authorization: AccessGrantAuthorization,
|
|
@@ -71,7 +88,7 @@ export async function getManagedHumanSessionCreateCapabilities(
|
|
|
71
88
|
): Promise<SessionTenancyCreateCapabilities> {
|
|
72
89
|
requirePermission(authorization.grant, "sessions:create");
|
|
73
90
|
try {
|
|
74
|
-
|
|
91
|
+
requireVerifiedOwningUser(authorization, workspaceId);
|
|
75
92
|
} catch (error) {
|
|
76
93
|
if (error instanceof SessionTenancyManagedHumanRequiredError) {
|
|
77
94
|
return SessionTenancyCreateCapabilities.parse({
|
|
@@ -132,7 +149,7 @@ async function requireSessionTenancyMutationGate(
|
|
|
132
149
|
// These checks are deliberately target-free. A rejected principal must not
|
|
133
150
|
// cause a session lookup or embedding-host callback that distinguishes a
|
|
134
151
|
// missing, shared, or another owner's private session.
|
|
135
|
-
|
|
152
|
+
requireVerifiedOwningUser(authorization, workspaceId);
|
|
136
153
|
for (const permission of permissions) requirePermission(authorization.grant, permission);
|
|
137
154
|
if (!(await sessionTenancyProductActivated(deps.db, workspaceId))) {
|
|
138
155
|
throw new SessionTenancyNotActivatedError();
|
|
@@ -234,6 +251,7 @@ export async function updateManagedHumanSessionVisibility(
|
|
|
234
251
|
request: UpdateSessionVisibilityRequest,
|
|
235
252
|
authorizationSurface: SessionAuthorizationSurface = "core",
|
|
236
253
|
): Promise<UpdateSessionVisibilityResponse> {
|
|
254
|
+
const beforeCommit = externalContinuationCommitAuthorizer(authorization);
|
|
237
255
|
await requireSessionTenancyMutationGate(deps, authorization, workspaceId, ["sessions:control"]);
|
|
238
256
|
await requireSessionAuthorization(deps, authorization.grant, {
|
|
239
257
|
sessionId,
|
|
@@ -250,6 +268,7 @@ export async function updateManagedHumanSessionVisibility(
|
|
|
250
268
|
targetVisibility: sessionVisibilityFromPublic(request.visibility),
|
|
251
269
|
expectedAuthorityEpoch: request.expectedAuthorityEpoch,
|
|
252
270
|
operationKey: request.idempotencyKey,
|
|
271
|
+
...(beforeCommit ? { beforeCommit } : {}),
|
|
253
272
|
}),
|
|
254
273
|
);
|
|
255
274
|
const response = UpdateSessionVisibilityResponse.parse({
|
|
@@ -284,6 +303,7 @@ export async function forkManagedHumanSession(
|
|
|
284
303
|
"sessions:read",
|
|
285
304
|
"sessions:create",
|
|
286
305
|
]);
|
|
306
|
+
const beforeCommit = externalContinuationCommitAuthorizer(authorization);
|
|
287
307
|
const forkInput = {
|
|
288
308
|
sourceWorkspaceId: workspaceId,
|
|
289
309
|
sourceSessionId,
|
|
@@ -294,6 +314,7 @@ export async function forkManagedHumanSession(
|
|
|
294
314
|
request.visibility === "private" ? ("user_private" as const) : ("workspace_shared" as const),
|
|
295
315
|
workspaceSharedAcknowledged: request.workspaceSharedAcknowledged,
|
|
296
316
|
operationKey: request.idempotencyKey,
|
|
317
|
+
...(beforeCommit ? { beforeCommit } : {}),
|
|
297
318
|
...(request.rigId !== undefined || request.variableSetIds !== undefined
|
|
298
319
|
? {
|
|
299
320
|
runtimeRequest: {
|
|
@@ -16,7 +16,7 @@ import { requirePermission, type AccessGrantAuthorization } from "../access";
|
|
|
16
16
|
import type { AppDependencies } from "../dependencies";
|
|
17
17
|
import { requireSessionAuthorization } from "../session-authorization";
|
|
18
18
|
import {
|
|
19
|
-
|
|
19
|
+
requireVerifiedOwningUser,
|
|
20
20
|
SessionTenancyManagedHumanRequiredError,
|
|
21
21
|
} from "./session-tenancy";
|
|
22
22
|
|
|
@@ -58,7 +58,7 @@ function requireOwnerAuthority(
|
|
|
58
58
|
permissions: readonly Permission[],
|
|
59
59
|
): void {
|
|
60
60
|
if (!authorization.canonicalLocalHumanSession) {
|
|
61
|
-
|
|
61
|
+
requireVerifiedOwningUser(authorization, workspaceId);
|
|
62
62
|
} else if (
|
|
63
63
|
!authorization.contextIntegrity ||
|
|
64
64
|
authorization.authenticatedSubjectId !== authorization.grant.subjectId ||
|
package/src/dependencies.ts
CHANGED
|
@@ -208,6 +208,8 @@ export type AppDependencies = {
|
|
|
208
208
|
fikenFetch?: typeof fetch;
|
|
209
209
|
/** Injectable Integration Definition OAuth/API transport for deterministic tests. */
|
|
210
210
|
apiIntegrationOAuthFetch?: typeof fetch;
|
|
211
|
+
/** Injectable specification/introspection transport, still network-policy checked. */
|
|
212
|
+
apiIntegrationSourceFetch?: typeof fetch;
|
|
211
213
|
atlassianFetch?: typeof fetch;
|
|
212
214
|
/** Injectable MCP OAuth setup deadline for deterministic stalled-provider tests. */
|
|
213
215
|
oauthStartDeadlineMs?: number;
|
|
@@ -231,7 +231,7 @@ export async function createCatalogItem(input: {
|
|
|
231
231
|
});
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
|
|
234
|
+
type EnableCapabilityInput = {
|
|
235
235
|
db: Database;
|
|
236
236
|
grant: AccessGrant;
|
|
237
237
|
accountId: string;
|
|
@@ -240,13 +240,28 @@ export async function enableCapability(input: {
|
|
|
240
240
|
capabilityId: string;
|
|
241
241
|
payload: EnableCapabilityRequest;
|
|
242
242
|
probeMcpServer?: McpCapabilityProbe;
|
|
243
|
-
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
export async function enableCapability(
|
|
246
|
+
input: EnableCapabilityInput,
|
|
247
|
+
): Promise<CapabilityInstallation> {
|
|
248
|
+
const prepared = await prepareCapabilityEnable(input);
|
|
249
|
+
return prepared.commit(input.db);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Probe outside a durable Connect commit; persist the exact prepared settings
|
|
253
|
+
* inside the caller's authorized receipt transaction. Native enable uses this too. */
|
|
254
|
+
export async function prepareCapabilityEnable(input: EnableCapabilityInput) {
|
|
244
255
|
const item = await requireCatalogItem(
|
|
245
256
|
input.db,
|
|
246
257
|
input.workspaceId,
|
|
247
258
|
input.settings,
|
|
248
259
|
input.capabilityId,
|
|
249
260
|
);
|
|
261
|
+
if (isReservedCodexAppsCatalogItem(item))
|
|
262
|
+
throw new HTTPException(422, {
|
|
263
|
+
message: "Codex Apps use the dedicated account designation flow",
|
|
264
|
+
});
|
|
250
265
|
if (item.kind === "skill") {
|
|
251
266
|
throw new HTTPException(409, {
|
|
252
267
|
message: "Install Skills through the Skill library or source import flow",
|
|
@@ -303,14 +318,15 @@ export async function enableCapability(input: {
|
|
|
303
318
|
);
|
|
304
319
|
}
|
|
305
320
|
}
|
|
306
|
-
|
|
321
|
+
const installation = {
|
|
307
322
|
accountId: input.accountId,
|
|
308
323
|
workspaceId: input.workspaceId,
|
|
309
324
|
capabilityId: item.id,
|
|
310
325
|
kind: item.kind,
|
|
311
326
|
config: installationConfig,
|
|
312
327
|
metadata: installationMetadata,
|
|
313
|
-
}
|
|
328
|
+
};
|
|
329
|
+
return { commit: (db: Database) => enableCapabilityInstallation(db, installation) };
|
|
314
330
|
}
|
|
315
331
|
|
|
316
332
|
/**
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AccessGrant } from "@opengeni/contracts";
|
|
2
|
+
import { HTTPException } from "hono/http-exception";
|
|
3
|
+
import { externalAttributionForAuthorization, type AccessGrantAuthorization } from "../access";
|
|
4
|
+
|
|
5
|
+
export const EXTERNAL_CREATION_ATTRIBUTION_KEY = "opengeniExternalCreationAttribution";
|
|
6
|
+
|
|
7
|
+
/** Retained alongside session.created metadata; this is not a live permission
|
|
8
|
+
* snapshot and must never authorize a follow-up, child, or scheduled turn. */
|
|
9
|
+
export function externalCreationMetadata(
|
|
10
|
+
metadata: Record<string, unknown> | undefined,
|
|
11
|
+
authorization: AccessGrantAuthorization | undefined,
|
|
12
|
+
grant: AccessGrant,
|
|
13
|
+
): Record<string, unknown> | undefined {
|
|
14
|
+
if (metadata && Object.hasOwn(metadata, EXTERNAL_CREATION_ATTRIBUTION_KEY)) {
|
|
15
|
+
throw new HTTPException(422, {
|
|
16
|
+
message: `${EXTERNAL_CREATION_ATTRIBUTION_KEY} is server-owned`,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
const attribution = externalAttributionForAuthorization(authorization, grant);
|
|
20
|
+
if (!attribution) return metadata;
|
|
21
|
+
return { ...metadata, [EXTERNAL_CREATION_ATTRIBUTION_KEY]: attribution };
|
|
22
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { HTTPException } from "hono/http-exception";
|
|
2
|
+
import type { Settings } from "@opengeni/config";
|
|
3
|
+
import type { ToolRef, ScheduledTask, AccessGrant } from "@opengeni/contracts";
|
|
4
|
+
import {
|
|
5
|
+
HostMcpBindingDefinition,
|
|
6
|
+
HostMcpCreateSelections,
|
|
7
|
+
} from "@opengeni/contracts/host-mcp-bindings";
|
|
8
|
+
import {
|
|
9
|
+
captureHostMcpTaskAuthorities,
|
|
10
|
+
HostMcpDelegationAuthorityError,
|
|
11
|
+
inheritHostMcpTaskAuthoritiesFromAttempt,
|
|
12
|
+
type Database,
|
|
13
|
+
} from "@opengeni/db";
|
|
14
|
+
import { hasPermission, type AccessGrantAuthorization } from "../access";
|
|
15
|
+
import { prepareHostMcpOwnerAuthorization } from "../application/host-mcp-owner";
|
|
16
|
+
import { assertHostMcpAuthoritySourceAdmissionEnabled } from "./host-mcp-authority-source-admission";
|
|
17
|
+
|
|
18
|
+
/** Internal closure, not a caller-provided callback or durable key dependency. */
|
|
19
|
+
export function prepareHostMcpTaskAdmission(input: {
|
|
20
|
+
settings: Settings;
|
|
21
|
+
tools: ToolRef[];
|
|
22
|
+
grant: AccessGrant;
|
|
23
|
+
authorization?: AccessGrantAuthorization;
|
|
24
|
+
selections: unknown;
|
|
25
|
+
}): (tx: Database, task: ScheduledTask) => Promise<void> {
|
|
26
|
+
const selections = HostMcpCreateSelections.parse(input.selections);
|
|
27
|
+
if (
|
|
28
|
+
!input.authorization ||
|
|
29
|
+
input.authorization.grant.accountId !== input.grant.accountId ||
|
|
30
|
+
input.authorization.grant.subjectId !== input.grant.subjectId ||
|
|
31
|
+
input.authorization?.grant.workspaceId !== input.grant.workspaceId ||
|
|
32
|
+
input.grant.metadata?.["sessionId"] ||
|
|
33
|
+
!hasPermission(input.authorization?.grant.permissions ?? [], "connections:read") ||
|
|
34
|
+
!hasPermission(input.grant.permissions, "connections:read")
|
|
35
|
+
)
|
|
36
|
+
throw new HTTPException(403, {
|
|
37
|
+
message: "Host task selection requires a verified owner",
|
|
38
|
+
});
|
|
39
|
+
const reauthorize = prepareHostMcpOwnerAuthorization(
|
|
40
|
+
input.authorization,
|
|
41
|
+
input.grant.workspaceId,
|
|
42
|
+
"connections:read",
|
|
43
|
+
);
|
|
44
|
+
const prepared = selections.map((selection) => {
|
|
45
|
+
const server = input.settings.mcpServers.find(
|
|
46
|
+
(candidate) => candidate.id === selection.serverId,
|
|
47
|
+
);
|
|
48
|
+
if (
|
|
49
|
+
!server?.url ||
|
|
50
|
+
server.connectionRef?.authoritySource !== "host" ||
|
|
51
|
+
!server.connectionRef.hostBinding ||
|
|
52
|
+
!input.tools.some((tool) => tool.kind === "mcp" && tool.id === selection.serverId)
|
|
53
|
+
)
|
|
54
|
+
throw new HTTPException(422, {
|
|
55
|
+
message: "Host task selection must match a selected configured server",
|
|
56
|
+
});
|
|
57
|
+
assertHostMcpAuthoritySourceAdmissionEnabled(input.settings, server.connectionRef);
|
|
58
|
+
const { hostBinding, ...connectionRef } = server.connectionRef;
|
|
59
|
+
return {
|
|
60
|
+
delegationId: selection.delegationId,
|
|
61
|
+
generation: selection.generation,
|
|
62
|
+
bindingId: hostBinding.bindingId,
|
|
63
|
+
bindingGeneration: hostBinding.generation,
|
|
64
|
+
definition: HostMcpBindingDefinition.parse({
|
|
65
|
+
serverId: selection.serverId,
|
|
66
|
+
destinationUrl: server.url,
|
|
67
|
+
connectionRef,
|
|
68
|
+
}),
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
return async (tx, task) => {
|
|
72
|
+
const owner = await reauthorize(tx);
|
|
73
|
+
try {
|
|
74
|
+
if (prepared.length) await captureHostMcpTaskAuthorities(tx, owner, task, prepared);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (error instanceof HostMcpDelegationAuthorityError)
|
|
77
|
+
throw new HTTPException(403, { message: error.message });
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function prepareInheritedHostMcpTaskAdmission(
|
|
84
|
+
settings: Settings,
|
|
85
|
+
tools: ToolRef[],
|
|
86
|
+
source: { sessionId: string; turnId: string; attemptId: string; executionGeneration: number },
|
|
87
|
+
): (tx: Database, task: ScheduledTask) => Promise<void> {
|
|
88
|
+
const configured = settings.mcpServers.flatMap((server) => {
|
|
89
|
+
if (
|
|
90
|
+
!tools.some((tool) => tool.kind === "mcp" && tool.id === server.id) ||
|
|
91
|
+
server.connectionRef?.authoritySource !== "host" ||
|
|
92
|
+
!server.connectionRef.hostBinding ||
|
|
93
|
+
!server.url
|
|
94
|
+
)
|
|
95
|
+
return [];
|
|
96
|
+
assertHostMcpAuthoritySourceAdmissionEnabled(settings, server.connectionRef);
|
|
97
|
+
const { hostBinding, ...connectionRef } = server.connectionRef;
|
|
98
|
+
return [
|
|
99
|
+
{
|
|
100
|
+
bindingId: hostBinding.bindingId,
|
|
101
|
+
bindingGeneration: hostBinding.generation,
|
|
102
|
+
definition: HostMcpBindingDefinition.parse({
|
|
103
|
+
serverId: server.id,
|
|
104
|
+
destinationUrl: server.url,
|
|
105
|
+
connectionRef,
|
|
106
|
+
}),
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
});
|
|
110
|
+
return (tx, task) => inheritHostMcpTaskAuthoritiesFromAttempt(tx, task, source, configured);
|
|
111
|
+
}
|