@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.
Files changed (102) hide show
  1. package/dist/canonical-human-identities.js +3 -3
  2. package/dist/{chunk-6GAPAYZR.js → chunk-7JPLGBSA.js} +562 -86
  3. package/dist/chunk-7JPLGBSA.js.map +1 -0
  4. package/dist/{chunk-SNLIKELL.js → chunk-IHADTE7C.js} +2 -2
  5. package/dist/{chunk-7WX3R3YL.js → chunk-ILKJKDII.js} +7 -2
  6. package/dist/chunk-ILKJKDII.js.map +1 -0
  7. package/dist/{chunk-MGVOYVPS.js → chunk-NCSPNCQK.js} +3 -3
  8. package/dist/{chunk-B2KR3TEM.js → chunk-SGJDIF2D.js} +2 -2
  9. package/dist/{chunk-S4Y42VSX.js → chunk-Y5XFVJJM.js} +207 -3
  10. package/dist/chunk-Y5XFVJJM.js.map +1 -0
  11. package/dist/{chunk-FPTI4NP4.js → chunk-YGBOVXC7.js} +2 -2
  12. package/dist/{chunk-6Y7ZDVJ5.js → chunk-YGRT6VZO.js} +2 -2
  13. package/dist/{chunk-6IQ4X35Y.js → chunk-YVYBAWBU.js} +46 -1
  14. package/dist/chunk-YVYBAWBU.js.map +1 -0
  15. package/dist/connect-attempts.d.ts +60 -0
  16. package/dist/connection-setup-authority.d.ts +10 -0
  17. package/dist/connection-token-resolver.d.ts +11 -2
  18. package/dist/editable-artifact-durable-export.js +2 -2
  19. package/dist/editable-artifacts.js +3 -3
  20. package/dist/external-identities.d.ts +16 -0
  21. package/dist/external-identity-links.d.ts +74 -0
  22. package/dist/external-link-work.d.ts +51 -0
  23. package/dist/host-mcp-binding-match.d.ts +5 -0
  24. package/dist/host-mcp-bindings.d.ts +73 -0
  25. package/dist/host-mcp-task-authority.d.ts +47 -0
  26. package/dist/host-selection-identity.d.ts +6 -0
  27. package/dist/index.d.ts +51 -35
  28. package/dist/index.js +15171 -13775
  29. package/dist/index.js.map +1 -1
  30. package/dist/live-session-attempt.d.ts +275 -0
  31. package/dist/managed-auth-session-sets.js +3 -3
  32. package/dist/memory-domain.d.ts +3 -3
  33. package/dist/provision-roles.js +1 -1
  34. package/dist/retained-provider-commands.js +2 -2
  35. package/dist/runtime-posture.d.ts +6 -6
  36. package/dist/scheduled-task-revision-authority.d.ts +11 -0
  37. package/dist/schema.d.ts +1279 -9
  38. package/dist/schema.js +11 -1
  39. package/dist/session-background-commands.js +4 -4
  40. package/dist/session-command-output.js +4 -4
  41. package/dist/session-event-slices.js +2 -2
  42. package/dist/session-queue-commands.d.ts +7 -0
  43. package/dist/session-tenancy.d.ts +2 -0
  44. package/dist/session-tenancy.js +3 -3
  45. package/dist/video-generation.js +3 -3
  46. package/dist/workspace-authority.d.ts +4 -4
  47. package/dist/workspace-tool-defaults.js +4 -4
  48. package/drizzle/0437_organization_scoped_external_workspaces.sql +71 -0
  49. package/drizzle/0438_durable_connect_attempts.sql +95 -0
  50. package/drizzle/0439_external_identity_provisioning.sql +128 -0
  51. package/drizzle/0440_external_workspace_member_removal.sql +59 -0
  52. package/drizzle/0441_external_identity_membership_lifecycle.sql +163 -0
  53. package/drizzle/0442_external_owning_user_authority.sql +81 -0
  54. package/drizzle/0443_host_mcp_binding_registry.sql +93 -0
  55. package/drizzle/0444_host_mcp_delegations.sql +129 -0
  56. package/drizzle/0445_host_mcp_turn_authorities.sql +145 -0
  57. package/drizzle/0446_host_mcp_causal_continuation.sql +126 -0
  58. package/drizzle/0447_host_mcp_task_authorities.sql +264 -0
  59. package/drizzle/0448_host_mcp_child_authority.sql +115 -0
  60. package/drizzle/0449_external_identity_link_lifecycle.sql +107 -0
  61. package/drizzle/0450_external_identity_link_work.sql +179 -0
  62. package/drizzle/0451_external_link_preview_and_permission_ceiling.sql +82 -0
  63. package/drizzle/0452_external_link_scheduled_origin.sql +34 -0
  64. package/drizzle/0453_host_mcp_native_owner.sql +35 -0
  65. package/drizzle/0454_connect_origin_authority.sql +42 -0
  66. package/drizzle/0455_external_link_inventory_labels.sql +44 -0
  67. package/drizzle/0456_social_connection_versions.sql +14 -0
  68. package/drizzle/0457_canonical_session_scope_subject.sql +46 -0
  69. package/drizzle/0458_skill_review_wire_compatibility.sql +36 -0
  70. package/package.json +6 -6
  71. package/src/connect-attempts.ts +317 -0
  72. package/src/connection-authority.ts +9 -0
  73. package/src/connection-setup-authority.ts +35 -0
  74. package/src/connection-token-resolver.ts +177 -48
  75. package/src/external-identities.ts +70 -0
  76. package/src/external-identity-links.ts +364 -0
  77. package/src/external-link-work.ts +228 -0
  78. package/src/host-mcp-binding-match.ts +33 -0
  79. package/src/host-mcp-bindings.ts +741 -0
  80. package/src/host-mcp-task-authority.ts +383 -0
  81. package/src/host-selection-identity.ts +29 -0
  82. package/src/index.ts +398 -147
  83. package/src/live-session-attempt.ts +85 -0
  84. package/src/managed-human-provisioning.ts +6 -1
  85. package/src/memory-domain.ts +6 -11
  86. package/src/provision-roles.ts +3 -0
  87. package/src/runtime-posture.ts +47 -0
  88. package/src/scheduled-task-revision-authority.ts +30 -0
  89. package/src/schema.ts +228 -3
  90. package/src/session-control.ts +14 -0
  91. package/src/session-queue-commands.ts +11 -0
  92. package/src/session-tenancy.ts +7 -0
  93. package/src/workspace-authority.ts +5 -5
  94. package/dist/chunk-6GAPAYZR.js.map +0 -1
  95. package/dist/chunk-6IQ4X35Y.js.map +0 -1
  96. package/dist/chunk-7WX3R3YL.js.map +0 -1
  97. package/dist/chunk-S4Y42VSX.js.map +0 -1
  98. /package/dist/{chunk-SNLIKELL.js.map → chunk-IHADTE7C.js.map} +0 -0
  99. /package/dist/{chunk-MGVOYVPS.js.map → chunk-NCSPNCQK.js.map} +0 -0
  100. /package/dist/{chunk-B2KR3TEM.js.map → chunk-SGJDIF2D.js.map} +0 -0
  101. /package/dist/{chunk-FPTI4NP4.js.map → chunk-YGBOVXC7.js.map} +0 -0
  102. /package/dist/{chunk-6Y7ZDVJ5.js.map → chunk-YGRT6VZO.js.map} +0 -0
