@opengeni/db 4.2.2 → 4.3.0-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/canonical-human-identities.js +3 -3
- package/dist/{chunk-6GAPAYZR.js → chunk-7JPLGBSA.js} +562 -86
- package/dist/chunk-7JPLGBSA.js.map +1 -0
- package/dist/{chunk-SNLIKELL.js → chunk-IHADTE7C.js} +2 -2
- package/dist/{chunk-7WX3R3YL.js → chunk-ILKJKDII.js} +7 -2
- package/dist/chunk-ILKJKDII.js.map +1 -0
- package/dist/{chunk-MGVOYVPS.js → chunk-NCSPNCQK.js} +3 -3
- package/dist/{chunk-B2KR3TEM.js → chunk-SGJDIF2D.js} +2 -2
- package/dist/{chunk-S4Y42VSX.js → chunk-Y5XFVJJM.js} +207 -3
- package/dist/chunk-Y5XFVJJM.js.map +1 -0
- package/dist/{chunk-FPTI4NP4.js → chunk-YGBOVXC7.js} +2 -2
- package/dist/{chunk-6Y7ZDVJ5.js → chunk-YGRT6VZO.js} +2 -2
- package/dist/{chunk-6IQ4X35Y.js → chunk-YVYBAWBU.js} +46 -1
- package/dist/chunk-YVYBAWBU.js.map +1 -0
- package/dist/connect-attempts.d.ts +60 -0
- package/dist/connection-setup-authority.d.ts +10 -0
- package/dist/connection-token-resolver.d.ts +11 -2
- package/dist/editable-artifact-durable-export.js +2 -2
- package/dist/editable-artifacts.js +3 -3
- package/dist/external-identities.d.ts +16 -0
- package/dist/external-identity-links.d.ts +74 -0
- package/dist/external-link-work.d.ts +51 -0
- package/dist/host-mcp-binding-match.d.ts +5 -0
- package/dist/host-mcp-bindings.d.ts +73 -0
- package/dist/host-mcp-task-authority.d.ts +47 -0
- package/dist/host-selection-identity.d.ts +6 -0
- package/dist/index.d.ts +51 -35
- package/dist/index.js +15171 -13775
- package/dist/index.js.map +1 -1
- package/dist/live-session-attempt.d.ts +275 -0
- package/dist/managed-auth-session-sets.js +3 -3
- package/dist/memory-domain.d.ts +3 -3
- package/dist/provision-roles.js +1 -1
- package/dist/retained-provider-commands.js +2 -2
- package/dist/runtime-posture.d.ts +6 -6
- package/dist/scheduled-task-revision-authority.d.ts +11 -0
- package/dist/schema.d.ts +1279 -9
- package/dist/schema.js +11 -1
- package/dist/session-background-commands.js +4 -4
- package/dist/session-command-output.js +4 -4
- package/dist/session-event-slices.js +2 -2
- package/dist/session-queue-commands.d.ts +7 -0
- package/dist/session-tenancy.d.ts +2 -0
- package/dist/session-tenancy.js +3 -3
- package/dist/video-generation.js +3 -3
- package/dist/workspace-authority.d.ts +4 -4
- package/dist/workspace-tool-defaults.js +4 -4
- package/drizzle/0437_organization_scoped_external_workspaces.sql +71 -0
- package/drizzle/0438_durable_connect_attempts.sql +95 -0
- package/drizzle/0439_external_identity_provisioning.sql +128 -0
- package/drizzle/0440_external_workspace_member_removal.sql +59 -0
- package/drizzle/0441_external_identity_membership_lifecycle.sql +163 -0
- package/drizzle/0442_external_owning_user_authority.sql +81 -0
- package/drizzle/0443_host_mcp_binding_registry.sql +93 -0
- package/drizzle/0444_host_mcp_delegations.sql +129 -0
- package/drizzle/0445_host_mcp_turn_authorities.sql +145 -0
- package/drizzle/0446_host_mcp_causal_continuation.sql +126 -0
- package/drizzle/0447_host_mcp_task_authorities.sql +264 -0
- package/drizzle/0448_host_mcp_child_authority.sql +115 -0
- package/drizzle/0449_external_identity_link_lifecycle.sql +107 -0
- package/drizzle/0450_external_identity_link_work.sql +179 -0
- package/drizzle/0451_external_link_preview_and_permission_ceiling.sql +82 -0
- package/drizzle/0452_external_link_scheduled_origin.sql +34 -0
- package/drizzle/0453_host_mcp_native_owner.sql +35 -0
- package/drizzle/0454_connect_origin_authority.sql +42 -0
- package/drizzle/0455_external_link_inventory_labels.sql +44 -0
- package/drizzle/0456_social_connection_versions.sql +14 -0
- package/drizzle/0457_canonical_session_scope_subject.sql +46 -0
- package/drizzle/0458_skill_review_wire_compatibility.sql +36 -0
- package/package.json +6 -6
- package/src/connect-attempts.ts +317 -0
- package/src/connection-authority.ts +9 -0
- package/src/connection-setup-authority.ts +35 -0
- package/src/connection-token-resolver.ts +177 -48
- package/src/external-identities.ts +70 -0
- package/src/external-identity-links.ts +364 -0
- package/src/external-link-work.ts +228 -0
- package/src/host-mcp-binding-match.ts +33 -0
- package/src/host-mcp-bindings.ts +741 -0
- package/src/host-mcp-task-authority.ts +383 -0
- package/src/host-selection-identity.ts +29 -0
- package/src/index.ts +398 -147
- package/src/live-session-attempt.ts +85 -0
- package/src/managed-human-provisioning.ts +6 -1
- package/src/memory-domain.ts +6 -11
- package/src/provision-roles.ts +3 -0
- package/src/runtime-posture.ts +47 -0
- package/src/scheduled-task-revision-authority.ts +30 -0
- package/src/schema.ts +228 -3
- package/src/session-control.ts +14 -0
- package/src/session-queue-commands.ts +11 -0
- package/src/session-tenancy.ts +7 -0
- package/src/workspace-authority.ts +5 -5
- package/dist/chunk-6GAPAYZR.js.map +0 -1
- package/dist/chunk-6IQ4X35Y.js.map +0 -1
- package/dist/chunk-7WX3R3YL.js.map +0 -1
- package/dist/chunk-S4Y42VSX.js.map +0 -1
- /package/dist/{chunk-SNLIKELL.js.map → chunk-IHADTE7C.js.map} +0 -0
- /package/dist/{chunk-MGVOYVPS.js.map → chunk-NCSPNCQK.js.map} +0 -0
- /package/dist/{chunk-B2KR3TEM.js.map → chunk-SGJDIF2D.js.map} +0 -0
- /package/dist/{chunk-FPTI4NP4.js.map → chunk-YGBOVXC7.js.map} +0 -0
- /package/dist/{chunk-6Y7ZDVJ5.js.map → chunk-YGRT6VZO.js.map} +0 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import {
|
|
3
|
+
stableJson,
|
|
4
|
+
ScheduledTaskRunAcceptedExecution,
|
|
5
|
+
type ScheduledTask,
|
|
6
|
+
} from "@opengeni/contracts";
|
|
7
|
+
import {
|
|
8
|
+
HostMcpAcceptedAuthority,
|
|
9
|
+
HostMcpTaskAuthority,
|
|
10
|
+
HostMcpBindingDefinition,
|
|
11
|
+
HostMcpOwnerSubject,
|
|
12
|
+
} from "@opengeni/contracts/host-mcp-bindings";
|
|
13
|
+
import { getScheduledTaskRevisionAuthority } from "./scheduled-task-revision-authority";
|
|
14
|
+
import { getHostMcpLiveAttempt } from "./live-session-attempt";
|
|
15
|
+
import { rawRows, withWorkspaceSubjectRls, type Database } from "./database";
|
|
16
|
+
import {
|
|
17
|
+
getHostMcpBinding,
|
|
18
|
+
getHostMcpDelegation,
|
|
19
|
+
HostMcpDelegationAuthorityError,
|
|
20
|
+
authorizeDirectHostMcpUse,
|
|
21
|
+
type HostMcpBindingOwner,
|
|
22
|
+
} from "./host-mcp-bindings";
|
|
23
|
+
|
|
24
|
+
export type SelectedHostMcpTaskGrant = {
|
|
25
|
+
delegationId: string;
|
|
26
|
+
generation: number;
|
|
27
|
+
bindingId: string;
|
|
28
|
+
bindingGeneration: number;
|
|
29
|
+
definition: HostMcpBindingDefinition;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Internal capture only. The API/core authenticates the external owner and
|
|
33
|
+
* rechecks that proof in this same task-write transaction before calling. */
|
|
34
|
+
export async function captureHostMcpTaskAuthorities(
|
|
35
|
+
db: Database,
|
|
36
|
+
owner: HostMcpBindingOwner,
|
|
37
|
+
task: ScheduledTask,
|
|
38
|
+
selections: SelectedHostMcpTaskGrant[],
|
|
39
|
+
): Promise<void> {
|
|
40
|
+
if (task.accountId !== owner.accountId || task.workspaceId !== owner.workspaceId)
|
|
41
|
+
throw new HostMcpDelegationAuthorityError("Host task scope mismatch");
|
|
42
|
+
const proof = await getScheduledTaskRevisionAuthority(db, {
|
|
43
|
+
...owner,
|
|
44
|
+
taskId: task.id,
|
|
45
|
+
taskAuthorityRevision: task.authorityRevision,
|
|
46
|
+
});
|
|
47
|
+
if (
|
|
48
|
+
!proof ||
|
|
49
|
+
proof.subjectId !== owner.subjectId ||
|
|
50
|
+
proof.membershipAuthorizationRevision !== owner.authorizationRevision
|
|
51
|
+
)
|
|
52
|
+
throw new HostMcpDelegationAuthorityError("Host task revision authority changed");
|
|
53
|
+
const target =
|
|
54
|
+
task.runMode === "existing_session" && task.targetSessionId
|
|
55
|
+
? ((
|
|
56
|
+
await withWorkspaceSubjectRls(db, owner.workspaceId, owner.subjectId, (tx) =>
|
|
57
|
+
rawRows<{
|
|
58
|
+
id: string;
|
|
59
|
+
visibility: "user_private" | "workspace_shared";
|
|
60
|
+
authority_epoch: number;
|
|
61
|
+
}>(
|
|
62
|
+
tx,
|
|
63
|
+
sql`select id, visibility, authority_epoch from sessions where id = ${task.targetSessionId}::uuid
|
|
64
|
+
and workspace_id = ${owner.workspaceId}::uuid and account_id = ${owner.accountId}::uuid`,
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
)[0] ?? null)
|
|
68
|
+
: null;
|
|
69
|
+
const context = target?.visibility ?? "workspace_shared";
|
|
70
|
+
await withWorkspaceSubjectRls(db, owner.workspaceId, owner.subjectId, async (tx) => {
|
|
71
|
+
for (const selection of selections) {
|
|
72
|
+
const delegation = await getHostMcpDelegation(tx, owner, selection.delegationId);
|
|
73
|
+
const binding = delegation ? await getHostMcpBinding(tx, owner, delegation.bindingId) : null;
|
|
74
|
+
const definition = HostMcpBindingDefinition.parse(selection.definition);
|
|
75
|
+
if (
|
|
76
|
+
!delegation ||
|
|
77
|
+
!binding ||
|
|
78
|
+
delegation.status !== "active" ||
|
|
79
|
+
binding.status !== "active" ||
|
|
80
|
+
delegation.generation !== selection.generation ||
|
|
81
|
+
binding.generation !== delegation.bindingGeneration ||
|
|
82
|
+
binding.id !== selection.bindingId ||
|
|
83
|
+
binding.generation !== selection.bindingGeneration ||
|
|
84
|
+
stableJson(binding.definition) !== stableJson(definition) ||
|
|
85
|
+
delegation.grant.context !== context ||
|
|
86
|
+
(delegation.grant.mode === "session" &&
|
|
87
|
+
(!target ||
|
|
88
|
+
delegation.grant.sessionId !== target.id ||
|
|
89
|
+
delegation.grant.expectedAuthorityEpoch !== target.authority_epoch))
|
|
90
|
+
)
|
|
91
|
+
throw new HostMcpDelegationAuthorityError("Host task delegation changed");
|
|
92
|
+
const authority = HostMcpTaskAuthority.parse({
|
|
93
|
+
version: 1,
|
|
94
|
+
accountId: owner.accountId,
|
|
95
|
+
workspaceId: owner.workspaceId,
|
|
96
|
+
taskId: task.id,
|
|
97
|
+
taskAuthorityRevision: task.authorityRevision,
|
|
98
|
+
taskExecutionDigest: task.executionDigest,
|
|
99
|
+
ownerSubjectId: owner.subjectId,
|
|
100
|
+
ownerOrganizationMembershipId: proof.organizationMembershipId,
|
|
101
|
+
ownerMembershipAuthorizationRevision: proof.membershipAuthorizationRevision,
|
|
102
|
+
bindingId: binding.id,
|
|
103
|
+
bindingGeneration: binding.generation,
|
|
104
|
+
delegationId: delegation.id,
|
|
105
|
+
delegationGeneration: delegation.generation,
|
|
106
|
+
definition,
|
|
107
|
+
context,
|
|
108
|
+
});
|
|
109
|
+
await tx.execute(sql`insert into host_mcp_task_authorities
|
|
110
|
+
(task_id, task_authority_revision, server_id, account_id, workspace_id, owner_subject_id, binding_id, delegation_id, canonical_snapshot)
|
|
111
|
+
values (${task.id}::uuid, ${task.authorityRevision}, ${definition.serverId}, ${owner.accountId}::uuid,
|
|
112
|
+
${owner.workspaceId}::uuid, ${owner.subjectId}, ${binding.id}::uuid, ${delegation.id}::uuid,
|
|
113
|
+
${JSON.stringify(authority)}::jsonb)`);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Backend-only read; native immutable task-revision proof chooses the owner.
|
|
119
|
+
* No caller-supplied subject and no ambient workspace account enumeration. */
|
|
120
|
+
export async function getHostMcpTaskAuthorities(
|
|
121
|
+
db: Database,
|
|
122
|
+
input: {
|
|
123
|
+
accountId: string;
|
|
124
|
+
workspaceId: string;
|
|
125
|
+
taskId: string;
|
|
126
|
+
taskAuthorityRevision: number;
|
|
127
|
+
},
|
|
128
|
+
): Promise<HostMcpTaskAuthority[]> {
|
|
129
|
+
const proof = await getScheduledTaskRevisionAuthority(db, input);
|
|
130
|
+
if (!proof || !HostMcpOwnerSubject.safeParse(proof.subjectId).success) return [];
|
|
131
|
+
return withWorkspaceSubjectRls(db, input.workspaceId, proof.subjectId, async (tx) => {
|
|
132
|
+
const rows = await rawRows<{ canonical_snapshot: unknown }>(
|
|
133
|
+
tx,
|
|
134
|
+
sql`
|
|
135
|
+
select canonical_snapshot from host_mcp_task_authorities where task_id = ${input.taskId}::uuid
|
|
136
|
+
and task_authority_revision = ${input.taskAuthorityRevision} and account_id = ${input.accountId}::uuid
|
|
137
|
+
and workspace_id = ${input.workspaceId}::uuid and owner_subject_id = ${proof.subjectId}
|
|
138
|
+
order by server_id`,
|
|
139
|
+
);
|
|
140
|
+
return rows.map((row) => HostMcpTaskAuthority.parse(row.canonical_snapshot));
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Native task revisions are the authority/version boundary, including ordinary
|
|
145
|
+
* lifecycle edits. Never silently drop or reassign retained host selections. */
|
|
146
|
+
export async function cloneHostMcpTaskAuthorities(
|
|
147
|
+
db: Database,
|
|
148
|
+
task: ScheduledTask,
|
|
149
|
+
sourceRevision: number,
|
|
150
|
+
): Promise<void> {
|
|
151
|
+
if (sourceRevision === task.authorityRevision) return;
|
|
152
|
+
const previous = await getHostMcpTaskAuthorities(db, {
|
|
153
|
+
accountId: task.accountId,
|
|
154
|
+
workspaceId: task.workspaceId,
|
|
155
|
+
taskId: task.id,
|
|
156
|
+
taskAuthorityRevision: sourceRevision,
|
|
157
|
+
});
|
|
158
|
+
if (!previous.length) return;
|
|
159
|
+
const existing = await getHostMcpTaskAuthorities(db, {
|
|
160
|
+
accountId: task.accountId,
|
|
161
|
+
workspaceId: task.workspaceId,
|
|
162
|
+
taskId: task.id,
|
|
163
|
+
taskAuthorityRevision: task.authorityRevision,
|
|
164
|
+
});
|
|
165
|
+
if (existing.length) {
|
|
166
|
+
const selection = (a: HostMcpTaskAuthority) => ({
|
|
167
|
+
definition: a.definition,
|
|
168
|
+
bindingId: a.bindingId,
|
|
169
|
+
bindingGeneration: a.bindingGeneration,
|
|
170
|
+
delegationId: a.delegationId,
|
|
171
|
+
delegationGeneration: a.delegationGeneration,
|
|
172
|
+
ownerSubjectId: a.ownerSubjectId,
|
|
173
|
+
ownerMembershipAuthorizationRevision: a.ownerMembershipAuthorizationRevision,
|
|
174
|
+
});
|
|
175
|
+
if (stableJson(existing.map(selection)) !== stableJson(previous.map(selection)))
|
|
176
|
+
throw new HostMcpDelegationAuthorityError("Host task materialization selection changed");
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const owner = previous[0]!;
|
|
180
|
+
await captureHostMcpTaskAuthorities(
|
|
181
|
+
db,
|
|
182
|
+
{
|
|
183
|
+
accountId: owner.accountId,
|
|
184
|
+
workspaceId: owner.workspaceId,
|
|
185
|
+
subjectId: owner.ownerSubjectId,
|
|
186
|
+
authorizationRevision: owner.ownerMembershipAuthorizationRevision,
|
|
187
|
+
},
|
|
188
|
+
task,
|
|
189
|
+
previous.map((a) => ({
|
|
190
|
+
delegationId: a.delegationId,
|
|
191
|
+
generation: a.delegationGeneration,
|
|
192
|
+
bindingId: a.bindingId,
|
|
193
|
+
bindingGeneration: a.bindingGeneration,
|
|
194
|
+
definition: a.definition,
|
|
195
|
+
})),
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Claim-time attachment to the exact accepted run. No host callback or key is
|
|
200
|
+
* used here; credentials are requested only when the accepted attempt uses MCP. */
|
|
201
|
+
export async function captureScheduledHostMcpTurnAuthorities(
|
|
202
|
+
db: Database,
|
|
203
|
+
input: {
|
|
204
|
+
accountId: string;
|
|
205
|
+
workspaceId: string;
|
|
206
|
+
sessionId: string;
|
|
207
|
+
turnId: string;
|
|
208
|
+
runId: string;
|
|
209
|
+
},
|
|
210
|
+
): Promise<void> {
|
|
211
|
+
const [run] = await rawRows<{
|
|
212
|
+
accepted_execution_snapshot: unknown;
|
|
213
|
+
task_id: string;
|
|
214
|
+
task_authority_revision: string | number;
|
|
215
|
+
task_execution_digest: string;
|
|
216
|
+
}>(
|
|
217
|
+
db,
|
|
218
|
+
sql`
|
|
219
|
+
select accepted_execution_snapshot, task_id, task_authority_revision, task_execution_digest from scheduled_task_runs where id = ${input.runId}::uuid
|
|
220
|
+
and account_id = ${input.accountId}::uuid and workspace_id = ${input.workspaceId}::uuid
|
|
221
|
+
and session_id = ${input.sessionId}::uuid and status = 'dispatched'`,
|
|
222
|
+
);
|
|
223
|
+
if (!run) throw new HostMcpDelegationAuthorityError("Host scheduled run unavailable");
|
|
224
|
+
const accepted = ScheduledTaskRunAcceptedExecution.parse(run.accepted_execution_snapshot);
|
|
225
|
+
const authorities = await getHostMcpTaskAuthorities(db, {
|
|
226
|
+
accountId: input.accountId,
|
|
227
|
+
workspaceId: input.workspaceId,
|
|
228
|
+
taskId: run.task_id,
|
|
229
|
+
taskAuthorityRevision: Number(run.task_authority_revision),
|
|
230
|
+
});
|
|
231
|
+
if (!authorities.length) return;
|
|
232
|
+
const subjectId = authorities[0]!.ownerSubjectId;
|
|
233
|
+
if (accepted.causalHumanSubjectId !== subjectId)
|
|
234
|
+
throw new HostMcpDelegationAuthorityError("Host scheduled causal human mismatch");
|
|
235
|
+
await withWorkspaceSubjectRls(db, input.workspaceId, subjectId, async (tx) => {
|
|
236
|
+
const [session] = await rawRows<{ visibility: string; authority_epoch: number }>(
|
|
237
|
+
tx,
|
|
238
|
+
sql`
|
|
239
|
+
select visibility, authority_epoch from sessions where id = ${input.sessionId}::uuid
|
|
240
|
+
and account_id = ${input.accountId}::uuid and workspace_id = ${input.workspaceId}::uuid`,
|
|
241
|
+
);
|
|
242
|
+
if (!session) throw new HostMcpDelegationAuthorityError("Host scheduled session unavailable");
|
|
243
|
+
for (const stored of authorities) {
|
|
244
|
+
const owner = {
|
|
245
|
+
accountId: input.accountId,
|
|
246
|
+
workspaceId: input.workspaceId,
|
|
247
|
+
subjectId,
|
|
248
|
+
authorizationRevision: stored.ownerMembershipAuthorizationRevision,
|
|
249
|
+
};
|
|
250
|
+
const grant = await getHostMcpDelegation(tx, owner, stored.delegationId);
|
|
251
|
+
const binding = await getHostMcpBinding(tx, owner, stored.bindingId);
|
|
252
|
+
if (
|
|
253
|
+
!grant ||
|
|
254
|
+
!binding ||
|
|
255
|
+
grant.status !== "active" ||
|
|
256
|
+
binding.status !== "active" ||
|
|
257
|
+
grant.generation !== stored.delegationGeneration ||
|
|
258
|
+
binding.generation !== stored.bindingGeneration
|
|
259
|
+
)
|
|
260
|
+
continue;
|
|
261
|
+
const { taskId, taskAuthorityRevision, taskExecutionDigest, context, ...base } = stored;
|
|
262
|
+
if (taskExecutionDigest !== run.task_execution_digest || context !== session.visibility)
|
|
263
|
+
throw new HostMcpDelegationAuthorityError("Host scheduled execution changed");
|
|
264
|
+
const scheduledOrigin = { taskId, taskAuthorityRevision, runId: input.runId };
|
|
265
|
+
const authority = HostMcpAcceptedAuthority.parse({
|
|
266
|
+
...base,
|
|
267
|
+
targetSessionId: input.sessionId,
|
|
268
|
+
targetSessionVisibility: session.visibility,
|
|
269
|
+
targetSessionAuthorityEpoch: session.authority_epoch,
|
|
270
|
+
acceptedWork: { kind: "scheduled_task", ...scheduledOrigin },
|
|
271
|
+
scheduledOrigin,
|
|
272
|
+
source: { kind: "scheduled_task" },
|
|
273
|
+
});
|
|
274
|
+
await tx.execute(sql`insert into host_mcp_turn_authorities
|
|
275
|
+
(turn_id, server_id, account_id, workspace_id, session_id, owner_subject_id, binding_id, delegation_id, canonical_snapshot)
|
|
276
|
+
values (${input.turnId}::uuid, ${stored.definition.serverId}, ${input.accountId}::uuid,
|
|
277
|
+
${input.workspaceId}::uuid, ${input.sessionId}::uuid, ${subjectId}, ${stored.bindingId}::uuid,
|
|
278
|
+
${stored.delegationId}::uuid, ${JSON.stringify(authority)}::jsonb)`);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Agent-created task: derive selections exclusively from its live accepted
|
|
284
|
+
* attempt, then re-admit only native successor-eligible grants. No user lookup
|
|
285
|
+
* or connection inventory is a fallback for a turn with no captured authority. */
|
|
286
|
+
export async function inheritHostMcpTaskAuthoritiesFromAttempt(
|
|
287
|
+
db: Database,
|
|
288
|
+
task: ScheduledTask,
|
|
289
|
+
source: { sessionId: string; turnId: string; attemptId: string; executionGeneration: number },
|
|
290
|
+
configured: Array<{
|
|
291
|
+
bindingId: string;
|
|
292
|
+
bindingGeneration: number;
|
|
293
|
+
definition: HostMcpBindingDefinition;
|
|
294
|
+
}>,
|
|
295
|
+
): Promise<void> {
|
|
296
|
+
const proof = await getScheduledTaskRevisionAuthority(db, {
|
|
297
|
+
accountId: task.accountId,
|
|
298
|
+
workspaceId: task.workspaceId,
|
|
299
|
+
taskId: task.id,
|
|
300
|
+
taskAuthorityRevision: task.authorityRevision,
|
|
301
|
+
});
|
|
302
|
+
if (!proof || !HostMcpOwnerSubject.safeParse(proof.subjectId).success) return;
|
|
303
|
+
const current = await getHostMcpLiveAttempt(
|
|
304
|
+
db,
|
|
305
|
+
task.workspaceId,
|
|
306
|
+
source.sessionId,
|
|
307
|
+
source.attemptId,
|
|
308
|
+
);
|
|
309
|
+
if (
|
|
310
|
+
!current ||
|
|
311
|
+
current.id !== source.turnId ||
|
|
312
|
+
current.executionGeneration !== source.executionGeneration
|
|
313
|
+
)
|
|
314
|
+
throw new HostMcpDelegationAuthorityError("Host task source attempt changed");
|
|
315
|
+
const owner = {
|
|
316
|
+
accountId: task.accountId,
|
|
317
|
+
workspaceId: task.workspaceId,
|
|
318
|
+
subjectId: proof.subjectId,
|
|
319
|
+
authorizationRevision: proof.membershipAuthorizationRevision,
|
|
320
|
+
};
|
|
321
|
+
const selections = await withWorkspaceSubjectRls(
|
|
322
|
+
db,
|
|
323
|
+
task.workspaceId,
|
|
324
|
+
proof.subjectId,
|
|
325
|
+
async (tx) => {
|
|
326
|
+
const rows = await rawRows<{ canonical_snapshot: unknown }>(
|
|
327
|
+
tx,
|
|
328
|
+
sql`
|
|
329
|
+
select canonical_snapshot from host_mcp_turn_authorities where account_id = ${task.accountId}::uuid
|
|
330
|
+
and workspace_id = ${task.workspaceId}::uuid and session_id = ${source.sessionId}::uuid
|
|
331
|
+
and turn_id = ${source.turnId}::uuid and owner_subject_id = ${proof.subjectId}`,
|
|
332
|
+
);
|
|
333
|
+
const selected: SelectedHostMcpTaskGrant[] = [];
|
|
334
|
+
for (const row of rows) {
|
|
335
|
+
const a = HostMcpAcceptedAuthority.parse(row.canonical_snapshot);
|
|
336
|
+
const server = configured.find((c) => c.definition.serverId === a.definition.serverId);
|
|
337
|
+
if (!server) continue;
|
|
338
|
+
if (
|
|
339
|
+
server.bindingId !== a.bindingId ||
|
|
340
|
+
server.bindingGeneration !== a.bindingGeneration ||
|
|
341
|
+
stableJson(server.definition) !== stableJson(a.definition)
|
|
342
|
+
)
|
|
343
|
+
throw new HostMcpDelegationAuthorityError("Host task source destination changed");
|
|
344
|
+
const delegation = await getHostMcpDelegation(tx, owner, a.delegationId);
|
|
345
|
+
if (
|
|
346
|
+
!delegation ||
|
|
347
|
+
(delegation.grant.mode === "session" &&
|
|
348
|
+
(task.runMode !== "existing_session" ||
|
|
349
|
+
task.targetSessionId !== delegation.grant.sessionId))
|
|
350
|
+
)
|
|
351
|
+
continue;
|
|
352
|
+
const authorized = await authorizeDirectHostMcpUse(tx, {
|
|
353
|
+
accountId: task.accountId,
|
|
354
|
+
workspaceId: task.workspaceId,
|
|
355
|
+
sessionId: source.sessionId,
|
|
356
|
+
rootSessionId: source.sessionId,
|
|
357
|
+
turnId: source.turnId,
|
|
358
|
+
attemptId: source.attemptId,
|
|
359
|
+
executionGeneration: source.executionGeneration,
|
|
360
|
+
initiator: current.initiator,
|
|
361
|
+
initiatorContext: current.initiatorContext,
|
|
362
|
+
surface: "model",
|
|
363
|
+
serverId: a.definition.serverId,
|
|
364
|
+
destinationUrl: a.definition.destinationUrl,
|
|
365
|
+
credentialTarget: "mcp",
|
|
366
|
+
forceRefresh: false,
|
|
367
|
+
connectionRef: {
|
|
368
|
+
...a.definition.connectionRef,
|
|
369
|
+
hostBinding: { bindingId: a.bindingId, generation: a.bindingGeneration },
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
if (!authorized) continue;
|
|
373
|
+
selected.push({
|
|
374
|
+
delegationId: a.delegationId,
|
|
375
|
+
generation: a.delegationGeneration,
|
|
376
|
+
...server,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
return selected;
|
|
380
|
+
},
|
|
381
|
+
);
|
|
382
|
+
if (selections.length) await captureHostMcpTaskAuthorities(db, owner, task, selections);
|
|
383
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HostMcpCreateSelections as selections,
|
|
3
|
+
type HostMcpCreateSelection,
|
|
4
|
+
} from "@opengeni/contracts/host-mcp-bindings";
|
|
5
|
+
export type { HostMcpCreateSelection } from "@opengeni/contracts/host-mcp-bindings";
|
|
6
|
+
const key = "_opengeni_session_create_host_delegations_v1";
|
|
7
|
+
|
|
8
|
+
/** Replay identity only: neither grant admission nor credential authority. */
|
|
9
|
+
export function normalizeHostCreateSelection(value: unknown): HostMcpCreateSelection[] {
|
|
10
|
+
return selections
|
|
11
|
+
.parse(value ?? [])
|
|
12
|
+
.sort((a, b) => (a.serverId < b.serverId ? -1 : a.serverId > b.serverId ? 1 : 0));
|
|
13
|
+
}
|
|
14
|
+
export function metadataWithHostCreateSelection(
|
|
15
|
+
metadata: Record<string, unknown>,
|
|
16
|
+
value: unknown,
|
|
17
|
+
): Record<string, unknown> {
|
|
18
|
+
const next = { ...metadata };
|
|
19
|
+
delete next[key];
|
|
20
|
+
const canonical = normalizeHostCreateSelection(value);
|
|
21
|
+
if (canonical.length) next[key] = canonical;
|
|
22
|
+
return next;
|
|
23
|
+
}
|
|
24
|
+
export function hostCreateSelectionFromMetadata(
|
|
25
|
+
metadata: Record<string, unknown>,
|
|
26
|
+
): HostMcpCreateSelection[] {
|
|
27
|
+
// A malformed stored identity fails closed; it must not become an empty grant.
|
|
28
|
+
return normalizeHostCreateSelection(metadata[key]);
|
|
29
|
+
}
|