@opengeni/core 3.0.1 → 3.1.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/README.md +4 -4
- package/dist/access/index.d.ts +131 -7
- package/dist/access/service-initiator.d.ts +3 -0
- package/dist/application/archived-session-imports.d.ts +6 -0
- package/dist/application/composer-submit.d.ts +1 -1
- package/dist/application/connect-operation.d.ts +6 -0
- package/dist/application/external-workspace-members.d.ts +13 -1
- package/dist/application/modal-native-live-declaration-join.d.ts +37 -0
- package/dist/application/modal-native-original-configuration.d.ts +79 -0
- package/dist/application/modal-native-worker-host-transport.d.ts +297 -0
- package/dist/application/new-session-drafts.d.ts +29 -2
- package/dist/application/organization-privacy.d.ts +57 -0
- package/dist/application/prepared-mcp-connection.d.ts +31 -0
- package/dist/application/sandbox-recovery.d.ts +58 -0
- package/dist/application/session-commands.d.ts +3 -0
- package/dist/application/session-tenancy.d.ts +13 -4
- package/dist/billing/agent-run-admission.d.ts +21 -0
- package/dist/billing/limits.d.ts +27 -1
- package/dist/billing/model-call-admission.d.ts +34 -0
- package/dist/billing/model-usage-settlement.d.ts +112 -0
- package/dist/canonical-human-identities.js +1 -1
- package/dist/{chunk-VCZFFEPB.js → chunk-7T2SZ7BI.js} +16 -2
- package/dist/chunk-7T2SZ7BI.js.map +1 -0
- package/dist/{chunk-ZYCNLYGH.js → chunk-OMJFIIWK.js} +178 -12
- package/dist/chunk-OMJFIIWK.js.map +1 -0
- package/dist/{chunk-H7X52RI2.js → chunk-QI2M2B2K.js} +83 -8
- package/dist/chunk-QI2M2B2K.js.map +1 -0
- package/dist/{chunk-OF65T3PM.js → chunk-TTWTXQB2.js} +94 -6
- package/dist/chunk-TTWTXQB2.js.map +1 -0
- package/dist/codex-model-availability.d.ts +55 -0
- package/dist/default-session-model.d.ts +192 -0
- package/dist/dependencies.d.ts +41 -7
- package/dist/developer-setup-credential-ceiling.d.ts +10 -0
- package/dist/domain/agent-config-resolution.d.ts +49 -0
- package/dist/domain/automations.d.ts +8 -1
- package/dist/domain/capabilities.d.ts +112 -8
- package/dist/domain/connection-account-selection-error.d.ts +8 -0
- package/dist/domain/connector-tool-permissions.d.ts +62 -0
- package/dist/domain/direct-model-provider.d.ts +3 -0
- package/dist/domain/editable-artifacts/agent-application.d.ts +10 -1
- package/dist/domain/editable-artifacts/durable-export.d.ts +22 -1
- package/dist/domain/editable-artifacts/ports.d.ts +10 -0
- package/dist/domain/editable-artifacts/service.d.ts +3 -1
- package/dist/domain/editable-artifacts/types.d.ts +1 -1
- package/dist/domain/encryption-key.d.ts +12 -0
- package/dist/domain/insights-timing.d.ts +16 -0
- package/dist/domain/insights-usage.d.ts +27 -0
- package/dist/domain/insights.d.ts +21 -3
- package/dist/domain/knowledge-files.d.ts +1 -1
- package/dist/domain/knowledge-preparation.d.ts +10 -0
- package/dist/domain/knowledge-preparation.test.d.ts +1 -0
- package/dist/domain/knowledge-search.d.ts +15 -76
- package/dist/domain/mcp-account-bindings.d.ts +17 -0
- package/dist/domain/mcp-account-routes.d.ts +21 -0
- package/dist/domain/native-mcp-connection-admission.d.ts +3 -0
- package/dist/domain/organization-integration-catalog.d.ts +15 -0
- package/dist/domain/personal-connection-delegations.d.ts +73 -12
- package/dist/domain/pr-review.d.ts +121 -6
- package/dist/domain/realtime-voice-billing.d.ts +103 -0
- package/dist/domain/resources.d.ts +17 -1
- package/dist/domain/scheduled-task-access.d.ts +265 -0
- package/dist/domain/scheduled-task-account-audit.d.ts +27 -0
- package/dist/domain/scheduled-task-audience.d.ts +70 -0
- package/dist/domain/scheduled-tasks.d.ts +61 -18
- package/dist/domain/session-connection-accounts.d.ts +53 -0
- package/dist/domain/session-file-access.d.ts +4 -0
- package/dist/domain/session-native-connection-replacement.d.ts +14 -0
- package/dist/domain/session-start-timing.d.ts +7 -0
- package/dist/domain/session-tool-policy.d.ts +102 -4
- package/dist/domain/sessions.d.ts +179 -15
- package/dist/domain/site-audience.d.ts +52 -0
- package/dist/domain/skills.d.ts +29 -1
- package/dist/domain/slack-bot.d.ts +46 -1
- package/dist/domain/slack-publication-secret-safety.d.ts +1 -1
- package/dist/domain/vaults/index.d.ts +7 -0
- package/dist/domain/vaults/mapping.d.ts +25 -0
- package/dist/domain/vaults/provider-registry.d.ts +72 -0
- package/dist/domain/vaults/provider-sync.d.ts +176 -0
- package/dist/domain/vaults/provider-writes.d.ts +57 -0
- package/dist/domain/vaults/secret-resolver.d.ts +34 -0
- package/dist/domain/vaults/types.d.ts +24 -0
- package/dist/domain/vaults/vault-service.d.ts +35 -0
- package/dist/domain/voice-input-billing.d.ts +77 -0
- package/dist/domain/web-search-billing.d.ts +51 -0
- package/dist/domain/workspace-credits.d.ts +7 -0
- package/dist/editable-artifact-live/application.d.ts +2 -1
- package/dist/editable-artifact-live/server.d.ts +5 -1
- package/dist/editable-artifact-live/ticket.d.ts +2 -1
- package/dist/editable-artifact-live/types.d.ts +9 -0
- package/dist/editable-artifact-live.js +2 -2
- package/dist/editable-artifacts.js +10 -2
- package/dist/goal-admission.d.ts +627 -0
- package/dist/host-session-usage-limit.d.ts +15 -0
- package/dist/index.d.ts +43 -8
- package/dist/index.js +22682 -11743
- package/dist/index.js.map +1 -1
- package/dist/managed-session.d.ts +110 -7
- package/dist/model-call.d.ts +58 -0
- package/dist/model-catalog.d.ts +29 -3
- package/dist/model-provider-credentials.d.ts +43 -0
- package/dist/product-usage-metrics.d.ts +30 -0
- package/dist/rigs/index.d.ts +1 -13
- package/dist/sandbox/fleet.d.ts +58 -4
- package/dist/sandbox/routing.d.ts +42 -2
- package/dist/sandbox/runtime-settings.d.ts +27 -17
- package/dist/session-authorization.d.ts +10 -0
- package/dist/session-usage-limit-metrics.d.ts +60 -0
- package/dist/session-usage-limits.d.ts +89 -0
- package/dist/site-agent-clamp.d.ts +71 -0
- package/dist/site-session-origin.d.ts +39 -2
- package/dist/transcription.d.ts +120 -3
- package/dist/turn-surface.d.ts +20 -0
- package/dist/user-presence.d.ts +32 -0
- package/dist/workflow-wake-contract.d.ts +13 -1
- package/package.json +15 -22
- package/src/access/index.ts +961 -38
- package/src/access/service-initiator.ts +50 -0
- package/src/application/api-integration-servers.ts +9 -0
- package/src/application/archived-session-imports.ts +161 -0
- package/src/application/composer-submit.ts +3 -2
- package/src/application/connect-authority.ts +3 -3
- package/src/application/connect-operation.ts +37 -3
- package/src/application/external-continuation.ts +20 -3
- package/src/application/external-link-work-admission.ts +3 -0
- package/src/application/external-workspace-members.ts +51 -3
- package/src/application/modal-native-live-declaration-join.ts +196 -0
- package/src/application/modal-native-original-configuration.ts +261 -0
- package/src/application/modal-native-worker-host-transport.ts +263 -0
- package/src/application/new-session-drafts.ts +172 -55
- package/src/application/organization-privacy.ts +161 -0
- package/src/application/prepared-mcp-connection.ts +133 -0
- package/src/application/sandbox-recovery.ts +78 -0
- package/src/application/session-commands.ts +73 -1
- package/src/application/session-mcp-credential-rotation.ts +61 -18
- package/src/application/session-tenancy.ts +262 -44
- package/src/application/user-resource-grants.ts +1 -29
- package/src/billing/agent-run-admission.ts +117 -0
- package/src/billing/limits.ts +138 -14
- package/src/billing/model-call-admission.ts +118 -0
- package/src/billing/model-usage-settlement.ts +472 -0
- package/src/codex-model-availability.ts +264 -0
- package/src/default-session-model.ts +692 -0
- package/src/dependencies.ts +48 -6
- package/src/developer-setup-credential-ceiling.ts +60 -0
- package/src/domain/agent-config-resolution.ts +148 -0
- package/src/domain/automations.ts +76 -2
- package/src/domain/capabilities.ts +830 -181
- package/src/domain/connection-account-selection-error.ts +19 -0
- package/src/domain/connector-tool-permissions.ts +572 -0
- package/src/domain/direct-model-provider.ts +62 -0
- package/src/domain/editable-artifacts/agent-application.ts +44 -6
- package/src/domain/editable-artifacts/durable-export.ts +100 -5
- package/src/domain/editable-artifacts/in-memory.ts +6 -0
- package/src/domain/editable-artifacts/ports.ts +11 -0
- package/src/domain/editable-artifacts/service.ts +20 -0
- package/src/domain/editable-artifacts/types.ts +1 -1
- package/src/domain/encryption-key.ts +26 -0
- package/src/domain/file-owner.ts +16 -1
- package/src/domain/github-action-policies.ts +1 -1
- package/src/domain/github-skill-source.ts +3 -1
- package/src/domain/insights-timing.ts +95 -0
- package/src/domain/insights-usage.ts +67 -0
- package/src/domain/insights.ts +167 -43
- package/src/domain/knowledge-files.ts +10 -3
- package/src/domain/knowledge-messages.ts +1 -0
- package/src/domain/knowledge-preparation.test.ts +276 -0
- package/src/domain/knowledge-preparation.ts +147 -0
- package/src/domain/knowledge-search.test.ts +617 -5
- package/src/domain/knowledge-search.ts +240 -23
- package/src/domain/mcp-account-bindings.ts +169 -0
- package/src/domain/mcp-account-routes.ts +152 -0
- package/src/domain/native-mcp-connection-admission.ts +14 -0
- package/src/domain/organization-integration-catalog.ts +88 -0
- package/src/domain/personal-connection-delegations.ts +399 -257
- package/src/domain/pr-review.ts +24 -8
- package/src/domain/realtime-voice-billing.ts +425 -0
- package/src/domain/resources.ts +50 -4
- package/src/domain/scheduled-task-access.ts +1089 -0
- package/src/domain/scheduled-task-account-audit.ts +90 -0
- package/src/domain/scheduled-task-audience.ts +249 -0
- package/src/domain/scheduled-tasks.ts +1313 -374
- package/src/domain/session-connection-accounts.ts +239 -0
- package/src/domain/session-file-access.ts +47 -0
- package/src/domain/session-native-connection-replacement.ts +64 -0
- package/src/domain/session-start-timing.ts +60 -0
- package/src/domain/session-tool-policy.ts +489 -14
- package/src/domain/sessions.ts +2254 -717
- package/src/domain/site-audience.ts +130 -0
- package/src/domain/skills.ts +9 -0
- package/src/domain/slack-bot.ts +141 -4
- package/src/domain/slack-publication-secret-safety.ts +1 -1
- package/src/domain/vaults/index.ts +19 -0
- package/src/domain/vaults/mapping.ts +190 -0
- package/src/domain/vaults/provider-registry.ts +201 -0
- package/src/domain/vaults/provider-sync.ts +531 -0
- package/src/domain/vaults/provider-writes.ts +288 -0
- package/src/domain/vaults/secret-resolver.ts +464 -0
- package/src/domain/vaults/types.ts +26 -0
- package/src/domain/vaults/vault-service.ts +1316 -0
- package/src/domain/voice-input-billing.ts +330 -0
- package/src/domain/web-search-billing.ts +169 -0
- package/src/domain/workspace-credits.ts +13 -0
- package/src/editable-artifact-live/application.ts +8 -1
- package/src/editable-artifact-live/server.ts +154 -6
- package/src/editable-artifact-live/ticket.ts +86 -1
- package/src/editable-artifact-live/types.ts +10 -0
- package/src/goal-admission.ts +220 -0
- package/src/host-session-usage-limit.ts +25 -0
- package/src/index.ts +75 -11
- package/src/managed-session.ts +137 -6
- package/src/model-call.ts +79 -0
- package/src/model-catalog.ts +345 -91
- package/src/model-provider-credentials.ts +394 -0
- package/src/product-usage-metrics.ts +101 -0
- package/src/rigs/index.ts +23 -111
- package/src/sandbox/fleet.ts +253 -28
- package/src/sandbox/routing.ts +204 -43
- package/src/sandbox/runtime-settings.ts +159 -85
- package/src/session-authorization.ts +172 -23
- package/src/session-usage-limit-metrics.ts +95 -0
- package/src/session-usage-limits.ts +415 -0
- package/src/site-agent-clamp.ts +226 -0
- package/src/site-session-origin.ts +84 -5
- package/src/transcription.ts +123 -4
- package/src/turn-surface.ts +47 -0
- package/src/user-presence.ts +134 -0
- package/src/workflow-wake-contract.ts +14 -1
- package/dist/application/host-mcp-owner.d.ts +0 -6
- package/dist/application/host-mcp-resolvers.d.ts +0 -13
- package/dist/chunk-5WZYFL45.js +0 -266
- package/dist/chunk-5WZYFL45.js.map +0 -1
- package/dist/chunk-H7X52RI2.js.map +0 -1
- package/dist/chunk-OF65T3PM.js.map +0 -1
- package/dist/chunk-VCZFFEPB.js.map +0 -1
- package/dist/chunk-ZYCNLYGH.js.map +0 -1
- package/dist/domain/connection-authority.d.ts +0 -97
- package/dist/domain/connection-authority.js +0 -169
- package/dist/domain/connection-authority.js.map +0 -1
- package/dist/domain/environments.d.ts +0 -33
- package/dist/domain/host-mcp-authority-source-admission.d.ts +0 -10
- package/dist/domain/host-mcp-task-admission.d.ts +0 -18
- package/dist/domain/packs.d.ts +0 -59
- package/dist/domain/product-integration-pack.d.ts +0 -44
- package/dist/domain/product-integration-skill.gen.d.ts +0 -5
- package/dist/remote-mcp-credentials.d.ts +0 -23
- package/dist/remote-mcp-credentials.js +0 -11
- package/dist/remote-mcp-credentials.js.map +0 -1
- package/src/application/host-mcp-owner.ts +0 -41
- package/src/application/host-mcp-resolvers.ts +0 -84
- package/src/domain/connection-authority.ts +0 -329
- package/src/domain/environments.ts +0 -120
- package/src/domain/host-mcp-authority-source-admission.ts +0 -25
- package/src/domain/host-mcp-task-admission.ts +0 -121
- package/src/domain/packs.ts +0 -699
- package/src/domain/product-integration-pack.ts +0 -42
- package/src/domain/product-integration-skill.gen.ts +0 -52
- package/src/remote-mcp-credentials.ts +0 -361
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import type { Settings } from "@opengeni/config";
|
|
2
|
+
import type {
|
|
3
|
+
OrganizationPrivacySettings,
|
|
4
|
+
UpdateOrganizationPrivacySettingsRequest,
|
|
5
|
+
} from "@opengeni/contracts";
|
|
6
|
+
import {
|
|
7
|
+
getOrganizationPrivacyPreferences,
|
|
8
|
+
getPrivateSessionCreatePolicy,
|
|
9
|
+
SessionTenancyNotActivatedError,
|
|
10
|
+
updateOrganizationPrivacyPreferences,
|
|
11
|
+
type Database,
|
|
12
|
+
type OrganizationPrivacyPreferences,
|
|
13
|
+
} from "@opengeni/db";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Privacy: who can see chats, schedules and their runs. An organization's
|
|
17
|
+
* owners and admins choose whether Only me is allowed (the private session
|
|
18
|
+
* setting), the default audience for new chats and schedules, and whether
|
|
19
|
+
* admins see that other people's Only me schedules exist. A deployment, such
|
|
20
|
+
* as a product that embeds Opengeni, can only narrow those choices. See
|
|
21
|
+
* docs/design/privacy-audiences-2026-10-11.md.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export type PrivacySettings = Pick<
|
|
25
|
+
Settings,
|
|
26
|
+
| "privacyOnlyMeAllowed"
|
|
27
|
+
| "privacyWorkspaceDefaultRequired"
|
|
28
|
+
| "privacyAdminScheduleSummariesRequired"
|
|
29
|
+
>;
|
|
30
|
+
|
|
31
|
+
export type PrivacyDeploymentLimits = OrganizationPrivacySettings["limits"];
|
|
32
|
+
|
|
33
|
+
export function privacyDeploymentLimits(settings: PrivacySettings): PrivacyDeploymentLimits {
|
|
34
|
+
const onlyMeAllowed = settings.privacyOnlyMeAllowed !== false;
|
|
35
|
+
return {
|
|
36
|
+
onlyMeAllowed,
|
|
37
|
+
// Without Only me, Workspace is the only possible default.
|
|
38
|
+
defaultVisibilityLocked: !onlyMeAllowed || settings.privacyWorkspaceDefaultRequired === true,
|
|
39
|
+
adminsSeePrivateSchedulesLocked: settings.privacyAdminScheduleSummariesRequired === true,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type EffectiveOrganizationPrivacy = {
|
|
44
|
+
defaultVisibility: "private" | "workspace";
|
|
45
|
+
adminsSeePrivateSchedules: boolean;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** The organization's choices with the deployment's limits applied. */
|
|
49
|
+
export function effectiveOrganizationPrivacy(
|
|
50
|
+
preferences: Pick<
|
|
51
|
+
OrganizationPrivacyPreferences,
|
|
52
|
+
"defaultVisibility" | "adminsSeePrivateSchedules"
|
|
53
|
+
>,
|
|
54
|
+
limits: PrivacyDeploymentLimits,
|
|
55
|
+
): EffectiveOrganizationPrivacy {
|
|
56
|
+
return {
|
|
57
|
+
defaultVisibility:
|
|
58
|
+
!limits.defaultVisibilityLocked && preferences.defaultVisibility === "user_private"
|
|
59
|
+
? "private"
|
|
60
|
+
: "workspace",
|
|
61
|
+
adminsSeePrivateSchedules:
|
|
62
|
+
limits.adminsSeePrivateSchedulesLocked || preferences.adminsSeePrivateSchedules,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function resolveOrganizationPrivacy(
|
|
67
|
+
deps: { db: Database; settings: PrivacySettings },
|
|
68
|
+
accountId: string,
|
|
69
|
+
): Promise<EffectiveOrganizationPrivacy> {
|
|
70
|
+
return effectiveOrganizationPrivacy(
|
|
71
|
+
await getOrganizationPrivacyPreferences(deps.db, accountId),
|
|
72
|
+
privacyDeploymentLimits(deps.settings),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Refuse a new Only me chat or schedule in a shared workspace when this
|
|
78
|
+
* deployment turns Only me off. A Personal workspace is only ever its owner's,
|
|
79
|
+
* so it is unaffected. Existing Only me items keep their audience: narrowing
|
|
80
|
+
* is always allowed, widening never happens silently.
|
|
81
|
+
*/
|
|
82
|
+
export async function requireDeploymentAllowsOnlyMe(
|
|
83
|
+
deps: { db: Database; settings: PrivacySettings },
|
|
84
|
+
input: { workspaceId: string; actorSubjectId: string },
|
|
85
|
+
): Promise<void> {
|
|
86
|
+
if (privacyDeploymentLimits(deps.settings).onlyMeAllowed) return;
|
|
87
|
+
const policy = await getPrivateSessionCreatePolicy(deps.db, input).catch(() => null);
|
|
88
|
+
if (!policy?.personalWorkspace) throw new SessionTenancyNotActivatedError();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Raised when a change would go past what this deployment allows. */
|
|
92
|
+
export class OrganizationPrivacyLockedError extends Error {
|
|
93
|
+
readonly name = "OrganizationPrivacyLockedError";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function settingsResponse(
|
|
97
|
+
organizationId: string,
|
|
98
|
+
preferences: OrganizationPrivacyPreferences,
|
|
99
|
+
limits: PrivacyDeploymentLimits,
|
|
100
|
+
): OrganizationPrivacySettings {
|
|
101
|
+
return {
|
|
102
|
+
organizationId,
|
|
103
|
+
...effectiveOrganizationPrivacy(preferences, limits),
|
|
104
|
+
limits,
|
|
105
|
+
updatedAt: preferences.updatedAt,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function getOrganizationPrivacySettings(
|
|
110
|
+
deps: { db: Database; settings: PrivacySettings },
|
|
111
|
+
organizationId: string,
|
|
112
|
+
): Promise<OrganizationPrivacySettings> {
|
|
113
|
+
return settingsResponse(
|
|
114
|
+
organizationId,
|
|
115
|
+
await getOrganizationPrivacyPreferences(deps.db, organizationId),
|
|
116
|
+
privacyDeploymentLimits(deps.settings),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Change the organization's privacy choices. Only an owner or admin may, and
|
|
122
|
+
* never past the deployment's limits.
|
|
123
|
+
*/
|
|
124
|
+
export async function updateOrganizationPrivacySettings(
|
|
125
|
+
deps: { db: Database; settings: PrivacySettings },
|
|
126
|
+
input: {
|
|
127
|
+
organizationId: string;
|
|
128
|
+
actorSubjectId: string;
|
|
129
|
+
request: UpdateOrganizationPrivacySettingsRequest;
|
|
130
|
+
},
|
|
131
|
+
): Promise<OrganizationPrivacySettings> {
|
|
132
|
+
const limits = privacyDeploymentLimits(deps.settings);
|
|
133
|
+
if (input.request.defaultVisibility === "private" && limits.defaultVisibilityLocked) {
|
|
134
|
+
throw new OrganizationPrivacyLockedError(
|
|
135
|
+
limits.onlyMeAllowed
|
|
136
|
+
? "New chats and schedules always start as Workspace here."
|
|
137
|
+
: "Only me is turned off here.",
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
if (input.request.adminsSeePrivateSchedules === false && limits.adminsSeePrivateSchedulesLocked) {
|
|
141
|
+
throw new OrganizationPrivacyLockedError(
|
|
142
|
+
"Admins always see that Only me schedules exist here.",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
const preferences = await updateOrganizationPrivacyPreferences(deps.db, {
|
|
146
|
+
organizationId: input.organizationId,
|
|
147
|
+
actorSubjectId: input.actorSubjectId,
|
|
148
|
+
...(input.request.defaultVisibility
|
|
149
|
+
? {
|
|
150
|
+
defaultVisibility:
|
|
151
|
+
input.request.defaultVisibility === "private"
|
|
152
|
+
? ("user_private" as const)
|
|
153
|
+
: ("workspace_shared" as const),
|
|
154
|
+
}
|
|
155
|
+
: {}),
|
|
156
|
+
...(input.request.adminsSeePrivateSchedules !== undefined
|
|
157
|
+
? { adminsSeePrivateSchedules: input.request.adminsSeePrivateSchedules }
|
|
158
|
+
: {}),
|
|
159
|
+
});
|
|
160
|
+
return settingsResponse(input.organizationId, preferences, limits);
|
|
161
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
3
|
+
import {
|
|
4
|
+
CapabilityCatalogItem,
|
|
5
|
+
McpServerConnectionRef,
|
|
6
|
+
stableJson,
|
|
7
|
+
type AccessGrant,
|
|
8
|
+
type ConnectionMetadata,
|
|
9
|
+
} from "@opengeni/contracts";
|
|
10
|
+
import type { ConnectOwnership, PreparedMcpSetup } from "@opengeni/contracts/connect";
|
|
11
|
+
import type { Settings } from "@opengeni/config";
|
|
12
|
+
import { getCapabilityCatalogItem, type Database } from "@opengeni/db";
|
|
13
|
+
import { HTTPException } from "hono/http-exception";
|
|
14
|
+
import {
|
|
15
|
+
createCatalogItem,
|
|
16
|
+
prepareCapabilityEnable,
|
|
17
|
+
validateMcpCapabilityConnection,
|
|
18
|
+
type McpCapabilityProbe,
|
|
19
|
+
} from "../domain/capabilities";
|
|
20
|
+
|
|
21
|
+
/** Initialize/list tools before committing anything. Secret headers stay in this
|
|
22
|
+
* invocation; the installation contains only an ordinary native connection ref.
|
|
23
|
+
* The caller commits the connection and this installation in one authorized
|
|
24
|
+
* Connect operation, so retries cannot leave an orphan credential or catalog. */
|
|
25
|
+
export async function prepareMcpConnectionActivation(input: {
|
|
26
|
+
grant: AccessGrant;
|
|
27
|
+
settings: Settings;
|
|
28
|
+
setup: PreparedMcpSetup;
|
|
29
|
+
ownership: ConnectOwnership;
|
|
30
|
+
headers: Record<string, string>;
|
|
31
|
+
probe?: McpCapabilityProbe;
|
|
32
|
+
}) {
|
|
33
|
+
const { grant, settings, ownership } = input;
|
|
34
|
+
const setup = structuredClone(input.setup);
|
|
35
|
+
const endpoint = new URL(setup.endpointUrl).toString();
|
|
36
|
+
const key = createHash("sha256")
|
|
37
|
+
.update(stableJson({ endpoint, ownership }))
|
|
38
|
+
.digest("hex")
|
|
39
|
+
.slice(0, 32);
|
|
40
|
+
const capabilityId = `mcp:custom-${key}`;
|
|
41
|
+
const item = CapabilityCatalogItem.parse({
|
|
42
|
+
id: capabilityId,
|
|
43
|
+
kind: "mcp",
|
|
44
|
+
source: "manual",
|
|
45
|
+
name: setup.name,
|
|
46
|
+
endpointUrl: endpoint,
|
|
47
|
+
authModel: "native_connection",
|
|
48
|
+
runtime: { available: true, mcpServerId: `custom-${key}` },
|
|
49
|
+
metadata: { mcpServerId: `custom-${key}` },
|
|
50
|
+
});
|
|
51
|
+
const connectivity = await validateMcpCapabilityConnection(
|
|
52
|
+
item,
|
|
53
|
+
input.probe,
|
|
54
|
+
{ ...input.headers },
|
|
55
|
+
settings,
|
|
56
|
+
);
|
|
57
|
+
return {
|
|
58
|
+
capabilityId,
|
|
59
|
+
commit: async (db: Database, connection: ConnectionMetadata) => {
|
|
60
|
+
const existing = await getCapabilityCatalogItem(db, grant.workspaceId, capabilityId);
|
|
61
|
+
if (existing && (existing.kind !== "mcp" || existing.endpointUrl !== endpoint))
|
|
62
|
+
throw new HTTPException(409, { message: "MCP catalog definition changed; reload setup" });
|
|
63
|
+
if (!existing)
|
|
64
|
+
await createCatalogItem({
|
|
65
|
+
db,
|
|
66
|
+
accountId: grant.accountId,
|
|
67
|
+
workspaceId: grant.workspaceId,
|
|
68
|
+
payload: {
|
|
69
|
+
id: capabilityId,
|
|
70
|
+
kind: "mcp",
|
|
71
|
+
source: "manual",
|
|
72
|
+
name: setup.name,
|
|
73
|
+
endpointUrl: endpoint,
|
|
74
|
+
authModel: "native_connection",
|
|
75
|
+
category: "custom",
|
|
76
|
+
tags: [],
|
|
77
|
+
metadata: item.metadata,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
const connectionRef: McpServerConnectionRef = {
|
|
81
|
+
providerDomain: new URL(endpoint).hostname,
|
|
82
|
+
subjectScope: ownership === "personal" ? "subject" : "workspace",
|
|
83
|
+
accountSelection: "all_eligible",
|
|
84
|
+
kind: "api_key",
|
|
85
|
+
resource: endpoint,
|
|
86
|
+
};
|
|
87
|
+
const prepared = await prepareCapabilityEnable({
|
|
88
|
+
db,
|
|
89
|
+
grant,
|
|
90
|
+
accountId: grant.accountId,
|
|
91
|
+
workspaceId: grant.workspaceId,
|
|
92
|
+
settings,
|
|
93
|
+
capabilityId,
|
|
94
|
+
payload: {
|
|
95
|
+
onlyIfUninstalled: true,
|
|
96
|
+
config: {},
|
|
97
|
+
headers: {},
|
|
98
|
+
metadata: {},
|
|
99
|
+
connectionRef,
|
|
100
|
+
},
|
|
101
|
+
verifiedConnection: {
|
|
102
|
+
id: connection.id,
|
|
103
|
+
version: connection.version,
|
|
104
|
+
endpointUrl: endpoint,
|
|
105
|
+
connectivity,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
const installation = await prepared.commit(db);
|
|
109
|
+
const savedRef = McpServerConnectionRef.safeParse(installation.config.connectionRef);
|
|
110
|
+
const retainedExactPin =
|
|
111
|
+
savedRef.success &&
|
|
112
|
+
savedRef.data.authoritySource !== "host" &&
|
|
113
|
+
savedRef.data.connectionId === connection.id &&
|
|
114
|
+
savedRef.data.providerDomain === connectionRef.providerDomain &&
|
|
115
|
+
(savedRef.data.subjectScope ?? "workspace") === connectionRef.subjectScope &&
|
|
116
|
+
(savedRef.data.kind ?? connection.kind) === connection.kind &&
|
|
117
|
+
(!savedRef.data.resource || savedRef.data.resource === endpoint);
|
|
118
|
+
// onlyIfUninstalled preserves existing account choices. Do not report a
|
|
119
|
+
// newly saved account as usable when a different binding was preserved;
|
|
120
|
+
// throw inside Connect's transaction so its credential also rolls back.
|
|
121
|
+
// Reconnecting the exact pinned account keeps the pin and its restrictions.
|
|
122
|
+
if (
|
|
123
|
+
installation.status !== "active" ||
|
|
124
|
+
(!retainedExactPin && !isDeepStrictEqual(installation.config.connectionRef, connectionRef))
|
|
125
|
+
) {
|
|
126
|
+
throw new HTTPException(409, {
|
|
127
|
+
message: "MCP account selection changed; review the existing connection before setup",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return installation;
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { SandboxRecoveryRequest } from "@opengeni/contracts";
|
|
2
|
+
import { consentPublicSandboxRecovery, readPublicSandboxRecovery } from "@opengeni/db";
|
|
3
|
+
import { requirePermission, type AccessGrantAuthorization } from "../access";
|
|
4
|
+
import type { AppDependencies } from "../dependencies";
|
|
5
|
+
import { requireSessionAuthorization } from "../session-authorization";
|
|
6
|
+
import { requireCanonicalManagedHuman } from "./session-tenancy";
|
|
7
|
+
|
|
8
|
+
type Dependencies = Pick<AppDependencies, "db" | "sessionAuthorization">;
|
|
9
|
+
|
|
10
|
+
/** The read-only projection also serves the exact built-in local human. Its
|
|
11
|
+
* provenance stamp is set only by the local resolver, never a bearer claim. */
|
|
12
|
+
export function requireSandboxRecoveryPreviewHuman(
|
|
13
|
+
authorization: AccessGrantAuthorization,
|
|
14
|
+
workspaceId: string,
|
|
15
|
+
): void {
|
|
16
|
+
if (
|
|
17
|
+
authorization.canonicalLocalHumanSession &&
|
|
18
|
+
authorization.contextIntegrity &&
|
|
19
|
+
authorization.authenticatedSubjectId === authorization.grant.subjectId &&
|
|
20
|
+
authorization.grant.workspaceId === workspaceId &&
|
|
21
|
+
authorization.grant.subjectId === "dev" &&
|
|
22
|
+
authorization.grant.principalKind === "human_session" &&
|
|
23
|
+
authorization.grant.metadata?.delegated !== true &&
|
|
24
|
+
!authorization.grant.serviceInitiator
|
|
25
|
+
)
|
|
26
|
+
return;
|
|
27
|
+
requireCanonicalManagedHuman(authorization, workspaceId);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function authorize(
|
|
31
|
+
deps: Dependencies,
|
|
32
|
+
authorization: AccessGrantAuthorization,
|
|
33
|
+
workspaceId: string,
|
|
34
|
+
sessionId: string,
|
|
35
|
+
intent: "preview" | "consent",
|
|
36
|
+
) {
|
|
37
|
+
// Cookie-verification stamp, never principal-shape inference or an actor from
|
|
38
|
+
// the body. The local human may only read; agents, API keys and delegated
|
|
39
|
+
// humans cannot accept data loss or broaden the consent mutation.
|
|
40
|
+
if (intent === "consent") requireCanonicalManagedHuman(authorization, workspaceId);
|
|
41
|
+
else requireSandboxRecoveryPreviewHuman(authorization, workspaceId);
|
|
42
|
+
requirePermission(authorization.grant, "sessions:read");
|
|
43
|
+
requirePermission(authorization.grant, "sessions:control");
|
|
44
|
+
await requireSessionAuthorization(deps, authorization.grant, {
|
|
45
|
+
sessionId,
|
|
46
|
+
operation: "session.control",
|
|
47
|
+
surface: "core",
|
|
48
|
+
});
|
|
49
|
+
return {
|
|
50
|
+
accountId: authorization.grant.accountId,
|
|
51
|
+
workspaceId,
|
|
52
|
+
sessionId,
|
|
53
|
+
subjectId: authorization.grant.subjectId,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function getManagedHumanSandboxRecovery(
|
|
58
|
+
deps: Dependencies,
|
|
59
|
+
authorization: AccessGrantAuthorization,
|
|
60
|
+
workspaceId: string,
|
|
61
|
+
sessionId: string,
|
|
62
|
+
) {
|
|
63
|
+
return readPublicSandboxRecovery(
|
|
64
|
+
deps.db,
|
|
65
|
+
await authorize(deps, authorization, workspaceId, sessionId, "preview"),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function consentManagedHumanSandboxRecovery(
|
|
70
|
+
deps: Dependencies,
|
|
71
|
+
authorization: AccessGrantAuthorization,
|
|
72
|
+
workspaceId: string,
|
|
73
|
+
sessionId: string,
|
|
74
|
+
request: SandboxRecoveryRequest,
|
|
75
|
+
) {
|
|
76
|
+
const scope = await authorize(deps, authorization, workspaceId, sessionId, "consent");
|
|
77
|
+
return consentPublicSandboxRecovery(deps.db, { ...scope, request });
|
|
78
|
+
}
|
|
@@ -17,7 +17,10 @@ import type {
|
|
|
17
17
|
WorkspaceInferenceControlResponse,
|
|
18
18
|
} from "@opengeni/contracts";
|
|
19
19
|
import { DraftTimelineAnnotations } from "@opengeni/contracts";
|
|
20
|
+
import type { Settings } from "@opengeni/config";
|
|
21
|
+
import { HTTPException } from "hono/http-exception";
|
|
20
22
|
import {
|
|
23
|
+
checkWorkspaceAllowance,
|
|
21
24
|
deleteSessionQueueItemInTransaction,
|
|
22
25
|
editQueuedTurnInTransaction,
|
|
23
26
|
getComposerDraftInTransaction,
|
|
@@ -25,6 +28,8 @@ import {
|
|
|
25
28
|
getSessionEvent,
|
|
26
29
|
getWorkspaceControlEvent,
|
|
27
30
|
getSessionQueueSnapshot,
|
|
31
|
+
frozenInitiatorForCommandActor,
|
|
32
|
+
isCodexBilledTurn,
|
|
28
33
|
moveQueuedTurnInTransaction,
|
|
29
34
|
mutateSessionControlInTransaction,
|
|
30
35
|
mutateWorkspaceControlInTransaction,
|
|
@@ -53,6 +58,9 @@ import {
|
|
|
53
58
|
import type { SessionWorkflowClient } from "../dependencies";
|
|
54
59
|
import { normalizeResources } from "../domain/resources";
|
|
55
60
|
import { validateDraftTimelineAnnotations } from "../domain/timeline-annotations";
|
|
61
|
+
import { initiatingHumanForAllowance } from "../domain/sessions";
|
|
62
|
+
import { coreRoutedSubscriptionFunding, modelFundingForAdmission } from "../billing/limits";
|
|
63
|
+
import { resolveWorkspaceCatalogSettings } from "../model-catalog";
|
|
56
64
|
import {
|
|
57
65
|
requireSessionAuthorization,
|
|
58
66
|
type ResolvedSessionAuthorization,
|
|
@@ -148,6 +156,63 @@ function agentActor(context: AgentSessionCommandContext) {
|
|
|
148
156
|
};
|
|
149
157
|
}
|
|
150
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Called inside the command's exact live-attempt fence, after receipt recovery
|
|
161
|
+
* but before any fresh work or Steer interruption. Attribution comes from the
|
|
162
|
+
* caller's accepted turn, never the target creator or request subject.
|
|
163
|
+
*/
|
|
164
|
+
async function assertFreshAgentCommandAllowance(
|
|
165
|
+
tx: SessionActivityDatabase,
|
|
166
|
+
deps: { settings?: Settings },
|
|
167
|
+
context: AgentSessionCommandContext,
|
|
168
|
+
targetSessionId: string,
|
|
169
|
+
): Promise<void> {
|
|
170
|
+
const target = await getSession(tx, context.workspaceId, targetSessionId);
|
|
171
|
+
if (!target || target.accountId !== context.accountId) {
|
|
172
|
+
throw new HTTPException(404, { message: "Target session not found" });
|
|
173
|
+
}
|
|
174
|
+
let fundedWithoutCredits = false;
|
|
175
|
+
const frozen = await frozenInitiatorForCommandActor(tx, context.workspaceId, agentActor(context));
|
|
176
|
+
if (deps.settings) {
|
|
177
|
+
const { settings } = await resolveWorkspaceCatalogSettings(tx, deps.settings, {
|
|
178
|
+
accountId: target.accountId,
|
|
179
|
+
workspaceId: context.workspaceId,
|
|
180
|
+
retainedProductModelId: target.model,
|
|
181
|
+
});
|
|
182
|
+
const codexBilled = await isCodexBilledTurn({
|
|
183
|
+
db: tx,
|
|
184
|
+
settings,
|
|
185
|
+
workspaceId: context.workspaceId,
|
|
186
|
+
model: target.model,
|
|
187
|
+
accountId: target.accountId,
|
|
188
|
+
subjectId: initiatingHumanForAllowance(frozen),
|
|
189
|
+
});
|
|
190
|
+
fundedWithoutCredits = modelFundingForAdmission(
|
|
191
|
+
settings,
|
|
192
|
+
target.model,
|
|
193
|
+
codexBilled,
|
|
194
|
+
await coreRoutedSubscriptionFunding(tx, settings, {
|
|
195
|
+
accountId: target.accountId,
|
|
196
|
+
workspaceId: context.workspaceId,
|
|
197
|
+
model: target.model,
|
|
198
|
+
subjectId: initiatingHumanForAllowance(frozen),
|
|
199
|
+
}),
|
|
200
|
+
).fundedWithoutCredits;
|
|
201
|
+
}
|
|
202
|
+
const refusal = await checkWorkspaceAllowance(tx, {
|
|
203
|
+
accountId: target.accountId,
|
|
204
|
+
workspaceId: context.workspaceId,
|
|
205
|
+
subjectId: initiatingHumanForAllowance(frozen),
|
|
206
|
+
...(fundedWithoutCredits ? { fundedWithoutCredits: true } : {}),
|
|
207
|
+
});
|
|
208
|
+
if (refusal) {
|
|
209
|
+
throw new HTTPException(402, {
|
|
210
|
+
message: refusal.message,
|
|
211
|
+
cause: { allowed: false, ...refusal },
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
151
216
|
/**
|
|
152
217
|
* Retry only one operation-keyed session command transaction. Callers keep
|
|
153
218
|
* event publication and Temporal wake delivery after this returns, so a
|
|
@@ -276,7 +341,8 @@ async function wakeSessionCommand(
|
|
|
276
341
|
/**
|
|
277
342
|
* Nudge the one bounded dispatcher after a set-based control transaction. The
|
|
278
343
|
* API never materializes descendant session ids; Postgres remains the complete
|
|
279
|
-
* wake ledger and the 10-second
|
|
344
|
+
* wake ledger and the control worker's 10-second repair cadence recovers a
|
|
345
|
+
* lost immediate trigger.
|
|
280
346
|
*/
|
|
281
347
|
async function requestControlWakeDispatch(
|
|
282
348
|
deps: {
|
|
@@ -334,6 +400,7 @@ async function publishWorkspaceControlEvent(
|
|
|
334
400
|
export async function sendAgentSessionMessage(
|
|
335
401
|
deps: {
|
|
336
402
|
db: Database;
|
|
403
|
+
settings?: Settings;
|
|
337
404
|
bus: EventBus;
|
|
338
405
|
workflowClient: Pick<SessionWorkflowClient, "wakeSessionWorkflow">;
|
|
339
406
|
sessionAuthorization?: SessionAuthorizationPort | null;
|
|
@@ -356,6 +423,8 @@ export async function sendAgentSessionMessage(
|
|
|
356
423
|
actor: agentActor(context),
|
|
357
424
|
operationKey: input.idempotencyKey,
|
|
358
425
|
text: input.text,
|
|
426
|
+
assertFreshAdmission: (scoped) =>
|
|
427
|
+
assertFreshAgentCommandAllowance(scoped, deps, context, input.targetSessionId),
|
|
359
428
|
controlLockTimeoutMs: workspaceControlRequestLockTimeoutMs(),
|
|
360
429
|
}),
|
|
361
430
|
},
|
|
@@ -400,6 +469,7 @@ export async function sendAgentSessionMessage(
|
|
|
400
469
|
export async function steerAgentSession(
|
|
401
470
|
deps: {
|
|
402
471
|
db: Database;
|
|
472
|
+
settings?: Settings;
|
|
403
473
|
bus: EventBus;
|
|
404
474
|
workflowClient: Pick<SessionWorkflowClient, "wakeSessionWorkflow">;
|
|
405
475
|
sessionAuthorization?: SessionAuthorizationPort | null;
|
|
@@ -426,6 +496,8 @@ export async function steerAgentSession(
|
|
|
426
496
|
actor: agentActor(context),
|
|
427
497
|
operationKey: input.idempotencyKey,
|
|
428
498
|
instruction: input.instruction,
|
|
499
|
+
assertFreshAdmission: (scoped) =>
|
|
500
|
+
assertFreshAgentCommandAllowance(scoped, deps, context, input.targetSessionId),
|
|
429
501
|
controlLockTimeoutMs: workspaceControlRequestLockTimeoutMs(),
|
|
430
502
|
}),
|
|
431
503
|
},
|
|
@@ -14,6 +14,7 @@ import { HTTPException } from "hono/http-exception";
|
|
|
14
14
|
import type { Settings } from "@opengeni/config";
|
|
15
15
|
import {
|
|
16
16
|
requirePermission,
|
|
17
|
+
hasVerifiedOwningUserAuthorization,
|
|
17
18
|
requireResolvedAccessGrantAuthorization,
|
|
18
19
|
type AccessGrantAuthorization,
|
|
19
20
|
} from "../access";
|
|
@@ -24,9 +25,14 @@ import {
|
|
|
24
25
|
SessionAuthorizationUnavailableError,
|
|
25
26
|
type SessionAuthorizationDependencies,
|
|
26
27
|
} from "../session-authorization";
|
|
27
|
-
import {
|
|
28
|
+
import { requireEnvironmentEncryption } from "../domain/encryption-key";
|
|
28
29
|
import { normalizedSessionMcpCredentialHeaders } from "../domain/sessions";
|
|
29
30
|
import { externalContinuationCommitAuthorizer } from "./external-continuation";
|
|
31
|
+
import {
|
|
32
|
+
listOwnConnectionAccountsForGrant,
|
|
33
|
+
personalConnectionDelegationSourceForGrant,
|
|
34
|
+
} from "../domain/personal-connection-delegations";
|
|
35
|
+
import { nativeSessionConnectionReplacement } from "../domain/session-native-connection-replacement";
|
|
30
36
|
|
|
31
37
|
/** HMAC domains separate request identity from key availability. The key tag
|
|
32
38
|
* carries no request material and neither fingerprint is returned publicly. */
|
|
@@ -70,7 +76,7 @@ export async function rotateSessionMcpCredentialsForRequest(
|
|
|
70
76
|
const parsed = RotateSessionMcpCredentialsRequest.safeParse(raw);
|
|
71
77
|
if (!parsed.success)
|
|
72
78
|
throw new HTTPException(422, { message: "invalid credential rotation request" });
|
|
73
|
-
const encryptionKey =
|
|
79
|
+
const encryptionKey = requireEnvironmentEncryption(deps.settings);
|
|
74
80
|
let request: RotateSessionMcpCredentialsRequest;
|
|
75
81
|
try {
|
|
76
82
|
request = {
|
|
@@ -80,11 +86,20 @@ export async function rotateSessionMcpCredentialsForRequest(
|
|
|
80
86
|
id: update.id,
|
|
81
87
|
expectedCredentialVersion: update.expectedCredentialVersion,
|
|
82
88
|
expectedServerUrl: update.expectedServerUrl,
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
...("nativeConnectionId" in update
|
|
90
|
+
? {
|
|
91
|
+
nativeConnectionId: update.nativeConnectionId,
|
|
92
|
+
...(update.replacementServerUrl
|
|
93
|
+
? { replacementServerUrl: update.replacementServerUrl }
|
|
94
|
+
: {}),
|
|
95
|
+
}
|
|
96
|
+
: {
|
|
97
|
+
headers: Object.fromEntries(
|
|
98
|
+
Object.entries(normalizedSessionMcpCredentialHeaders(update.headers))
|
|
99
|
+
.map(([name, value]) => [name.toLowerCase(), value] as const)
|
|
100
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)),
|
|
101
|
+
),
|
|
102
|
+
}),
|
|
88
103
|
}))
|
|
89
104
|
.sort((left, right) => (left.id < right.id ? -1 : left.id > right.id ? 1 : 0)),
|
|
90
105
|
};
|
|
@@ -102,20 +117,45 @@ export async function rotateSessionMcpCredentialsForRequest(
|
|
|
102
117
|
actorType: grant.principalKind === "service" ? ("service" as const) : ("human" as const),
|
|
103
118
|
};
|
|
104
119
|
const externalAuthorize = externalContinuationCommitAuthorizer(authorization);
|
|
120
|
+
let authorizedGrant = grant;
|
|
105
121
|
try {
|
|
106
122
|
return await rotateSessionMcpCredentialsAtomically(deps.db, {
|
|
107
123
|
...scope,
|
|
108
124
|
operationKey: request.operationKey,
|
|
109
125
|
...sessionMcpRotationFingerprint(encryptionKey, scope, request),
|
|
110
|
-
updates: request.updates.map((
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
126
|
+
updates: request.updates.map((update) =>
|
|
127
|
+
"nativeConnectionId" in update
|
|
128
|
+
? update
|
|
129
|
+
: {
|
|
130
|
+
id: update.id,
|
|
131
|
+
expectedCredentialVersion: update.expectedCredentialVersion,
|
|
132
|
+
expectedServerUrl: update.expectedServerUrl,
|
|
133
|
+
headersEncrypted: Object.fromEntries(
|
|
134
|
+
Object.entries(update.headers).map(([name, value]) => [
|
|
135
|
+
name,
|
|
136
|
+
encryptVariableSetValue(encryptionKey, value),
|
|
137
|
+
]),
|
|
138
|
+
),
|
|
139
|
+
},
|
|
140
|
+
),
|
|
141
|
+
resolveNativeConnection: async (tx, server, nativeConnectionId, replacementServerUrl) => {
|
|
142
|
+
const connections = await listOwnConnectionAccountsForGrant(tx, authorizedGrant);
|
|
143
|
+
const source = personalConnectionDelegationSourceForGrant(authorizedGrant);
|
|
144
|
+
try {
|
|
145
|
+
return nativeSessionConnectionReplacement({
|
|
146
|
+
accountId: grant.accountId,
|
|
147
|
+
workspaceId: grant.workspaceId,
|
|
148
|
+
subjectId: source.kind === "subject" ? source.subjectId : null,
|
|
149
|
+
serverUrl: server.url,
|
|
150
|
+
...(replacementServerUrl ? { replacementServerUrl } : {}),
|
|
151
|
+
currentRef: server.connectionRef,
|
|
152
|
+
nativeConnectionId,
|
|
153
|
+
connections,
|
|
154
|
+
});
|
|
155
|
+
} catch {
|
|
156
|
+
throw new SessionMcpCredentialRotationError("connection_unavailable");
|
|
157
|
+
}
|
|
158
|
+
},
|
|
119
159
|
authorize: async (tx) => {
|
|
120
160
|
await externalAuthorize?.(tx);
|
|
121
161
|
const fresh = await reauthorize(tx);
|
|
@@ -133,7 +173,7 @@ export async function rotateSessionMcpCredentialsForRequest(
|
|
|
133
173
|
const live = await getWorkspaceGrant(tx, fresh.subjectId, fresh.workspaceId);
|
|
134
174
|
if (live) {
|
|
135
175
|
requireRotationGrant(live);
|
|
136
|
-
} else if (!authorization
|
|
176
|
+
} else if (!hasVerifiedOwningUserAuthorization(authorization)) {
|
|
137
177
|
throw new HTTPException(403, { message: "credential rotation authority changed" });
|
|
138
178
|
}
|
|
139
179
|
if (!(await subjectHasLiveWorkspaceAuthorityInScope(tx, scope))) {
|
|
@@ -145,6 +185,7 @@ export async function rotateSessionMcpCredentialsForRequest(
|
|
|
145
185
|
operation: "session.mcp.credentials.rotate",
|
|
146
186
|
surface: "http",
|
|
147
187
|
});
|
|
188
|
+
authorizedGrant = fresh;
|
|
148
189
|
},
|
|
149
190
|
});
|
|
150
191
|
} catch (error) {
|
|
@@ -162,7 +203,9 @@ export async function rotateSessionMcpCredentialsForRequest(
|
|
|
162
203
|
? 403
|
|
163
204
|
: error.code === "not_found"
|
|
164
205
|
? 404
|
|
165
|
-
: error.code === "invalid_request" ||
|
|
206
|
+
: error.code === "invalid_request" ||
|
|
207
|
+
error.code === "brokered_server" ||
|
|
208
|
+
error.code === "connection_unavailable"
|
|
166
209
|
? 422
|
|
167
210
|
: 409;
|
|
168
211
|
throw new HTTPException(status, { message: `credential_rotation_${error.code}` });
|