@opengeni/core 2.8.3 → 2.9.1-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
package/src/domain/sessions.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { CODEX_MODEL_ID_PREFIX, isCodexBilledModel } from "@opengeni/codex";
|
|
2
2
|
import { sessionCreationMetadata } from "../site-session-origin";
|
|
3
|
+
import {
|
|
4
|
+
HostMcpBindingDefinition,
|
|
5
|
+
HostMcpCreateSelections,
|
|
6
|
+
type HostMcpCreateSelection,
|
|
7
|
+
} from "@opengeni/contracts/host-mcp-bindings";
|
|
8
|
+
import {
|
|
9
|
+
captureDirectHostMcpAuthority,
|
|
10
|
+
HostMcpDelegationAuthorityError,
|
|
11
|
+
HostMcpBindingConflictError,
|
|
12
|
+
getHostMcpBinding,
|
|
13
|
+
getHostMcpDelegation,
|
|
14
|
+
} from "@opengeni/db";
|
|
3
15
|
import {
|
|
4
16
|
canonicalizeConfiguredModelId,
|
|
5
17
|
configuredAllowedModels,
|
|
@@ -46,7 +58,7 @@ import {
|
|
|
46
58
|
type Session,
|
|
47
59
|
type SessionAgentAccess,
|
|
48
60
|
type SessionCommandReceipt,
|
|
49
|
-
type
|
|
61
|
+
type SessionScopeSubjectId,
|
|
50
62
|
type SessionMemoryScope,
|
|
51
63
|
type SessionSkill,
|
|
52
64
|
type SessionEvent,
|
|
@@ -133,6 +145,10 @@ import {
|
|
|
133
145
|
} from "@opengeni/events";
|
|
134
146
|
import { HTTPException } from "hono/http-exception";
|
|
135
147
|
import { hasPermission, requirePermission, type AccessGrantAuthorization } from "../access";
|
|
148
|
+
import { externalCreationMetadata } from "./external-creation-attribution";
|
|
149
|
+
import { prepareExternalLinkTurnAdmission } from "../application/external-link-work-admission";
|
|
150
|
+
import { externalContinuationCommitAuthorizer } from "../application/external-continuation";
|
|
151
|
+
import { prepareHostMcpOwnerAuthorization } from "../application/host-mcp-owner";
|
|
136
152
|
import { recordWorkspaceUsage, requireLimit } from "../billing/limits";
|
|
137
153
|
import type {
|
|
138
154
|
AcceptSessionUserMessageDependencies,
|
|
@@ -167,7 +183,7 @@ import {
|
|
|
167
183
|
} from "./personal-connection-delegations";
|
|
168
184
|
import { hasReservedOpenGeniSlackBotSessionMetadata } from "./slack-bot";
|
|
169
185
|
import {
|
|
170
|
-
|
|
186
|
+
requireVerifiedOwningUser,
|
|
171
187
|
requireManagedHumanPrivateSessionCreate,
|
|
172
188
|
} from "../application/session-tenancy";
|
|
173
189
|
import {
|
|
@@ -213,7 +229,7 @@ async function requireAtomicPersonalResourceAttachment(
|
|
|
213
229
|
});
|
|
214
230
|
}
|
|
215
231
|
try {
|
|
216
|
-
|
|
232
|
+
requireVerifiedOwningUser(authorization, workspaceId);
|
|
217
233
|
} catch (error) {
|
|
218
234
|
throw new HTTPException(403, {
|
|
219
235
|
message: "Personal resources require the owning managed-human session.",
|
|
@@ -729,6 +745,13 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
729
745
|
/** Internal database-only composition seam. The exact session shell and this
|
|
730
746
|
* linkage commit together before its first event/turn can be initialized. */
|
|
731
747
|
beforeCreateCommit?: (tx: Database, sessionId: string) => Promise<void>;
|
|
748
|
+
/** Backend-only accepted-work composition. Recheck live caller authority
|
|
749
|
+
* here before capturing; session-shell authorization may have happened in
|
|
750
|
+
* an earlier transaction. Called only for a newly inserted initial turn. */
|
|
751
|
+
captureInitialTurnAuthority?: (tx: Database, sessionId: string, turnId: string) => Promise<void>;
|
|
752
|
+
/** Internal replay identity; the verified caller must also capture authority.
|
|
753
|
+
* Supplying selection metadata alone never grants runtime use. */
|
|
754
|
+
selectedHostMcpDelegations?: HostMcpCreateSelection[];
|
|
732
755
|
/** The custom workspace model was frozen by an earlier accepted boundary or
|
|
733
756
|
* inherited from an existing session, so retirement must not invalidate it. */
|
|
734
757
|
retainWorkspaceGatewayModel?: boolean;
|
|
@@ -801,7 +824,7 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
801
824
|
// (migration 0427), already resolved against the parent by the caller.
|
|
802
825
|
// Omitted keeps the workspace defaults for internal lifecycle callers.
|
|
803
826
|
agentAccess?: SessionAgentAccess;
|
|
804
|
-
|
|
827
|
+
scopeSubjectId?: SessionScopeSubjectId | null;
|
|
805
828
|
memoryScope?: SessionMemoryScope;
|
|
806
829
|
// Encrypted DB rows plus matching safe metadata for create-time per-session
|
|
807
830
|
// MCP servers. Metadata is the only shape emitted in events/responses.
|
|
@@ -1001,6 +1024,7 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
1001
1024
|
tools: input.tools,
|
|
1002
1025
|
toolPolicy: input.toolPolicy,
|
|
1003
1026
|
metadata: sessionMetadata,
|
|
1027
|
+
selectedHostMcpDelegations: input.selectedHostMcpDelegations ?? [],
|
|
1004
1028
|
...(input.createdBy ? { createdBy: input.createdBy } : {}),
|
|
1005
1029
|
...(frozenCreatedByContext ? { createdByContext: frozenCreatedByContext } : {}),
|
|
1006
1030
|
createdByActor: input.createdByActor ?? null,
|
|
@@ -1018,7 +1042,7 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
1018
1042
|
instructions: input.instructions ?? null,
|
|
1019
1043
|
policyRole: input.policyRole ?? null,
|
|
1020
1044
|
...(input.agentAccess ? { agentAccess: input.agentAccess } : {}),
|
|
1021
|
-
...(input.
|
|
1045
|
+
...(input.scopeSubjectId !== undefined ? { scopeSubjectId: input.scopeSubjectId } : {}),
|
|
1022
1046
|
...(input.memoryScope ? { memoryScope: input.memoryScope } : {}),
|
|
1023
1047
|
parentSessionId: input.parentSessionId ?? null,
|
|
1024
1048
|
createIdempotencyKey: input.createIdempotencyKey,
|
|
@@ -1096,6 +1120,7 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
1096
1120
|
toolPolicy: input.toolPolicy,
|
|
1097
1121
|
metadata: sessionMetadata,
|
|
1098
1122
|
...(input.createdBy ? { createdBy: input.createdBy } : {}),
|
|
1123
|
+
selectedHostMcpDelegations: input.selectedHostMcpDelegations ?? [],
|
|
1099
1124
|
...(frozenCreatedByContext ? { createdByContext: frozenCreatedByContext } : {}),
|
|
1100
1125
|
createdByActor: input.createdByActor ?? null,
|
|
1101
1126
|
model: input.model,
|
|
@@ -1112,7 +1137,7 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
1112
1137
|
instructions: input.instructions ?? null,
|
|
1113
1138
|
policyRole: input.policyRole ?? null,
|
|
1114
1139
|
...(input.agentAccess ? { agentAccess: input.agentAccess } : {}),
|
|
1115
|
-
...(input.
|
|
1140
|
+
...(input.scopeSubjectId !== undefined ? { scopeSubjectId: input.scopeSubjectId } : {}),
|
|
1116
1141
|
...(input.memoryScope ? { memoryScope: input.memoryScope } : {}),
|
|
1117
1142
|
parentSessionId: input.parentSessionId ?? null,
|
|
1118
1143
|
sandboxGroupId: input.sandboxGroupId ?? null,
|
|
@@ -1168,6 +1193,11 @@ async function finishStartSession(
|
|
|
1168
1193
|
bus: EventBus;
|
|
1169
1194
|
workflowClient: Pick<SessionWorkflowClient, "wakeSessionWorkflow">;
|
|
1170
1195
|
initialMessage: string;
|
|
1196
|
+
captureInitialTurnAuthority?: (
|
|
1197
|
+
tx: Database,
|
|
1198
|
+
sessionId: string,
|
|
1199
|
+
turnId: string,
|
|
1200
|
+
) => Promise<void>;
|
|
1171
1201
|
deferInitialTurn?: boolean;
|
|
1172
1202
|
modelContext?: string | null;
|
|
1173
1203
|
resources: ResourceRef[];
|
|
@@ -1253,6 +1283,12 @@ async function finishStartSession(
|
|
|
1253
1283
|
accountId: session.accountId,
|
|
1254
1284
|
workspaceId: session.workspaceId,
|
|
1255
1285
|
sessionId: session.id,
|
|
1286
|
+
...(input.captureInitialTurnAuthority
|
|
1287
|
+
? {
|
|
1288
|
+
captureInitialTurnAuthority: (tx: Database, turnId: string) =>
|
|
1289
|
+
input.captureInitialTurnAuthority!(tx, session.id, turnId),
|
|
1290
|
+
}
|
|
1291
|
+
: {}),
|
|
1256
1292
|
...(input.clientEventId ? { clientEventId: input.clientEventId } : {}),
|
|
1257
1293
|
reasoningEffortFallback: input.reasoningEffort,
|
|
1258
1294
|
turnExecutionPolicy: input.turnExecutionPolicy,
|
|
@@ -1500,6 +1536,8 @@ type PostUserMessageTurnInput = {
|
|
|
1500
1536
|
clientEventId?: string;
|
|
1501
1537
|
mcpCredentialUpdates?: UpdateSessionMcpServerCredentialsInput[];
|
|
1502
1538
|
personalConnectionDelegations?: McpPersonalConnectionDelegation[];
|
|
1539
|
+
selectedHostMcpDelegations?: HostMcpCreateSelection[];
|
|
1540
|
+
captureTurnAuthority?: (tx: Database, turnId: string) => Promise<void>;
|
|
1503
1541
|
personalResourceAttachment?: PersonalResourceAttachmentIntent;
|
|
1504
1542
|
delivery?: "send" | "steer";
|
|
1505
1543
|
origin?: "human" | "operator";
|
|
@@ -1706,6 +1744,12 @@ export async function postUserMessageTurn(
|
|
|
1706
1744
|
? { recordAgentRunUsage: input.recordAgentRunUsage }
|
|
1707
1745
|
: {}),
|
|
1708
1746
|
personalConnectionDelegations: input.personalConnectionDelegations ?? [],
|
|
1747
|
+
...(input.selectedHostMcpDelegations?.length
|
|
1748
|
+
? { selectedHostMcpDelegations: input.selectedHostMcpDelegations }
|
|
1749
|
+
: {}),
|
|
1750
|
+
...(input.captureTurnAuthority
|
|
1751
|
+
? { captureTurnAuthority: input.captureTurnAuthority }
|
|
1752
|
+
: {}),
|
|
1709
1753
|
...(input.personalResourceAttachment
|
|
1710
1754
|
? {
|
|
1711
1755
|
personalResourceAttachment: input.personalResourceAttachment,
|
|
@@ -1820,7 +1864,7 @@ export function resolveSessionCreateVisibility(input: {
|
|
|
1820
1864
|
|
|
1821
1865
|
export type SessionCreateScope = {
|
|
1822
1866
|
agentAccess: SessionAgentAccess;
|
|
1823
|
-
|
|
1867
|
+
scopeSubjectId: SessionScopeSubjectId | null;
|
|
1824
1868
|
memoryScope: SessionMemoryScope;
|
|
1825
1869
|
};
|
|
1826
1870
|
|
|
@@ -1832,7 +1876,6 @@ const AGENT_ACCESS_WIDTH: Record<SessionAgentAccess, number> = {
|
|
|
1832
1876
|
|
|
1833
1877
|
const MEMORY_SCOPE_WIDTH: Record<SessionMemoryScope, number> = {
|
|
1834
1878
|
off: 0,
|
|
1835
|
-
session: 1,
|
|
1836
1879
|
user: 2,
|
|
1837
1880
|
workspace: 3,
|
|
1838
1881
|
};
|
|
@@ -1851,7 +1894,7 @@ export function resolveSessionCreateScope(input: {
|
|
|
1851
1894
|
requested: {
|
|
1852
1895
|
agentAccess: SessionAgentAccess;
|
|
1853
1896
|
agentAccessProvided: boolean;
|
|
1854
|
-
|
|
1897
|
+
scopeSubjectId: SessionScopeSubjectId | null;
|
|
1855
1898
|
endUserProvided: boolean;
|
|
1856
1899
|
memoryScope: SessionMemoryScope;
|
|
1857
1900
|
memoryScopeProvided: boolean;
|
|
@@ -1863,7 +1906,7 @@ export function resolveSessionCreateScope(input: {
|
|
|
1863
1906
|
if (!parent) {
|
|
1864
1907
|
resolved = {
|
|
1865
1908
|
agentAccess: requested.agentAccess,
|
|
1866
|
-
|
|
1909
|
+
scopeSubjectId: requested.scopeSubjectId,
|
|
1867
1910
|
memoryScope: requested.memoryScope,
|
|
1868
1911
|
};
|
|
1869
1912
|
} else {
|
|
@@ -1875,10 +1918,9 @@ export function resolveSessionCreateScope(input: {
|
|
|
1875
1918
|
}
|
|
1876
1919
|
if (requested.endUserProvided) {
|
|
1877
1920
|
const same =
|
|
1878
|
-
requested.
|
|
1879
|
-
parent.
|
|
1880
|
-
requested.
|
|
1881
|
-
requested.endUser.id === parent.endUser.id;
|
|
1921
|
+
requested.scopeSubjectId !== null &&
|
|
1922
|
+
parent.scopeSubjectId !== null &&
|
|
1923
|
+
requested.scopeSubjectId === parent.scopeSubjectId;
|
|
1882
1924
|
if (!same) {
|
|
1883
1925
|
throw new HTTPException(403, {
|
|
1884
1926
|
message: "child end-user label must equal the parent session label",
|
|
@@ -1891,10 +1933,10 @@ export function resolveSessionCreateScope(input: {
|
|
|
1891
1933
|
message: "child memory scope may only narrow the parent session",
|
|
1892
1934
|
});
|
|
1893
1935
|
}
|
|
1894
|
-
resolved = { agentAccess,
|
|
1936
|
+
resolved = { agentAccess, scopeSubjectId: parent.scopeSubjectId, memoryScope };
|
|
1895
1937
|
}
|
|
1896
|
-
if (resolved.memoryScope === "user" && resolved.
|
|
1897
|
-
throw new HTTPException(422, { message: 'memoryScope "user" requires an
|
|
1938
|
+
if (resolved.memoryScope === "user" && resolved.scopeSubjectId === null) {
|
|
1939
|
+
throw new HTTPException(422, { message: 'memoryScope "user" requires an authenticated user' });
|
|
1898
1940
|
}
|
|
1899
1941
|
return resolved;
|
|
1900
1942
|
}
|
|
@@ -1972,6 +2014,26 @@ export async function createSessionForRequestWithOutcome(
|
|
|
1972
2014
|
): Promise<CreateSessionRequestOutcome> {
|
|
1973
2015
|
const payload = CreateSessionRequest.parse(rawPayload);
|
|
1974
2016
|
payload.metadata = sessionCreationMetadata(payload.metadata);
|
|
2017
|
+
const creationMetadata = externalCreationMetadata(payload.metadata, authorization, grant);
|
|
2018
|
+
const externalBeforeCreateCommit = externalContinuationCommitAuthorizer(authorization);
|
|
2019
|
+
const hostSelections = payload.selectedHostMcpDelegations ?? [];
|
|
2020
|
+
if (
|
|
2021
|
+
hostSelections.length &&
|
|
2022
|
+
(!authorization ||
|
|
2023
|
+
authorization.grant.accountId !== grant.accountId ||
|
|
2024
|
+
authorization.grant.subjectId !== grant.subjectId ||
|
|
2025
|
+
authorization?.grant.workspaceId !== workspaceId ||
|
|
2026
|
+
!hasPermission(authorization?.grant.permissions ?? [], "connections:read") ||
|
|
2027
|
+
!hasPermission(grant.permissions, "connections:read") ||
|
|
2028
|
+
grant.metadata?.["sessionId"] ||
|
|
2029
|
+
payload.startMode === "realtime")
|
|
2030
|
+
) {
|
|
2031
|
+
throw new HTTPException(403, {
|
|
2032
|
+
message: "Host selection requires a verified direct owner and non-realtime start",
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
2035
|
+
if (hostSelections.length)
|
|
2036
|
+
prepareHostMcpOwnerAuthorization(authorization!, workspaceId, "connections:read");
|
|
1975
2037
|
if (hasReservedOpenGeniSlackBotSessionMetadata(payload.metadata)) {
|
|
1976
2038
|
throw new HTTPException(422, {
|
|
1977
2039
|
message: `${OPENGENI_SLACK_BOT_SESSION_METADATA_KEY} is reserved for scheduler routing`,
|
|
@@ -2057,24 +2119,26 @@ export async function createSessionForRequestWithOutcome(
|
|
|
2057
2119
|
// visibility: presence is read from the raw request because the Zod
|
|
2058
2120
|
// defaults erase absent-vs-explicit, and the parent side comes from the
|
|
2059
2121
|
// durable row rather than anything the caller sent.
|
|
2122
|
+
const creationInitiator = creationInitiatorForGrant(grant);
|
|
2123
|
+
const scopeUser =
|
|
2124
|
+
creationInitiator.initiator?.kind === "subject" ? creationInitiator.initiator.subjectId : null;
|
|
2060
2125
|
const sessionScope = resolveSessionCreateScope({
|
|
2061
2126
|
requested: {
|
|
2062
2127
|
agentAccess: payload.agentAccess,
|
|
2063
2128
|
agentAccessProvided: hasOwnProperty(rawPayload, "agentAccess"),
|
|
2064
|
-
|
|
2065
|
-
endUserProvided:
|
|
2129
|
+
scopeSubjectId: scopeUser && /^(?:user:|external_user:)/u.test(scopeUser) ? scopeUser : null,
|
|
2130
|
+
endUserProvided: false,
|
|
2066
2131
|
memoryScope: payload.memoryScope,
|
|
2067
2132
|
memoryScopeProvided: hasOwnProperty(rawPayload, "memoryScope"),
|
|
2068
2133
|
},
|
|
2069
2134
|
parent: parentAuthority
|
|
2070
2135
|
? {
|
|
2071
2136
|
agentAccess: parentAuthority.agentAccess,
|
|
2072
|
-
|
|
2137
|
+
scopeSubjectId: parentAuthority.scopeSubjectId,
|
|
2073
2138
|
memoryScope: parentAuthority.memoryScope,
|
|
2074
2139
|
}
|
|
2075
2140
|
: null,
|
|
2076
2141
|
});
|
|
2077
|
-
const creationInitiator = creationInitiatorForGrant(grant);
|
|
2078
2142
|
const parentCallingTurn =
|
|
2079
2143
|
parentSession && creationInitiator.actor
|
|
2080
2144
|
? await getSessionTurnForAttempt(
|
|
@@ -2116,6 +2180,7 @@ export async function createSessionForRequestWithOutcome(
|
|
|
2116
2180
|
: {}),
|
|
2117
2181
|
createIdempotencyKey: payload.idempotencyKey,
|
|
2118
2182
|
selectedInstalledSkillIds: payload.installedSkillIds ?? [],
|
|
2183
|
+
selectedHostMcpDelegations: hostSelections,
|
|
2119
2184
|
...(payload.requestedSessionId ? { requestedSessionId: payload.requestedSessionId } : {}),
|
|
2120
2185
|
visibility: effectiveVisibility,
|
|
2121
2186
|
variableSetIds: payload.variableSetIds ?? [],
|
|
@@ -2375,6 +2440,15 @@ export async function createSessionForRequestWithOutcome(
|
|
|
2375
2440
|
// tool's permission/target authorization predicate, so attachment alone
|
|
2376
2441
|
// exposes nothing.
|
|
2377
2442
|
const tools = withFirstPartyTools(selectedTools, runtimeSettings);
|
|
2443
|
+
const captureSelectedHostAuthority = prepareSelectedHostTurnAuthority(
|
|
2444
|
+
runtimeSettings,
|
|
2445
|
+
tools,
|
|
2446
|
+
grant,
|
|
2447
|
+
workspaceId,
|
|
2448
|
+
hostSelections,
|
|
2449
|
+
authorization,
|
|
2450
|
+
);
|
|
2451
|
+
const captureLinkedAuthority = prepareExternalLinkTurnAdmission(authorization);
|
|
2378
2452
|
await validateGitHubRepositorySelection(db, workspaceId, resources);
|
|
2379
2453
|
if (resources.some((resource) => resource.kind === "file") && !objectStorage) {
|
|
2380
2454
|
throw new HTTPException(503, {
|
|
@@ -2975,7 +3049,21 @@ export async function createSessionForRequestWithOutcome(
|
|
|
2975
3049
|
// create carries a derived OS; shared spawns inherit the exact parent box.
|
|
2976
3050
|
...(effectiveSandboxOs ? { sandboxOs: effectiveSandboxOs } : {}),
|
|
2977
3051
|
sandboxGroupId,
|
|
2978
|
-
metadata:
|
|
3052
|
+
metadata: creationMetadata ?? {},
|
|
3053
|
+
...(externalBeforeCreateCommit ? { beforeCreateCommit: externalBeforeCreateCommit } : {}),
|
|
3054
|
+
selectedHostMcpDelegations: hostSelections,
|
|
3055
|
+
...(captureSelectedHostAuthority || captureLinkedAuthority
|
|
3056
|
+
? {
|
|
3057
|
+
captureInitialTurnAuthority: async (
|
|
3058
|
+
tx: Database,
|
|
3059
|
+
sessionId: string,
|
|
3060
|
+
turnId: string,
|
|
3061
|
+
) => {
|
|
3062
|
+
await captureLinkedAuthority?.(tx, sessionId, turnId);
|
|
3063
|
+
await captureSelectedHostAuthority?.(tx, sessionId, turnId);
|
|
3064
|
+
},
|
|
3065
|
+
}
|
|
3066
|
+
: {}),
|
|
2979
3067
|
...(creationInitiator.initiator ? { createdBy: creationInitiator.initiator } : {}),
|
|
2980
3068
|
...(creationInitiator.context ? { createdByContext: creationInitiator.context } : {}),
|
|
2981
3069
|
createdByActor: creationInitiator.actor ?? null,
|
|
@@ -2996,7 +3084,7 @@ export async function createSessionForRequestWithOutcome(
|
|
|
2996
3084
|
instructions: payload.instructions ?? null,
|
|
2997
3085
|
policyRole: payload.policyRole ?? null,
|
|
2998
3086
|
agentAccess: sessionScope.agentAccess,
|
|
2999
|
-
|
|
3087
|
+
scopeSubjectId: sessionScope.scopeSubjectId,
|
|
3000
3088
|
memoryScope: sessionScope.memoryScope,
|
|
3001
3089
|
firstPartyMcpPermissions,
|
|
3002
3090
|
firstPartyMcpTools,
|
|
@@ -3057,6 +3145,18 @@ export async function createSessionForRequestWithOutcome(
|
|
|
3057
3145
|
{ message: error.message, cause: error },
|
|
3058
3146
|
);
|
|
3059
3147
|
}
|
|
3148
|
+
if (error instanceof HostMcpDelegationAuthorityError) {
|
|
3149
|
+
throw new HTTPException(403, {
|
|
3150
|
+
message: "Host delegation authority unavailable",
|
|
3151
|
+
cause: error,
|
|
3152
|
+
});
|
|
3153
|
+
}
|
|
3154
|
+
if (error instanceof HostMcpBindingConflictError) {
|
|
3155
|
+
throw new HTTPException(409, {
|
|
3156
|
+
message: "Host delegation selection conflicts",
|
|
3157
|
+
cause: error,
|
|
3158
|
+
});
|
|
3159
|
+
}
|
|
3060
3160
|
if (error instanceof AgentCommandAuthorityError) {
|
|
3061
3161
|
throw new HTTPException(403, { message: error.message });
|
|
3062
3162
|
}
|
|
@@ -3111,6 +3211,94 @@ export async function createSessionForRequest(
|
|
|
3111
3211
|
).session;
|
|
3112
3212
|
}
|
|
3113
3213
|
|
|
3214
|
+
/** Snapshot the selected destination, then recheck live actor, binding and grant
|
|
3215
|
+
* inside the transaction which accepts the turn. Neither selection nor a
|
|
3216
|
+
* registered binding alone is execution authority. Shared by create/send/steer. */
|
|
3217
|
+
function prepareSelectedHostTurnAuthority(
|
|
3218
|
+
settings: Settings,
|
|
3219
|
+
tools: ToolRef[],
|
|
3220
|
+
grant: AccessGrant,
|
|
3221
|
+
workspaceId: string,
|
|
3222
|
+
selections: HostMcpCreateSelection[],
|
|
3223
|
+
authorization?: AccessGrantAuthorization,
|
|
3224
|
+
): ((tx: Database, sessionId: string, turnId: string) => Promise<void>) | undefined {
|
|
3225
|
+
if (!selections.length) return undefined;
|
|
3226
|
+
if (
|
|
3227
|
+
!authorization ||
|
|
3228
|
+
authorization.grant.accountId !== grant.accountId ||
|
|
3229
|
+
authorization.grant.subjectId !== grant.subjectId ||
|
|
3230
|
+
authorization?.grant.workspaceId !== workspaceId ||
|
|
3231
|
+
!hasPermission(authorization?.grant.permissions ?? [], "connections:read") ||
|
|
3232
|
+
!hasPermission(grant.permissions, "connections:read") ||
|
|
3233
|
+
grant.metadata?.["sessionId"]
|
|
3234
|
+
)
|
|
3235
|
+
throw new HTTPException(403, {
|
|
3236
|
+
message: "Host selection requires a verified direct owner",
|
|
3237
|
+
});
|
|
3238
|
+
const beforeCommit = prepareHostMcpOwnerAuthorization(
|
|
3239
|
+
authorization,
|
|
3240
|
+
workspaceId,
|
|
3241
|
+
"connections:read",
|
|
3242
|
+
);
|
|
3243
|
+
const configs = new Map(
|
|
3244
|
+
selections.map((selection) => {
|
|
3245
|
+
const configured = settings.mcpServers.find((server) => server.id === selection.serverId);
|
|
3246
|
+
if (
|
|
3247
|
+
!configured ||
|
|
3248
|
+
!tools.some((tool) => tool.kind === "mcp" && tool.id === selection.serverId) ||
|
|
3249
|
+
configured.connectionRef?.authoritySource !== "host" ||
|
|
3250
|
+
!configured.connectionRef.hostBinding ||
|
|
3251
|
+
!configured.url
|
|
3252
|
+
)
|
|
3253
|
+
throw new HTTPException(422, {
|
|
3254
|
+
message: "Host delegation must match a selected configured host server",
|
|
3255
|
+
});
|
|
3256
|
+
assertHostMcpAuthoritySourceAdmissionEnabled(settings, configured.connectionRef);
|
|
3257
|
+
return [selection.serverId, structuredClone(configured)] as const;
|
|
3258
|
+
}),
|
|
3259
|
+
);
|
|
3260
|
+
return async (tx, sessionId, turnId) => {
|
|
3261
|
+
const owner = await beforeCommit(tx);
|
|
3262
|
+
for (const selection of selections) {
|
|
3263
|
+
const delegation = await getHostMcpDelegation(tx, owner, selection.delegationId);
|
|
3264
|
+
const binding = delegation ? await getHostMcpBinding(tx, owner, delegation.bindingId) : null;
|
|
3265
|
+
const configured = configs.get(selection.serverId)!;
|
|
3266
|
+
const { hostBinding, ...connectionRef } = configured.connectionRef!;
|
|
3267
|
+
const definition = HostMcpBindingDefinition.safeParse({
|
|
3268
|
+
serverId: selection.serverId,
|
|
3269
|
+
destinationUrl: configured.url,
|
|
3270
|
+
connectionRef,
|
|
3271
|
+
});
|
|
3272
|
+
if (
|
|
3273
|
+
!delegation ||
|
|
3274
|
+
!binding ||
|
|
3275
|
+
delegation.status !== "active" ||
|
|
3276
|
+
binding.status !== "active" ||
|
|
3277
|
+
!definition.success ||
|
|
3278
|
+
delegation.generation !== selection.generation ||
|
|
3279
|
+
binding.id !== hostBinding!.bindingId ||
|
|
3280
|
+
binding.generation !== hostBinding!.generation ||
|
|
3281
|
+
stableJson(definition.data) !== stableJson(binding.definition)
|
|
3282
|
+
)
|
|
3283
|
+
throw new HTTPException(403, { message: "Host delegation selection changed" });
|
|
3284
|
+
try {
|
|
3285
|
+
await captureDirectHostMcpAuthority(tx, owner, {
|
|
3286
|
+
sessionId,
|
|
3287
|
+
turnId,
|
|
3288
|
+
delegationId: delegation.id,
|
|
3289
|
+
expectedDelegationGeneration: selection.generation,
|
|
3290
|
+
});
|
|
3291
|
+
} catch (error) {
|
|
3292
|
+
if (error instanceof HostMcpDelegationAuthorityError)
|
|
3293
|
+
throw new HTTPException(403, { message: "Host delegation authority unavailable" });
|
|
3294
|
+
if (error instanceof HostMcpBindingConflictError)
|
|
3295
|
+
throw new HTTPException(409, { message: "Host delegation selection conflicts" });
|
|
3296
|
+
throw error;
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
};
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3114
3302
|
function sessionPromptBoundaryRequestHash(input: {
|
|
3115
3303
|
delivery: "send" | "steer";
|
|
3116
3304
|
controlEtag: string | null;
|
|
@@ -3126,6 +3314,7 @@ function sessionPromptBoundaryRequestHash(input: {
|
|
|
3126
3314
|
source: "user" | "api";
|
|
3127
3315
|
mcpCredentialUpdates: SessionMcpCredentialUpdateInput[];
|
|
3128
3316
|
connectionAuthorities?: McpConnectionAuthoritySelection[];
|
|
3317
|
+
selectedHostMcpDelegations?: HostMcpCreateSelection[];
|
|
3129
3318
|
personalResourceAttachment?: PersonalResourceAttachmentIntent;
|
|
3130
3319
|
commandActor: SessionCommandActor;
|
|
3131
3320
|
}): string {
|
|
@@ -3145,6 +3334,9 @@ function sessionPromptBoundaryRequestHash(input: {
|
|
|
3145
3334
|
source: input.source,
|
|
3146
3335
|
mcpCredentialUpdates: input.mcpCredentialUpdates,
|
|
3147
3336
|
connectionAuthorities: input.connectionAuthorities ?? [],
|
|
3337
|
+
...(input.selectedHostMcpDelegations?.length
|
|
3338
|
+
? { selectedHostMcpDelegations: input.selectedHostMcpDelegations }
|
|
3339
|
+
: {}),
|
|
3148
3340
|
personalResourceAttachment: input.personalResourceAttachment ?? null,
|
|
3149
3341
|
...(input.commandActor.type === "service"
|
|
3150
3342
|
? {
|
|
@@ -3183,6 +3375,7 @@ export async function acceptSessionUserMessageWithOutcome(
|
|
|
3183
3375
|
clientEventId?: string;
|
|
3184
3376
|
mcpCredentialUpdates?: SessionMcpCredentialUpdateInput[];
|
|
3185
3377
|
connectionAuthorities?: McpConnectionAuthoritySelection[];
|
|
3378
|
+
selectedHostMcpDelegations?: HostMcpCreateSelection[];
|
|
3186
3379
|
delivery?: "send" | "steer";
|
|
3187
3380
|
origin?: "human" | "operator";
|
|
3188
3381
|
controlEtag?: string | null;
|
|
@@ -3200,6 +3393,24 @@ export async function acceptSessionUserMessageWithOutcome(
|
|
|
3200
3393
|
replay: boolean;
|
|
3201
3394
|
}> {
|
|
3202
3395
|
const { db, bus, workflowClient, objectStorage } = deps;
|
|
3396
|
+
const hostSelections = HostMcpCreateSelections.parse(input.selectedHostMcpDelegations ?? []).sort(
|
|
3397
|
+
(a, b) => (a.serverId < b.serverId ? -1 : a.serverId > b.serverId ? 1 : 0),
|
|
3398
|
+
);
|
|
3399
|
+
if (
|
|
3400
|
+
hostSelections.length &&
|
|
3401
|
+
(!input.authorization ||
|
|
3402
|
+
input.authorization.grant.accountId !== grant.accountId ||
|
|
3403
|
+
input.authorization.grant.subjectId !== grant.subjectId ||
|
|
3404
|
+
input.authorization?.grant.workspaceId !== workspaceId ||
|
|
3405
|
+
!hasPermission(input.authorization?.grant.permissions ?? [], "connections:read") ||
|
|
3406
|
+
!hasPermission(grant.permissions, "connections:read") ||
|
|
3407
|
+
grant.metadata?.["sessionId"])
|
|
3408
|
+
)
|
|
3409
|
+
throw new HTTPException(403, {
|
|
3410
|
+
message: "Host selection requires a verified direct owner",
|
|
3411
|
+
});
|
|
3412
|
+
if (hostSelections.length)
|
|
3413
|
+
prepareHostMcpOwnerAuthorization(input.authorization!, workspaceId, "connections:read");
|
|
3203
3414
|
const delegatedServiceInitiator = serviceInitiatorForGrant(grant);
|
|
3204
3415
|
const delivery = input.delivery ?? "send";
|
|
3205
3416
|
const source = delegatedServiceInitiator || input.origin === "operator" ? "api" : "user";
|
|
@@ -3237,6 +3448,7 @@ export async function acceptSessionUserMessageWithOutcome(
|
|
|
3237
3448
|
latencyMode: input.latencyMode ?? null,
|
|
3238
3449
|
source,
|
|
3239
3450
|
mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
|
|
3451
|
+
selectedHostMcpDelegations: hostSelections,
|
|
3240
3452
|
...(input.connectionAuthorities
|
|
3241
3453
|
? { connectionAuthorities: input.connectionAuthorities }
|
|
3242
3454
|
: {}),
|
|
@@ -3421,6 +3633,15 @@ export async function acceptSessionUserMessageWithOutcome(
|
|
|
3421
3633
|
existingSession.firstPartyMcpPermissions.includes("connections:read")),
|
|
3422
3634
|
...(input.connectionAuthorities ? { authoritySelections: input.connectionAuthorities } : {}),
|
|
3423
3635
|
});
|
|
3636
|
+
const captureSelectedHostAuthority = prepareSelectedHostTurnAuthority(
|
|
3637
|
+
runtimeSettings,
|
|
3638
|
+
existingSession.tools,
|
|
3639
|
+
grant,
|
|
3640
|
+
workspaceId,
|
|
3641
|
+
hostSelections,
|
|
3642
|
+
input.authorization,
|
|
3643
|
+
);
|
|
3644
|
+
const captureLinkedAuthority = prepareExternalLinkTurnAdmission(input.authorization);
|
|
3424
3645
|
const { accepted, turn, draft, receipt, routing, interruptionCount, replay } =
|
|
3425
3646
|
await postUserMessageTurn({
|
|
3426
3647
|
db,
|
|
@@ -3442,6 +3663,15 @@ export async function acceptSessionUserMessageWithOutcome(
|
|
|
3442
3663
|
turnExecutionPolicy,
|
|
3443
3664
|
mcpCredentialUpdates,
|
|
3444
3665
|
personalConnectionDelegations,
|
|
3666
|
+
selectedHostMcpDelegations: hostSelections,
|
|
3667
|
+
...(captureSelectedHostAuthority || captureLinkedAuthority
|
|
3668
|
+
? {
|
|
3669
|
+
captureTurnAuthority: async (tx: Database, turnId: string) => {
|
|
3670
|
+
await captureLinkedAuthority?.(tx, sessionId, turnId);
|
|
3671
|
+
await captureSelectedHostAuthority?.(tx, sessionId, turnId);
|
|
3672
|
+
},
|
|
3673
|
+
}
|
|
3674
|
+
: {}),
|
|
3445
3675
|
...(input.personalResourceAttachment
|
|
3446
3676
|
? { personalResourceAttachment: input.personalResourceAttachment }
|
|
3447
3677
|
: {}),
|
package/src/index.ts
CHANGED
|
@@ -61,6 +61,13 @@ export * from "./sandbox/runtime-settings";
|
|
|
61
61
|
|
|
62
62
|
// Access layer (transport-neutral grant resolution + permission checks).
|
|
63
63
|
export * from "./access";
|
|
64
|
+
export * from "./application/external-workspace-members";
|
|
65
|
+
export * from "./application/external-identity-lifecycle";
|
|
66
|
+
export * from "./application/external-continuation";
|
|
67
|
+
export * from "./application/external-link-work-admission";
|
|
68
|
+
export * from "./application/connect-authority";
|
|
69
|
+
export * from "./application/host-mcp-owner";
|
|
70
|
+
export * from "./application/connect-operation";
|
|
64
71
|
export * from "./session-authorization";
|
|
65
72
|
|
|
66
73
|
// Billing / usage-limit admission (checkLimit / requireLimit / recordWorkspaceUsage).
|