@@ -0,0 +1,741 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { sql } from "drizzle-orm";
3
+ import { stableJson, type McpCredentialsRequest } from "@opengeni/contracts";
4
+ import { getHostMcpLiveAttempt } from "./live-session-attempt";
5
+ import { hostMcpBindingMatchesRequest } from "./host-mcp-binding-match";
6
+ import {
7
+ HostMcpBinding,
8
+ CreateHostMcpBindingRequest,
9
+ HostMcpDelegation,
10
+ IssueHostMcpDelegationRequest,
11
+ HostMcpAcceptedAuthority,
12
+ HostMcpOwnerSubject,
13
+ } from "@opengeni/contracts/host-mcp-bindings";
14
+ import { rawRows, withWorkspaceSubjectRls, type Database } from "./database";
15
+ import { listSelfOrganizationMemberships } from "./organization-membership-lifecycle";
16
+ import { lockExternalWorkspaceMembershipLifecycle } from "./external-identities";
17
+ import { subjectHasLiveWorkspaceAuthorityInScope } from "./workspace-authority";
18
+
19
+ export type HostMcpBindingOwner = {
20
+ accountId: string;
21
+ workspaceId: string;
22
+ subjectId: string;
23
+ authorizationRevision: number;
24
+ };
25
+
26
+ /** Resolve the effective member's revision, not the authenticating external
27
+ * identity revision. Caller must separately prove its authority to act as them. */
28
+ export async function resolveHostMcpBindingOwner(
29
+ db: Database,
30
+ input: Omit<HostMcpBindingOwner, "authorizationRevision">,
31
+ ): Promise<HostMcpBindingOwner> {
32
+ if (!HostMcpOwnerSubject.safeParse(input.subjectId).success)
33
+ throw new HostMcpDelegationAuthorityError("Host owner unavailable");
34
+ return withWorkspaceSubjectRls(db, input.workspaceId, input.subjectId, async (tx) => {
35
+ await lockExternalWorkspaceMembershipLifecycle(tx, input.accountId);
36
+ const membership = (await listSelfOrganizationMemberships(tx, input.subjectId)).find(
37
+ (value) => value.organizationId === input.accountId,
38
+ );
39
+ if (
40
+ !membership ||
41
+ membership.status !== "active" ||
42
+ !(await subjectHasLiveWorkspaceAuthorityInScope(tx, input))
43
+ )
44
+ throw new HostMcpDelegationAuthorityError("Host owner unavailable");
45
+ return { ...input, authorizationRevision: membership.authorizationRevision };
46
+ });
47
+ }
48
+ export class HostMcpBindingConflictError extends Error {
49
+ constructor() {
50
+ super("Host binding changed or operation conflicts");
51
+ this.name = "HostMcpBindingConflictError";
52
+ }
53
+ }
54
+ export class HostMcpDelegationAuthorityError extends Error {}
55
+ type Row = {
56
+ id: string;
57
+ account_id: string;
58
+ workspace_id: string;
59
+ owner_subject_id: string;
60
+ authorization_revision: number | string;
61
+ generation: number | string;
62
+ status: string;
63
+ definition: unknown;
64
+ created_at: Date | string;
65
+ revoked_at: Date | string | null;
66
+ request_digest: string;
67
+ };
68
+ const timestamp = (value: Date | string) => new Date(value).toISOString();
69
+ function project(row: Row): HostMcpBinding {
70
+ return HostMcpBinding.parse({
71
+ id: row.id,
72
+ accountId: row.account_id,
73
+ workspaceId: row.workspace_id,
74
+ ownerSubjectId: row.owner_subject_id,
75
+ authorizationRevision: Number(row.authorization_revision),
76
+ generation: Number(row.generation),
77
+ status: row.status,
78
+ definition: row.definition,
79
+ createdAt: timestamp(row.created_at),
80
+ revokedAt: row.revoked_at === null ? null : timestamp(row.revoked_at),
81
+ });
82
+ }
83
+
84
+ async function scoped<T>(
85
+ db: Database,
86
+ owner: HostMcpBindingOwner,
87
+ operation: (tx: Database) => Promise<T>,
88
+ ): Promise<T> {
89
+ return withWorkspaceSubjectRls(db, owner.workspaceId, owner.subjectId, async (tx) => {
90
+ await lockExternalWorkspaceMembershipLifecycle(tx, owner.accountId);
91
+ const memberships = await listSelfOrganizationMemberships(tx, owner.subjectId);
92
+ const membership = memberships.find((item) => item.organizationId === owner.accountId);
93
+ if (
94
+ !HostMcpOwnerSubject.safeParse(owner.subjectId).success ||
95
+ !membership ||
96
+ membership.status !== "active" ||
97
+ membership.authorizationRevision !== owner.authorizationRevision
98
+ )
99
+ throw new Error("Host binding owner authority unavailable");
100
+ return operation(tx);
101
+ });
102
+ }
103
+
104
+ /** Low-level persistence, not HTTP authentication. Callers must establish the
105
+ * exact external owner and workspace grant before entering this scoped seam. */
106
+ export async function createHostMcpBinding(
107
+ db: Database,
108
+ owner: HostMcpBindingOwner,
109
+ raw: unknown,
110
+ ): Promise<HostMcpBinding> {
111
+ const input = CreateHostMcpBindingRequest.parse(raw);
112
+ const digest = createHash("sha256").update(stableJson(input.definition)).digest("hex");
113
+ return scoped(db, owner, async (tx) => {
114
+ await tx.execute(sql`insert into host_mcp_bindings (id, account_id, workspace_id, owner_subject_id, authorization_revision, operation_id, request_digest, definition)
115
+ values (${randomUUID()}::uuid, ${owner.accountId}::uuid, ${owner.workspaceId}::uuid, ${owner.subjectId}, ${owner.authorizationRevision}, ${input.operationId}::uuid, ${digest}, ${JSON.stringify(input.definition)}::jsonb)
116
+ on conflict (workspace_id, owner_subject_id, operation_id) do nothing`);
117
+ const [row] = await rawRows<Row>(
118
+ tx,
119
+ sql`select * from host_mcp_bindings where workspace_id = ${owner.workspaceId}::uuid and owner_subject_id = ${owner.subjectId} and operation_id = ${input.operationId}::uuid`,
120
+ );
121
+ if (!row || row.request_digest !== digest) throw new HostMcpBindingConflictError();
122
+ return project(row);
123
+ });
124
+ }
125
+
126
+ export async function getHostMcpBinding(
127
+ db: Database,
128
+ owner: HostMcpBindingOwner,
129
+ id: string,
130
+ ): Promise<HostMcpBinding | null> {
131
+ return scoped(db, owner, async (tx) => {
132
+ const [row] = await rawRows<Row>(
133
+ tx,
134
+ sql`select * from host_mcp_bindings where id = ${id}::uuid and account_id = ${owner.accountId}::uuid and workspace_id = ${owner.workspaceId}::uuid and owner_subject_id = ${owner.subjectId}`,
135
+ );
136
+ return row ? project(row) : null;
137
+ });
138
+ }
139
+
140
+ export async function revokeHostMcpBinding(
141
+ db: Database,
142
+ owner: HostMcpBindingOwner,
143
+ id: string,
144
+ expectedGeneration: number,
145
+ ): Promise<HostMcpBinding> {
146
+ if (!Number.isSafeInteger(expectedGeneration) || expectedGeneration < 1)
147
+ throw new Error("Invalid binding generation");
148
+ return scoped(db, owner, async (tx) => {
149
+ const [row] = await rawRows<Row>(
150
+ tx,
151
+ sql`select * from host_mcp_bindings where id = ${id}::uuid and account_id = ${owner.accountId}::uuid and workspace_id = ${owner.workspaceId}::uuid and owner_subject_id = ${owner.subjectId} for update`,
152
+ );
153
+ if (!row) throw new Error("Host binding unavailable");
154
+ const current = project(row);
155
+ if (current.status === "revoked" && current.generation === expectedGeneration + 1)
156
+ return current;
157
+ if (current.status !== "active" || current.generation !== expectedGeneration)
158
+ throw new HostMcpBindingConflictError();
159
+ const [updated] = await rawRows<Row>(
160
+ tx,
161
+ sql`update host_mcp_bindings set status = 'revoked', generation = generation + 1, revoked_at = clock_timestamp() where id = ${id}::uuid returning *`,
162
+ );
163
+ if (!updated) throw new Error("Host binding unavailable");
164
+ return project(updated);
165
+ });
166
+ }
167
+
168
+ type DelegationRow = {
169
+ id: string;
170
+ account_id: string;
171
+ workspace_id: string;
172
+ owner_subject_id: string;
173
+ owner_authorization_revision: number | string;
174
+ binding_id: string;
175
+ binding_generation: number | string;
176
+ grant_definition: unknown;
177
+ generation: number | string;
178
+ status: string;
179
+ created_at: Date | string;
180
+ revoked_at: Date | string | null;
181
+ request_digest: string;
182
+ };
183
+ function projectDelegation(row: DelegationRow): HostMcpDelegation {
184
+ return HostMcpDelegation.parse({
185
+ id: row.id,
186
+ accountId: row.account_id,
187
+ workspaceId: row.workspace_id,
188
+ ownerSubjectId: row.owner_subject_id,
189
+ ownerAuthorizationRevision: Number(row.owner_authorization_revision),
190
+ bindingId: row.binding_id,
191
+ bindingGeneration: Number(row.binding_generation),
192
+ grant: row.grant_definition,
193
+ generation: Number(row.generation),
194
+ status: row.status,
195
+ createdAt: timestamp(row.created_at),
196
+ revokedAt: row.revoked_at === null ? null : timestamp(row.revoked_at),
197
+ });
198
+ }
199
+
200
+ /** Internal persistence only. Caller must first verify owner identity and grant
201
+ * issuance permission. No worker consumes these rows without accepted-work capture. */
202
+ export async function issueHostMcpDelegation(
203
+ db: Database,
204
+ owner: HostMcpBindingOwner,
205
+ raw: unknown,
206
+ ): Promise<HostMcpDelegation> {
207
+ const input = IssueHostMcpDelegationRequest.parse(raw);
208
+ const digest = createHash("sha256").update(stableJson(input)).digest("hex");
209
+ return scoped(db, owner, async (tx) => {
210
+ if (!(await subjectHasLiveWorkspaceAuthorityInScope(tx, owner)))
211
+ throw new HostMcpDelegationAuthorityError("Host delegation workspace unavailable");
212
+ // Serialize operation replays without re-running INSERT guards on a revoked binding.
213
+ await tx.execute(
214
+ sql`select pg_advisory_xact_lock(hashtextextended(${`host-delegation:${owner.workspaceId}:${owner.subjectId}:${input.operationId}`}, 0))`,
215
+ );
216
+ const [prior] = await rawRows<DelegationRow>(
217
+ tx,
218
+ sql`select * from host_mcp_delegations where workspace_id = ${owner.workspaceId}::uuid and owner_subject_id = ${owner.subjectId} and operation_id = ${input.operationId}::uuid`,
219
+ );
220
+ if (prior) {
221
+ if (prior.request_digest !== digest) throw new HostMcpBindingConflictError();
222
+ return projectDelegation(prior);
223
+ }
224
+ const [binding] = await rawRows<Row>(
225
+ tx,
226
+ sql`select * from host_mcp_bindings where id = ${input.bindingId}::uuid and account_id = ${owner.accountId}::uuid and workspace_id = ${owner.workspaceId}::uuid and owner_subject_id = ${owner.subjectId} for share`,
227
+ );
228
+ if (
229
+ !binding ||
230
+ binding.status !== "active" ||
231
+ Number(binding.generation) !== input.expectedBindingGeneration ||
232
+ Number(binding.authorization_revision) !== owner.authorizationRevision
233
+ )
234
+ throw new HostMcpBindingConflictError();
235
+ if (input.grant.sessionId) {
236
+ const [session] = await rawRows<{
237
+ visibility: string;
238
+ authority_epoch: number;
239
+ owner_subject_id: string | null;
240
+ }>(
241
+ tx,
242
+ sql`select visibility, authority_epoch, owner_subject_id from sessions where id = ${input.grant.sessionId}::uuid and workspace_id = ${owner.workspaceId}::uuid and account_id = ${owner.accountId}::uuid for share`,
243
+ );
244
+ if (
245
+ !session ||
246
+ session.visibility !== input.grant.context ||
247
+ session.authority_epoch !== input.grant.expectedAuthorityEpoch ||
248
+ (session.visibility === "user_private" && session.owner_subject_id !== owner.subjectId)
249
+ )
250
+ throw new HostMcpDelegationAuthorityError("Host delegation session authority unavailable");
251
+ } else if (input.grant.expectedAuthorityEpoch != null)
252
+ throw new HostMcpDelegationAuthorityError("Host delegation epoch requires a session");
253
+ const [row] = await rawRows<DelegationRow>(
254
+ tx,
255
+ sql`insert into host_mcp_delegations (id, account_id, workspace_id, owner_subject_id, owner_authorization_revision, binding_id, binding_generation, operation_id, request_digest, grant_definition)
256
+ values (${randomUUID()}::uuid, ${owner.accountId}::uuid, ${owner.workspaceId}::uuid, ${owner.subjectId}, ${owner.authorizationRevision}, ${input.bindingId}::uuid, ${input.expectedBindingGeneration}, ${input.operationId}::uuid, ${digest}, ${JSON.stringify(input.grant)}::jsonb) returning *`,
257
+ );
258
+ if (!row) throw new Error("Host delegation unavailable");
259
+ return projectDelegation(row);
260
+ });
261
+ }
262
+
263
+ export async function getHostMcpDelegation(
264
+ db: Database,
265
+ owner: HostMcpBindingOwner,
266
+ id: string,
267
+ ): Promise<HostMcpDelegation | null> {
268
+ return scoped(db, owner, async (tx) => {
269
+ const [row] = await rawRows<DelegationRow>(
270
+ tx,
271
+ sql`select * from host_mcp_delegations where id = ${id}::uuid and account_id = ${owner.accountId}::uuid and workspace_id = ${owner.workspaceId}::uuid and owner_subject_id = ${owner.subjectId}`,
272
+ );
273
+ return row ? projectDelegation(row) : null;
274
+ });
275
+ }
276
+
277
+ export async function revokeHostMcpDelegation(
278
+ db: Database,
279
+ owner: HostMcpBindingOwner,
280
+ id: string,
281
+ expectedGeneration: number,
282
+ ): Promise<HostMcpDelegation> {
283
+ if (!Number.isSafeInteger(expectedGeneration) || expectedGeneration < 1)
284
+ throw new Error("Invalid delegation generation");
285
+ return scoped(db, owner, async (tx) => {
286
+ const [row] = await rawRows<DelegationRow>(
287
+ tx,
288
+ sql`select * from host_mcp_delegations where id = ${id}::uuid and account_id = ${owner.accountId}::uuid and workspace_id = ${owner.workspaceId}::uuid and owner_subject_id = ${owner.subjectId} for update`,
289
+ );
290
+ if (!row) throw new Error("Host delegation unavailable");
291
+ const current = projectDelegation(row);
292
+ if (current.status === "revoked" && current.generation === expectedGeneration + 1)
293
+ return current;
294
+ if (current.status !== "active" || current.generation !== expectedGeneration)
295
+ throw new HostMcpBindingConflictError();
296
+ const [updated] = await rawRows<DelegationRow>(
297
+ tx,
298
+ sql`update host_mcp_delegations set status = 'revoked', generation = generation + 1, revoked_at = clock_timestamp() where id = ${id}::uuid returning *`,
299
+ );
300
+ if (!updated) throw new Error("Host delegation unavailable");
301
+ return projectDelegation(updated);
302
+ });
303
+ }
304
+
305
+ /** Internal direct-admission boundary, NOT authentication and NOT persistence.
306
+ * The caller must establish a verified external owner and explicit selection,
307
+ * and capture the returned authority with accepted work INSIDE `capture`.
308
+ * Locks remain held through that callback. Historical initiator metadata only
309
+ * narrows the verified caller's selection; it never establishes who called.
310
+ * Schedules, continuations and inherited turns need their own frozen causal
311
+ * authority path and deliberately cannot use this direct-only boundary.
312
+ */
313
+ export async function withDirectHostMcpAdmission<T>(
314
+ db: Database,
315
+ owner: HostMcpBindingOwner,
316
+ input: {
317
+ sessionId: string;
318
+ turnId: string;
319
+ delegationId: string;
320
+ expectedDelegationGeneration: number;
321
+ },
322
+ capture: (tx: Database, authority: HostMcpAcceptedAuthority) => Promise<T>,
323
+ ): Promise<T> {
324
+ if (
325
+ !Number.isSafeInteger(input.expectedDelegationGeneration) ||
326
+ input.expectedDelegationGeneration < 1
327
+ )
328
+ throw new HostMcpDelegationAuthorityError("Invalid host admission generation");
329
+ return scoped(db, owner, async (tx) => {
330
+ const deny = (): never => {
331
+ throw new HostMcpDelegationAuthorityError("Host direct admission unavailable");
332
+ };
333
+ if (!(await subjectHasLiveWorkspaceAuthorityInScope(tx, owner))) deny();
334
+ const membership = (await listSelfOrganizationMemberships(tx, owner.subjectId)).find(
335
+ (value) => value.organizationId === owner.accountId,
336
+ );
337
+ if (
338
+ !membership ||
339
+ membership.status !== "active" ||
340
+ membership.authorizationRevision !== owner.authorizationRevision
341
+ )
342
+ return deny();
343
+ const [session] = await rawRows<{
344
+ visibility: "user_private" | "workspace_shared";
345
+ authority_epoch: number;
346
+ owner_subject_id: string | null;
347
+ }>(
348
+ tx,
349
+ sql`select visibility, authority_epoch, owner_subject_id from sessions where id = ${input.sessionId}::uuid and account_id = ${owner.accountId}::uuid and workspace_id = ${owner.workspaceId}::uuid for update`,
350
+ );
351
+ if (
352
+ !session ||
353
+ (session.visibility === "user_private" && session.owner_subject_id !== owner.subjectId)
354
+ )
355
+ return deny();
356
+ const [turn] = await rawRows<{ id: string }>(
357
+ tx,
358
+ sql`
359
+ select id from session_turns where id = ${input.turnId}::uuid
360
+ and session_id = ${input.sessionId}::uuid and account_id = ${owner.accountId}::uuid and workspace_id = ${owner.workspaceId}::uuid
361
+ and status = 'queued' and active_attempt_id is null and source in ('user', 'api')
362
+ and initiator_kind = 'subject' and initiator_subject_id = ${owner.subjectId}
363
+ and (initiating_human_subject_id is null or initiating_human_subject_id = ${owner.subjectId})
364
+ and not (initiator_context ?| array['via','viaTruncated','provenanceError','backfill'])
365
+ and not exists (select 1 from session_turn_attempts a where a.turn_id = session_turns.id and a.workspace_id = session_turns.workspace_id)
366
+ for update`,
367
+ );
368
+ if (!turn) return deny();
369
+ const [row] = await rawRows<DelegationRow>(
370
+ tx,
371
+ sql`select * from host_mcp_delegations where id = ${input.delegationId}::uuid and account_id = ${owner.accountId}::uuid and workspace_id = ${owner.workspaceId}::uuid and owner_subject_id = ${owner.subjectId} for share`,
372
+ );
373
+ if (!row) return deny();
374
+ const delegation = projectDelegation(row);
375
+ if (
376
+ delegation.status !== "active" ||
377
+ delegation.revokedAt !== null ||
378
+ delegation.generation !== input.expectedDelegationGeneration ||
379
+ delegation.ownerAuthorizationRevision !== owner.authorizationRevision ||
380
+ delegation.grant.context !== session.visibility
381
+ )
382
+ return deny();
383
+ if (
384
+ delegation.grant.mode === "session" &&
385
+ (delegation.grant.sessionId !== input.sessionId ||
386
+ delegation.grant.expectedAuthorityEpoch !== session.authority_epoch)
387
+ )
388
+ return deny();
389
+ const [bindingRow] = await rawRows<Row>(
390
+ tx,
391
+ sql`select * from host_mcp_bindings where id = ${delegation.bindingId}::uuid and account_id = ${owner.accountId}::uuid and workspace_id = ${owner.workspaceId}::uuid and owner_subject_id = ${owner.subjectId} for share`,
392
+ );
393
+ if (!bindingRow) return deny();
394
+ const binding = project(bindingRow);
395
+ if (
396
+ binding.status !== "active" ||
397
+ binding.revokedAt !== null ||
398
+ binding.generation !== delegation.bindingGeneration ||
399
+ binding.authorizationRevision !== owner.authorizationRevision
400
+ )
401
+ return deny();
402
+ const authority = HostMcpAcceptedAuthority.parse({
403
+ version: 1,
404
+ accountId: owner.accountId,
405
+ workspaceId: owner.workspaceId,
406
+ targetSessionId: input.sessionId,
407
+ targetSessionVisibility: session.visibility,
408
+ targetSessionAuthorityEpoch: session.authority_epoch,
409
+ acceptedWork: { kind: "turn", turnId: turn.id },
410
+ bindingId: binding.id,
411
+ bindingGeneration: binding.generation,
412
+ definition: binding.definition,
413
+ ownerSubjectId: owner.subjectId,
414
+ ownerOrganizationMembershipId: membership.id,
415
+ ownerMembershipAuthorizationRevision: membership.authorizationRevision,
416
+ delegationId: delegation.id,
417
+ delegationGeneration: delegation.generation,
418
+ source: { kind: "direct" },
419
+ });
420
+ return capture(tx, authority);
421
+ });
422
+ }
423
+
424
+ /** Internal direct-turn persistence. The authenticated acceptance transaction
425
+ * must call this before claim; this does not authenticate the supplied owner.
426
+ * Exact replay is allowed, but a turn/server selection cannot be replaced. */
427
+ export async function captureDirectHostMcpAuthority(
428
+ db: Database,
429
+ owner: HostMcpBindingOwner,
430
+ input: Parameters<typeof withDirectHostMcpAdmission>[2],
431
+ ): Promise<HostMcpAcceptedAuthority> {
432
+ return withDirectHostMcpAdmission(db, owner, input, async (tx, authority) => {
433
+ await tx.execute(sql`insert into host_mcp_turn_authorities
434
+ (turn_id, server_id, account_id, workspace_id, session_id, owner_subject_id, binding_id, delegation_id, canonical_snapshot)
435
+ values (${input.turnId}::uuid, ${authority.definition.serverId}, ${owner.accountId}::uuid,
436
+ ${owner.workspaceId}::uuid, ${input.sessionId}::uuid, ${owner.subjectId},
437
+ ${authority.bindingId}::uuid, ${authority.delegationId}::uuid, ${JSON.stringify(authority)}::jsonb)
438
+ on conflict (turn_id, server_id) do nothing`);
439
+ const [stored] = await rawRows<{ canonical_snapshot: unknown }>(
440
+ tx,
441
+ sql`
442
+ select canonical_snapshot from host_mcp_turn_authorities where turn_id = ${input.turnId}::uuid
443
+ and server_id = ${authority.definition.serverId} and account_id = ${owner.accountId}::uuid
444
+ and workspace_id = ${owner.workspaceId}::uuid and owner_subject_id = ${owner.subjectId}`,
445
+ );
446
+ if (!stored || stableJson(stored.canonical_snapshot) !== stableJson(authority))
447
+ throw new HostMcpBindingConflictError();
448
+ return HostMcpAcceptedAuthority.parse(stored.canonical_snapshot);
449
+ });
450
+ }
451
+
452
+ /** Copy only the authority of an exact causal turn after its canonical machine
453
+ * update has been delivered. The insert trigger independently proves delivery,
454
+ * live generations and byte-equivalent authority; no creator fallback exists.
455
+ * Revoked selections are omitted, allowing a continuation to explain loss of
456
+ * access without making its whole session permanently unclaimable. */
457
+ export async function inheritCausalHostMcpTurnAuthorities(
458
+ db: Database,
459
+ input: {
460
+ accountId: string;
461
+ workspaceId: string;
462
+ sessionId: string;
463
+ subjectId: string;
464
+ sourceTurnId: string;
465
+ targetTurnId: string;
466
+ },
467
+ ): Promise<void> {
468
+ if (
469
+ !HostMcpOwnerSubject.safeParse(input.subjectId).success ||
470
+ input.sourceTurnId === input.targetTurnId
471
+ )
472
+ return;
473
+ await withWorkspaceSubjectRls(db, input.workspaceId, input.subjectId, async (tx) => {
474
+ await lockExternalWorkspaceMembershipLifecycle(tx, input.accountId);
475
+ const membership = (await listSelfOrganizationMemberships(tx, input.subjectId)).find(
476
+ (m) => m.organizationId === input.accountId && m.status === "active",
477
+ );
478
+ if (!membership || !(await subjectHasLiveWorkspaceAuthorityInScope(tx, input))) return;
479
+ const rows = await rawRows<{ canonical_snapshot: unknown }>(
480
+ tx,
481
+ sql`
482
+ select a.canonical_snapshot from host_mcp_turn_authorities a
483
+ join host_mcp_delegations d on d.id = a.delegation_id
484
+ join host_mcp_bindings b on b.id = a.binding_id
485
+ join sessions s on s.id = a.session_id
486
+ where a.account_id = ${input.accountId}::uuid and a.workspace_id = ${input.workspaceId}::uuid
487
+ and a.session_id = ${input.sessionId}::uuid and a.turn_id = ${input.sourceTurnId}::uuid
488
+ and a.owner_subject_id = ${input.subjectId}
489
+ and d.status = 'active' and b.status = 'active'
490
+ and d.revoked_at is null and b.revoked_at is null
491
+ and d.generation::text = a.canonical_snapshot ->> 'delegationGeneration'
492
+ and b.generation::text = a.canonical_snapshot ->> 'bindingGeneration'
493
+ and s.authority_epoch::text = a.canonical_snapshot ->> 'targetSessionAuthorityEpoch'
494
+ and s.visibility = a.canonical_snapshot ->> 'targetSessionVisibility'
495
+ and d.owner_authorization_revision = ${membership.authorizationRevision}
496
+ and a.canonical_snapshot ->> 'ownerOrganizationMembershipId' = ${membership.id}
497
+ for share of d, b`,
498
+ );
499
+ for (const row of rows) {
500
+ const prior = HostMcpAcceptedAuthority.parse(row.canonical_snapshot);
501
+ const authority = HostMcpAcceptedAuthority.parse({
502
+ ...prior,
503
+ acceptedWork: { kind: "turn", turnId: input.targetTurnId },
504
+ source: { kind: "inherited_turn", sessionId: input.sessionId, turnId: input.sourceTurnId },
505
+ });
506
+ await tx.execute(sql`insert into host_mcp_turn_authorities
507
+ (turn_id, server_id, account_id, workspace_id, session_id, owner_subject_id, binding_id, delegation_id, canonical_snapshot)
508
+ values (${input.targetTurnId}::uuid, ${authority.definition.serverId}, ${input.accountId}::uuid,
509
+ ${input.workspaceId}::uuid, ${input.sessionId}::uuid, ${input.subjectId},
510
+ ${authority.bindingId}::uuid, ${authority.delegationId}::uuid, ${JSON.stringify(authority)}::jsonb)`);
511
+ }
512
+ });
513
+ }
514
+
515
+ /** Initial child turn only. The stored parent-turn pointer was admitted by the
516
+ * native signed-attempt creation boundary; caller metadata never chooses it. */
517
+ export async function inheritChildHostMcpTurnAuthorities(
518
+ db: Database,
519
+ input: {
520
+ accountId: string;
521
+ workspaceId: string;
522
+ sessionId: string;
523
+ turnId: string;
524
+ subjectId: string;
525
+ },
526
+ ): Promise<void> {
527
+ if (!HostMcpOwnerSubject.safeParse(input.subjectId).success) return;
528
+ await withWorkspaceSubjectRls(db, input.workspaceId, input.subjectId, async (tx) => {
529
+ await lockExternalWorkspaceMembershipLifecycle(tx, input.accountId);
530
+ const membership = (await listSelfOrganizationMemberships(tx, input.subjectId)).find(
531
+ (m) => m.organizationId === input.accountId && m.status === "active",
532
+ );
533
+ if (!membership || !(await subjectHasLiveWorkspaceAuthorityInScope(tx, input))) return;
534
+ const rows = await rawRows<{
535
+ canonical_snapshot: unknown;
536
+ authority_epoch: number;
537
+ visibility: string;
538
+ parent_session_id: string;
539
+ parent_turn_id: string;
540
+ }>(
541
+ tx,
542
+ sql`
543
+ select a.canonical_snapshot, c.authority_epoch, c.visibility, c.parent_session_id, c.parent_turn_id
544
+ from sessions c join sessions p on p.id = c.parent_session_id and p.workspace_id = c.workspace_id
545
+ join host_mcp_turn_authorities a on a.turn_id = c.parent_turn_id and a.session_id = p.id
546
+ join host_mcp_delegations d on d.id = a.delegation_id
547
+ join host_mcp_bindings b on b.id = a.binding_id
548
+ where c.id = ${input.sessionId}::uuid and c.workspace_id = ${input.workspaceId}::uuid
549
+ and c.account_id = ${input.accountId}::uuid and a.account_id = c.account_id and a.workspace_id = c.workspace_id
550
+ and a.owner_subject_id = ${input.subjectId} and c.visibility = p.visibility
551
+ and a.canonical_snapshot ->> 'targetSessionAuthorityEpoch' = p.authority_epoch::text
552
+ and a.canonical_snapshot ->> 'ownerOrganizationMembershipId' = ${membership.id}
553
+ and a.canonical_snapshot ->> 'ownerMembershipAuthorizationRevision' = ${String(membership.authorizationRevision)}
554
+ and d.status = 'active' and b.status = 'active' and d.grant_definition ->> 'mode' = 'always'
555
+ and d.generation::text = a.canonical_snapshot ->> 'delegationGeneration'
556
+ and b.generation::text = a.canonical_snapshot ->> 'bindingGeneration'
557
+ and exists (select 1 from jsonb_array_elements(c.tools) tool where tool ->> 'kind' = 'mcp' and tool ->> 'id' = a.server_id)
558
+ for share of d, b`,
559
+ );
560
+ for (const row of rows) {
561
+ const parent = HostMcpAcceptedAuthority.parse(row.canonical_snapshot);
562
+ const authority = HostMcpAcceptedAuthority.parse({
563
+ ...parent,
564
+ targetSessionId: input.sessionId,
565
+ targetSessionVisibility: row.visibility,
566
+ targetSessionAuthorityEpoch: row.authority_epoch,
567
+ acceptedWork: { kind: "turn", turnId: input.turnId },
568
+ source: {
569
+ kind: "child_turn",
570
+ sessionId: row.parent_session_id,
571
+ turnId: row.parent_turn_id,
572
+ },
573
+ });
574
+ await tx.execute(sql`insert into host_mcp_turn_authorities
575
+ (turn_id, server_id, account_id, workspace_id, session_id, owner_subject_id, binding_id, delegation_id, canonical_snapshot)
576
+ values (${input.turnId}::uuid, ${authority.definition.serverId}, ${input.accountId}::uuid,
577
+ ${input.workspaceId}::uuid, ${input.sessionId}::uuid, ${input.subjectId}, ${authority.bindingId}::uuid,
578
+ ${authority.delegationId}::uuid, ${JSON.stringify(authority)}::jsonb)`);
579
+ }
580
+ });
581
+ }
582
+
583
+ /** Live consumption of captured direct or same-session causal turn authority.
584
+ * Request context must come from the accepted attempt, never caller JSON.
585
+ * Missing/revoked authority denies; infrastructure failures propagate so the
586
+ * broker reports refresh_failed rather than misclassifying an outage.
587
+ * The historical function name is retained for existing internal consumers. */
588
+ export async function authorizeDirectHostMcpUse(
589
+ db: Database,
590
+ request: McpCredentialsRequest,
591
+ ): Promise<boolean> {
592
+ if (!request.attemptId || !request.connectionRef.hostBinding) return false;
593
+ const current = await getHostMcpLiveAttempt(
594
+ db,
595
+ request.workspaceId,
596
+ request.sessionId,
597
+ request.attemptId,
598
+ );
599
+ if (
600
+ !current ||
601
+ current.id !== request.turnId ||
602
+ current.executionGeneration !== request.executionGeneration
603
+ )
604
+ return false;
605
+ // The accepted initiator only scopes this read. Permission comes from the
606
+ // immutable captured row and live checks below, not from initiator metadata.
607
+ const direct =
608
+ ["user", "api"].includes(current.source) &&
609
+ current.initiator.kind === "subject" &&
610
+ !["via", "viaTruncated", "provenanceError", "backfill"].some((key) =>
611
+ Object.hasOwn(current.initiatorContext, key),
612
+ );
613
+ const inherited = ["goal", "system"].includes(current.source) && !current.scheduledTaskRunId;
614
+ const scheduled = current.source === "system" && Boolean(current.scheduledTaskRunId);
615
+ const child = ["user", "api"].includes(current.source) && !direct;
616
+ const subjectId =
617
+ direct && current.initiator.kind === "subject"
618
+ ? current.initiator.subjectId
619
+ : inherited || scheduled || child
620
+ ? current.initiatingHumanSubjectId
621
+ : null;
622
+ if (!subjectId) return false;
623
+ return withWorkspaceSubjectRls(db, request.workspaceId, subjectId, async (tx) => {
624
+ await lockExternalWorkspaceMembershipLifecycle(tx, request.accountId);
625
+ const [stored] = await rawRows<{ canonical_snapshot: unknown }>(
626
+ tx,
627
+ sql`
628
+ select canonical_snapshot from host_mcp_turn_authorities where turn_id = ${request.turnId}::uuid
629
+ and server_id = ${request.serverId} and account_id = ${request.accountId}::uuid
630
+ and workspace_id = ${request.workspaceId}::uuid and session_id = ${request.sessionId}::uuid
631
+ and owner_subject_id = ${subjectId}`,
632
+ );
633
+ const parsed = HostMcpAcceptedAuthority.safeParse(stored?.canonical_snapshot);
634
+ if (!parsed.success) return false;
635
+ const a = parsed.data;
636
+ if (
637
+ !(
638
+ (direct && a.source.kind === "direct") ||
639
+ (inherited &&
640
+ a.source.kind === "inherited_turn" &&
641
+ a.source.sessionId === request.sessionId) ||
642
+ (scheduled &&
643
+ a.source.kind === "scheduled_task" &&
644
+ a.acceptedWork.kind === "scheduled_task" &&
645
+ a.acceptedWork.runId === current.scheduledTaskRunId) ||
646
+ (child && a.source.kind === "child_turn" && a.acceptedWork.kind === "turn")
647
+ ) ||
648
+ (a.acceptedWork.kind === "turn" && a.acceptedWork.turnId !== request.turnId) ||
649
+ a.accountId !== request.accountId ||
650
+ a.workspaceId !== request.workspaceId ||
651
+ a.targetSessionId !== request.sessionId ||
652
+ a.ownerSubjectId !== subjectId
653
+ )
654
+ return false;
655
+ if (a.scheduledOrigin) {
656
+ const origin = a.scheduledOrigin;
657
+ const [run] = await rawRows<{ denial: string | null }>(
658
+ tx,
659
+ sql`
660
+ select validate_scheduled_agent_run_live_authority(${request.accountId}::uuid,
661
+ ${request.workspaceId}::uuid, r.id) as denial from scheduled_task_runs r
662
+ where r.id = ${origin.runId}::uuid and r.task_id = ${origin.taskId}::uuid
663
+ and r.task_authority_revision = ${origin.taskAuthorityRevision}
664
+ and r.account_id = ${request.accountId}::uuid and r.workspace_id = ${request.workspaceId}::uuid
665
+ and r.accepted_execution_snapshot ->> 'causalHumanSubjectId' = ${subjectId}`,
666
+ );
667
+ if (!run || run.denial !== null) return false;
668
+ }
669
+ const membership = (await listSelfOrganizationMemberships(tx, subjectId)).find(
670
+ (m) => m.organizationId === request.accountId,
671
+ );
672
+ if (
673
+ !membership ||
674
+ membership.status !== "active" ||
675
+ membership.id !== a.ownerOrganizationMembershipId ||
676
+ membership.authorizationRevision !== a.ownerMembershipAuthorizationRevision ||
677
+ !(await subjectHasLiveWorkspaceAuthorityInScope(tx, {
678
+ accountId: request.accountId,
679
+ workspaceId: request.workspaceId,
680
+ subjectId,
681
+ }))
682
+ )
683
+ return false;
684
+ // Migration 0418's self-membership boundary independently checks active
685
+ // external identity; runtime has no direct external_identities privileges.
686
+ const [session] = await rawRows<{
687
+ visibility: string;
688
+ authority_epoch: number;
689
+ owner_subject_id: string | null;
690
+ }>(
691
+ tx,
692
+ sql`select visibility, authority_epoch, owner_subject_id from sessions where id = ${request.sessionId}::uuid and account_id = ${request.accountId}::uuid and workspace_id = ${request.workspaceId}::uuid`,
693
+ );
694
+ if (
695
+ !session ||
696
+ session.visibility !== a.targetSessionVisibility ||
697
+ session.authority_epoch !== a.targetSessionAuthorityEpoch ||
698
+ (session.visibility === "user_private" && session.owner_subject_id !== subjectId)
699
+ )
700
+ return false;
701
+ const [d] = await rawRows<DelegationRow>(
702
+ tx,
703
+ sql`select * from host_mcp_delegations where id = ${a.delegationId}::uuid and account_id = ${request.accountId}::uuid and workspace_id = ${request.workspaceId}::uuid and owner_subject_id = ${subjectId}`,
704
+ );
705
+ const [b] = await rawRows<Row>(
706
+ tx,
707
+ sql`select * from host_mcp_bindings where id = ${a.bindingId}::uuid and account_id = ${request.accountId}::uuid and workspace_id = ${request.workspaceId}::uuid and owner_subject_id = ${subjectId}`,
708
+ );
709
+ if (!d || !b) return false;
710
+ const delegation = projectDelegation(d),
711
+ binding = project(b);
712
+ if (
713
+ delegation.status !== "active" ||
714
+ delegation.revokedAt !== null ||
715
+ delegation.generation !== a.delegationGeneration ||
716
+ delegation.ownerAuthorizationRevision !== a.ownerMembershipAuthorizationRevision ||
717
+ delegation.bindingId !== a.bindingId ||
718
+ delegation.bindingGeneration !== a.bindingGeneration ||
719
+ delegation.grant.context !== a.targetSessionVisibility ||
720
+ (delegation.grant.mode === "session" &&
721
+ (delegation.grant.sessionId !== request.sessionId ||
722
+ delegation.grant.expectedAuthorityEpoch !== a.targetSessionAuthorityEpoch)) ||
723
+ binding.authorizationRevision !== a.ownerMembershipAuthorizationRevision ||
724
+ binding.generation !== a.bindingGeneration ||
725
+ stableJson(binding.definition) !== stableJson(a.definition) ||
726
+ !hostMcpBindingMatchesRequest(binding, request)
727
+ )
728
+ return false;
729
+ const live = await getHostMcpLiveAttempt(
730
+ tx,
731
+ request.workspaceId,
732
+ request.sessionId,
733
+ request.attemptId!,
734
+ );
735
+ return (
736
+ live !== null &&
737
+ live.id === request.turnId &&
738
+ live.executionGeneration === request.executionGeneration
739
+ );
740
+ });
741
+ }