@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,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
|
+
}
|