@opengeni/db 4.4.0-canary.0 → 4.4.0-canary.2
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 +4 -3
- package/dist/{chunk-2ZMTQVTR.js → chunk-2KOLL33E.js} +2 -2
- package/dist/chunk-45CP2YKY.js +1062 -0
- package/dist/chunk-45CP2YKY.js.map +1 -0
- package/dist/{chunk-6TKL3GQO.js → chunk-5UUWZDAV.js} +6531 -6480
- package/dist/chunk-5UUWZDAV.js.map +1 -0
- package/dist/{chunk-QLA3UQNX.js → chunk-6IJD3XXF.js} +17 -3
- package/dist/chunk-6IJD3XXF.js.map +1 -0
- package/dist/{chunk-232VC2LW.js → chunk-6V3HDEMR.js} +2 -2
- package/dist/{chunk-M7PSMVFF.js → chunk-CHBLBEU6.js} +2 -2
- package/dist/chunk-M4PKBJRS.js +20318 -0
- package/dist/chunk-M4PKBJRS.js.map +1 -0
- package/dist/chunk-S2TZ3F4L.js +237 -0
- package/dist/chunk-S2TZ3F4L.js.map +1 -0
- package/dist/{chunk-UK57QA2H.js → chunk-UOXWVP3T.js} +2 -2
- package/dist/{chunk-GVZJ7XSR.js → chunk-WFU6PS54.js} +11 -4
- package/dist/chunk-WFU6PS54.js.map +1 -0
- package/dist/{chunk-THJGMEK5.js → chunk-XJP7KJ6U.js} +121 -66
- package/dist/chunk-XJP7KJ6U.js.map +1 -0
- package/dist/connection-token-resolver.d.ts +2 -0
- package/dist/database.d.ts +2 -0
- package/dist/editable-artifact-durable-export.js +3 -2
- package/dist/editable-artifact-durable-export.js.map +1 -1
- package/dist/editable-artifacts.js +4 -3
- package/dist/external-identities.d.ts +1 -0
- package/dist/external-membership-operations.d.ts +44 -0
- package/dist/governed-learning-evaluator.d.ts +1 -14
- package/dist/host-mcp-bindings.d.ts +4 -0
- package/dist/host-mcp-resolvers.d.ts +47 -0
- package/dist/host-mcp-task-authority.d.ts +2 -6
- package/dist/index.d.ts +29 -31
- package/dist/index.js +8616 -8321
- package/dist/index.js.map +1 -1
- package/dist/knowledge-document-preparation.d.ts +56 -0
- package/dist/knowledge-entries-schema.d.ts +2479 -0
- package/dist/knowledge-entries.d.ts +421 -0
- package/dist/knowledge-indexing.d.ts +99 -0
- package/dist/knowledge-migration.d.ts +6 -0
- package/dist/managed-auth-session-sets.js +4 -3
- package/dist/mcp-operations.js +3 -2
- package/dist/mcp-operations.js.map +1 -1
- package/dist/migrate.js +2 -1
- package/dist/provision-roles.js +1 -1
- package/dist/retained-provider-commands.js +3 -2
- package/dist/retained-provider-commands.js.map +1 -1
- package/dist/runtime-posture.d.ts +7 -7
- package/dist/schema.d.ts +115 -12
- package/dist/schema.js +26 -1
- package/dist/session-background-commands.js +5 -4
- package/dist/session-command-output.js +8 -6
- package/dist/session-command-output.js.map +1 -1
- package/dist/session-event-slices.js +6 -4
- package/dist/session-event-slices.js.map +1 -1
- package/dist/session-mcp-credential-rotation.js +5 -4
- package/dist/session-mcp-credential-rotation.js.map +1 -1
- package/dist/session-tenancy.js +4 -3
- package/dist/video-generation.js +4 -3
- package/dist/workspace-artifacts.d.ts +1 -0
- package/dist/workspace-instruction-policies-schema.d.ts +21 -2
- package/dist/workspace-instruction-policies.d.ts +1 -1
- package/dist/workspace-membership-access.d.ts +11 -0
- package/dist/workspace-tool-defaults.js +5 -4
- package/dist/workspace-tool-defaults.js.map +1 -1
- package/drizzle/0461_unified_knowledge.sql +2859 -0
- package/drizzle/0462_agent_instruction_non_destructive_edits.sql +212 -0
- package/drizzle/0463_host_mcp_resolver_registration.sql +111 -0
- package/drizzle/0464_external_workspace_membership_operations.sql +166 -0
- package/package.json +6 -6
- package/src/connection-token-resolver.ts +75 -6
- package/src/database.ts +12 -1
- package/src/external-identities.ts +1 -0
- package/src/external-membership-operations.ts +134 -0
- package/src/governed-learning-evaluator.ts +0 -69
- package/src/host-mcp-binding-match.ts +1 -0
- package/src/host-mcp-bindings.ts +24 -1
- package/src/host-mcp-resolvers.ts +266 -0
- package/src/host-mcp-task-authority.ts +11 -10
- package/src/index.ts +307 -128
- package/src/knowledge-document-preparation.ts +90 -0
- package/src/knowledge-entries-schema.ts +264 -0
- package/src/knowledge-entries.ts +860 -0
- package/src/knowledge-indexing.ts +126 -0
- package/src/knowledge-migration.ts +761 -0
- package/src/knowledge-source-sync.ts +46 -7
- package/src/migrate.ts +19 -0
- package/src/provision-roles.ts +23 -57
- package/src/runtime-posture.ts +103 -7
- package/src/schema.ts +15 -3
- package/src/skills.ts +4 -0
- package/src/video-generation.ts +14 -0
- package/src/workspace-artifacts.ts +2 -0
- package/src/workspace-instruction-policies-schema.ts +1 -0
- package/src/workspace-membership-access.ts +61 -0
- package/dist/chunk-6TKL3GQO.js.map +0 -1
- package/dist/chunk-EYF6MH6C.js +0 -4772
- package/dist/chunk-EYF6MH6C.js.map +0 -1
- package/dist/chunk-GSBONN55.js +0 -533
- package/dist/chunk-GSBONN55.js.map +0 -1
- package/dist/chunk-GVZJ7XSR.js.map +0 -1
- package/dist/chunk-QLA3UQNX.js.map +0 -1
- package/dist/chunk-THJGMEK5.js.map +0 -1
- /package/dist/{chunk-2ZMTQVTR.js.map → chunk-2KOLL33E.js.map} +0 -0
- /package/dist/{chunk-232VC2LW.js.map → chunk-6V3HDEMR.js.map} +0 -0
- /package/dist/{chunk-M7PSMVFF.js.map → chunk-CHBLBEU6.js.map} +0 -0
- /package/dist/{chunk-UK57QA2H.js.map → chunk-UOXWVP3T.js.map} +0 -0
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
TurnInitiator,
|
|
18
18
|
TurnInitiatorContext,
|
|
19
19
|
} from "@opengeni/contracts";
|
|
20
|
+
import { hostMcpCredentialUseGuard } from "@opengeni/contracts";
|
|
20
21
|
import {
|
|
21
22
|
ConnectionUseAuthoritySnapshot,
|
|
22
23
|
type ConnectionUseAttribution,
|
|
@@ -48,6 +49,7 @@ import type {
|
|
|
48
49
|
AcceptedConnectionUseResolution,
|
|
49
50
|
} from "./connection-authority";
|
|
50
51
|
import { connectionScopeKey } from "./connection-scopes";
|
|
52
|
+
import { hostMcpBindingMatchesSelection } from "@opengeni/contracts/host-mcp-bindings";
|
|
51
53
|
|
|
52
54
|
const MAX_CREDENTIAL_PLACEMENTS = 32;
|
|
53
55
|
const MAX_CREDENTIAL_NAME_BYTES = 256;
|
|
@@ -193,6 +195,10 @@ export type HostMcpCredentialResolverContext = {
|
|
|
193
195
|
/** Backend-owned live validator. Never populate from caller JSON or a host
|
|
194
196
|
* credential response. Omission denies every explicit durable reference. */
|
|
195
197
|
authorizeDurableBinding?: (request: McpCredentialsRequest) => Promise<boolean>;
|
|
198
|
+
/** Backend-only accepted-work lookup, not a host response or registry fallback. */
|
|
199
|
+
resolveAcceptedBinding?: (
|
|
200
|
+
request: McpCredentialsRequest,
|
|
201
|
+
) => Promise<McpServerConnectionRef | null>;
|
|
196
202
|
};
|
|
197
203
|
|
|
198
204
|
export class HostMcpCredentialScopeError extends Error {
|
|
@@ -325,8 +331,8 @@ export function buildHostConnectionTokenResolver(
|
|
|
325
331
|
...(toolName ? { toolName } : {}),
|
|
326
332
|
...(input.subjectId ? { callerSubjectId: input.subjectId } : {}),
|
|
327
333
|
};
|
|
328
|
-
|
|
329
|
-
|
|
334
|
+
// Freeze configuration before any asynchronous authority lookup.
|
|
335
|
+
let requestedRef = structuredClone(request.connectionRef);
|
|
330
336
|
const credentialTarget = input.credentialTarget ?? "mcp";
|
|
331
337
|
const durableDenial = (
|
|
332
338
|
reason: McpCredentialAuthNeededReason,
|
|
@@ -337,6 +343,32 @@ export function buildHostConnectionTokenResolver(
|
|
|
337
343
|
authoritySource: "host",
|
|
338
344
|
...(requestedRef.connectionId ? { connectionId: requestedRef.connectionId } : {}),
|
|
339
345
|
});
|
|
346
|
+
if (requestedRef.hostBinding && "selection" in requestedRef.hostBinding) {
|
|
347
|
+
if (!context.resolveAcceptedBinding || !context.authorizeDurableBinding)
|
|
348
|
+
return durableDenial("unsupported_auth");
|
|
349
|
+
try {
|
|
350
|
+
const concrete = await context.resolveAcceptedBinding(structuredClone(request));
|
|
351
|
+
if (!concrete?.hostBinding || "selection" in concrete.hostBinding)
|
|
352
|
+
return durableDenial("personal_authority_unavailable");
|
|
353
|
+
const { hostBinding, ...connectionRef } = concrete;
|
|
354
|
+
if (
|
|
355
|
+
!hostMcpBindingMatchesSelection(
|
|
356
|
+
{
|
|
357
|
+
id: hostBinding.bindingId,
|
|
358
|
+
generation: hostBinding.generation,
|
|
359
|
+
definition: { serverId: request.serverId, destinationUrl, connectionRef },
|
|
360
|
+
},
|
|
361
|
+
request,
|
|
362
|
+
)
|
|
363
|
+
)
|
|
364
|
+
return durableDenial("personal_authority_unavailable");
|
|
365
|
+
request.connectionRef = structuredClone(concrete);
|
|
366
|
+
requestedRef = structuredClone(concrete);
|
|
367
|
+
} catch {
|
|
368
|
+
return durableDenial("refresh_failed");
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const snapshot = structuredClone(request);
|
|
340
372
|
const authorized = async () => {
|
|
341
373
|
try {
|
|
342
374
|
if (
|
|
@@ -362,14 +394,27 @@ export function buildHostConnectionTokenResolver(
|
|
|
362
394
|
if (denial) return durableDenial(denial);
|
|
363
395
|
}
|
|
364
396
|
const result = await resolve(structuredClone(snapshot));
|
|
397
|
+
const routeGuard = result.status === "ok" ? result[hostMcpCredentialUseGuard] : undefined;
|
|
398
|
+
const routeAuthorized = async () => {
|
|
399
|
+
try {
|
|
400
|
+
return routeGuard ? await routeGuard() : true;
|
|
401
|
+
} catch {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
};
|
|
365
405
|
if (requiresAuthorization) {
|
|
366
406
|
const denial = await authorized();
|
|
367
407
|
if (denial) return durableDenial(denial);
|
|
368
408
|
}
|
|
409
|
+
if (routeGuard && !(await routeAuthorized())) return durableDenial("refresh_failed");
|
|
369
410
|
assertHostMcpCredentialScope(result, context);
|
|
370
411
|
const normalized = normalizeHostCredentialResolution(result, requestedRef, credentialTarget);
|
|
371
|
-
return normalized.status === "ok" && requiresAuthorization
|
|
372
|
-
? {
|
|
412
|
+
return normalized.status === "ok" && (requiresAuthorization || routeGuard)
|
|
413
|
+
? {
|
|
414
|
+
...normalized,
|
|
415
|
+
authorizeProviderRequest: async () =>
|
|
416
|
+
(await authorized()) === null && (await routeAuthorized()),
|
|
417
|
+
}
|
|
373
418
|
: normalized;
|
|
374
419
|
};
|
|
375
420
|
}
|
|
@@ -415,6 +460,14 @@ export function buildHostGatewayConnectionTokenResolver(
|
|
|
415
460
|
await reauthorize();
|
|
416
461
|
const result = await resolve(structuredClone(request));
|
|
417
462
|
await reauthorize();
|
|
463
|
+
const routeGuard = result.status === "ok" ? result[hostMcpCredentialUseGuard] : undefined;
|
|
464
|
+
if (routeGuard) {
|
|
465
|
+
try {
|
|
466
|
+
if (!(await routeGuard())) return deny("refresh_failed");
|
|
467
|
+
} catch {
|
|
468
|
+
return deny("refresh_failed");
|
|
469
|
+
}
|
|
470
|
+
}
|
|
418
471
|
for (const field of ["accountId", "workspaceId", "requestId"] as const) {
|
|
419
472
|
if (result[field] !== request[field])
|
|
420
473
|
throw new Error(`host gateway credential ${field} scope mismatch`);
|
|
@@ -430,7 +483,7 @@ export function buildHostGatewayConnectionTokenResolver(
|
|
|
430
483
|
authorizeProviderRequest: async () => {
|
|
431
484
|
try {
|
|
432
485
|
await reauthorize();
|
|
433
|
-
return true;
|
|
486
|
+
return routeGuard ? await routeGuard() : true;
|
|
434
487
|
} catch {
|
|
435
488
|
return false;
|
|
436
489
|
}
|
|
@@ -1548,6 +1601,15 @@ export async function refreshOAuthConnectionCredential(
|
|
|
1548
1601
|
const expiresAt = expiresAtFromTokenResponse(payload, cred.expiresAt);
|
|
1549
1602
|
const scopeText = stringValue(payload.scope);
|
|
1550
1603
|
const returnedScopes = scopeText?.split(/[\s,]+/).filter(Boolean) ?? [];
|
|
1604
|
+
// A successful Microsoft refresh proves offline access, although Microsoft
|
|
1605
|
+
// reports only access-token permissions in the response's scope field.
|
|
1606
|
+
if (
|
|
1607
|
+
scopeText &&
|
|
1608
|
+
ref.providerDomain.toLowerCase() === "graph.microsoft.com" &&
|
|
1609
|
+
!returnedScopes.some((scope) => scope.toLowerCase() === "offline_access")
|
|
1610
|
+
) {
|
|
1611
|
+
returnedScopes.push("offline_access");
|
|
1612
|
+
}
|
|
1551
1613
|
const refreshTokenExpiresIn =
|
|
1552
1614
|
typeof payload.refresh_token_expires_in === "number"
|
|
1553
1615
|
? payload.refresh_token_expires_in
|
|
@@ -1574,7 +1636,14 @@ export async function refreshOAuthConnectionCredential(
|
|
|
1574
1636
|
"Bearer",
|
|
1575
1637
|
...(expiresAt ? { expires_at: expiresAt.toISOString() } : {}),
|
|
1576
1638
|
...(resource ? { resource } : {}),
|
|
1577
|
-
...(scopeText
|
|
1639
|
+
...(scopeText
|
|
1640
|
+
? {
|
|
1641
|
+
scope:
|
|
1642
|
+
ref.providerDomain.toLowerCase() === "graph.microsoft.com"
|
|
1643
|
+
? returnedScopes.join(" ")
|
|
1644
|
+
: scopeText,
|
|
1645
|
+
}
|
|
1646
|
+
: {}),
|
|
1578
1647
|
...(refreshTokenExpiresAt ? { refresh_token_expires_at: refreshTokenExpiresAt } : {}),
|
|
1579
1648
|
...(clientSecret && !personalGitHub
|
|
1580
1649
|
? { client_secret: clientSecret, token_endpoint_auth_method: authMethod }
|
package/src/database.ts
CHANGED
|
@@ -53,6 +53,8 @@ export type RlsContext = {
|
|
|
53
53
|
|
|
54
54
|
export type SessionRlsActorContext = {
|
|
55
55
|
subjectId: string;
|
|
56
|
+
/** Host-verified personal file ownership, never a request-supplied subject. */
|
|
57
|
+
privateFileOwnerSubjectId?: string | null;
|
|
56
58
|
initiatingHumanSubjectId?: string | null;
|
|
57
59
|
};
|
|
58
60
|
|
|
@@ -74,6 +76,15 @@ export async function withSessionRlsActorContext<T>(
|
|
|
74
76
|
"withSessionRlsActorContext: initiatingHumanSubjectId must be null or non-empty",
|
|
75
77
|
);
|
|
76
78
|
}
|
|
79
|
+
if (
|
|
80
|
+
actor.privateFileOwnerSubjectId !== undefined &&
|
|
81
|
+
actor.privateFileOwnerSubjectId !== null &&
|
|
82
|
+
!actor.privateFileOwnerSubjectId.trim()
|
|
83
|
+
) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
"withSessionRlsActorContext: privateFileOwnerSubjectId must be null or non-empty",
|
|
86
|
+
);
|
|
87
|
+
}
|
|
77
88
|
return await sessionRlsActorContext.run(actor, fn);
|
|
78
89
|
}
|
|
79
90
|
|
|
@@ -389,7 +400,7 @@ export async function setRlsContext(db: Database, context: RlsContext): Promise<
|
|
|
389
400
|
if (sessionActor) {
|
|
390
401
|
await setSubjectRlsContext(db, sessionActor.subjectId);
|
|
391
402
|
await db.execute(
|
|
392
|
-
sql`select set_config(
|
|
403
|
+
sql`select set_config('opengeni.private_file_owner', ${sessionActor.privateFileOwnerSubjectId ?? ""}, true), set_config(
|
|
393
404
|
'opengeni.initiating_human_subject_id',
|
|
394
405
|
${sessionActor.initiatingHumanSubjectId ?? ""},
|
|
395
406
|
true
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
ExternalIdentityReference,
|
|
6
6
|
} from "@opengeni/contracts/external-identities";
|
|
7
7
|
import { rawRows, withAccountRls, type Database } from "./database";
|
|
8
|
+
export * from "./external-membership-operations";
|
|
8
9
|
|
|
9
10
|
/** Pass an open transaction to retain the key lock through the protected write. */
|
|
10
11
|
export async function lockActiveExternalOrganizationKey(
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import {
|
|
4
|
+
ExternalIdentity,
|
|
5
|
+
ExternalIdentityLookup,
|
|
6
|
+
CancelExternalWorkspaceMemberGrantResponse,
|
|
7
|
+
type AddExternalWorkspaceMemberRequest,
|
|
8
|
+
type CancelExternalWorkspaceMemberGrantRequest,
|
|
9
|
+
type ExternalIdentityReference,
|
|
10
|
+
} from "@opengeni/contracts/external-identities";
|
|
11
|
+
import {
|
|
12
|
+
rawRows,
|
|
13
|
+
withAccountRls,
|
|
14
|
+
withWorkspaceSubjectRls,
|
|
15
|
+
setSubjectRlsContext,
|
|
16
|
+
type Database,
|
|
17
|
+
} from "./database";
|
|
18
|
+
import { grantWorkspaceAccess, listWorkspaceMembers } from "./workspace-membership-access";
|
|
19
|
+
import { removeWorkspaceMember } from "./organization-membership-lifecycle";
|
|
20
|
+
|
|
21
|
+
type ServiceScope = { organizationId: string; actorSubjectId: string };
|
|
22
|
+
|
|
23
|
+
export async function lookupExternalIdentity(
|
|
24
|
+
db: Database,
|
|
25
|
+
scope: ServiceScope,
|
|
26
|
+
identity: ExternalIdentityReference,
|
|
27
|
+
) {
|
|
28
|
+
return withAccountRls(db, scope.organizationId, async (tx) => {
|
|
29
|
+
await setSubjectRlsContext(tx, scope.actorSubjectId);
|
|
30
|
+
const [row] = await rawRows<{ result: unknown }>(
|
|
31
|
+
tx,
|
|
32
|
+
sql`select lookup_external_identity(
|
|
33
|
+
${scope.organizationId}::uuid, ${scope.actorSubjectId}, ${identity.source}, ${identity.externalId}
|
|
34
|
+
) as result`,
|
|
35
|
+
);
|
|
36
|
+
return ExternalIdentityLookup.parse(row?.result);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function prepare(db: Database, command: Record<string, unknown>) {
|
|
41
|
+
const [row] = await rawRows<{ result: unknown }>(
|
|
42
|
+
db,
|
|
43
|
+
sql`select prepare_external_workspace_membership_operation(${JSON.stringify(command)}::jsonb) as result`,
|
|
44
|
+
);
|
|
45
|
+
return z
|
|
46
|
+
.object({
|
|
47
|
+
replay: z.boolean(),
|
|
48
|
+
result: z.unknown().optional(),
|
|
49
|
+
identity: z.unknown().optional(),
|
|
50
|
+
})
|
|
51
|
+
.parse(row?.result);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function record(db: Database, command: Record<string, unknown>, result: unknown) {
|
|
55
|
+
await db.execute(sql`select record_external_workspace_membership_operation(
|
|
56
|
+
${JSON.stringify(command)}::jsonb, ${JSON.stringify(result)}::jsonb)`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** One keyed grant. An operation replay never restores a removed/reduced row. */
|
|
60
|
+
export async function addExternalWorkspaceMemberOperation(
|
|
61
|
+
db: Database,
|
|
62
|
+
scope: ServiceScope & { workspaceId: string },
|
|
63
|
+
request: AddExternalWorkspaceMemberRequest & { operationId: string },
|
|
64
|
+
) {
|
|
65
|
+
const command = {
|
|
66
|
+
...scope,
|
|
67
|
+
action: "grant",
|
|
68
|
+
...request,
|
|
69
|
+
permissions: [...new Set(request.permissions)].sort(),
|
|
70
|
+
};
|
|
71
|
+
return withWorkspaceSubjectRls(db, scope.workspaceId, scope.actorSubjectId, async (tx) => {
|
|
72
|
+
const prepared = await prepare(tx, command);
|
|
73
|
+
if (prepared.replay)
|
|
74
|
+
return z.object({ identity: ExternalIdentity }).parse(prepared.result).identity;
|
|
75
|
+
const identity = ExternalIdentity.parse(prepared.identity);
|
|
76
|
+
const existing = (await listWorkspaceMembers(tx, scope.workspaceId)).find(
|
|
77
|
+
(member) => member.subjectId === identity.subjectId,
|
|
78
|
+
);
|
|
79
|
+
if (
|
|
80
|
+
existing &&
|
|
81
|
+
(existing.permissions.length !== command.permissions.length ||
|
|
82
|
+
existing.permissions.some((permission) => !command.permissions.includes(permission)))
|
|
83
|
+
) {
|
|
84
|
+
// SQLSTATE matches the native optimistic-concurrency contract.
|
|
85
|
+
await tx.execute(
|
|
86
|
+
sql`do $$ begin raise exception 'existing membership differs' using errcode = '40001'; end $$`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (!existing)
|
|
90
|
+
await grantWorkspaceAccess(tx, {
|
|
91
|
+
accountId: scope.organizationId,
|
|
92
|
+
workspaceId: scope.workspaceId,
|
|
93
|
+
subjectId: identity.subjectId,
|
|
94
|
+
role: "member",
|
|
95
|
+
permissions: command.permissions,
|
|
96
|
+
});
|
|
97
|
+
await record(tx, command, { workspaceId: scope.workspaceId, identity });
|
|
98
|
+
return identity;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The native removal owns all teardown; this wrapper adds an immutable causal
|
|
103
|
+
* fence even when an earlier grant has not reached the database yet. */
|
|
104
|
+
export async function cancelExternalWorkspaceMemberGrant(
|
|
105
|
+
db: Database,
|
|
106
|
+
scope: ServiceScope & { workspaceId: string; membershipId: string },
|
|
107
|
+
request: CancelExternalWorkspaceMemberGrantRequest,
|
|
108
|
+
) {
|
|
109
|
+
const command = { ...scope, action: "revoke", ...request };
|
|
110
|
+
return withWorkspaceSubjectRls(db, scope.workspaceId, scope.actorSubjectId, async (tx) => {
|
|
111
|
+
const prepared = await prepare(tx, command);
|
|
112
|
+
if (prepared.replay) {
|
|
113
|
+
const stored = z
|
|
114
|
+
.object({ removed: z.boolean(), fencedGrantOperationId: z.string().uuid() })
|
|
115
|
+
.parse(prepared.result);
|
|
116
|
+
return CancelExternalWorkspaceMemberGrantResponse.parse({ ...stored, replay: true });
|
|
117
|
+
}
|
|
118
|
+
const identity = z.object({ subjectId: z.string() }).parse(prepared.identity);
|
|
119
|
+
const removed = await removeWorkspaceMember(tx, {
|
|
120
|
+
accountId: scope.organizationId,
|
|
121
|
+
workspaceId: scope.workspaceId,
|
|
122
|
+
actorSubjectId: scope.actorSubjectId,
|
|
123
|
+
targetSubjectId: identity.subjectId,
|
|
124
|
+
operationId: request.operationId,
|
|
125
|
+
});
|
|
126
|
+
const result = {
|
|
127
|
+
removed,
|
|
128
|
+
replay: false,
|
|
129
|
+
fencedGrantOperationId: request.cancelGrantOperationId,
|
|
130
|
+
};
|
|
131
|
+
await record(tx, command, { ...result, workspaceId: scope.workspaceId });
|
|
132
|
+
return result;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
EvaluateGovernedLearningProposalRequest,
|
|
3
2
|
GovernedLearningDecisionReceipt,
|
|
4
|
-
GovernedLearningEvaluationAttempt,
|
|
5
3
|
type GovernedLearningDecisionReceipt as GovernedLearningDecisionReceiptType,
|
|
6
|
-
type GovernedLearningEvaluationAttempt as GovernedLearningEvaluationAttemptType,
|
|
7
|
-
type EvaluateGovernedLearningProposalRequest as EvaluateGovernedLearningProposalRequestType,
|
|
8
4
|
} from "@opengeni/contracts";
|
|
9
5
|
import { sql } from "drizzle-orm";
|
|
10
6
|
import type { Database } from "./database";
|
|
@@ -15,15 +11,6 @@ export class GovernedLearningEvaluationAuthorityError extends Error {
|
|
|
15
11
|
readonly name = "GovernedLearningEvaluationAuthorityError";
|
|
16
12
|
}
|
|
17
13
|
|
|
18
|
-
export class GovernedLearningEvaluationConflictError extends Error {
|
|
19
|
-
readonly name = "GovernedLearningEvaluationConflictError";
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export type GovernedLearningEvaluationInput = Readonly<{
|
|
23
|
-
attempt: GovernedLearningEvaluationAttemptType;
|
|
24
|
-
request: EvaluateGovernedLearningProposalRequestType;
|
|
25
|
-
}>;
|
|
26
|
-
|
|
27
14
|
type ReceiptRow = {
|
|
28
15
|
receipt_id: string;
|
|
29
16
|
operation_id: string;
|
|
@@ -159,59 +146,3 @@ export async function listGovernedLearningDecisionReceipts(
|
|
|
159
146
|
throw error;
|
|
160
147
|
}
|
|
161
148
|
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Persist one inert, content-free governed-learning decision. The database
|
|
165
|
-
* capability independently revalidates the accepted attempt, policy snapshot,
|
|
166
|
-
* proposal lineage, current evidence authority, and deterministic verdict.
|
|
167
|
-
*/
|
|
168
|
-
export async function evaluateGovernedLearningProposal(
|
|
169
|
-
db: Database,
|
|
170
|
-
input: GovernedLearningEvaluationInput,
|
|
171
|
-
): Promise<GovernedLearningDecisionReceiptType> {
|
|
172
|
-
const attempt = GovernedLearningEvaluationAttempt.parse(input.attempt);
|
|
173
|
-
const request = EvaluateGovernedLearningProposalRequest.parse(input.request);
|
|
174
|
-
try {
|
|
175
|
-
return await withWorkspaceSubjectRls(
|
|
176
|
-
db,
|
|
177
|
-
attempt.workspaceId,
|
|
178
|
-
attempt.subjectId,
|
|
179
|
-
async (scopedDb) => {
|
|
180
|
-
const rows = await rawRows<ReceiptRow>(
|
|
181
|
-
scopedDb,
|
|
182
|
-
sql`SELECT * FROM evaluate_governed_learning_proposal(
|
|
183
|
-
current_setting('opengeni.account_id')::uuid,
|
|
184
|
-
${attempt.workspaceId}::uuid,
|
|
185
|
-
${attempt.sessionId}::uuid,
|
|
186
|
-
${attempt.turnId}::uuid,
|
|
187
|
-
${attempt.attemptId}::uuid,
|
|
188
|
-
${attempt.executionGeneration}::integer,
|
|
189
|
-
${request.operationId}::uuid,
|
|
190
|
-
${request.policySnapshotId}::uuid,
|
|
191
|
-
${request.proposalId}::uuid,
|
|
192
|
-
${request.claimId}::uuid,
|
|
193
|
-
${request.evidenceId}::uuid
|
|
194
|
-
)`,
|
|
195
|
-
);
|
|
196
|
-
const row = rows[0];
|
|
197
|
-
if (!row || rows.length !== 1) {
|
|
198
|
-
throw new Error("Governed-learning evaluation returned no unique receipt");
|
|
199
|
-
}
|
|
200
|
-
return receiptFromRow(row);
|
|
201
|
-
},
|
|
202
|
-
);
|
|
203
|
-
} catch (error) {
|
|
204
|
-
const state = nestedPostgresSqlState(error);
|
|
205
|
-
if (state === "42501") {
|
|
206
|
-
throw new GovernedLearningEvaluationAuthorityError(
|
|
207
|
-
"Governed-learning proposal evaluation is unavailable",
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
if (state === "23505" || state === "40001") {
|
|
211
|
-
throw new GovernedLearningEvaluationConflictError(
|
|
212
|
-
"Governed-learning proposal evaluation conflicted",
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
throw error;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
@@ -14,6 +14,7 @@ export function hostMcpBindingMatchesRequest(
|
|
|
14
14
|
const { hostBinding, ...connectionRef } = request.connectionRef;
|
|
15
15
|
if (
|
|
16
16
|
!hostBinding ||
|
|
17
|
+
"selection" in hostBinding ||
|
|
17
18
|
binding.status !== "active" ||
|
|
18
19
|
binding.revokedAt !== null ||
|
|
19
20
|
binding.id !== hostBinding.bindingId ||
|
package/src/host-mcp-bindings.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
IssueHostMcpDelegationRequest,
|
|
11
11
|
HostMcpAcceptedAuthority,
|
|
12
12
|
HostMcpOwnerSubject,
|
|
13
|
+
hostMcpBindingMatchesSelection,
|
|
13
14
|
} from "@opengeni/contracts/host-mcp-bindings";
|
|
14
15
|
import { rawRows, withWorkspaceSubjectRls, type Database } from "./database";
|
|
15
16
|
import { listSelfOrganizationMemberships } from "./organization-membership-lifecycle";
|
|
@@ -732,10 +733,14 @@ async function resolveDirectHostMcpUseAuthority(
|
|
|
732
733
|
(delegation.grant.mode === "session" &&
|
|
733
734
|
(delegation.grant.sessionId !== request.sessionId ||
|
|
734
735
|
delegation.grant.expectedAuthorityEpoch !== a.targetSessionAuthorityEpoch)) ||
|
|
736
|
+
binding.status !== "active" ||
|
|
737
|
+
binding.revokedAt !== null ||
|
|
735
738
|
binding.authorizationRevision !== a.ownerMembershipAuthorizationRevision ||
|
|
736
739
|
binding.generation !== a.bindingGeneration ||
|
|
737
740
|
stableJson(binding.definition) !== stableJson(a.definition) ||
|
|
738
|
-
!
|
|
741
|
+
!(request.connectionRef.hostBinding && "selection" in request.connectionRef.hostBinding
|
|
742
|
+
? request.credentialTarget === "mcp" && hostMcpBindingMatchesSelection(binding, request)
|
|
743
|
+
: hostMcpBindingMatchesRequest(binding, request))
|
|
739
744
|
)
|
|
740
745
|
return false;
|
|
741
746
|
const live = await getHostMcpLiveAttempt(
|
|
@@ -752,3 +757,21 @@ async function resolveDirectHostMcpUseAuthority(
|
|
|
752
757
|
);
|
|
753
758
|
});
|
|
754
759
|
}
|
|
760
|
+
|
|
761
|
+
/** Resolve a configuration selector from the exact live accepted turn, never
|
|
762
|
+
* registry inventory or session creator metadata. The broker still revalidates
|
|
763
|
+
* the resulting concrete reference around resolution and every physical use. */
|
|
764
|
+
export async function resolveAcceptedHostMcpBinding(
|
|
765
|
+
db: Database,
|
|
766
|
+
request: McpCredentialsRequest,
|
|
767
|
+
): Promise<McpCredentialsRequest["connectionRef"] | null> {
|
|
768
|
+
if (!request.connectionRef.hostBinding || !("selection" in request.connectionRef.hostBinding))
|
|
769
|
+
return null;
|
|
770
|
+
const authority = await resolveDirectHostMcpUseAuthority(db, request);
|
|
771
|
+
return authority
|
|
772
|
+
? {
|
|
773
|
+
...authority.definition.connectionRef,
|
|
774
|
+
hostBinding: { bindingId: authority.bindingId, generation: authority.bindingGeneration },
|
|
775
|
+
}
|
|
776
|
+
: null;
|
|
777
|
+
}
|