@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
package/src/domain/sessions.ts
CHANGED
|
@@ -1,34 +1,80 @@
|
|
|
1
|
+
import { isDirectModelId, ModelUnavailableError } from "@opengeni/contracts";
|
|
2
|
+
import { requireExplicitPermissionDelegation } from "../access";
|
|
3
|
+
import { acceptSessionFileAttachments } from "@opengeni/db";
|
|
1
4
|
import { knowledgeContextForAccess } from "./knowledge";
|
|
5
|
+
import {
|
|
6
|
+
getSessionEvent,
|
|
7
|
+
getSessionRetryReceiptInTransaction,
|
|
8
|
+
setSessionModelInTransaction,
|
|
9
|
+
withWorkspaceSessionActivityRls,
|
|
10
|
+
retryFailedSessionInTransaction,
|
|
11
|
+
SessionRetryConflictError,
|
|
12
|
+
} from "@opengeni/db";
|
|
13
|
+
import type {
|
|
14
|
+
SessionRetryRequest,
|
|
15
|
+
SessionRetryResponse,
|
|
16
|
+
SessionTurnSurface,
|
|
17
|
+
} from "@opengeni/contracts";
|
|
18
|
+
import { resolveTurnSurface } from "../turn-surface";
|
|
19
|
+
import {
|
|
20
|
+
recordSessionCreated,
|
|
21
|
+
recordUserMessageAccepted,
|
|
22
|
+
type ProductUsageMetricsSink,
|
|
23
|
+
} from "../product-usage-metrics";
|
|
2
24
|
import { saveAgentLearningSettings } from "@opengeni/db";
|
|
3
25
|
import { withSessionRlsActorContext } from "@opengeni/db";
|
|
4
26
|
import { fileOwnerContextForAccess, fileOwnerContextForAgent } from "./file-owner";
|
|
27
|
+
import { assertSessionIsNotImported } from "@opengeni/db";
|
|
5
28
|
import { CODEX_MODEL_ID_PREFIX, isCodexBilledModel } from "@opengeni/codex";
|
|
6
|
-
import { sessionCreationMetadata } from "../site-session-origin";
|
|
7
29
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from "
|
|
30
|
+
currentSiteSessionOrigin,
|
|
31
|
+
sessionCreationMetadata,
|
|
32
|
+
SITE_ORIGIN_METADATA_KEY,
|
|
33
|
+
} from "../site-session-origin";
|
|
34
|
+
import { assertSiteAgentMayChooseCompute } from "../site-agent-clamp";
|
|
35
|
+
import { currentHostSessionUsageLimit } from "../host-session-usage-limit";
|
|
36
|
+
import { measureSessionStartPhase, type SessionStartObservability } from "./session-start-timing";
|
|
12
37
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
getHostMcpDelegation,
|
|
18
|
-
} from "@opengeni/db";
|
|
38
|
+
requireDeveloperSetupDelegatedPermissions,
|
|
39
|
+
withDeveloperSetupCredentialRestriction,
|
|
40
|
+
} from "../developer-setup-credential-ceiling";
|
|
41
|
+
|
|
19
42
|
import {
|
|
43
|
+
CLAUDE_CONNECTION_KINDS,
|
|
44
|
+
claudeProviderId,
|
|
20
45
|
canonicalizeConfiguredModelId,
|
|
21
46
|
configuredAllowedModels,
|
|
47
|
+
withCodexCatalogProvider,
|
|
22
48
|
ORGANIZATION_GATEWAY_MODEL_ID_PREFIX,
|
|
23
49
|
ORGANIZATION_OPENROUTER_MODEL_ID_PREFIX,
|
|
50
|
+
ORGANIZATION_OPPER_MODEL_ID_PREFIX,
|
|
51
|
+
WORKSPACE_OPPER_MODEL_ID_PREFIX,
|
|
52
|
+
allowedFirstPartyMcpToolsForSession,
|
|
53
|
+
resolveSessionFirstPartyMcpTools,
|
|
54
|
+
deploymentUnavailableFirstPartyMcpTools,
|
|
24
55
|
resolveFirstPartyMcpToolPolicy,
|
|
56
|
+
policyChargesCreditsForModel,
|
|
25
57
|
policyProviderIdForModel,
|
|
26
58
|
resolveTurnExecutionPolicyV1,
|
|
27
59
|
WORKSPACE_GATEWAY_MODEL_ID_PREFIX,
|
|
28
60
|
WORKSPACE_OPENROUTER_MODEL_ID_PREFIX,
|
|
29
61
|
XAI_SUBSCRIPTION_MODEL_ID_PREFIX,
|
|
62
|
+
codeSearchDeploymentPolicy,
|
|
63
|
+
agentConfigDeploymentPolicy,
|
|
30
64
|
type Settings,
|
|
31
65
|
} from "@opengeni/config";
|
|
66
|
+
import type { CodeSearchDeploymentPolicy } from "@opengeni/contracts/code-search";
|
|
67
|
+
import {
|
|
68
|
+
agentConfigAddedFirstPartyMcpTools,
|
|
69
|
+
intersectVaultPolicies,
|
|
70
|
+
resolveAgentConfigUpdate,
|
|
71
|
+
VAULT_POLICY_ALL,
|
|
72
|
+
type AgentConfigCreator,
|
|
73
|
+
type ResolvedAgentCapabilities,
|
|
74
|
+
type ResolvedAgentConfig,
|
|
75
|
+
type UpdateSessionAgentRequest,
|
|
76
|
+
type VaultPolicy,
|
|
77
|
+
} from "@opengeni/contracts";
|
|
32
78
|
import {
|
|
33
79
|
AUTOMATIC_SESSION_TITLE_FALLBACK,
|
|
34
80
|
CreateSessionRequest,
|
|
@@ -40,6 +86,7 @@ import {
|
|
|
40
86
|
OPENGENI_SLACK_BOT_SESSION_METADATA_KEY,
|
|
41
87
|
SessionSkills,
|
|
42
88
|
resolveBundledSkillSelection,
|
|
89
|
+
bundledSkillSelectionForAgentConfig,
|
|
43
90
|
SessionSpawnDenial,
|
|
44
91
|
ServiceTurnInitiator,
|
|
45
92
|
ServiceTurnInitiatorContext,
|
|
@@ -55,7 +102,9 @@ import {
|
|
|
55
102
|
type GoalSpec,
|
|
56
103
|
type FirstPartyMcpToolName,
|
|
57
104
|
type McpPersonalConnectionDelegation,
|
|
58
|
-
type
|
|
105
|
+
type McpConnectionAccountBinding,
|
|
106
|
+
type ConnectionAccountIntent,
|
|
107
|
+
type McpConnectionAccountSelection,
|
|
59
108
|
type Permission,
|
|
60
109
|
type PersonalResourceAttachmentIntent,
|
|
61
110
|
type ReasoningEffort,
|
|
@@ -68,14 +117,18 @@ import {
|
|
|
68
117
|
type SessionSkill,
|
|
69
118
|
type SessionEvent,
|
|
70
119
|
SessionMcpApprovalPolicy,
|
|
120
|
+
SetSessionModelRequest,
|
|
71
121
|
type SessionMcpCredentialUpdateInput,
|
|
72
122
|
type SessionMcpServerInput,
|
|
73
123
|
type SessionMcpServerMetadata,
|
|
124
|
+
type SessionMcpApprovalPolicyTarget,
|
|
74
125
|
type SubmittedTimelineAnnotation,
|
|
75
126
|
type TimelineAnnotation,
|
|
76
127
|
type UpdateSessionMcpApprovalPolicyResponse,
|
|
77
128
|
type UpdateSessionToolPolicyRequest,
|
|
129
|
+
type UpdateSessionSkillsRequest,
|
|
78
130
|
type SessionAuthorizationPort,
|
|
131
|
+
type SessionAuthorizationSurface,
|
|
79
132
|
type SessionToolPolicy,
|
|
80
133
|
type SessionTurn,
|
|
81
134
|
type SessionPromptRouting,
|
|
@@ -86,36 +139,44 @@ import {
|
|
|
86
139
|
type TurnExecutionPolicyV1,
|
|
87
140
|
type VariableSet,
|
|
88
141
|
type XaiProviderAccountAuthoritySnapshotV1,
|
|
142
|
+
type ClaudeProviderAccountAuthoritySnapshotV1,
|
|
89
143
|
} from "@opengeni/contracts";
|
|
90
144
|
import {
|
|
91
145
|
assertExactNewSessionDraftInTransaction,
|
|
92
146
|
createSession,
|
|
93
147
|
createSessionWithIdempotencyKeyResult,
|
|
94
148
|
canonicalSessionCommandHash,
|
|
149
|
+
frozenInitiatorForCommandActor,
|
|
150
|
+
getComposerDraftInTransaction,
|
|
95
151
|
encryptVariableSetValue,
|
|
96
152
|
getAnySessionInGroup,
|
|
97
153
|
getEnrollment,
|
|
98
154
|
getChannel,
|
|
99
155
|
getRig,
|
|
100
156
|
getWorkspaceDefaultRigId,
|
|
101
|
-
listDistinctVariableSetSelectionsInGroup,
|
|
102
157
|
listDistinctRigVersionIdsInGroup,
|
|
103
158
|
listInstalledPortableSkills,
|
|
159
|
+
listEnabledMcpCapabilityServers,
|
|
160
|
+
resolveMcpApprovalRecommendation,
|
|
104
161
|
getSandbox,
|
|
105
162
|
getSession,
|
|
106
163
|
getInitializedSessionCreateReplay,
|
|
164
|
+
getSessionGoal,
|
|
107
165
|
getSessionAuthorityProjection,
|
|
108
166
|
SessionIdConflictError,
|
|
109
167
|
NewSessionDraftConflictError,
|
|
110
168
|
getWorkspaceControlEvent,
|
|
111
169
|
getSessionLineage,
|
|
112
170
|
getSessionTurn,
|
|
171
|
+
getSessionTurnInitiatingHumanSubjectId,
|
|
113
172
|
getSessionTurnForAttempt,
|
|
114
173
|
getSessionTurnPersonalConnectionDelegations,
|
|
115
174
|
getSessionTurnXaiProviderAccountAuthoritySnapshot,
|
|
175
|
+
getSessionTurnClaudeProviderAccountAuthoritySnapshot,
|
|
116
176
|
getWorkspaceModelPolicy,
|
|
117
177
|
requireWorkspace,
|
|
118
178
|
initializeSessionStartAtomically,
|
|
179
|
+
discardUninitializedSessionShell,
|
|
119
180
|
listSessionTurns,
|
|
120
181
|
listSessionMcpServersForChildInheritance,
|
|
121
182
|
requireSession,
|
|
@@ -125,9 +186,12 @@ import {
|
|
|
125
186
|
submitHumanPromptInTransaction,
|
|
126
187
|
appendSessionEventsWithLockedSessionUpdate,
|
|
127
188
|
updateSessionTitleWithEvent,
|
|
189
|
+
setSessionKeepLive,
|
|
190
|
+
type SetSessionKeepLiveResult,
|
|
128
191
|
withWorkspaceSubjectSessionActivityRls,
|
|
129
192
|
type CreateSessionMcpServerInput,
|
|
130
193
|
type Database,
|
|
194
|
+
type FrozenTurnInitiator,
|
|
131
195
|
type UpdateSessionMcpServerCredentialsInput,
|
|
132
196
|
QueueCommandConflictError,
|
|
133
197
|
AgentCommandAuthorityError,
|
|
@@ -137,7 +201,6 @@ import {
|
|
|
137
201
|
SessionToolPolicyVersionConflictError,
|
|
138
202
|
SessionCreateIdempotencyConflictError,
|
|
139
203
|
PersonalResourceAttachmentAcceptanceError,
|
|
140
|
-
sessionTenancyProductActivated,
|
|
141
204
|
workspaceControlRequestLockTimeoutMs,
|
|
142
205
|
WorkspaceControlBusyError,
|
|
143
206
|
type SessionCommandActor,
|
|
@@ -153,8 +216,16 @@ import { hasPermission, requirePermission, type AccessGrantAuthorization } from
|
|
|
153
216
|
import { externalCreationMetadata } from "./external-creation-attribution";
|
|
154
217
|
import { prepareExternalLinkTurnAdmission } from "../application/external-link-work-admission";
|
|
155
218
|
import { externalContinuationCommitAuthorizer } from "../application/external-continuation";
|
|
156
|
-
|
|
219
|
+
|
|
157
220
|
import { recordWorkspaceUsage, requireLimit } from "../billing/limits";
|
|
221
|
+
import {
|
|
222
|
+
applyInitialSessionUsageLimit,
|
|
223
|
+
requireSessionUsageLimitAllows,
|
|
224
|
+
SessionUsageLimitHttpError,
|
|
225
|
+
sessionUsageLimitsProducerEnabled,
|
|
226
|
+
sessionUsageLimitWriteActorForAccess,
|
|
227
|
+
} from "../session-usage-limits";
|
|
228
|
+
import { recordSessionUsageLimitEvent } from "../session-usage-limit-metrics";
|
|
158
229
|
import type {
|
|
159
230
|
AcceptSessionUserMessageDependencies,
|
|
160
231
|
ApiRouteDeps,
|
|
@@ -166,7 +237,7 @@ import {
|
|
|
166
237
|
requireSessionAuthorization,
|
|
167
238
|
SessionAuthorizationDeniedError,
|
|
168
239
|
} from "../session-authorization";
|
|
169
|
-
import {
|
|
240
|
+
import { assertNativeMcpConnectionRef } from "./native-mcp-connection-admission";
|
|
170
241
|
import {
|
|
171
242
|
preflightCreateTimeSandboxTarget,
|
|
172
243
|
swapActiveSandbox,
|
|
@@ -179,11 +250,26 @@ import {
|
|
|
179
250
|
resolveWorkspaceCatalogSettings,
|
|
180
251
|
workspaceCustomModelReference,
|
|
181
252
|
} from "../model-catalog";
|
|
253
|
+
import {
|
|
254
|
+
resolveDefaultSessionModel,
|
|
255
|
+
resolveCallerWorkspaceModelSelections,
|
|
256
|
+
admissibleWorkspaceModel,
|
|
257
|
+
} from "../default-session-model";
|
|
182
258
|
import { settingsWithEnabledCapabilityMcpServers } from "./capabilities";
|
|
259
|
+
import {
|
|
260
|
+
applySessionAgentConfigWriteThrough,
|
|
261
|
+
legacySessionAgentCapabilities,
|
|
262
|
+
resolveSessionAgentConfigForCreate,
|
|
263
|
+
withAgentConfigHttpErrors,
|
|
264
|
+
} from "./agent-config-resolution";
|
|
265
|
+
import {
|
|
266
|
+
resolveSessionToolPolicy,
|
|
267
|
+
workspaceSessionToolPolicyDefaultServerIdsFor,
|
|
268
|
+
} from "./session-tool-policy";
|
|
183
269
|
import { validateSubmittedTimelineAnnotations } from "./timeline-annotations";
|
|
184
|
-
import {
|
|
270
|
+
import { requireEnvironmentEncryption, variableSetsRemovedError } from "./encryption-key";
|
|
185
271
|
import {
|
|
186
|
-
|
|
272
|
+
freezeConnectionAccounts,
|
|
187
273
|
personalConnectionDelegationSourceForGrant,
|
|
188
274
|
} from "./personal-connection-delegations";
|
|
189
275
|
import { hasReservedOpenGeniSlackBotSessionMetadata } from "./slack-bot";
|
|
@@ -209,12 +295,40 @@ const maxSessionMcpCredentialHeaderValueLength = 4096;
|
|
|
209
295
|
// the generic lossy projection cannot silently rewrite this audit fact.
|
|
210
296
|
const maxToolPolicyAuditRefs = 40;
|
|
211
297
|
|
|
298
|
+
function withoutExcludedMcpServers(
|
|
299
|
+
tools: ToolRef[],
|
|
300
|
+
excludedIds: readonly string[] = [],
|
|
301
|
+
): ToolRef[] {
|
|
302
|
+
const excluded = new Set(excludedIds);
|
|
303
|
+
return tools.filter((tool) => tool.id === "opengeni" || !excluded.has(tool.id));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function defaultPolicyExclusions(
|
|
307
|
+
ids: readonly string[] = [],
|
|
308
|
+
): Pick<SessionToolPolicy, "excludedMcpServerIds"> {
|
|
309
|
+
const sorted = [...new Set(ids)].filter((id) => id !== "opengeni").sort();
|
|
310
|
+
if (sorted.length > 64 || sorted.some((id) => id.length > 200 || !/^[A-Za-z0-9_-]+$/.test(id))) {
|
|
311
|
+
throw new HTTPException(422, {
|
|
312
|
+
message: "connector exclusions must contain at most 64 valid MCP server IDs",
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return sorted.length ? { excludedMcpServerIds: sorted } : {};
|
|
316
|
+
}
|
|
317
|
+
|
|
212
318
|
function isCatalogOverlayModel(modelId: string | null | undefined): boolean {
|
|
213
319
|
return (
|
|
320
|
+
(modelId != null && isDirectModelId(modelId)) ||
|
|
214
321
|
modelId?.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX) === true ||
|
|
215
322
|
modelId?.startsWith(WORKSPACE_OPENROUTER_MODEL_ID_PREFIX) === true ||
|
|
216
323
|
modelId?.startsWith(ORGANIZATION_GATEWAY_MODEL_ID_PREFIX) === true ||
|
|
217
|
-
modelId?.startsWith(ORGANIZATION_OPENROUTER_MODEL_ID_PREFIX) === true
|
|
324
|
+
modelId?.startsWith(ORGANIZATION_OPENROUTER_MODEL_ID_PREFIX) === true ||
|
|
325
|
+
modelId?.startsWith(WORKSPACE_OPPER_MODEL_ID_PREFIX) === true ||
|
|
326
|
+
modelId?.startsWith(ORGANIZATION_OPPER_MODEL_ID_PREFIX) === true ||
|
|
327
|
+
CLAUDE_CONNECTION_KINDS.some((kind) =>
|
|
328
|
+
["workspace", "organization"].some((scope) =>
|
|
329
|
+
modelId?.startsWith(claudeProviderId(kind, scope as "workspace" | "organization") + "/"),
|
|
330
|
+
),
|
|
331
|
+
)
|
|
218
332
|
);
|
|
219
333
|
}
|
|
220
334
|
// RFC 9110 field-name token characters.
|
|
@@ -241,11 +355,6 @@ async function requireAtomicPersonalResourceAttachment(
|
|
|
241
355
|
cause: error,
|
|
242
356
|
});
|
|
243
357
|
}
|
|
244
|
-
if (!(await sessionTenancyProductActivated(deps.db, workspaceId))) {
|
|
245
|
-
throw new HTTPException(409, {
|
|
246
|
-
message: "Session tenancy is not activated for this organization.",
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
358
|
if (existingSession && intent.expectedAuthorityEpoch === undefined) {
|
|
250
359
|
throw new HTTPException(422, {
|
|
251
360
|
message: "Personal-resource attachment requires expectedAuthorityEpoch.",
|
|
@@ -307,6 +416,13 @@ export function resolveFirstPartyMcpToolsForCreate(
|
|
|
307
416
|
},
|
|
308
417
|
): FirstPartyMcpToolName[] {
|
|
309
418
|
if (requested !== undefined) {
|
|
419
|
+
// Check the caller's own names, before legacy aliases expand them.
|
|
420
|
+
requireDeploymentFirstPartyMcpTools(
|
|
421
|
+
requested,
|
|
422
|
+
policy.allowed,
|
|
423
|
+
[],
|
|
424
|
+
"Omit firstPartyMcpTools to inherit the effective selection.",
|
|
425
|
+
);
|
|
310
426
|
requested = currentAgentLearningToolSelection(requested);
|
|
311
427
|
if (parentStored !== undefined) {
|
|
312
428
|
const parentCeiling = effectiveFirstPartyMcpToolCeiling(parentStored, policy);
|
|
@@ -324,6 +440,29 @@ export function resolveFirstPartyMcpToolsForCreate(
|
|
|
324
440
|
return currentAgentLearningToolSelection(inherited).filter((tool) => allowed.has(tool));
|
|
325
441
|
}
|
|
326
442
|
|
|
443
|
+
/**
|
|
444
|
+
* An explicit first-party tool selection is a promise, not a best-effort
|
|
445
|
+
* default: a tool the deployment disables fails the request, naming every such
|
|
446
|
+
* tool and the allowed set, instead of silently shrinking the selection.
|
|
447
|
+
* `retained` names tools the caller may echo from its stored selection; those
|
|
448
|
+
* stay deployment-filtered like any default, so a later policy change never
|
|
449
|
+
* blocks an unrelated edit.
|
|
450
|
+
*/
|
|
451
|
+
export function requireDeploymentFirstPartyMcpTools(
|
|
452
|
+
requested: readonly FirstPartyMcpToolName[],
|
|
453
|
+
allowed: readonly FirstPartyMcpToolName[],
|
|
454
|
+
retained: Iterable<FirstPartyMcpToolName> = [],
|
|
455
|
+
hint = "",
|
|
456
|
+
): void {
|
|
457
|
+
const permitted = new Set([...allowed, ...retained]);
|
|
458
|
+
const disallowed = [...new Set(requested.filter((tool) => !permitted.has(tool)))];
|
|
459
|
+
if (disallowed.length > 0) {
|
|
460
|
+
throw new HTTPException(422, {
|
|
461
|
+
message: `first-party MCP tools are disabled by deployment policy: ${disallowed.join(", ")}. Allowed tools: ${allowed.join(", ") || "(none)"}.${hint ? ` ${hint}` : ""}`,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
327
466
|
function sessionSpawnDeniedMessage(denial: SessionSpawnDenial): string {
|
|
328
467
|
if (denial.code === "nested_agent_depth_override_forbidden") {
|
|
329
468
|
return `requested nested-agent depth limit ${denial.requestedMaxNestedAgentDepthOverride ?? "unknown"} exceeds inherited limit ${denial.effectiveMaxNestedAgentDepth}; workspace:admin is required to increase it`;
|
|
@@ -353,6 +492,21 @@ export type FrozenCreationInitiator = {
|
|
|
353
492
|
actor?: Extract<SessionCommandActor, { type: "agent_attempt" }>;
|
|
354
493
|
};
|
|
355
494
|
|
|
495
|
+
/** Attribution is immutable accepted-work data, never the current request principal. */
|
|
496
|
+
export function initiatingHumanForAllowance(
|
|
497
|
+
frozen: Pick<FrozenTurnInitiator, "initiator" | "initiatingHumanSubjectId">,
|
|
498
|
+
): string | null {
|
|
499
|
+
const subjectId =
|
|
500
|
+
frozen.initiatingHumanSubjectId === undefined
|
|
501
|
+
? frozen.initiator.kind === "subject"
|
|
502
|
+
? frozen.initiator.subjectId
|
|
503
|
+
: null
|
|
504
|
+
: frozen.initiatingHumanSubjectId;
|
|
505
|
+
// Older accepted API-key turns used the subject initiator kind. They are
|
|
506
|
+
// still service-funded work, not a human member's allowance.
|
|
507
|
+
return subjectId?.startsWith("api_key:") ? null : subjectId;
|
|
508
|
+
}
|
|
509
|
+
|
|
356
510
|
function serviceInitiatorForGrant(grant: AccessGrant): {
|
|
357
511
|
initiator: ServiceTurnInitiator;
|
|
358
512
|
context: ServiceTurnInitiatorContext;
|
|
@@ -432,6 +586,20 @@ export function creationInitiatorForGrant(grant: AccessGrant): FrozenCreationIni
|
|
|
432
586
|
if (serviceInitiator) {
|
|
433
587
|
return serviceInitiator;
|
|
434
588
|
}
|
|
589
|
+
if (
|
|
590
|
+
grant.principalKind === "service" ||
|
|
591
|
+
grant.principalKind === "api_key" ||
|
|
592
|
+
grant.principalKind === "configured_key"
|
|
593
|
+
) {
|
|
594
|
+
return {
|
|
595
|
+
initiator: {
|
|
596
|
+
kind: "service",
|
|
597
|
+
subjectId: grant.subjectId,
|
|
598
|
+
...(grant.subjectLabel ? { label: grant.subjectLabel } : {}),
|
|
599
|
+
},
|
|
600
|
+
context: {},
|
|
601
|
+
};
|
|
602
|
+
}
|
|
435
603
|
return {
|
|
436
604
|
initiator: {
|
|
437
605
|
kind: "subject",
|
|
@@ -558,7 +726,7 @@ function validateSessionMcpServersForCreate(
|
|
|
558
726
|
}
|
|
559
727
|
requirePermission(grant, "mcp_servers:attach");
|
|
560
728
|
const encryptionKey = servers.some((server) => Object.keys(server.headers ?? {}).length > 0)
|
|
561
|
-
?
|
|
729
|
+
? requireEnvironmentEncryption(settings)
|
|
562
730
|
: null;
|
|
563
731
|
const existingIds = new Set(settings.mcpServers.map((server) => server.id));
|
|
564
732
|
const seenIds = new Set<string>();
|
|
@@ -566,7 +734,7 @@ function validateSessionMcpServersForCreate(
|
|
|
566
734
|
const dbServers: CreateSessionMcpServerInput[] = [];
|
|
567
735
|
const metadata: SessionMcpServerMetadata[] = [];
|
|
568
736
|
for (const server of servers) {
|
|
569
|
-
|
|
737
|
+
assertNativeMcpConnectionRef(server.connectionRef);
|
|
570
738
|
if (seenIds.has(server.id)) {
|
|
571
739
|
throw new HTTPException(422, {
|
|
572
740
|
message: `duplicate session MCP server id: ${server.id}`,
|
|
@@ -653,7 +821,7 @@ function validateInheritedSessionMcpServersForCreate(
|
|
|
653
821
|
};
|
|
654
822
|
}
|
|
655
823
|
|
|
656
|
-
function validateSessionMcpCredentialUpdates(input: {
|
|
824
|
+
export function validateSessionMcpCredentialUpdates(input: {
|
|
657
825
|
settings: Settings;
|
|
658
826
|
grant: AccessGrant;
|
|
659
827
|
session: Session;
|
|
@@ -663,7 +831,7 @@ function validateSessionMcpCredentialUpdates(input: {
|
|
|
663
831
|
return [];
|
|
664
832
|
}
|
|
665
833
|
requirePermission(input.grant, "mcp_servers:attach");
|
|
666
|
-
const encryptionKey =
|
|
834
|
+
const encryptionKey = requireEnvironmentEncryption(input.settings);
|
|
667
835
|
const knownIds = new Set(input.session.mcpServers.map((server) => server.id));
|
|
668
836
|
const seenIds = new Set<string>();
|
|
669
837
|
const encryptedUpdates = input.updates.map((update) => {
|
|
@@ -713,6 +881,27 @@ type AgentChildSessionCreatePresentation = {
|
|
|
713
881
|
automaticTitleCandidate?: string | null;
|
|
714
882
|
};
|
|
715
883
|
|
|
884
|
+
/** Trusted entry-point options for creating a session outside the REST body. */
|
|
885
|
+
export type SessionCreateRequestOptions = {
|
|
886
|
+
/**
|
|
887
|
+
* Product surface when the entry point knows it (for example Slack or an
|
|
888
|
+
* in-process embedding host). Omitted derives it from the grant.
|
|
889
|
+
*/
|
|
890
|
+
surface?: SessionTurnSurface;
|
|
891
|
+
/**
|
|
892
|
+
* Agent-configuration creator kind (renderer default and default-config
|
|
893
|
+
* policy). Omitted derives `slack` from a Slack surface, else the public API.
|
|
894
|
+
*/
|
|
895
|
+
agentConfigCreator?: AgentConfigCreator;
|
|
896
|
+
/**
|
|
897
|
+
* Built-in tools to guarantee on top of the defaults a new session follows,
|
|
898
|
+
* for an entry point whose sessions need their own tools (a chat surface's
|
|
899
|
+
* read tools). Ignored when the request names an explicit built-in list.
|
|
900
|
+
* Trusted entry points only; not part of the public create request.
|
|
901
|
+
*/
|
|
902
|
+
firstPartyMcpToolAdditions?: readonly FirstPartyMcpToolName[];
|
|
903
|
+
};
|
|
904
|
+
|
|
716
905
|
const AGENT_CHILD_AUTOMATIC_TITLE_CONTEXT_KEY = "agentChildAutomaticTitle" as const;
|
|
717
906
|
|
|
718
907
|
/** @internal Exported for the keyed-create repair regression. */
|
|
@@ -745,6 +934,13 @@ function automaticTitleForAgentChildCreate(
|
|
|
745
934
|
return null;
|
|
746
935
|
}
|
|
747
936
|
|
|
937
|
+
/** Vault IDs are compared as lowercase text in SQL. */
|
|
938
|
+
function lowercaseVaultPolicy(policy: VaultPolicy): VaultPolicy {
|
|
939
|
+
return policy.mode === "all"
|
|
940
|
+
? { mode: "all" }
|
|
941
|
+
: { mode: "selected", vaultIds: [...new Set(policy.vaultIds.map((id) => id.toLowerCase()))] };
|
|
942
|
+
}
|
|
943
|
+
|
|
748
944
|
export async function createAndStartSessionWithOutcome(input: {
|
|
749
945
|
initialAgentLearning?: import("@opengeni/contracts").AgentLearningOverrides | undefined;
|
|
750
946
|
requestedSessionId?: string;
|
|
@@ -760,7 +956,7 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
760
956
|
captureInitialTurnAuthority?: (tx: Database, sessionId: string, turnId: string) => Promise<void>;
|
|
761
957
|
/** Internal replay identity; the verified caller must also capture authority.
|
|
762
958
|
* Supplying selection metadata alone never grants runtime use. */
|
|
763
|
-
|
|
959
|
+
|
|
764
960
|
/** The custom workspace model was frozen by an earlier accepted boundary or
|
|
765
961
|
* inherited from an existing session, so retirement must not invalidate it. */
|
|
766
962
|
retainWorkspaceGatewayModel?: boolean;
|
|
@@ -775,6 +971,15 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
775
971
|
workspaceId: string;
|
|
776
972
|
visibility?: "user_private" | "workspace_shared";
|
|
777
973
|
initialMessage: string;
|
|
974
|
+
/** Content-free product surface the create request entered through. */
|
|
975
|
+
surface?: SessionTurnSurface | null;
|
|
976
|
+
/**
|
|
977
|
+
* Live product usage counters: a newly created session (never a replay)
|
|
978
|
+
* counts once, and its initial user message once unless deferred.
|
|
979
|
+
*/
|
|
980
|
+
metrics?: ProductUsageMetricsSink | null;
|
|
981
|
+
/** Optional content-free child spans; never a lifecycle dependency. */
|
|
982
|
+
startupObservability?: SessionStartObservability | null;
|
|
778
983
|
/** Create the session shell without an initial user event/agent turn. */
|
|
779
984
|
deferInitialTurn?: boolean;
|
|
780
985
|
modelContext?: string | null;
|
|
@@ -829,6 +1034,12 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
829
1034
|
// Model-visible first-party tool names. Authorization remains controlled by
|
|
830
1035
|
// firstPartyMcpPermissions and the target resource checks.
|
|
831
1036
|
firstPartyMcpTools: FirstPartyMcpToolName[];
|
|
1037
|
+
// Frozen agent configuration (migration 0559), already resolved and written
|
|
1038
|
+
// through to tools/firstPartyMcpTools by the caller. Omitted/null = legacy.
|
|
1039
|
+
agentConfig?: ResolvedAgentConfig | null;
|
|
1040
|
+
// Vault narrowing for this session (0739), already intersected with the
|
|
1041
|
+
// agent configuration's policy. Omitted = no narrowing.
|
|
1042
|
+
vaultPolicy?: VaultPolicy;
|
|
832
1043
|
// Agent-access scope, opaque end-user label, and typed Memory selector
|
|
833
1044
|
// (migration 0427), already resolved against the parent by the caller.
|
|
834
1045
|
// Omitted keeps the workspace defaults for internal lifecycle callers.
|
|
@@ -838,14 +1049,22 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
838
1049
|
// Encrypted DB rows plus matching safe metadata for create-time per-session
|
|
839
1050
|
// MCP servers. Metadata is the only shape emitted in events/responses.
|
|
840
1051
|
mcpServers?: CreateSessionMcpServerInput[];
|
|
1052
|
+
mcpApprovalPolicies?: Record<string, SessionMcpApprovalPolicy>;
|
|
841
1053
|
sessionMcpServers?: SessionMcpServerMetadata[];
|
|
842
1054
|
personalConnectionDelegations?: McpPersonalConnectionDelegation[];
|
|
1055
|
+
mcpAccountBindings?: McpConnectionAccountBinding[] | null;
|
|
1056
|
+
/** The creator's account intent, carried by work that continues the first turn. */
|
|
1057
|
+
connectionAccountIntent?: ConnectionAccountIntent | null;
|
|
843
1058
|
initialPersonalResourceAttachmentIntent?: PersonalResourceAttachmentIntent | null;
|
|
844
1059
|
xaiProviderAccountAuthoritySnapshot?: XaiProviderAccountAuthoritySnapshotV1;
|
|
1060
|
+
claudeProviderAccountAuthoritySnapshot?: ClaudeProviderAccountAuthoritySnapshotV1;
|
|
845
1061
|
// The manager session spawning this worker (a worker-signed sessionId claim
|
|
846
1062
|
// on the creating grant); null for direct API creates and scheduled runs.
|
|
847
1063
|
// When set, the worker's terminal-for-now transitions wake this parent.
|
|
848
1064
|
parentSessionId?: string | null;
|
|
1065
|
+
// Deployment code_search policy for freezing a new root session's decision.
|
|
1066
|
+
// Omitted falls back to the policy the process installed at boot.
|
|
1067
|
+
codeSearchDeploymentPolicy?: CodeSearchDeploymentPolicy;
|
|
849
1068
|
// Workspace-scoped CREATE idempotency key. When present, a double-fire with
|
|
850
1069
|
// the same key (sequential retry OR concurrent race) collapses to a single
|
|
851
1070
|
// session. Every caller repairs or re-delivers the winner's one atomic start;
|
|
@@ -904,6 +1123,13 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
904
1123
|
allowNestedAgentDepthIncrease?: boolean;
|
|
905
1124
|
subjectId?: string | null;
|
|
906
1125
|
}): Promise<CreateSessionOutcome> {
|
|
1126
|
+
const frozenBundledSkillIds = bundledSkillSelectionForAgentConfig(
|
|
1127
|
+
input.bundledSkillIds,
|
|
1128
|
+
input.agentConfig,
|
|
1129
|
+
);
|
|
1130
|
+
if (frozenBundledSkillIds !== input.bundledSkillIds) {
|
|
1131
|
+
input = { ...input, bundledSkillIds: frozenBundledSkillIds };
|
|
1132
|
+
}
|
|
907
1133
|
const sessionMetadata = metadataWithTurnExecutionPolicyV1(
|
|
908
1134
|
{
|
|
909
1135
|
...input.metadata,
|
|
@@ -965,20 +1191,27 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
965
1191
|
scope: input.turnExecutionPolicy.providerId.startsWith("organization-")
|
|
966
1192
|
? ("organization" as const)
|
|
967
1193
|
: ("workspace" as const),
|
|
968
|
-
providerKind:
|
|
969
|
-
|
|
970
|
-
|
|
1194
|
+
providerKind:
|
|
1195
|
+
CLAUDE_CONNECTION_KINDS.find(
|
|
1196
|
+
(kind) =>
|
|
1197
|
+
claudeProviderId(kind) === input.turnExecutionPolicy.providerId ||
|
|
1198
|
+
claudeProviderId(kind, "workspace") === input.turnExecutionPolicy.providerId,
|
|
1199
|
+
) ??
|
|
1200
|
+
(input.turnExecutionPolicy.providerId.includes("openrouter")
|
|
1201
|
+
? ("openrouter" as const)
|
|
1202
|
+
: input.turnExecutionPolicy.providerId.endsWith("-opper")
|
|
1203
|
+
? ("opper" as const)
|
|
1204
|
+
: ("vercel_gateway" as const)),
|
|
971
1205
|
upstreamModelId: input.turnExecutionPolicy.upstreamModelId,
|
|
972
1206
|
};
|
|
973
1207
|
const active = await lockActiveCustomModelForAdmission(tx, {
|
|
974
1208
|
accountId: input.accountId,
|
|
975
1209
|
workspaceId: input.workspaceId,
|
|
976
1210
|
reference,
|
|
1211
|
+
claudeAuthority: { sessionId },
|
|
977
1212
|
});
|
|
978
1213
|
if (!active) {
|
|
979
|
-
throw
|
|
980
|
-
message: `model is not available: ${input.model}`,
|
|
981
|
-
});
|
|
1214
|
+
throw modelUnavailableHttpException(input.model);
|
|
982
1215
|
}
|
|
983
1216
|
}
|
|
984
1217
|
// Reject an already-stale browser draft before the newly inserted
|
|
@@ -1020,59 +1253,76 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
1020
1253
|
// session or the committed denial atomically; an application-side lookup
|
|
1021
1254
|
// cannot serialize those two source tables against an older writer.
|
|
1022
1255
|
if (input.createIdempotencyKey) {
|
|
1023
|
-
const keyedResult = await
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1256
|
+
const keyedResult = await measureSessionStartPhase(
|
|
1257
|
+
input.startupObservability,
|
|
1258
|
+
"shell_insert",
|
|
1259
|
+
() =>
|
|
1260
|
+
createSessionWithIdempotencyKeyResult(input.db, {
|
|
1261
|
+
...(input.requestedSessionId ? { requestedSessionId: input.requestedSessionId } : {}),
|
|
1262
|
+
accountId: input.accountId,
|
|
1263
|
+
workspaceId: input.workspaceId,
|
|
1264
|
+
visibility: input.visibility ?? "workspace_shared",
|
|
1265
|
+
initialMessage: input.initialMessage,
|
|
1266
|
+
initialModelContext: input.modelContext ?? null,
|
|
1267
|
+
resources: input.resources,
|
|
1268
|
+
skills: input.skills ?? [],
|
|
1269
|
+
bundledSkillIds: input.bundledSkillIds,
|
|
1270
|
+
tools: input.tools,
|
|
1271
|
+
toolPolicy: input.toolPolicy,
|
|
1272
|
+
metadata: sessionMetadata,
|
|
1273
|
+
initialAgentLearning: input.initialAgentLearning,
|
|
1274
|
+
|
|
1275
|
+
...(input.createdBy ? { createdBy: input.createdBy } : {}),
|
|
1276
|
+
...(frozenCreatedByContext ? { createdByContext: frozenCreatedByContext } : {}),
|
|
1277
|
+
createdByActor: input.createdByActor ?? null,
|
|
1278
|
+
model: input.model,
|
|
1279
|
+
reasoningEffort: input.reasoningEffort,
|
|
1280
|
+
latencyMode: input.latencyMode ?? "standard",
|
|
1281
|
+
sandboxBackend: input.sandboxBackend,
|
|
1282
|
+
variableSetIds: input.variableSets?.map((variableSet) => variableSet.id) ?? [],
|
|
1283
|
+
variableSetId: input.variableSets?.at(-1)?.id ?? null,
|
|
1284
|
+
rigId: input.rigId ?? null,
|
|
1285
|
+
rigVersionId: input.rigVersionId ?? null,
|
|
1286
|
+
channelId: input.channelId ?? null,
|
|
1287
|
+
firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
|
|
1288
|
+
firstPartyMcpTools: input.firstPartyMcpTools,
|
|
1289
|
+
...(input.agentConfig !== undefined ? { agentConfig: input.agentConfig } : {}),
|
|
1290
|
+
instructions: input.instructions ?? null,
|
|
1291
|
+
...(input.vaultPolicy ? { vaultPolicy: input.vaultPolicy } : {}),
|
|
1292
|
+
policyRole: input.policyRole ?? null,
|
|
1293
|
+
...(input.agentAccess ? { agentAccess: input.agentAccess } : {}),
|
|
1294
|
+
...(input.scopeSubjectId !== undefined ? { scopeSubjectId: input.scopeSubjectId } : {}),
|
|
1295
|
+
...(input.memoryScope ? { memoryScope: input.memoryScope } : {}),
|
|
1296
|
+
parentSessionId: input.parentSessionId ?? null,
|
|
1297
|
+
createIdempotencyKey: input.createIdempotencyKey!,
|
|
1298
|
+
selectedInstalledSkillIds: input.selectedInstalledSkillIds ?? [],
|
|
1299
|
+
sandboxGroupId: input.sandboxGroupId ?? null,
|
|
1300
|
+
...(input.sandboxOs ? { sandboxOs: input.sandboxOs } : {}),
|
|
1301
|
+
mcpServers: input.mcpServers ?? [],
|
|
1302
|
+
mcpApprovalPolicies: input.mcpApprovalPolicies ?? {},
|
|
1303
|
+
personalConnectionDelegations: input.personalConnectionDelegations ?? [],
|
|
1304
|
+
mcpAccountBindings: input.mcpAccountBindings ?? null,
|
|
1305
|
+
connectionAccountIntent: input.connectionAccountIntent ?? null,
|
|
1306
|
+
initialPersonalResourceAttachmentIntent:
|
|
1307
|
+
input.initialPersonalResourceAttachmentIntent ?? null,
|
|
1308
|
+
...(input.xaiProviderAccountAuthoritySnapshot
|
|
1309
|
+
? {
|
|
1310
|
+
initialXaiProviderAccountAuthoritySnapshot:
|
|
1311
|
+
input.xaiProviderAccountAuthoritySnapshot,
|
|
1312
|
+
}
|
|
1313
|
+
: {}),
|
|
1314
|
+
...(input.claudeProviderAccountAuthoritySnapshot
|
|
1315
|
+
? {
|
|
1316
|
+
initialClaudeProviderAccountAuthoritySnapshot:
|
|
1317
|
+
input.claudeProviderAccountAuthoritySnapshot,
|
|
1318
|
+
}
|
|
1319
|
+
: {}),
|
|
1320
|
+
maxNestedAgentDepthOverride: input.maxNestedAgentDepthOverride ?? null,
|
|
1321
|
+
allowNestedAgentDepthIncrease: input.allowNestedAgentDepthIncrease ?? false,
|
|
1322
|
+
subjectId: input.subjectId ?? null,
|
|
1323
|
+
...(beforeCreateCommit ? { beforeCreateCommit } : {}),
|
|
1324
|
+
}),
|
|
1325
|
+
);
|
|
1076
1326
|
if (keyedResult.denied) {
|
|
1077
1327
|
throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(keyedResult.denial));
|
|
1078
1328
|
}
|
|
@@ -1103,10 +1353,13 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
1103
1353
|
changed: finished.changed,
|
|
1104
1354
|
};
|
|
1105
1355
|
}
|
|
1356
|
+
// A keyed shell whose start fails stays durable: a retry with the same key
|
|
1357
|
+
// repairs it with the parameters it was first accepted with.
|
|
1106
1358
|
const finished = await finishStartSession(
|
|
1107
1359
|
targetSeededBeforeCreateCommit ? { ...input, seedTargetSandbox: null } : input,
|
|
1108
1360
|
keyed,
|
|
1109
1361
|
);
|
|
1362
|
+
recordCreatedSessionUsage(input);
|
|
1110
1363
|
return {
|
|
1111
1364
|
session: finished.session,
|
|
1112
1365
|
outcome: "created",
|
|
@@ -1116,67 +1369,84 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
1116
1369
|
}
|
|
1117
1370
|
let session: Session;
|
|
1118
1371
|
try {
|
|
1119
|
-
session = await
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
? {
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
:
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1372
|
+
session = await measureSessionStartPhase(input.startupObservability, "shell_insert", () =>
|
|
1373
|
+
createSession(input.db, {
|
|
1374
|
+
...(input.requestedSessionId ? { requestedSessionId: input.requestedSessionId } : {}),
|
|
1375
|
+
accountId: input.accountId,
|
|
1376
|
+
workspaceId: input.workspaceId,
|
|
1377
|
+
visibility: input.visibility ?? "workspace_shared",
|
|
1378
|
+
initialMessage: input.initialMessage,
|
|
1379
|
+
initialModelContext: input.modelContext ?? null,
|
|
1380
|
+
resources: input.resources,
|
|
1381
|
+
skills: input.skills ?? [],
|
|
1382
|
+
bundledSkillIds: input.bundledSkillIds,
|
|
1383
|
+
tools: input.tools,
|
|
1384
|
+
toolPolicy: input.toolPolicy,
|
|
1385
|
+
metadata: sessionMetadata,
|
|
1386
|
+
initialAgentLearning: input.initialAgentLearning,
|
|
1387
|
+
...(input.createdBy ? { createdBy: input.createdBy } : {}),
|
|
1388
|
+
|
|
1389
|
+
...(frozenCreatedByContext ? { createdByContext: frozenCreatedByContext } : {}),
|
|
1390
|
+
createdByActor: input.createdByActor ?? null,
|
|
1391
|
+
model: input.model,
|
|
1392
|
+
reasoningEffort: input.reasoningEffort,
|
|
1393
|
+
latencyMode: input.latencyMode ?? "standard",
|
|
1394
|
+
sandboxBackend: input.sandboxBackend,
|
|
1395
|
+
variableSetIds: input.variableSets?.map((variableSet) => variableSet.id) ?? [],
|
|
1396
|
+
variableSetId: input.variableSets?.at(-1)?.id ?? null,
|
|
1397
|
+
rigId: input.rigId ?? null,
|
|
1398
|
+
rigVersionId: input.rigVersionId ?? null,
|
|
1399
|
+
channelId: input.channelId ?? null,
|
|
1400
|
+
firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
|
|
1401
|
+
firstPartyMcpTools: input.firstPartyMcpTools,
|
|
1402
|
+
...(input.agentConfig !== undefined ? { agentConfig: input.agentConfig } : {}),
|
|
1403
|
+
instructions: input.instructions ?? null,
|
|
1404
|
+
...(input.vaultPolicy ? { vaultPolicy: input.vaultPolicy } : {}),
|
|
1405
|
+
policyRole: input.policyRole ?? null,
|
|
1406
|
+
...(input.agentAccess ? { agentAccess: input.agentAccess } : {}),
|
|
1407
|
+
...(input.scopeSubjectId !== undefined ? { scopeSubjectId: input.scopeSubjectId } : {}),
|
|
1408
|
+
...(input.memoryScope ? { memoryScope: input.memoryScope } : {}),
|
|
1409
|
+
parentSessionId: input.parentSessionId ?? null,
|
|
1410
|
+
...(input.codeSearchDeploymentPolicy
|
|
1411
|
+
? { codeSearchDeploymentPolicy: input.codeSearchDeploymentPolicy }
|
|
1412
|
+
: {}),
|
|
1413
|
+
sandboxGroupId: input.sandboxGroupId ?? null,
|
|
1414
|
+
...(input.sandboxOs ? { sandboxOs: input.sandboxOs } : {}),
|
|
1415
|
+
mcpServers: input.mcpServers ?? [],
|
|
1416
|
+
mcpApprovalPolicies: input.mcpApprovalPolicies ?? {},
|
|
1417
|
+
personalConnectionDelegations: input.personalConnectionDelegations ?? [],
|
|
1418
|
+
mcpAccountBindings: input.mcpAccountBindings ?? null,
|
|
1419
|
+
connectionAccountIntent: input.connectionAccountIntent ?? null,
|
|
1420
|
+
initialPersonalResourceAttachmentIntent:
|
|
1421
|
+
input.initialPersonalResourceAttachmentIntent ?? null,
|
|
1422
|
+
...(input.xaiProviderAccountAuthoritySnapshot
|
|
1423
|
+
? {
|
|
1424
|
+
initialXaiProviderAccountAuthoritySnapshot: input.xaiProviderAccountAuthoritySnapshot,
|
|
1425
|
+
}
|
|
1426
|
+
: {}),
|
|
1427
|
+
...(input.claudeProviderAccountAuthoritySnapshot
|
|
1428
|
+
? {
|
|
1429
|
+
initialClaudeProviderAccountAuthoritySnapshot:
|
|
1430
|
+
input.claudeProviderAccountAuthoritySnapshot,
|
|
1431
|
+
}
|
|
1432
|
+
: {}),
|
|
1433
|
+
maxNestedAgentDepthOverride: input.maxNestedAgentDepthOverride ?? null,
|
|
1434
|
+
allowNestedAgentDepthIncrease: input.allowNestedAgentDepthIncrease ?? false,
|
|
1435
|
+
subjectId: input.subjectId ?? null,
|
|
1436
|
+
...(beforeCreateCommit ? { beforeCreateCommit } : {}),
|
|
1437
|
+
}),
|
|
1438
|
+
);
|
|
1170
1439
|
} catch (error) {
|
|
1171
1440
|
if (error instanceof SessionSpawnDeniedDbError) {
|
|
1172
1441
|
throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(error.denial));
|
|
1173
1442
|
}
|
|
1174
1443
|
throw error;
|
|
1175
1444
|
}
|
|
1176
|
-
const finished = await
|
|
1445
|
+
const finished = await finishNewlyCreatedSession(
|
|
1177
1446
|
targetSeededBeforeCreateCommit ? { ...input, seedTargetSandbox: null } : input,
|
|
1178
1447
|
session,
|
|
1179
1448
|
);
|
|
1449
|
+
recordCreatedSessionUsage(input);
|
|
1180
1450
|
return {
|
|
1181
1451
|
session: finished.session,
|
|
1182
1452
|
outcome: "created",
|
|
@@ -1185,6 +1455,51 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
1185
1455
|
};
|
|
1186
1456
|
}
|
|
1187
1457
|
|
|
1458
|
+
/**
|
|
1459
|
+
* Start an unkeyed shell this call just inserted. If the start fails before
|
|
1460
|
+
* the first event or turn commits, the caller gets the error and has no way to
|
|
1461
|
+
* resume that session, so the bare shell is discarded instead of lingering as
|
|
1462
|
+
* a queued session nothing runs. A shell that did initialize (including an
|
|
1463
|
+
* outcome-unknown commit) is kept.
|
|
1464
|
+
*/
|
|
1465
|
+
async function finishNewlyCreatedSession(
|
|
1466
|
+
input: Parameters<typeof finishStartSession>[0],
|
|
1467
|
+
session: Session,
|
|
1468
|
+
): Promise<Awaited<ReturnType<typeof finishStartSession>>> {
|
|
1469
|
+
try {
|
|
1470
|
+
return await finishStartSession(input, session);
|
|
1471
|
+
} catch (error) {
|
|
1472
|
+
try {
|
|
1473
|
+
await discardUninitializedSessionShell(input.db, {
|
|
1474
|
+
accountId: session.accountId,
|
|
1475
|
+
workspaceId: session.workspaceId,
|
|
1476
|
+
sessionId: session.id,
|
|
1477
|
+
});
|
|
1478
|
+
} catch {
|
|
1479
|
+
// The original start failure is the actionable error; a shell that
|
|
1480
|
+
// cannot be discarded now is no worse than before this cleanup ran.
|
|
1481
|
+
}
|
|
1482
|
+
throw error;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
function recordCreatedSessionUsage(input: {
|
|
1487
|
+
metrics?: ProductUsageMetricsSink | null;
|
|
1488
|
+
surface?: SessionTurnSurface | null;
|
|
1489
|
+
createdBy?: TurnInitiator;
|
|
1490
|
+
parentSessionId?: string | null;
|
|
1491
|
+
deferInitialTurn?: boolean;
|
|
1492
|
+
}): void {
|
|
1493
|
+
recordSessionCreated(input.metrics, {
|
|
1494
|
+
surface: input.surface,
|
|
1495
|
+
createdByKind: input.createdBy?.kind ?? "service",
|
|
1496
|
+
parentSessionId: input.parentSessionId,
|
|
1497
|
+
});
|
|
1498
|
+
if (!input.deferInitialTurn) {
|
|
1499
|
+
recordUserMessageAccepted(input.metrics, { surface: input.surface });
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1188
1503
|
/** Backward-compatible entity-returning create path used by existing callers. */
|
|
1189
1504
|
export async function createAndStartSession(
|
|
1190
1505
|
input: Parameters<typeof createAndStartSessionWithOutcome>[0],
|
|
@@ -1203,6 +1518,7 @@ async function finishStartSession(
|
|
|
1203
1518
|
db: Database;
|
|
1204
1519
|
bus: EventBus;
|
|
1205
1520
|
workflowClient: Pick<SessionWorkflowClient, "wakeSessionWorkflow">;
|
|
1521
|
+
startupObservability?: SessionStartObservability | null;
|
|
1206
1522
|
initialMessage: string;
|
|
1207
1523
|
captureInitialTurnAuthority?: (
|
|
1208
1524
|
tx: Database,
|
|
@@ -1210,6 +1526,7 @@ async function finishStartSession(
|
|
|
1210
1526
|
turnId: string,
|
|
1211
1527
|
) => Promise<void>;
|
|
1212
1528
|
deferInitialTurn?: boolean;
|
|
1529
|
+
surface?: SessionTurnSurface | null;
|
|
1213
1530
|
modelContext?: string | null;
|
|
1214
1531
|
resources: ResourceRef[];
|
|
1215
1532
|
tools: ToolRef[];
|
|
@@ -1290,68 +1607,92 @@ async function finishStartSession(
|
|
|
1290
1607
|
});
|
|
1291
1608
|
}
|
|
1292
1609
|
}
|
|
1293
|
-
const started = await
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
captureInitialTurnAuthority: (tx: Database, turnId: string) =>
|
|
1300
|
-
input.captureInitialTurnAuthority!(tx, session.id, turnId),
|
|
1301
|
-
}
|
|
1302
|
-
: {}),
|
|
1303
|
-
...(input.clientEventId ? { clientEventId: input.clientEventId } : {}),
|
|
1304
|
-
reasoningEffortFallback: input.reasoningEffort,
|
|
1305
|
-
turnExecutionPolicy: input.turnExecutionPolicy,
|
|
1306
|
-
createdEventPayload: {
|
|
1307
|
-
toolPolicy: input.toolPolicy,
|
|
1308
|
-
...(input.variableSets?.length
|
|
1610
|
+
const started = await measureSessionStartPhase(input.startupObservability, "initialize", () =>
|
|
1611
|
+
initializeSessionStartAtomically(input.db, {
|
|
1612
|
+
accountId: session.accountId,
|
|
1613
|
+
workspaceId: session.workspaceId,
|
|
1614
|
+
sessionId: session.id,
|
|
1615
|
+
...(input.captureInitialTurnAuthority
|
|
1309
1616
|
? {
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
// Legacy highest-precedence aliases.
|
|
1313
|
-
variableSetId: input.variableSets.at(-1)!.id,
|
|
1314
|
-
variableSetName: input.variableSets.at(-1)!.name,
|
|
1617
|
+
captureInitialTurnAuthority: (tx: Database, turnId: string) =>
|
|
1618
|
+
input.captureInitialTurnAuthority!(tx, session.id, turnId),
|
|
1315
1619
|
}
|
|
1316
1620
|
: {}),
|
|
1317
|
-
...(input.
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1621
|
+
...(input.clientEventId ? { clientEventId: input.clientEventId } : {}),
|
|
1622
|
+
reasoningEffortFallback: input.reasoningEffort,
|
|
1623
|
+
turnExecutionPolicy: input.turnExecutionPolicy,
|
|
1624
|
+
surface: input.surface ?? null,
|
|
1625
|
+
createdEventPayload: {
|
|
1626
|
+
toolPolicy: input.toolPolicy,
|
|
1627
|
+
...(input.variableSets?.length
|
|
1628
|
+
? {
|
|
1629
|
+
variableSetIds: input.variableSets.map((variableSet) => variableSet.id),
|
|
1630
|
+
variableSets: input.variableSets,
|
|
1631
|
+
// Legacy highest-precedence aliases.
|
|
1632
|
+
variableSetId: input.variableSets.at(-1)!.id,
|
|
1633
|
+
variableSetName: input.variableSets.at(-1)!.name,
|
|
1634
|
+
}
|
|
1635
|
+
: {}),
|
|
1636
|
+
...(input.sessionMcpServers?.length ? { mcpServers: input.sessionMcpServers } : {}),
|
|
1637
|
+
},
|
|
1638
|
+
goal: input.goal
|
|
1639
|
+
? {
|
|
1640
|
+
text: input.goal.text,
|
|
1641
|
+
...(input.goal.successCriteria !== undefined
|
|
1642
|
+
? { successCriteria: input.goal.successCriteria }
|
|
1643
|
+
: {}),
|
|
1644
|
+
...(input.goal.rootConstraints !== undefined
|
|
1645
|
+
? { rootConstraints: input.goal.rootConstraints }
|
|
1646
|
+
: {}),
|
|
1647
|
+
...(input.goal.reportRequirements !== undefined
|
|
1648
|
+
? { reportRequirements: input.goal.reportRequirements }
|
|
1649
|
+
: {}),
|
|
1650
|
+
...(input.goal.maxAutoContinuations !== undefined
|
|
1651
|
+
? { maxAutoContinuations: input.goal.maxAutoContinuations }
|
|
1652
|
+
: {}),
|
|
1653
|
+
...(input.goal.mutationPolicy !== undefined
|
|
1654
|
+
? { mutationPolicy: input.goal.mutationPolicy }
|
|
1655
|
+
: {}),
|
|
1656
|
+
}
|
|
1657
|
+
: null,
|
|
1658
|
+
initialAutomaticTitle: initialAutomaticTitleForSessionStart(
|
|
1659
|
+
session,
|
|
1660
|
+
input.initialAutomaticTitle,
|
|
1661
|
+
),
|
|
1662
|
+
consumeNewSessionDraft: input.consumeNewSessionDraft ?? null,
|
|
1663
|
+
rememberNewSessionSelection: input.rememberNewSessionSelection ?? null,
|
|
1664
|
+
deferInitialTurn: input.deferInitialTurn === true,
|
|
1665
|
+
}),
|
|
1666
|
+
);
|
|
1667
|
+
// Initialization committed the initial turn, events and wake outbox together.
|
|
1668
|
+
// Live fanout is not workflow authority: signal from the durable revision
|
|
1669
|
+
// without waiting for NATS, but retain and fully join both notifications.
|
|
1670
|
+
const wakeRevision = started.workflowWakeRevision;
|
|
1671
|
+
const [fanout, wake] = await Promise.allSettled([
|
|
1672
|
+
measureSessionStartPhase(input.startupObservability, "event_fanout", () =>
|
|
1673
|
+
publishDurableSessionEvents(input.bus, session.workspaceId, session.id, started.events),
|
|
1339
1674
|
),
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1675
|
+
wakeRevision === null
|
|
1676
|
+
? Promise.resolve()
|
|
1677
|
+
: measureSessionStartPhase(input.startupObservability, "workflow_wake", () =>
|
|
1678
|
+
input.workflowClient.wakeSessionWorkflow({
|
|
1679
|
+
accountId: session.accountId,
|
|
1680
|
+
workspaceId: session.workspaceId,
|
|
1681
|
+
sessionId: session.id,
|
|
1682
|
+
workflowId: started.temporalWorkflowId,
|
|
1683
|
+
wakeRevision,
|
|
1684
|
+
}),
|
|
1685
|
+
),
|
|
1686
|
+
]);
|
|
1687
|
+
// Keep the prior serial error priority without detaching attempt work. Normal
|
|
1688
|
+
// transport failure remains best-effort inside publishDurableSessionEvents.
|
|
1689
|
+
if (fanout.status === "rejected") throw fanout.reason;
|
|
1690
|
+
if (wake.status === "rejected") throw wake.reason;
|
|
1691
|
+
const persisted = await measureSessionStartPhase(
|
|
1692
|
+
input.startupObservability,
|
|
1693
|
+
"session_reload",
|
|
1694
|
+
() => requireSession(input.db, session.workspaceId, session.id),
|
|
1695
|
+
);
|
|
1355
1696
|
const initialTurnId =
|
|
1356
1697
|
started.turn?.id ??
|
|
1357
1698
|
(await listSessionTurns(input.db, session.workspaceId, session.id, 1))[0]?.id ??
|
|
@@ -1366,6 +1707,17 @@ export function workflowIdForSession(sessionId: string): string {
|
|
|
1366
1707
|
return `session-${sessionId}`;
|
|
1367
1708
|
}
|
|
1368
1709
|
|
|
1710
|
+
/**
|
|
1711
|
+
* The one 422 for a model that is not selectable in the live catalog (retired,
|
|
1712
|
+
* removed, or never offered). The message keeps its historical text; the typed
|
|
1713
|
+
* cause lets the API envelope add `details.code: "model_unavailable"` so
|
|
1714
|
+
* clients can ask for another model instead of offering a futile retry.
|
|
1715
|
+
*/
|
|
1716
|
+
export function modelUnavailableHttpException(model: string): HTTPException {
|
|
1717
|
+
const cause = new ModelUnavailableError(model);
|
|
1718
|
+
return new HTTPException(422, { message: cause.message, cause });
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1369
1721
|
/**
|
|
1370
1722
|
* Reject an explicit model that the host does not expose. The set of usable
|
|
1371
1723
|
* models is the union surfaced by `configuredAllowedModels` (the built-in
|
|
@@ -1374,8 +1726,9 @@ export function workflowIdForSession(sessionId: string): string {
|
|
|
1374
1726
|
* the API edge with 422 rather than enqueuing a turn the worker can't honor.
|
|
1375
1727
|
*
|
|
1376
1728
|
* `model` is the effective value selected by the caller's boundary. Top-level
|
|
1377
|
-
* omission
|
|
1378
|
-
* the worker-signed calling turn before
|
|
1729
|
+
* omission resolves the new-chat default (`resolveDefaultSessionModel`);
|
|
1730
|
+
* child-session omission inherits the worker-signed calling turn before
|
|
1731
|
+
* reaching this helper. Centralized here
|
|
1379
1732
|
* so every model-carrying choke point
|
|
1380
1733
|
* (create-session, user-message/turn-accept, queued-turn update, and
|
|
1381
1734
|
* scheduled-task agentConfig — a scheduled task is a session the worker runs
|
|
@@ -1390,6 +1743,18 @@ export function canonicalConfiguredModel(
|
|
|
1390
1743
|
return model;
|
|
1391
1744
|
}
|
|
1392
1745
|
const canonicalModel = canonicalizeConfiguredModelId(settings, model);
|
|
1746
|
+
if (
|
|
1747
|
+
canonicalModel.startsWith(CODEX_MODEL_ID_PREFIX) &&
|
|
1748
|
+
settings.resolvedCodexModelsJson !== undefined
|
|
1749
|
+
) {
|
|
1750
|
+
if (
|
|
1751
|
+
settings.codexSubscriptionEnabled &&
|
|
1752
|
+
configuredAllowedModels(withCodexCatalogProvider(settings)).includes(canonicalModel)
|
|
1753
|
+
) {
|
|
1754
|
+
return canonicalModel;
|
|
1755
|
+
}
|
|
1756
|
+
throw modelUnavailableHttpException(model);
|
|
1757
|
+
}
|
|
1393
1758
|
if (configuredAllowedModels(settings).includes(canonicalModel)) {
|
|
1394
1759
|
return canonicalModel;
|
|
1395
1760
|
}
|
|
@@ -1412,7 +1777,7 @@ export function canonicalConfiguredModel(
|
|
|
1412
1777
|
) {
|
|
1413
1778
|
return canonicalModel;
|
|
1414
1779
|
}
|
|
1415
|
-
throw
|
|
1780
|
+
throw modelUnavailableHttpException(model);
|
|
1416
1781
|
}
|
|
1417
1782
|
|
|
1418
1783
|
export function assertConfiguredModel(settings: Settings, model: string | null | undefined): void {
|
|
@@ -1455,10 +1820,10 @@ export function assertSessionAllowsProductModel(
|
|
|
1455
1820
|
* worker's authoritative post-resolution gate would fail. `model` is the
|
|
1456
1821
|
* EFFECTIVE value the caller is about to persist: pass the explicit value at
|
|
1457
1822
|
* message/turn-update/scheduled-task edges (omitted inherits an
|
|
1458
|
-
* already-validated stored default), but at session CREATION pass
|
|
1459
|
-
*
|
|
1460
|
-
*
|
|
1461
|
-
*
|
|
1823
|
+
* already-validated stored default), but at session CREATION pass the
|
|
1824
|
+
* effective model: an omitted model stamps the resolved default onto the
|
|
1825
|
+
* session, and under a restricted policy that default may be exactly the
|
|
1826
|
+
* provider the policy exists to block.
|
|
1462
1827
|
*/
|
|
1463
1828
|
export async function assertWorkspaceModelPolicyAllows(
|
|
1464
1829
|
db: Database,
|
|
@@ -1481,13 +1846,16 @@ export async function assertWorkspaceModelPolicyAllows(
|
|
|
1481
1846
|
const verdict = evaluateWorkspaceModelPolicy(policy, {
|
|
1482
1847
|
providerId,
|
|
1483
1848
|
modelId: canonicalModel,
|
|
1849
|
+
chargesCredits: policyChargesCreditsForModel(settings, canonicalModel),
|
|
1484
1850
|
});
|
|
1485
1851
|
if (!verdict.allowed) {
|
|
1486
1852
|
throw new HTTPException(422, {
|
|
1487
1853
|
message:
|
|
1488
|
-
verdict.reason === "
|
|
1489
|
-
? `model "${canonicalModel}"
|
|
1490
|
-
:
|
|
1854
|
+
verdict.reason === "credits"
|
|
1855
|
+
? `model "${canonicalModel}" uses Opengeni credits, which are turned off in this workspace. Choose a model paid by a subscription or API key, or ask a workspace admin to turn Opengeni credits back on`
|
|
1856
|
+
: verdict.reason === "provider"
|
|
1857
|
+
? `model "${canonicalModel}" is not allowed by this workspace's model policy: provider "${providerId}" is not in the allowed providers`
|
|
1858
|
+
: `model "${canonicalModel}" is not allowed by this workspace's model policy`,
|
|
1491
1859
|
});
|
|
1492
1860
|
}
|
|
1493
1861
|
}
|
|
@@ -1547,11 +1915,16 @@ type PostUserMessageTurnInput = {
|
|
|
1547
1915
|
clientEventId?: string;
|
|
1548
1916
|
mcpCredentialUpdates?: UpdateSessionMcpServerCredentialsInput[];
|
|
1549
1917
|
personalConnectionDelegations?: McpPersonalConnectionDelegation[];
|
|
1550
|
-
|
|
1918
|
+
mcpAccountBindings?: McpConnectionAccountBinding[] | null;
|
|
1919
|
+
/** The sender's account intent, carried by work that continues this turn. */
|
|
1920
|
+
connectionAccountIntent?: ConnectionAccountIntent | null;
|
|
1921
|
+
|
|
1551
1922
|
captureTurnAuthority?: (tx: Database, turnId: string) => Promise<void>;
|
|
1552
1923
|
personalResourceAttachment?: PersonalResourceAttachmentIntent;
|
|
1553
1924
|
delivery?: "send" | "steer";
|
|
1554
1925
|
origin?: "human" | "operator";
|
|
1926
|
+
/** Content-free product surface the request entered through. */
|
|
1927
|
+
surface?: SessionTurnSurface | null;
|
|
1555
1928
|
actor?: string;
|
|
1556
1929
|
actorLabel?: string;
|
|
1557
1930
|
commandActor?: SessionCommandActor;
|
|
@@ -1682,6 +2055,8 @@ export async function postUserMessageTurn(
|
|
|
1682
2055
|
const { db, settings, accountId, workspaceId, sessionId } = input;
|
|
1683
2056
|
const requestedModel = canonicalConfiguredModel(settings, input.model ?? null) ?? null;
|
|
1684
2057
|
const requestedReasoningEffort = input.reasoningEffort ?? null;
|
|
2058
|
+
// A message sent from a Site page records which Site (and version) on its turn.
|
|
2059
|
+
const siteOrigin = currentSiteSessionOrigin();
|
|
1685
2060
|
// Reject an explicit per-message model the host does not expose; an omitted
|
|
1686
2061
|
// model inherits the session's model downstream (always a configured id).
|
|
1687
2062
|
assertConfiguredModel(settings, requestedModel);
|
|
@@ -1751,13 +2126,15 @@ export async function postUserMessageTurn(
|
|
|
1751
2126
|
input.reasoningEffortFallback ?? settings.openaiReasoningEffort,
|
|
1752
2127
|
turnExecutionPolicy: input.turnExecutionPolicy,
|
|
1753
2128
|
source: input.origin === "operator" ? "api" : "user",
|
|
2129
|
+
surface: input.surface ?? null,
|
|
1754
2130
|
...(input.recordAgentRunUsage !== undefined
|
|
1755
2131
|
? { recordAgentRunUsage: input.recordAgentRunUsage }
|
|
1756
2132
|
: {}),
|
|
1757
2133
|
personalConnectionDelegations: input.personalConnectionDelegations ?? [],
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
2134
|
+
mcpAccountBindings: input.mcpAccountBindings ?? null,
|
|
2135
|
+
connectionAccountIntent: input.connectionAccountIntent ?? null,
|
|
2136
|
+
...(siteOrigin ? { turnMetadata: { [SITE_ORIGIN_METADATA_KEY]: siteOrigin } } : {}),
|
|
2137
|
+
|
|
1761
2138
|
...(input.captureTurnAuthority
|
|
1762
2139
|
? { captureTurnAuthority: input.captureTurnAuthority }
|
|
1763
2140
|
: {}),
|
|
@@ -1769,7 +2146,10 @@ export async function postUserMessageTurn(
|
|
|
1769
2146
|
mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
|
|
1770
2147
|
...(freshWorkspaceCustomModel
|
|
1771
2148
|
? {
|
|
1772
|
-
beforeFreshPromptCommit: async (
|
|
2149
|
+
beforeFreshPromptCommit: async (
|
|
2150
|
+
tx: Database,
|
|
2151
|
+
{ claudeProviderAccountAuthoritySnapshot, claudeAcceptedSource },
|
|
2152
|
+
): Promise<void> => {
|
|
1773
2153
|
const reference = workspaceCustomModelReference(
|
|
1774
2154
|
settings,
|
|
1775
2155
|
freshWorkspaceCustomModel,
|
|
@@ -1781,17 +2161,24 @@ export async function postUserMessageTurn(
|
|
|
1781
2161
|
accountId,
|
|
1782
2162
|
workspaceId,
|
|
1783
2163
|
reference,
|
|
2164
|
+
claudeAuthority: {
|
|
2165
|
+
authoritySnapshot: claudeProviderAccountAuthoritySnapshot,
|
|
2166
|
+
...(claudeAcceptedSource ? { acceptedSource: claudeAcceptedSource } : {}),
|
|
2167
|
+
},
|
|
1784
2168
|
});
|
|
1785
2169
|
if (!active) {
|
|
1786
|
-
throw
|
|
1787
|
-
message: `model is not available: ${freshWorkspaceCustomModel}`,
|
|
1788
|
-
});
|
|
2170
|
+
throw modelUnavailableHttpException(freshWorkspaceCustomModel);
|
|
1789
2171
|
}
|
|
1790
2172
|
},
|
|
1791
2173
|
}
|
|
1792
2174
|
: {}),
|
|
1793
2175
|
controlLockTimeoutMs: workspaceControlRequestLockTimeoutMs(),
|
|
1794
2176
|
}),
|
|
2177
|
+
undefined,
|
|
2178
|
+
"shared",
|
|
2179
|
+
// Linked-actor capture reauthorizes under the organization-membership
|
|
2180
|
+
// lock; take it before the tenancy fence and the canonical prefix.
|
|
2181
|
+
Boolean(input.captureTurnAuthority),
|
|
1795
2182
|
),
|
|
1796
2183
|
);
|
|
1797
2184
|
} catch (error) {
|
|
@@ -1981,11 +2368,144 @@ async function resolveWorkspaceModelBoundarySettings(
|
|
|
1981
2368
|
).settings;
|
|
1982
2369
|
}
|
|
1983
2370
|
|
|
2371
|
+
/** Explicit recovery, never a new prompt or a Pause/Resume command. */
|
|
2372
|
+
export async function retryFailedSession(
|
|
2373
|
+
deps: ApiRouteDeps,
|
|
2374
|
+
grant: AccessGrant,
|
|
2375
|
+
workspaceId: string,
|
|
2376
|
+
sessionId: string,
|
|
2377
|
+
request: SessionRetryRequest,
|
|
2378
|
+
): Promise<SessionRetryResponse> {
|
|
2379
|
+
requirePermission(grant, "sessions:control");
|
|
2380
|
+
await requireSessionAuthorization(deps, grant, {
|
|
2381
|
+
sessionId,
|
|
2382
|
+
operation: "session.control",
|
|
2383
|
+
surface: "core",
|
|
2384
|
+
});
|
|
2385
|
+
const replay = await withWorkspaceSubjectSessionActivityRls(
|
|
2386
|
+
deps.db,
|
|
2387
|
+
workspaceId,
|
|
2388
|
+
grant.subjectId,
|
|
2389
|
+
async (db) =>
|
|
2390
|
+
await getSessionRetryReceiptInTransaction(db, {
|
|
2391
|
+
workspaceId,
|
|
2392
|
+
sessionId,
|
|
2393
|
+
subjectId: grant.subjectId,
|
|
2394
|
+
request,
|
|
2395
|
+
}),
|
|
2396
|
+
);
|
|
2397
|
+
if (replay) return replay;
|
|
2398
|
+
const session = await requireSession(deps.db, workspaceId, sessionId);
|
|
2399
|
+
// A retry is a new model request: a reached task usage limit refuses it.
|
|
2400
|
+
await requireSessionUsageLimitAllows(
|
|
2401
|
+
deps.db,
|
|
2402
|
+
{ accountId: grant.accountId, workspaceId, sessionId, runtime: true },
|
|
2403
|
+
{ sink: deps.observability, boundary: "send" },
|
|
2404
|
+
);
|
|
2405
|
+
const failure = await getSessionEvent(deps.db, workspaceId, request.failureEventId);
|
|
2406
|
+
if (!failure || failure.sessionId !== sessionId)
|
|
2407
|
+
throw new SessionRetryConflictError("RETRY_STALE_FAILURE", "The failure event is unavailable");
|
|
2408
|
+
const turn = failure?.turnId ? await getSessionTurn(deps.db, workspaceId, failure.turnId) : null;
|
|
2409
|
+
const retainedModel = turn?.model ?? session.model;
|
|
2410
|
+
const settings = await resolveWorkspaceModelBoundarySettings(
|
|
2411
|
+
deps,
|
|
2412
|
+
grant,
|
|
2413
|
+
workspaceId,
|
|
2414
|
+
[request.model ?? retainedModel],
|
|
2415
|
+
retainedModel,
|
|
2416
|
+
);
|
|
2417
|
+
const model = canonicalConfiguredModel(settings, request.model ?? retainedModel)!;
|
|
2418
|
+
try {
|
|
2419
|
+
assertSessionAllowsProductModel(session, model);
|
|
2420
|
+
} catch (error) {
|
|
2421
|
+
if (error instanceof CodexCompactionV2ProviderLockedError)
|
|
2422
|
+
throw new HTTPException(422, { message: error.message, cause: error });
|
|
2423
|
+
throw error;
|
|
2424
|
+
}
|
|
2425
|
+
await assertWorkspaceModelPolicyAllows(deps.db, settings, workspaceId, model);
|
|
2426
|
+
const executionPolicy = withDeveloperSetupCredentialRestriction(
|
|
2427
|
+
resolveTurnExecutionPolicyV1(settings, {
|
|
2428
|
+
modelId: model,
|
|
2429
|
+
requestedModelId: request.model ?? null,
|
|
2430
|
+
modelSource: request.model === undefined ? "session" : "explicit",
|
|
2431
|
+
reasoningEffort: request.reasoningEffort ?? turn?.reasoningEffort ?? session.reasoningEffort,
|
|
2432
|
+
reasoningSource: request.reasoningEffort === undefined ? "session" : "explicit",
|
|
2433
|
+
latencyMode: request.latencyMode ?? turn?.latencyMode ?? session.latencyMode,
|
|
2434
|
+
latencyModeSource: request.latencyMode === undefined ? "session" : "explicit",
|
|
2435
|
+
}),
|
|
2436
|
+
{ grant, trustedMetadata: [session.metadata, turn?.metadata] },
|
|
2437
|
+
);
|
|
2438
|
+
await requireLimit(
|
|
2439
|
+
{ ...deps, settings },
|
|
2440
|
+
{
|
|
2441
|
+
accountId: grant.accountId,
|
|
2442
|
+
workspaceId,
|
|
2443
|
+
initiatingHumanSubjectId: turn
|
|
2444
|
+
? initiatingHumanForAllowance({
|
|
2445
|
+
initiator: turn.initiator,
|
|
2446
|
+
initiatingHumanSubjectId: await getSessionTurnInitiatingHumanSubjectId(
|
|
2447
|
+
deps.db,
|
|
2448
|
+
workspaceId,
|
|
2449
|
+
turn.id,
|
|
2450
|
+
),
|
|
2451
|
+
})
|
|
2452
|
+
: null,
|
|
2453
|
+
action: "agent_run:create",
|
|
2454
|
+
quantity: 1,
|
|
2455
|
+
model,
|
|
2456
|
+
...(turn ? { acceptedTurn: { sessionId: session.id, turnId: turn.id } } : {}),
|
|
2457
|
+
},
|
|
2458
|
+
);
|
|
2459
|
+
const result = await runIdempotentPersistenceTransaction(
|
|
2460
|
+
{
|
|
2461
|
+
stage: "session.retry",
|
|
2462
|
+
eventTypes: ["turn.recovery.requested", "session.status.changed"],
|
|
2463
|
+
maxAttempts: 3,
|
|
2464
|
+
},
|
|
2465
|
+
async () =>
|
|
2466
|
+
await withWorkspaceSubjectSessionActivityRls(
|
|
2467
|
+
deps.db,
|
|
2468
|
+
workspaceId,
|
|
2469
|
+
grant.subjectId,
|
|
2470
|
+
async (db) =>
|
|
2471
|
+
await retryFailedSessionInTransaction(db, {
|
|
2472
|
+
accountId: grant.accountId,
|
|
2473
|
+
workspaceId,
|
|
2474
|
+
sessionId,
|
|
2475
|
+
subjectId: grant.subjectId,
|
|
2476
|
+
request,
|
|
2477
|
+
executionPolicy,
|
|
2478
|
+
}),
|
|
2479
|
+
),
|
|
2480
|
+
);
|
|
2481
|
+
// Fanout and workflow dispatch are reconstructible after the durable commit.
|
|
2482
|
+
// Never report an admitted retry as failed because one notification is down.
|
|
2483
|
+
void Promise.all([
|
|
2484
|
+
deps.workflowClient.requestSessionWorkflowWakeDispatch(),
|
|
2485
|
+
(async () => {
|
|
2486
|
+
const events = await Promise.all(
|
|
2487
|
+
result.eventIds.map((id) => getSessionEvent(deps.db, workspaceId, id)),
|
|
2488
|
+
);
|
|
2489
|
+
await publishDurableSessionEvents(
|
|
2490
|
+
deps.bus,
|
|
2491
|
+
workspaceId,
|
|
2492
|
+
sessionId,
|
|
2493
|
+
events.filter((event): event is SessionEvent => event !== null),
|
|
2494
|
+
);
|
|
2495
|
+
})(),
|
|
2496
|
+
]).catch(() => {
|
|
2497
|
+
console.warn("[sessions] retry notification failed; durable recovery remains", {
|
|
2498
|
+
errorCode: "session_retry_notification_failed",
|
|
2499
|
+
});
|
|
2500
|
+
});
|
|
2501
|
+
return { outcome: result.outcome, turnId: result.turnId, failureEventId: result.failureEventId };
|
|
2502
|
+
}
|
|
2503
|
+
|
|
1984
2504
|
async function withSessionCreateUsageRecording(input: {
|
|
1985
2505
|
deps: ApiRouteDeps;
|
|
1986
2506
|
grant: AccessGrant;
|
|
1987
2507
|
workspaceId: string;
|
|
1988
|
-
startMode: "realtime" | undefined;
|
|
2508
|
+
startMode: "realtime" | "standard" | undefined;
|
|
1989
2509
|
origin: "system" | "user";
|
|
1990
2510
|
createOutcome: CreateSessionOutcome;
|
|
1991
2511
|
}): Promise<CreateSessionRequestOutcome> {
|
|
@@ -2022,29 +2542,30 @@ async function createSessionForRequestInFileScope(
|
|
|
2022
2542
|
rawPayload: unknown,
|
|
2023
2543
|
authorization?: AccessGrantAuthorization,
|
|
2024
2544
|
agentChildPresentation?: AgentChildSessionCreatePresentation,
|
|
2545
|
+
requestOptions: SessionCreateRequestOptions = {},
|
|
2025
2546
|
): Promise<CreateSessionRequestOutcome> {
|
|
2026
2547
|
const payload = CreateSessionRequest.parse(rawPayload);
|
|
2548
|
+
// An agent started from a Site page keeps its workers off chosen machines
|
|
2549
|
+
// and other sessions' sandboxes.
|
|
2550
|
+
await assertSiteAgentMayChooseCompute(unresolvedDeps.db, grant, payload);
|
|
2551
|
+
// A tool whose backing workload this deployment does not run is dropped, not
|
|
2552
|
+
// rejected: a client echoing a full catalog must not fail on a deployment fact.
|
|
2553
|
+
if (payload.firstPartyMcpTools) {
|
|
2554
|
+
const unavailable = deploymentUnavailableFirstPartyMcpTools(unresolvedDeps.settings);
|
|
2555
|
+
payload.firstPartyMcpTools = payload.firstPartyMcpTools.filter(
|
|
2556
|
+
(tool) => !unavailable.has(tool),
|
|
2557
|
+
);
|
|
2558
|
+
}
|
|
2559
|
+
// Read before any await: the Site scope is request-local provenance.
|
|
2560
|
+
const surface = resolveTurnSurface({
|
|
2561
|
+
grant,
|
|
2562
|
+
authorization,
|
|
2563
|
+
requested: requestOptions.surface,
|
|
2564
|
+
});
|
|
2027
2565
|
payload.metadata = sessionCreationMetadata(payload.metadata);
|
|
2028
2566
|
const creationMetadata = externalCreationMetadata(payload.metadata, authorization, grant);
|
|
2029
2567
|
const externalBeforeCreateCommit = externalContinuationCommitAuthorizer(authorization);
|
|
2030
|
-
|
|
2031
|
-
if (
|
|
2032
|
-
hostSelections.length &&
|
|
2033
|
-
(!authorization ||
|
|
2034
|
-
authorization.grant.accountId !== grant.accountId ||
|
|
2035
|
-
authorization.grant.subjectId !== grant.subjectId ||
|
|
2036
|
-
authorization?.grant.workspaceId !== workspaceId ||
|
|
2037
|
-
!hasPermission(authorization?.grant.permissions ?? [], "connections:read") ||
|
|
2038
|
-
!hasPermission(grant.permissions, "connections:read") ||
|
|
2039
|
-
grant.metadata?.["sessionId"] ||
|
|
2040
|
-
payload.startMode === "realtime")
|
|
2041
|
-
) {
|
|
2042
|
-
throw new HTTPException(403, {
|
|
2043
|
-
message: "Host selection requires a verified direct owner and non-realtime start",
|
|
2044
|
-
});
|
|
2045
|
-
}
|
|
2046
|
-
if (hostSelections.length)
|
|
2047
|
-
prepareHostMcpOwnerAuthorization(authorization!, workspaceId, "connections:read");
|
|
2568
|
+
|
|
2048
2569
|
if (hasReservedOpenGeniSlackBotSessionMetadata(payload.metadata)) {
|
|
2049
2570
|
throw new HTTPException(422, {
|
|
2050
2571
|
message: `${OPENGENI_SLACK_BOT_SESSION_METADATA_KEY} is reserved for scheduler routing`,
|
|
@@ -2059,11 +2580,10 @@ async function createSessionForRequestInFileScope(
|
|
|
2059
2580
|
});
|
|
2060
2581
|
}
|
|
2061
2582
|
await requireManagedHumanPrivateSessionCreate(unresolvedDeps, authorization, workspaceId);
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
}
|
|
2583
|
+
// Who can read a session and who can use a sandbox are separate: an
|
|
2584
|
+
// Only-me session may join a sandbox group the creator can already see.
|
|
2585
|
+
// Its own records stay scoped to its audience; files on the box are as
|
|
2586
|
+
// shared as the box.
|
|
2067
2587
|
}
|
|
2068
2588
|
// Parent linkage and execution-context inheritance come ONLY from the
|
|
2069
2589
|
// worker-signed sessionId claim. A caller cannot nominate a parent in the
|
|
@@ -2087,13 +2607,76 @@ async function createSessionForRequestInFileScope(
|
|
|
2087
2607
|
throw error;
|
|
2088
2608
|
}
|
|
2089
2609
|
}
|
|
2610
|
+
// a create-time usage limit belongs to a new ROOT task and only a
|
|
2611
|
+
// person or the trusted host may set it. Agents (including children, which
|
|
2612
|
+
// always share their root's limit) are rejected even when the value is null.
|
|
2613
|
+
let initialUsageLimitActor: ReturnType<typeof sessionUsageLimitWriteActorForAccess> | null = null;
|
|
2614
|
+
if (payload.usageLimitMicros !== undefined) {
|
|
2615
|
+
try {
|
|
2616
|
+
if (parentSessionId || !authorization) {
|
|
2617
|
+
throw new SessionUsageLimitHttpError(
|
|
2618
|
+
403,
|
|
2619
|
+
"session_usage_limit_forbidden",
|
|
2620
|
+
"Only a person or the application's server can set a task usage limit.",
|
|
2621
|
+
);
|
|
2622
|
+
}
|
|
2623
|
+
initialUsageLimitActor = sessionUsageLimitWriteActorForAccess(authorization);
|
|
2624
|
+
} catch (error) {
|
|
2625
|
+
recordSessionUsageLimitEvent(unresolvedDeps.observability, "create", "forbidden");
|
|
2626
|
+
throw error;
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
if (
|
|
2630
|
+
payload.usageLimitMicros !== undefined &&
|
|
2631
|
+
payload.usageLimitMicros !== null &&
|
|
2632
|
+
!sessionUsageLimitsProducerEnabled(unresolvedDeps.settings)
|
|
2633
|
+
) {
|
|
2634
|
+
recordSessionUsageLimitEvent(unresolvedDeps.observability, "create", "disabled");
|
|
2635
|
+
throw new SessionUsageLimitHttpError(
|
|
2636
|
+
422,
|
|
2637
|
+
"session_usage_limit_disabled",
|
|
2638
|
+
"Usage limits are not enabled on this server.",
|
|
2639
|
+
);
|
|
2640
|
+
}
|
|
2641
|
+
// The trusted server in this process (published Site runtime) may attach a
|
|
2642
|
+
// limit to a root session it creates as its own service principal, so the
|
|
2643
|
+
// limit is in place before the first model call. Requests cannot set it.
|
|
2644
|
+
const hostUsageLimit = currentHostSessionUsageLimit();
|
|
2645
|
+
const hostInitialUsageLimit =
|
|
2646
|
+
payload.usageLimitMicros === undefined &&
|
|
2647
|
+
!parentSessionId &&
|
|
2648
|
+
hostUsageLimit !== undefined &&
|
|
2649
|
+
hostUsageLimit.subjectId === grant.subjectId
|
|
2650
|
+
? hostUsageLimit
|
|
2651
|
+
: null;
|
|
2652
|
+
if (hostInitialUsageLimit && !sessionUsageLimitsProducerEnabled(unresolvedDeps.settings)) {
|
|
2653
|
+
recordSessionUsageLimitEvent(unresolvedDeps.observability, "create", "disabled");
|
|
2654
|
+
throw new SessionUsageLimitHttpError(
|
|
2655
|
+
422,
|
|
2656
|
+
"session_usage_limit_disabled",
|
|
2657
|
+
"Usage limits are not enabled on this server.",
|
|
2658
|
+
);
|
|
2659
|
+
}
|
|
2660
|
+
// A task whose usage limit is reached cannot start more inference by
|
|
2661
|
+
// spawning another agent.
|
|
2662
|
+
if (parentSessionId) {
|
|
2663
|
+
await requireSessionUsageLimitAllows(
|
|
2664
|
+
db,
|
|
2665
|
+
{ accountId: grant.accountId, workspaceId, sessionId: parentSessionId },
|
|
2666
|
+
{ sink: unresolvedDeps.observability, boundary: "child_create" },
|
|
2667
|
+
);
|
|
2668
|
+
}
|
|
2090
2669
|
const parentSession = parentSessionId ? await getSession(db, workspaceId, parentSessionId) : null;
|
|
2091
2670
|
if (parentSessionId && !parentSession) {
|
|
2092
2671
|
throw new HTTPException(404, {
|
|
2093
2672
|
message: `parent session not found in workspace: ${parentSessionId}`,
|
|
2094
2673
|
});
|
|
2095
2674
|
}
|
|
2096
|
-
const workspace = await
|
|
2675
|
+
const workspace = await measureSessionStartPhase(
|
|
2676
|
+
unresolvedDeps.observability,
|
|
2677
|
+
"workspace_read",
|
|
2678
|
+
() => requireWorkspace(db, workspaceId),
|
|
2679
|
+
);
|
|
2097
2680
|
const workspaceSessionToolDefaults = resolveWorkspaceSessionToolDefaults(workspace.settings);
|
|
2098
2681
|
const parentAuthority = parentSession
|
|
2099
2682
|
? await getSessionAuthorityProjection(db, workspaceId, parentSession.id)
|
|
@@ -2150,6 +2733,27 @@ async function createSessionForRequestInFileScope(
|
|
|
2150
2733
|
}
|
|
2151
2734
|
: null,
|
|
2152
2735
|
});
|
|
2736
|
+
// One frozen agent configuration for this session (null = exact legacy).
|
|
2737
|
+
// Resolved before keyed replay so a retry that resolves differently is a
|
|
2738
|
+
// conflict rather than a silent replay.
|
|
2739
|
+
const agentConfigCreator: AgentConfigCreator =
|
|
2740
|
+
requestOptions.agentConfigCreator ?? (requestOptions.surface === "slack" ? "slack" : "api");
|
|
2741
|
+
const agentResolution = resolveSessionAgentConfigForCreate({
|
|
2742
|
+
settings: unresolvedDeps.settings,
|
|
2743
|
+
creator: agentConfigCreator,
|
|
2744
|
+
request: payload.agent,
|
|
2745
|
+
instructions: payload.instructions,
|
|
2746
|
+
workspaceSettings: workspace.settings,
|
|
2747
|
+
parent: parentSession,
|
|
2748
|
+
goal: payload.goal !== undefined,
|
|
2749
|
+
});
|
|
2750
|
+
const agentConfig = agentResolution.config;
|
|
2751
|
+
if (agentResolution.instructions !== undefined) {
|
|
2752
|
+
payload.instructions = agentResolution.instructions;
|
|
2753
|
+
}
|
|
2754
|
+
// Freeze what the agent configuration implies before keyed replay compares
|
|
2755
|
+
// it: a `"none"` agent omits the bundled guides unless the request lists them.
|
|
2756
|
+
bundledSkillIds = bundledSkillSelectionForAgentConfig(bundledSkillIds, agentConfig);
|
|
2153
2757
|
const parentCallingTurn =
|
|
2154
2758
|
parentSession && creationInitiator.actor
|
|
2155
2759
|
? await getSessionTurnForAttempt(
|
|
@@ -2176,36 +2780,47 @@ async function createSessionForRequestInFileScope(
|
|
|
2176
2780
|
? grant.subjectId
|
|
2177
2781
|
: null;
|
|
2178
2782
|
let retainedKeyedShellModel: string | null = null;
|
|
2783
|
+
let retainedKeyedShellReasoningEffort: Session["reasoningEffort"] | null = null;
|
|
2179
2784
|
if (
|
|
2180
2785
|
payload.idempotencyKey &&
|
|
2181
2786
|
(effectiveVisibility !== "user_private" || replayManagedHumanSubjectId !== null)
|
|
2182
2787
|
) {
|
|
2788
|
+
const createIdempotencyKey = payload.idempotencyKey;
|
|
2183
2789
|
try {
|
|
2184
|
-
const initializedReplay = await
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2790
|
+
const initializedReplay = await measureSessionStartPhase(
|
|
2791
|
+
unresolvedDeps.observability,
|
|
2792
|
+
"idempotent_replay",
|
|
2793
|
+
() =>
|
|
2794
|
+
getInitializedSessionCreateReplay(db, {
|
|
2795
|
+
bundledSkillIds,
|
|
2796
|
+
accountId: grant.accountId,
|
|
2797
|
+
workspaceId,
|
|
2798
|
+
subjectId: replayManagedHumanSubjectId ?? grant.subjectId,
|
|
2799
|
+
...(replayManagedHumanSubjectId
|
|
2800
|
+
? { activeManagedHumanSubjectId: replayManagedHumanSubjectId }
|
|
2801
|
+
: {}),
|
|
2802
|
+
createIdempotencyKey,
|
|
2803
|
+
selectedInstalledSkillIds: payload.installedSkillIds ?? [],
|
|
2804
|
+
initialAgentLearning: payload.agentLearning,
|
|
2805
|
+
...sessionScope,
|
|
2806
|
+
|
|
2807
|
+
...(payload.requestedSessionId
|
|
2808
|
+
? { requestedSessionId: payload.requestedSessionId }
|
|
2809
|
+
: {}),
|
|
2810
|
+
visibility: effectiveVisibility,
|
|
2811
|
+
variableSetIds: payload.variableSetIds ?? [],
|
|
2812
|
+
initialPersonalResourceAttachmentIntent: payload.personalResourceAttachment ?? null,
|
|
2813
|
+
deferInitialTurn: payload.startMode === "realtime",
|
|
2814
|
+
agentConfig,
|
|
2815
|
+
}),
|
|
2816
|
+
);
|
|
2817
|
+
if (initializedReplay) {
|
|
2818
|
+
if (initializedReplay.outcome === "denied") {
|
|
2819
|
+
throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(initializedReplay.denial));
|
|
2820
|
+
}
|
|
2207
2821
|
if (initializedReplay.outcome === "pending") {
|
|
2208
2822
|
retainedKeyedShellModel = initializedReplay.session.model;
|
|
2823
|
+
retainedKeyedShellReasoningEffort = initializedReplay.session.reasoningEffort;
|
|
2209
2824
|
} else {
|
|
2210
2825
|
if (initializedReplay.workflowWakeRevision !== null) {
|
|
2211
2826
|
await unresolvedDeps.workflowClient.wakeSessionWorkflow({
|
|
@@ -2243,12 +2858,17 @@ async function createSessionForRequestInFileScope(
|
|
|
2243
2858
|
throw error;
|
|
2244
2859
|
}
|
|
2245
2860
|
}
|
|
2246
|
-
let settings = await
|
|
2247
|
-
unresolvedDeps,
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2861
|
+
let settings = await measureSessionStartPhase(
|
|
2862
|
+
unresolvedDeps.observability,
|
|
2863
|
+
"model_catalog_initial",
|
|
2864
|
+
() =>
|
|
2865
|
+
resolveWorkspaceModelBoundarySettings(
|
|
2866
|
+
unresolvedDeps,
|
|
2867
|
+
grant,
|
|
2868
|
+
workspaceId,
|
|
2869
|
+
[payload.model],
|
|
2870
|
+
retainedKeyedShellModel,
|
|
2871
|
+
),
|
|
2252
2872
|
);
|
|
2253
2873
|
let deps =
|
|
2254
2874
|
settings === unresolvedDeps.settings
|
|
@@ -2266,14 +2886,42 @@ async function createSessionForRequestInFileScope(
|
|
|
2266
2886
|
payload.personalResourceAttachment,
|
|
2267
2887
|
false,
|
|
2268
2888
|
);
|
|
2269
|
-
|
|
2889
|
+
// A top-level create that names no model gets the resolved new-chat default
|
|
2890
|
+
// (saved workspace default, then a usable connected subscription, then the
|
|
2891
|
+
// credits default while the organization holds credits, then the deployment
|
|
2892
|
+
// default). Children still inherit their calling turn, never this default.
|
|
2893
|
+
// A keyed retry of a still-uninitialized shell keeps the default that shell
|
|
2894
|
+
// already persisted instead of resolving again.
|
|
2895
|
+
const resolvedDefault =
|
|
2896
|
+
parentSession || payload.model !== undefined
|
|
2897
|
+
? null
|
|
2898
|
+
: retainedKeyedShellModel !== null && retainedKeyedShellReasoningEffort !== null
|
|
2899
|
+
? { model: retainedKeyedShellModel, reasoningEffort: retainedKeyedShellReasoningEffort }
|
|
2900
|
+
: await measureSessionStartPhase(unresolvedDeps.observability, "default_model", () =>
|
|
2901
|
+
resolveDefaultSessionModel(db, settings, {
|
|
2902
|
+
accountId: grant.accountId,
|
|
2903
|
+
workspaceId,
|
|
2904
|
+
subjectId: grant.subjectId,
|
|
2905
|
+
workspaceSettings: workspace.settings,
|
|
2906
|
+
}),
|
|
2907
|
+
);
|
|
2908
|
+
const inheritedModel =
|
|
2909
|
+
parentCallingTurn?.model ??
|
|
2910
|
+
parentSession?.model ??
|
|
2911
|
+
resolvedDefault?.model ??
|
|
2912
|
+
settings.openaiModel;
|
|
2270
2913
|
const effectiveModelId = payload.model ?? inheritedModel;
|
|
2271
|
-
const effectiveCatalogSettings = await
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2914
|
+
const effectiveCatalogSettings = await measureSessionStartPhase(
|
|
2915
|
+
unresolvedDeps.observability,
|
|
2916
|
+
"model_catalog_effective",
|
|
2917
|
+
() =>
|
|
2918
|
+
resolveWorkspaceModelBoundarySettings(
|
|
2919
|
+
deps,
|
|
2920
|
+
grant,
|
|
2921
|
+
workspaceId,
|
|
2922
|
+
[effectiveModelId],
|
|
2923
|
+
parentSession ? inheritedModel : null,
|
|
2924
|
+
),
|
|
2277
2925
|
);
|
|
2278
2926
|
if (effectiveCatalogSettings !== settings) {
|
|
2279
2927
|
deps = {
|
|
@@ -2320,6 +2968,15 @@ async function createSessionForRequestInFileScope(
|
|
|
2320
2968
|
creationInitiator.actor.turnId,
|
|
2321
2969
|
)
|
|
2322
2970
|
: undefined;
|
|
2971
|
+
const claudeProviderAccountAuthoritySnapshot =
|
|
2972
|
+
parentSession && creationInitiator.actor
|
|
2973
|
+
? await getSessionTurnClaudeProviderAccountAuthoritySnapshot(
|
|
2974
|
+
db,
|
|
2975
|
+
workspaceId,
|
|
2976
|
+
parentSession.id,
|
|
2977
|
+
creationInitiator.actor.turnId,
|
|
2978
|
+
)
|
|
2979
|
+
: undefined;
|
|
2323
2980
|
const connectionDelegationSource = personalConnectionDelegationSourceForGrant(grant);
|
|
2324
2981
|
const inheritedPersonalConnectionDelegations =
|
|
2325
2982
|
connectionDelegationSource.kind === "turn"
|
|
@@ -2330,15 +2987,20 @@ async function createSessionForRequestInFileScope(
|
|
|
2330
2987
|
connectionDelegationSource.turnId,
|
|
2331
2988
|
)
|
|
2332
2989
|
: null;
|
|
2333
|
-
const capabilityRuntimeSettings = await
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2990
|
+
const capabilityRuntimeSettings = await measureSessionStartPhase(
|
|
2991
|
+
unresolvedDeps.observability,
|
|
2992
|
+
"capability_settings",
|
|
2993
|
+
() =>
|
|
2994
|
+
settingsWithEnabledCapabilityMcpServers(
|
|
2995
|
+
db,
|
|
2996
|
+
workspaceId,
|
|
2997
|
+
settings,
|
|
2998
|
+
inheritedPersonalConnectionDelegations
|
|
2999
|
+
? {
|
|
3000
|
+
personalConnectionDelegations: inheritedPersonalConnectionDelegations,
|
|
3001
|
+
}
|
|
3002
|
+
: { subjectId: grant.subjectId },
|
|
3003
|
+
),
|
|
2342
3004
|
);
|
|
2343
3005
|
const sessionMcpServers = hasOwnProperty(rawPayload, "mcpServers")
|
|
2344
3006
|
? validateSessionMcpServersForCreate(capabilityRuntimeSettings, grant, payload.mcpServers)
|
|
@@ -2351,6 +3013,27 @@ async function createSessionForRequestInFileScope(
|
|
|
2351
3013
|
capabilityRuntimeSettings,
|
|
2352
3014
|
sessionMcpServers.runtimeServers,
|
|
2353
3015
|
);
|
|
3016
|
+
const requestedMcpPolicies =
|
|
3017
|
+
payload.mcpApprovalPolicies ?? parentSession?.mcpApprovalPolicies ?? {};
|
|
3018
|
+
const mcpApprovalPolicies: Record<string, SessionMcpApprovalPolicy> = {};
|
|
3019
|
+
if (Object.keys(requestedMcpPolicies).length > 0) {
|
|
3020
|
+
requirePermission(grant, "sessions:control");
|
|
3021
|
+
const inheritedServers = await listEnabledMcpCapabilityServers(db, workspaceId);
|
|
3022
|
+
for (const [id, policy] of Object.entries(requestedMcpPolicies)) {
|
|
3023
|
+
const inherited = inheritedServers.find((server) => server.id === id);
|
|
3024
|
+
if (!inherited || sessionMcpServers.runtimeServers.some((server) => server.id === id)) {
|
|
3025
|
+
throw new HTTPException(422, {
|
|
3026
|
+
message: `MCP approval policy must name an enabled inherited capability: ${id}${
|
|
3027
|
+
/^api_(openapi|graphql)_/.test(id)
|
|
3028
|
+
? "; API Integration tool approval is set at install with autoApprovedTools"
|
|
3029
|
+
: ""
|
|
3030
|
+
}`,
|
|
3031
|
+
});
|
|
3032
|
+
}
|
|
3033
|
+
mcpApprovalPolicies[id] =
|
|
3034
|
+
resolveMcpApprovalRecommendation(policy, inherited.approvalFloor) ?? false;
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
2354
3037
|
const resources = normalizeResources(
|
|
2355
3038
|
hasOwnProperty(rawPayload, "resources")
|
|
2356
3039
|
? payload.resources
|
|
@@ -2395,6 +3078,11 @@ async function createSessionForRequestInFileScope(
|
|
|
2395
3078
|
});
|
|
2396
3079
|
}
|
|
2397
3080
|
const toolsProvided = hasOwnProperty(rawPayload, "tools");
|
|
3081
|
+
if (toolsProvided && payload.excludedMcpServerIds !== undefined) {
|
|
3082
|
+
throw new HTTPException(422, {
|
|
3083
|
+
message: "connector exclusions require workspace-default tools",
|
|
3084
|
+
});
|
|
3085
|
+
}
|
|
2398
3086
|
// Visibility became durable draft state after older clients had already
|
|
2399
3087
|
// written rows without it. Compare it only when the create request supplied
|
|
2400
3088
|
// the field explicitly; the parsed schema default must not manufacture a
|
|
@@ -2417,6 +3105,9 @@ async function createSessionForRequestInFileScope(
|
|
|
2417
3105
|
)
|
|
2418
3106
|
: parentSession.tools,
|
|
2419
3107
|
runtimeSettings,
|
|
3108
|
+
).filter(
|
|
3109
|
+
(tool) =>
|
|
3110
|
+
tool.id === "opengeni" || !parentSession.toolPolicy.excludedMcpServerIds?.includes(tool.id),
|
|
2420
3111
|
);
|
|
2421
3112
|
if (toolsProvided) {
|
|
2422
3113
|
assertToolRefsSubset(
|
|
@@ -2434,6 +3125,7 @@ async function createSessionForRequestInFileScope(
|
|
|
2434
3125
|
toolPolicy = {
|
|
2435
3126
|
mode: parentTracksWorkspaceDefaults ? "workspace_default" : "inherited",
|
|
2436
3127
|
inheritedFromSessionId: parentSession.id,
|
|
3128
|
+
...defaultPolicyExclusions(parentSession.toolPolicy.excludedMcpServerIds),
|
|
2437
3129
|
};
|
|
2438
3130
|
}
|
|
2439
3131
|
} else if (toolsProvided) {
|
|
@@ -2448,50 +3140,87 @@ async function createSessionForRequestInFileScope(
|
|
|
2448
3140
|
);
|
|
2449
3141
|
toolPolicy = { mode: "workspace_default", inheritedFromSessionId: null };
|
|
2450
3142
|
}
|
|
3143
|
+
if (!parentSession) {
|
|
3144
|
+
selectedTools = withSameRequestSessionMcpServerTools(
|
|
3145
|
+
selectedTools,
|
|
3146
|
+
sessionMcpServers.runtimeServers,
|
|
3147
|
+
);
|
|
3148
|
+
}
|
|
3149
|
+
if (payload.excludedMcpServerIds !== undefined) {
|
|
3150
|
+
if (toolPolicy.mode !== "workspace_default") {
|
|
3151
|
+
throw new HTTPException(403, {
|
|
3152
|
+
message: "connector exclusions require workspace-default tools",
|
|
3153
|
+
});
|
|
3154
|
+
}
|
|
3155
|
+
toolPolicy = {
|
|
3156
|
+
...toolPolicy,
|
|
3157
|
+
...defaultPolicyExclusions([
|
|
3158
|
+
...(toolPolicy.excludedMcpServerIds ?? []),
|
|
3159
|
+
...payload.excludedMcpServerIds,
|
|
3160
|
+
]),
|
|
3161
|
+
};
|
|
3162
|
+
}
|
|
3163
|
+
selectedTools = withoutExcludedMcpServers(selectedTools, toolPolicy.excludedMcpServerIds);
|
|
3164
|
+
if (agentConfig) {
|
|
3165
|
+
// Product refs survive a connectors-off configuration: servers attached to
|
|
3166
|
+
// this session, refs the request named, and a parent's explicit refs.
|
|
3167
|
+
const productServerIds = new Set<string>([
|
|
3168
|
+
...sessionMcpServers.runtimeServers.map((server) => server.id),
|
|
3169
|
+
...(toolsProvided ? payload.tools.map((tool) => tool.id) : []),
|
|
3170
|
+
...(parentSession && parentSession.toolPolicy.mode !== "workspace_default"
|
|
3171
|
+
? parentSession.tools.map((tool) => tool.id)
|
|
3172
|
+
: []),
|
|
3173
|
+
]);
|
|
3174
|
+
const writeThrough = applySessionAgentConfigWriteThrough({
|
|
3175
|
+
config: agentConfig,
|
|
3176
|
+
firstPartyMcpTools: [],
|
|
3177
|
+
tools: selectedTools,
|
|
3178
|
+
toolPolicy,
|
|
3179
|
+
productServerIds,
|
|
3180
|
+
...(toolsProvided && agentConfigCreator !== "slack"
|
|
3181
|
+
? { explicitServerIds: payload.tools.map((tool) => tool.id) }
|
|
3182
|
+
: {}),
|
|
3183
|
+
});
|
|
3184
|
+
selectedTools = writeThrough.tools;
|
|
3185
|
+
toolPolicy = writeThrough.toolPolicy;
|
|
3186
|
+
}
|
|
2451
3187
|
// The first-party MCP server is attached to EVERY session. Registration is
|
|
2452
3188
|
// independently intersected with the exact model-visible selection and the
|
|
2453
3189
|
// tool's permission/target authorization predicate, so attachment alone
|
|
2454
3190
|
// exposes nothing.
|
|
2455
3191
|
const tools = withFirstPartyTools(selectedTools, runtimeSettings);
|
|
2456
|
-
|
|
2457
|
-
runtimeSettings,
|
|
2458
|
-
tools,
|
|
2459
|
-
grant,
|
|
2460
|
-
workspaceId,
|
|
2461
|
-
hostSelections,
|
|
2462
|
-
authorization,
|
|
2463
|
-
);
|
|
3192
|
+
|
|
2464
3193
|
const captureLinkedAuthority = prepareExternalLinkTurnAdmission(authorization);
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
resources,
|
|
2477
|
-
(authorization || grant.principalKind === "agent_attempt") &&
|
|
2478
|
-
(effectiveVisibility === "user_private" ||
|
|
2479
|
-
sessionScope.memoryScope === "user" ||
|
|
2480
|
-
workspace.kind === "personal")
|
|
2481
|
-
? authorization
|
|
3194
|
+
const { attachmentOwnerContext, variableSets } = await measureSessionStartPhase(
|
|
3195
|
+
unresolvedDeps.observability,
|
|
3196
|
+
"resource_validation",
|
|
3197
|
+
async () => {
|
|
3198
|
+
await validateGitHubRepositorySelection(db, workspaceId, resources);
|
|
3199
|
+
if (resources.some((resource) => resource.kind === "file") && !objectStorage) {
|
|
3200
|
+
throw new HTTPException(503, {
|
|
3201
|
+
message: "object storage is not configured",
|
|
3202
|
+
});
|
|
3203
|
+
}
|
|
3204
|
+
const ownerContext = authorization
|
|
2482
3205
|
? await fileOwnerContextForAccess({ db }, authorization, "sessions:create")
|
|
2483
|
-
:
|
|
2484
|
-
|
|
3206
|
+
: grant.principalKind === "agent_attempt"
|
|
3207
|
+
? await fileOwnerContextForAgent({ db }, grant, "sessions:create")
|
|
3208
|
+
: undefined;
|
|
3209
|
+
await validateFileResources(
|
|
3210
|
+
db,
|
|
3211
|
+
grant.accountId,
|
|
3212
|
+
workspaceId,
|
|
3213
|
+
personalResourceSubjectId ?? grant.subjectId,
|
|
3214
|
+
resources,
|
|
3215
|
+
ownerContext,
|
|
3216
|
+
);
|
|
3217
|
+
// Variable Sets were replaced by vaults (RM-1); a session never selects one.
|
|
3218
|
+
if (payload.variableSetIds?.length) throw variableSetsRemovedError("variableSetIds");
|
|
3219
|
+
return { attachmentOwnerContext: ownerContext, variableSets: [] as VariableSet[] };
|
|
3220
|
+
},
|
|
2485
3221
|
);
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
// collisions.
|
|
2489
|
-
const variableSets: VariableSet[] = [];
|
|
2490
|
-
for (const variableSetId of payload.variableSetIds ?? []) {
|
|
2491
|
-
variableSets.push(
|
|
2492
|
-
await validateVariableSetAttachment({ settings, db }, grant, workspaceId, variableSetId),
|
|
2493
|
-
);
|
|
2494
|
-
}
|
|
3222
|
+
const attachmentOwner =
|
|
3223
|
+
attachmentOwnerContext?.privateFileOwnerSubjectId === grant.subjectId ? grant.subjectId : null;
|
|
2495
3224
|
// RIG BINDING (M3). Resolve the rig this session rides — a UUID binds that
|
|
2496
3225
|
// rig, null explicitly opts out, and omission inherits the workspace default
|
|
2497
3226
|
// (workspaces.default_rig_id) — then FREEZE both the rig id and its currently-
|
|
@@ -2503,34 +3232,33 @@ async function createSessionForRequestInFileScope(
|
|
|
2503
3232
|
// - A stale workspace-default rig (deleted → FK-nulled, or somehow with no
|
|
2504
3233
|
// active version) degrades SILENTLY to rig-less: an operator-side default
|
|
2505
3234
|
// must never brick every create in the workspace.
|
|
2506
|
-
const
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
workspaceId,
|
|
2527
|
-
defaultVariableSetId,
|
|
2528
|
-
);
|
|
3235
|
+
const { frozenRigId, frozenRigVersionId } = await measureSessionStartPhase(
|
|
3236
|
+
unresolvedDeps.observability,
|
|
3237
|
+
"rig_binding",
|
|
3238
|
+
async () => {
|
|
3239
|
+
const requestedRigId =
|
|
3240
|
+
payload.rigId === undefined
|
|
3241
|
+
? await getWorkspaceDefaultRigId(db, workspaceId)
|
|
3242
|
+
: payload.rigId;
|
|
3243
|
+
if (!requestedRigId) return { frozenRigId: null, frozenRigVersionId: null };
|
|
3244
|
+
const rig = await getRig(db, grant, requestedRigId);
|
|
3245
|
+
if (!rig || !rig.activeVersion) {
|
|
3246
|
+
if (payload.rigId) {
|
|
3247
|
+
throw new HTTPException(422, {
|
|
3248
|
+
message: rig
|
|
3249
|
+
? `sandbox environment ${payload.rigId} has no active version to bind`
|
|
3250
|
+
: `unknown rigId: ${payload.rigId}`,
|
|
3251
|
+
});
|
|
3252
|
+
}
|
|
3253
|
+
// else: workspace-default fallback that no longer resolves → rig-less.
|
|
3254
|
+
return { frozenRigId: null, frozenRigVersionId: null };
|
|
2529
3255
|
}
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
3256
|
+
return {
|
|
3257
|
+
frozenRigId: rig.id as string | null,
|
|
3258
|
+
frozenRigVersionId: rig.activeVersion.id as string | null,
|
|
3259
|
+
};
|
|
3260
|
+
},
|
|
3261
|
+
);
|
|
2534
3262
|
// CHANNEL FILING. Pure rail organization: a UUID files the session into that
|
|
2535
3263
|
// workspace channel, omission/null leaves it unfiled (inbox). Resolved
|
|
2536
3264
|
// workspace-scoped so a foreign channel id can never attach; an explicit
|
|
@@ -2546,10 +3274,10 @@ async function createSessionForRequestInFileScope(
|
|
|
2546
3274
|
channelId = channel.id;
|
|
2547
3275
|
}
|
|
2548
3276
|
// A spawned worker is causally part of the exact turn that created it. Omitted
|
|
2549
|
-
//
|
|
3277
|
+
// model and reasoning fields therefore inherit that calling turn rather than the
|
|
2550
3278
|
// deployment defaults. This is especially important for Codex subscription
|
|
2551
3279
|
// managers: falling back to the deployment model would silently move a child
|
|
2552
|
-
// onto the
|
|
3280
|
+
// onto the Opengeni-credits billing path. Legacy session-bound grants without
|
|
2553
3281
|
// exact attempt claims fall back to the parent session's persisted defaults.
|
|
2554
3282
|
const model = canonicalConfiguredModel(settings, effectiveModelId);
|
|
2555
3283
|
if (model === null || model === undefined) {
|
|
@@ -2559,15 +3287,34 @@ async function createSessionForRequestInFileScope(
|
|
|
2559
3287
|
// inherited calling-turn model, or deployment default — so the policy must
|
|
2560
3288
|
// vet that effective value, not just explicit ones (a restricted workspace's
|
|
2561
3289
|
// inherited/default-model session would otherwise be born blocked).
|
|
2562
|
-
await
|
|
3290
|
+
await measureSessionStartPhase(unresolvedDeps.observability, "model_admission", async () => {
|
|
3291
|
+
await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, model);
|
|
3292
|
+
// Direct creation is a fresh model selection. Child inheritance and keyed
|
|
3293
|
+
// repair preserve the existing accepted-model/authority rules.
|
|
3294
|
+
if (retainedKeyedShellModel === null && !parentSession) {
|
|
3295
|
+
const selections = await resolveCallerWorkspaceModelSelections(db, settings, {
|
|
3296
|
+
accountId: grant.accountId,
|
|
3297
|
+
workspaceId,
|
|
3298
|
+
subjectId: personalResourceSubjectId ?? grant.subjectId,
|
|
3299
|
+
...(claudeProviderAccountAuthoritySnapshot
|
|
3300
|
+
? { claudeAuthoritySnapshot: claudeProviderAccountAuthoritySnapshot }
|
|
3301
|
+
: {}),
|
|
3302
|
+
});
|
|
3303
|
+
if (!admissibleWorkspaceModel(selections, model)) {
|
|
3304
|
+
throw new HTTPException(422, { message: `model is not selectable: ${model}` });
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
});
|
|
2563
3308
|
const inheritedReasoningEffort =
|
|
2564
3309
|
parentCallingTurn?.reasoningEffort ??
|
|
2565
3310
|
parentSession?.reasoningEffort ??
|
|
3311
|
+
resolvedDefault?.reasoningEffort ??
|
|
2566
3312
|
settings.openaiReasoningEffort;
|
|
2567
|
-
const inheritedLatencyMode =
|
|
2568
|
-
parentCallingTurn?.latencyMode ?? parentSession?.latencyMode ?? "standard";
|
|
2569
3313
|
const reasoningEffort = payload.reasoningEffort ?? inheritedReasoningEffort;
|
|
2570
|
-
|
|
3314
|
+
// A fresh session never implicitly opts into a faster, higher-cost tier.
|
|
3315
|
+
// This is a fresh-creation default only: replay/repair preserves persisted
|
|
3316
|
+
// policy, and follow-ups use the recipient session's own frozen settings.
|
|
3317
|
+
const latencyMode = payload.latencyMode ?? "standard";
|
|
2571
3318
|
if (payload.expectedNewSessionDraftRevision !== undefined && payload.rigId === null) {
|
|
2572
3319
|
throw new HTTPException(409, {
|
|
2573
3320
|
message: "The submitted session options are not represented by the new-session draft",
|
|
@@ -2584,6 +3331,9 @@ async function createSessionForRequestInFileScope(
|
|
|
2584
3331
|
reasoningEffort,
|
|
2585
3332
|
latencyMode,
|
|
2586
3333
|
options: {
|
|
3334
|
+
...(payload.excludedMcpServerIds !== undefined
|
|
3335
|
+
? { excludedMcpServerIds: payload.excludedMcpServerIds }
|
|
3336
|
+
: {}),
|
|
2587
3337
|
...(visibilityProvided ? { visibility: payload.visibility } : {}),
|
|
2588
3338
|
...(payload.sandboxBackend ? { sandboxBackend: payload.sandboxBackend } : {}),
|
|
2589
3339
|
...(payload.targetSandboxId ? { targetSandboxId: payload.targetSandboxId } : {}),
|
|
@@ -2602,34 +3352,33 @@ async function createSessionForRequestInFileScope(
|
|
|
2602
3352
|
...(payload.firstPartyMcpTools
|
|
2603
3353
|
? { firstPartyMcpTools: payload.firstPartyMcpTools }
|
|
2604
3354
|
: {}),
|
|
3355
|
+
...(payload.agent ? { agent: payload.agent } : {}),
|
|
2605
3356
|
},
|
|
2606
3357
|
}
|
|
2607
3358
|
: null;
|
|
2608
3359
|
const inheritedFromParent = parentSession !== null;
|
|
2609
|
-
const turnExecutionPolicy =
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
?
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
?
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
payload.latencyMode === undefined
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
: "explicit",
|
|
2632
|
-
});
|
|
3360
|
+
const turnExecutionPolicy = withDeveloperSetupCredentialRestriction(
|
|
3361
|
+
resolveTurnExecutionPolicyV1(settings, {
|
|
3362
|
+
modelId: model,
|
|
3363
|
+
requestedModelId: payload.model ?? null,
|
|
3364
|
+
modelSource:
|
|
3365
|
+
payload.model === undefined
|
|
3366
|
+
? inheritedFromParent
|
|
3367
|
+
? "continuation"
|
|
3368
|
+
: "deployment"
|
|
3369
|
+
: "explicit",
|
|
3370
|
+
reasoningEffort,
|
|
3371
|
+
reasoningSource:
|
|
3372
|
+
payload.reasoningEffort === undefined
|
|
3373
|
+
? inheritedFromParent
|
|
3374
|
+
? "continuation"
|
|
3375
|
+
: "deployment"
|
|
3376
|
+
: "explicit",
|
|
3377
|
+
latencyMode,
|
|
3378
|
+
latencyModeSource: payload.latencyMode === undefined ? "deployment" : "explicit",
|
|
3379
|
+
}),
|
|
3380
|
+
{ grant, authorization, trustedMetadata: [parentSession?.metadata] },
|
|
3381
|
+
);
|
|
2633
3382
|
// Parent linkage was resolved above, before context validation. A child with
|
|
2634
3383
|
// no explicit permission override inherits the creating session's effective
|
|
2635
3384
|
// grant instead of silently expanding to standalone worker defaults.
|
|
@@ -2643,6 +3392,7 @@ async function createSessionForRequestInFileScope(
|
|
|
2643
3392
|
const parentFirstPartyMcpPermissions = parentSession
|
|
2644
3393
|
? [...(parentSession.firstPartyMcpPermissions ?? DEFAULT_FIRST_PARTY_MCP_PERMISSIONS)]
|
|
2645
3394
|
: null;
|
|
3395
|
+
requireDeveloperSetupDelegatedPermissions(turnExecutionPolicy, payload.firstPartyMcpPermissions);
|
|
2646
3396
|
if (
|
|
2647
3397
|
parentFirstPartyMcpPermissions &&
|
|
2648
3398
|
payload.firstPartyMcpPermissions?.some(
|
|
@@ -2663,7 +3413,11 @@ async function createSessionForRequestInFileScope(
|
|
|
2663
3413
|
? parentFirstPartyMcpPermissions.filter((permission) =>
|
|
2664
3414
|
hasPermission(grant.permissions, permission),
|
|
2665
3415
|
)
|
|
2666
|
-
:
|
|
3416
|
+
: grant.permissionMode === "explicit"
|
|
3417
|
+
? DEFAULT_FIRST_PARTY_MCP_PERMISSIONS.filter((permission) =>
|
|
3418
|
+
hasPermission(grant.permissions, permission),
|
|
3419
|
+
)
|
|
3420
|
+
: null);
|
|
2667
3421
|
if (firstPartyMcpPermissions && firstPartyMcpPermissions.length === 0) {
|
|
2668
3422
|
// An empty set would sign an unusable zero-permission token; the default
|
|
2669
3423
|
// worker set is expressed by omitting the field.
|
|
@@ -2672,6 +3426,7 @@ async function createSessionForRequestInFileScope(
|
|
|
2672
3426
|
"firstPartyMcpPermissions must not be empty; omit it for the default worker permission set",
|
|
2673
3427
|
});
|
|
2674
3428
|
}
|
|
3429
|
+
requireExplicitPermissionDelegation(grant, firstPartyMcpPermissions ?? []);
|
|
2675
3430
|
for (const permission of firstPartyMcpPermissions ?? []) {
|
|
2676
3431
|
if (!hasPermission(grant.permissions, permission)) {
|
|
2677
3432
|
throw new HTTPException(403, {
|
|
@@ -2700,20 +3455,14 @@ async function createSessionForRequestInFileScope(
|
|
|
2700
3455
|
// top-level omission selects the workspace's exact default catalog (or the
|
|
2701
3456
|
// deployment default when the workspace has not configured one).
|
|
2702
3457
|
const deploymentFirstPartyMcpToolPolicy = resolveFirstPartyMcpToolPolicy(settings);
|
|
2703
|
-
const disallowedFirstPartyMcpTool = payload.firstPartyMcpTools?.find(
|
|
2704
|
-
(tool) => !deploymentFirstPartyMcpToolPolicy.allowed.includes(tool),
|
|
2705
|
-
);
|
|
2706
|
-
if (disallowedFirstPartyMcpTool) {
|
|
2707
|
-
throw new HTTPException(422, {
|
|
2708
|
-
message: `first-party MCP tool is disabled by deployment policy: ${disallowedFirstPartyMcpTool}`,
|
|
2709
|
-
});
|
|
2710
|
-
}
|
|
2711
3458
|
const workspaceFirstPartyDefaults = workspaceSessionToolDefaults?.firstPartyMcpTools?.filter(
|
|
2712
3459
|
(tool) => deploymentFirstPartyMcpToolPolicy.allowed.includes(tool),
|
|
2713
3460
|
);
|
|
2714
|
-
const
|
|
3461
|
+
const resolvedCreatorFirstPartyMcpTools = resolveFirstPartyMcpToolsForCreate(
|
|
2715
3462
|
payload.firstPartyMcpTools,
|
|
2716
|
-
parentSession
|
|
3463
|
+
parentSession
|
|
3464
|
+
? resolveSessionFirstPartyMcpTools(settings, parentSession, workspace.settings)
|
|
3465
|
+
: undefined,
|
|
2717
3466
|
workspaceFirstPartyDefaults && !parentSession
|
|
2718
3467
|
? {
|
|
2719
3468
|
...deploymentFirstPartyMcpToolPolicy,
|
|
@@ -2721,6 +3470,41 @@ async function createSessionForRequestInFileScope(
|
|
|
2721
3470
|
}
|
|
2722
3471
|
: deploymentFirstPartyMcpToolPolicy,
|
|
2723
3472
|
);
|
|
3473
|
+
// Capabilities only narrow the creator's exact legacy selection; "all" keeps it.
|
|
3474
|
+
const followsFirstPartyDefaults =
|
|
3475
|
+
payload.firstPartyMcpTools === undefined &&
|
|
3476
|
+
(!parentSession || parentSession.toolPolicy.firstPartyMode === "workspace_default");
|
|
3477
|
+
// A child following defaults keeps its parent's guaranteed additions; a
|
|
3478
|
+
// top-level session takes them from its trusted entry point.
|
|
3479
|
+
const firstPartyAdditions = followsFirstPartyDefaults
|
|
3480
|
+
? (parentSession
|
|
3481
|
+
? (parentSession.toolPolicy.firstPartyAdditions ?? [])
|
|
3482
|
+
: (requestOptions.firstPartyMcpToolAdditions ?? [])
|
|
3483
|
+
).filter((tool) => deploymentFirstPartyMcpToolPolicy.allowed.includes(tool))
|
|
3484
|
+
: [];
|
|
3485
|
+
if (followsFirstPartyDefaults) {
|
|
3486
|
+
toolPolicy = {
|
|
3487
|
+
...toolPolicy,
|
|
3488
|
+
firstPartyMode: "workspace_default",
|
|
3489
|
+
...(firstPartyAdditions.length
|
|
3490
|
+
? { firstPartyAdditions: [...new Set(firstPartyAdditions)] }
|
|
3491
|
+
: {}),
|
|
3492
|
+
};
|
|
3493
|
+
}
|
|
3494
|
+
const creatorFirstPartyMcpTools = firstPartyAdditions.length
|
|
3495
|
+
? [...new Set([...resolvedCreatorFirstPartyMcpTools, ...firstPartyAdditions])]
|
|
3496
|
+
: resolvedCreatorFirstPartyMcpTools;
|
|
3497
|
+
const firstPartyMcpTools = applySessionAgentConfigWriteThrough({
|
|
3498
|
+
config: agentConfig,
|
|
3499
|
+
firstPartyMcpTools: creatorFirstPartyMcpTools,
|
|
3500
|
+
// Slack computes its own baseline selection; only a caller's own explicit
|
|
3501
|
+
// list is checked strictly against the capabilities.
|
|
3502
|
+
explicitFirstPartyMcpTools:
|
|
3503
|
+
agentConfigCreator === "slack" ? undefined : payload.firstPartyMcpTools,
|
|
3504
|
+
tools: [],
|
|
3505
|
+
toolPolicy,
|
|
3506
|
+
productServerIds: [],
|
|
3507
|
+
}).firstPartyMcpTools;
|
|
2724
3508
|
const googleDrivePublicationEnabled =
|
|
2725
3509
|
firstPartyMcpTools.includes("editable_artifact_export") &&
|
|
2726
3510
|
firstPartyMcpTools.includes("editable_artifact_export_status") &&
|
|
@@ -2730,17 +3514,32 @@ async function createSessionForRequestInFileScope(
|
|
|
2730
3514
|
const atlassianEnabled =
|
|
2731
3515
|
firstPartyMcpTools.some((tool) => tool.startsWith("atlassian_")) &&
|
|
2732
3516
|
(!firstPartyMcpPermissions?.length || firstPartyMcpPermissions.includes("connections:read"));
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
tools,
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
3517
|
+
// Creation's stored tool snapshot can omit configured workspace defaults.
|
|
3518
|
+
// Freeze initial-turn accounts against the same executable policy as later
|
|
3519
|
+
// messages; explicit selections and exclusions remain exact.
|
|
3520
|
+
const connectionAccountTools = sessionToolsForConnectionAccounts({
|
|
3521
|
+
session: { tools, toolPolicy },
|
|
3522
|
+
runtimeMcpServers: runtimeSettings.mcpServers,
|
|
3523
|
+
defaultMcpServerIds: workspaceSessionToolPolicyDefaultServerIdsFor(
|
|
3524
|
+
capabilityRuntimeSettings.mcpServers,
|
|
3525
|
+
workspace.settings,
|
|
3526
|
+
),
|
|
2743
3527
|
});
|
|
3528
|
+
const { personalConnectionDelegations, mcpAccountBindings, connectionAccountIntent } =
|
|
3529
|
+
await measureSessionStartPhase(unresolvedDeps.observability, "connection_freeze", () =>
|
|
3530
|
+
freezeConnectionAccounts({
|
|
3531
|
+
db,
|
|
3532
|
+
accountId: grant.accountId,
|
|
3533
|
+
workspaceId,
|
|
3534
|
+
settings: runtimeSettings,
|
|
3535
|
+
tools: connectionAccountTools,
|
|
3536
|
+
resources,
|
|
3537
|
+
source: connectionDelegationSource,
|
|
3538
|
+
authoritySelections: payload.connectionAccounts,
|
|
3539
|
+
googleDrivePublicationEnabled,
|
|
3540
|
+
atlassianEnabled,
|
|
3541
|
+
}),
|
|
3542
|
+
);
|
|
2744
3543
|
if (effectiveGoal) {
|
|
2745
3544
|
const missingGoalTools = ["goal_update", "goal_progress", "goal_complete", "goal_pause"].filter(
|
|
2746
3545
|
(name) => !firstPartyMcpTools.includes(name as FirstPartyMcpToolName),
|
|
@@ -2798,27 +3597,10 @@ async function createSessionForRequestInFileScope(
|
|
|
2798
3597
|
sandboxId: string;
|
|
2799
3598
|
workingDir: string | null;
|
|
2800
3599
|
} | null = null;
|
|
2801
|
-
//
|
|
2802
|
-
//
|
|
2803
|
-
//
|
|
2804
|
-
//
|
|
2805
|
-
// is therefore a genuine shared-state conflict TODAY: its first turn on a warm
|
|
2806
|
-
// box dies with "Live sandbox sessions cannot change manifest variableSet
|
|
2807
|
-
// variables" (proven live, sessions 5aee77e9 + 63d18823). Until the VariableSet
|
|
2808
|
-
// is evicted from the manifest (per-exec, like the git token), grouping must be
|
|
2809
|
-
// env-aware: the INHERITED default falls back to an own box on mismatch (a
|
|
2810
|
-
// credentialed worker spawned from a credential-less manager just works), and
|
|
2811
|
-
// an EXPLICIT shared/{groupId} request with a mismatched VariableSet fails
|
|
2812
|
-
// fast at create (422) instead of poisoning the session's first turn.
|
|
2813
|
-
// The env conflict is a BOX property, so a boxless group is exempt: a
|
|
2814
|
-
// backend:"none" session runs in-process with no sandbox, no manifest, and no
|
|
2815
|
-
// provided-session attach — no shared box state exists to conflict, and
|
|
2816
|
-
// env-differing spawns from such parents shared safely before the env-aware
|
|
2817
|
-
// check. They keep sharing (and keep inheriting "none").
|
|
2818
|
-
const requestedVariableSetIds = variableSets.map((variableSet) => variableSet.id);
|
|
2819
|
-
const variableSetsMatchGroup = (memberVariableSetIds: readonly string[]): boolean =>
|
|
2820
|
-
stableJson(memberVariableSetIds) === stableJson(requestedVariableSetIds);
|
|
2821
|
-
// RIG-AWARE GROUPING (M3), the exact sibling of the env-aware gate above: the
|
|
3600
|
+
// Variable Sets no longer split boxes (W1 secrets delivery): user secrets are
|
|
3601
|
+
// resolved per command from `{{secret:…}}` references and never ride the box
|
|
3602
|
+
// manifest, so sessions with different selections share a box safely.
|
|
3603
|
+
// RIG-AWARE GROUPING (M3): the
|
|
2822
3604
|
// box's rig-baked setup/tooling is fixed at cold-create, so a session joining a
|
|
2823
3605
|
// shared box must ride the SAME frozen rig_version_id. A mismatch is a genuine
|
|
2824
3606
|
// shared-state conflict (the box was set up for a different rig) — the INHERITED
|
|
@@ -2841,7 +3623,6 @@ async function createSessionForRequestInFileScope(
|
|
|
2841
3623
|
}
|
|
2842
3624
|
const parent = parentSession;
|
|
2843
3625
|
const parentBoxed = parent.sandboxBackend !== "none";
|
|
2844
|
-
const variableSetMismatch = parentBoxed && !variableSetsMatchGroup(parent.variableSetIds);
|
|
2845
3626
|
let rigMismatch = parentBoxed && !rigVersionMatchesGroup(parent.rigVersionId ?? null);
|
|
2846
3627
|
if (parentBoxed && !rigMismatch) {
|
|
2847
3628
|
const memberRigVersionIds = await listDistinctRigVersionIdsInGroup(
|
|
@@ -2853,16 +3634,13 @@ async function createSessionForRequestInFileScope(
|
|
|
2853
3634
|
rigVersionMatchesGroup(memberRigVersionId),
|
|
2854
3635
|
);
|
|
2855
3636
|
}
|
|
2856
|
-
if (
|
|
3637
|
+
if (rigMismatch) {
|
|
2857
3638
|
if (payload.sandbox === "shared") {
|
|
2858
|
-
// The caller explicitly asked to share while carrying a different
|
|
2859
|
-
//
|
|
2860
|
-
// VariableSet is checked first so its (pre-rig) message is unchanged for
|
|
2861
|
-
// the env-only mismatch the existing gate already covered.
|
|
3639
|
+
// The caller explicitly asked to share while carrying a different rig —
|
|
3640
|
+
// surface the conflict at create time, not turn time.
|
|
2862
3641
|
throw new HTTPException(422, {
|
|
2863
|
-
message:
|
|
2864
|
-
|
|
2865
|
-
: "sandbox:'shared' requires the same rig as the creator's box (the box's rig setup is fixed at creation); omit sandbox or pass 'new' when binding a different rig.",
|
|
3642
|
+
message:
|
|
3643
|
+
"sandbox:'shared' requires the same sandbox environment as the creator's box (setup is fixed at creation); omit sandbox or pass 'new' when selecting a different sandbox environment.",
|
|
2866
3644
|
});
|
|
2867
3645
|
}
|
|
2868
3646
|
// Inherited default: deterministic separation on the genuine shared-state
|
|
@@ -2892,26 +3670,7 @@ async function createSessionForRequestInFileScope(
|
|
|
2892
3670
|
});
|
|
2893
3671
|
}
|
|
2894
3672
|
if (member.sandboxBackend !== "none") {
|
|
2895
|
-
//
|
|
2896
|
-
// group can carry mixed variableSetIds, and an any-member read would make
|
|
2897
|
-
// the join verdict nondeterministic. Post-env-aware groups are homogeneous
|
|
2898
|
-
// (both join paths enforce equality), so this reads one distinct value in
|
|
2899
|
-
// the common case; a mixed legacy group deterministically rejects.
|
|
2900
|
-
const memberVariableSetSelections = await listDistinctVariableSetSelectionsInGroup(
|
|
2901
|
-
db,
|
|
2902
|
-
workspaceId,
|
|
2903
|
-
sandboxChoice.groupId,
|
|
2904
|
-
);
|
|
2905
|
-
if (
|
|
2906
|
-
!memberVariableSetSelections.every((memberVariableSetIds) =>
|
|
2907
|
-
variableSetsMatchGroup(memberVariableSetIds),
|
|
2908
|
-
)
|
|
2909
|
-
) {
|
|
2910
|
-
throw new HTTPException(422, {
|
|
2911
|
-
message: `sandbox group ${sandboxChoice.groupId} runs a different variableSet / different environment (the box variable set/environment is fixed at creation); create with the group's variableSet/environment or omit sandbox for an own box.`,
|
|
2912
|
-
});
|
|
2913
|
-
}
|
|
2914
|
-
// Same deterministic all-members check for the frozen rig version (M3): the
|
|
3673
|
+
// Deterministic all-members check for the frozen rig version (M3): the
|
|
2915
3674
|
// box's rig setup is fixed at creation, so every member must ride the rig
|
|
2916
3675
|
// this create resolved (or the group is rig-less and so is this create).
|
|
2917
3676
|
const memberRigVersionIds = await listDistinctRigVersionIdsInGroup(
|
|
@@ -2925,13 +3684,25 @@ async function createSessionForRequestInFileScope(
|
|
|
2925
3684
|
)
|
|
2926
3685
|
) {
|
|
2927
3686
|
throw new HTTPException(422, {
|
|
2928
|
-
message: `sandbox group ${sandboxChoice.groupId}
|
|
3687
|
+
message: `sandbox group ${sandboxChoice.groupId} uses a different sandbox environment (setup is fixed at creation); select the group's sandbox environment or omit sandbox for a separate box.`,
|
|
2929
3688
|
});
|
|
2930
3689
|
}
|
|
2931
3690
|
}
|
|
2932
3691
|
sandboxGroupId = sandboxChoice.groupId;
|
|
2933
3692
|
inheritedBackend = member.sandboxBackend;
|
|
2934
3693
|
inheritedSandboxOs = member.sandboxOs;
|
|
3694
|
+
// Naming the creator's own group is the explicit spelling of "shared".
|
|
3695
|
+
// The machine route is session-local, so copy the trusted parent's exact
|
|
3696
|
+
// route as the omitted default does; otherwise the child lands on the
|
|
3697
|
+
// group's managed box, a different filesystem from the creator's machine.
|
|
3698
|
+
if (parentSession && parentSession.sandboxGroupId === sandboxChoice.groupId) {
|
|
3699
|
+
inheritedActiveTarget = parentSession.activeSandboxId
|
|
3700
|
+
? {
|
|
3701
|
+
sandboxId: parentSession.activeSandboxId,
|
|
3702
|
+
workingDir: parentSession.workingDir,
|
|
3703
|
+
}
|
|
3704
|
+
: null;
|
|
3705
|
+
}
|
|
2935
3706
|
}
|
|
2936
3707
|
// else "new": leave sandboxGroupId null → own singleton group (group ≡ id).
|
|
2937
3708
|
// A working dir is only meaningful for a TARGETED machine (it is the chosen
|
|
@@ -3034,14 +3805,37 @@ async function createSessionForRequestInFileScope(
|
|
|
3034
3805
|
"self-hosted execution runs on a Connected Machine, but no machine was selected or inherited; connect the parent session to a machine or provide machineTarget",
|
|
3035
3806
|
});
|
|
3036
3807
|
}
|
|
3037
|
-
if (payload.startMode !== "realtime") {
|
|
3038
|
-
await
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3808
|
+
if (payload.startMode !== "realtime" && !model.startsWith("codex/")) {
|
|
3809
|
+
const frozenCreationInitiator = await measureSessionStartPhase(
|
|
3810
|
+
unresolvedDeps.observability,
|
|
3811
|
+
"initiator_freeze",
|
|
3812
|
+
() =>
|
|
3813
|
+
withWorkspaceSessionActivityRls(db, workspaceId, (scopedDb) =>
|
|
3814
|
+
frozenInitiatorForCommandActor(
|
|
3815
|
+
scopedDb,
|
|
3816
|
+
workspaceId,
|
|
3817
|
+
creationInitiator.actor ??
|
|
3818
|
+
(creationInitiator.initiator?.kind === "service"
|
|
3819
|
+
? {
|
|
3820
|
+
type: "service",
|
|
3821
|
+
subjectId: creationInitiator.initiator.subjectId,
|
|
3822
|
+
...(creationInitiator.context ? { context: creationInitiator.context } : {}),
|
|
3823
|
+
}
|
|
3824
|
+
: { type: "human", subjectId: creationInitiator.initiator!.subjectId }),
|
|
3825
|
+
grant.subjectLabel,
|
|
3826
|
+
),
|
|
3827
|
+
),
|
|
3828
|
+
);
|
|
3829
|
+
await measureSessionStartPhase(unresolvedDeps.observability, "allowance", () =>
|
|
3830
|
+
requireLimit(deps, {
|
|
3831
|
+
accountId: grant.accountId,
|
|
3832
|
+
workspaceId,
|
|
3833
|
+
initiatingHumanSubjectId: initiatingHumanForAllowance(frozenCreationInitiator),
|
|
3834
|
+
action: "agent_run:create",
|
|
3835
|
+
quantity: 1,
|
|
3836
|
+
model,
|
|
3837
|
+
}),
|
|
3838
|
+
);
|
|
3045
3839
|
}
|
|
3046
3840
|
const initialLearning =
|
|
3047
3841
|
payload.agentLearning && Object.keys(payload.agentLearning).length
|
|
@@ -3063,10 +3857,31 @@ async function createSessionForRequestInFileScope(
|
|
|
3063
3857
|
? ("personal" as const)
|
|
3064
3858
|
: ("workspace" as const);
|
|
3065
3859
|
const initialLearningActor = initialLearningContext?.actor;
|
|
3860
|
+
const initialUsageLimit =
|
|
3861
|
+
initialUsageLimitActor && payload.usageLimitMicros != null
|
|
3862
|
+
? { actor: initialUsageLimitActor, limitMicros: payload.usageLimitMicros }
|
|
3863
|
+
: hostInitialUsageLimit
|
|
3864
|
+
? {
|
|
3865
|
+
actor: { type: "host" as const, subjectId: hostInitialUsageLimit.subjectId },
|
|
3866
|
+
limitMicros: hostInitialUsageLimit.limitMicros,
|
|
3867
|
+
}
|
|
3868
|
+
: null;
|
|
3869
|
+
const applyInitialUsageLimit = initialUsageLimit
|
|
3870
|
+
? async (tx: Database, sessionId: string) =>
|
|
3871
|
+
await applyInitialSessionUsageLimit(tx, {
|
|
3872
|
+
accountId: grant.accountId,
|
|
3873
|
+
workspaceId,
|
|
3874
|
+
sessionId,
|
|
3875
|
+
limitMicros: initialUsageLimit.limitMicros,
|
|
3876
|
+
actor: initialUsageLimit.actor,
|
|
3877
|
+
producerEnabled: sessionUsageLimitsProducerEnabled(unresolvedDeps.settings),
|
|
3878
|
+
})
|
|
3879
|
+
: null;
|
|
3066
3880
|
const beforeCreateCommit =
|
|
3067
3881
|
initialLearning && initialLearningContext && initialLearningActor?.kind === "human"
|
|
3068
3882
|
? async (tx: Database, sessionId: string) => {
|
|
3069
3883
|
await externalBeforeCreateCommit?.(tx);
|
|
3884
|
+
await applyInitialUsageLimit?.(tx, sessionId);
|
|
3070
3885
|
await saveAgentLearningSettings(
|
|
3071
3886
|
tx,
|
|
3072
3887
|
{
|
|
@@ -3082,7 +3897,12 @@ async function createSessionForRequestInFileScope(
|
|
|
3082
3897
|
},
|
|
3083
3898
|
);
|
|
3084
3899
|
}
|
|
3085
|
-
:
|
|
3900
|
+
: applyInitialUsageLimit
|
|
3901
|
+
? async (tx: Database, sessionId: string) => {
|
|
3902
|
+
await externalBeforeCreateCommit?.(tx);
|
|
3903
|
+
await applyInitialUsageLimit(tx, sessionId);
|
|
3904
|
+
}
|
|
3905
|
+
: externalBeforeCreateCommit;
|
|
3086
3906
|
let createOutcome: CreateSessionOutcome;
|
|
3087
3907
|
try {
|
|
3088
3908
|
createOutcome = await createAndStartSessionWithOutcome({
|
|
@@ -3090,10 +3910,16 @@ async function createSessionForRequestInFileScope(
|
|
|
3090
3910
|
db,
|
|
3091
3911
|
bus,
|
|
3092
3912
|
workflowClient,
|
|
3913
|
+
codeSearchDeploymentPolicy: codeSearchDeploymentPolicy(deps.settings),
|
|
3093
3914
|
accountId: grant.accountId,
|
|
3094
3915
|
workspaceId,
|
|
3095
3916
|
visibility: effectiveVisibility,
|
|
3096
3917
|
initialMessage: payload.initialMessage ?? "",
|
|
3918
|
+
surface,
|
|
3919
|
+
metrics: unresolvedDeps.observability ?? null,
|
|
3920
|
+
...(unresolvedDeps.observability
|
|
3921
|
+
? { startupObservability: unresolvedDeps.observability }
|
|
3922
|
+
: {}),
|
|
3097
3923
|
deferInitialTurn: payload.startMode === "realtime",
|
|
3098
3924
|
modelContext: payload.modelContext ?? null,
|
|
3099
3925
|
resources,
|
|
@@ -3118,8 +3944,8 @@ async function createSessionForRequestInFileScope(
|
|
|
3118
3944
|
sandboxGroupId,
|
|
3119
3945
|
metadata: creationMetadata ?? {},
|
|
3120
3946
|
...(beforeCreateCommit ? { beforeCreateCommit } : {}),
|
|
3121
|
-
|
|
3122
|
-
...(
|
|
3947
|
+
|
|
3948
|
+
...(payload.startMode !== "realtime"
|
|
3123
3949
|
? {
|
|
3124
3950
|
captureInitialTurnAuthority: async (
|
|
3125
3951
|
tx: Database,
|
|
@@ -3127,7 +3953,33 @@ async function createSessionForRequestInFileScope(
|
|
|
3127
3953
|
turnId: string,
|
|
3128
3954
|
) => {
|
|
3129
3955
|
await captureLinkedAuthority?.(tx, sessionId, turnId);
|
|
3130
|
-
|
|
3956
|
+
if (model.startsWith("codex/")) {
|
|
3957
|
+
await requireLimit(
|
|
3958
|
+
{ ...deps, db: tx },
|
|
3959
|
+
{
|
|
3960
|
+
accountId: grant.accountId,
|
|
3961
|
+
workspaceId,
|
|
3962
|
+
initiatingHumanSubjectId: await getSessionTurnInitiatingHumanSubjectId(
|
|
3963
|
+
tx,
|
|
3964
|
+
workspaceId,
|
|
3965
|
+
turnId,
|
|
3966
|
+
),
|
|
3967
|
+
action: "agent_run:create",
|
|
3968
|
+
quantity: 1,
|
|
3969
|
+
model,
|
|
3970
|
+
acceptedTurn: { sessionId, turnId },
|
|
3971
|
+
},
|
|
3972
|
+
);
|
|
3973
|
+
}
|
|
3974
|
+
if (attachmentOwner)
|
|
3975
|
+
await acceptSessionFileAttachments(tx, {
|
|
3976
|
+
accountId: grant.accountId,
|
|
3977
|
+
workspaceId,
|
|
3978
|
+
sessionId,
|
|
3979
|
+
turnId,
|
|
3980
|
+
subjectId: attachmentOwner,
|
|
3981
|
+
resources,
|
|
3982
|
+
});
|
|
3131
3983
|
},
|
|
3132
3984
|
}
|
|
3133
3985
|
: {}),
|
|
@@ -3149,19 +4001,30 @@ async function createSessionForRequestInFileScope(
|
|
|
3149
4001
|
// contracts schema). Persisted on the row; composed system-level at turn
|
|
3150
4002
|
// time. Not surfaced as an event.
|
|
3151
4003
|
instructions: payload.instructions ?? null,
|
|
4004
|
+
// Session and agent-config vault narrowing, intersected (AX-2). Ancestor
|
|
4005
|
+
// policies apply again whenever this session resolves a reference.
|
|
4006
|
+
vaultPolicy: intersectVaultPolicies(
|
|
4007
|
+
lowercaseVaultPolicy(payload.vaultPolicy ?? VAULT_POLICY_ALL),
|
|
4008
|
+
lowercaseVaultPolicy(payload.agent?.vaultPolicy ?? VAULT_POLICY_ALL),
|
|
4009
|
+
),
|
|
3152
4010
|
policyRole: payload.policyRole ?? null,
|
|
3153
4011
|
agentAccess: sessionScope.agentAccess,
|
|
3154
4012
|
scopeSubjectId: sessionScope.scopeSubjectId,
|
|
3155
4013
|
memoryScope: sessionScope.memoryScope,
|
|
3156
4014
|
firstPartyMcpPermissions,
|
|
3157
4015
|
firstPartyMcpTools,
|
|
4016
|
+
agentConfig,
|
|
3158
4017
|
mcpServers: sessionMcpServers.dbServers,
|
|
4018
|
+
mcpApprovalPolicies,
|
|
3159
4019
|
sessionMcpServers: sessionMcpServers.metadata,
|
|
3160
4020
|
personalConnectionDelegations,
|
|
4021
|
+
mcpAccountBindings,
|
|
4022
|
+
connectionAccountIntent,
|
|
3161
4023
|
initialPersonalResourceAttachmentIntent: payload.personalResourceAttachment ?? null,
|
|
3162
4024
|
workspaceCustomModel: isWorkspaceCustomModelId(settings, model),
|
|
3163
4025
|
retainWorkspaceCustomModel: parentSession !== null && model === inheritedModel,
|
|
3164
4026
|
...(xaiProviderAccountAuthoritySnapshot ? { xaiProviderAccountAuthoritySnapshot } : {}),
|
|
4027
|
+
...(claudeProviderAccountAuthoritySnapshot ? { claudeProviderAccountAuthoritySnapshot } : {}),
|
|
3165
4028
|
parentSessionId,
|
|
3166
4029
|
createIdempotencyKey: payload.idempotencyKey ?? null,
|
|
3167
4030
|
selectedInstalledSkillIds,
|
|
@@ -3213,18 +4076,7 @@ async function createSessionForRequestInFileScope(
|
|
|
3213
4076
|
{ message: error.message, cause: error },
|
|
3214
4077
|
);
|
|
3215
4078
|
}
|
|
3216
|
-
|
|
3217
|
-
throw new HTTPException(403, {
|
|
3218
|
-
message: "Host delegation authority unavailable",
|
|
3219
|
-
cause: error,
|
|
3220
|
-
});
|
|
3221
|
-
}
|
|
3222
|
-
if (error instanceof HostMcpBindingConflictError) {
|
|
3223
|
-
throw new HTTPException(409, {
|
|
3224
|
-
message: "Host delegation selection conflicts",
|
|
3225
|
-
cause: error,
|
|
3226
|
-
});
|
|
3227
|
-
}
|
|
4079
|
+
|
|
3228
4080
|
if (error instanceof AgentCommandAuthorityError) {
|
|
3229
4081
|
throw new HTTPException(403, { message: error.message });
|
|
3230
4082
|
}
|
|
@@ -3273,95 +4125,21 @@ export async function createSessionForRequest(
|
|
|
3273
4125
|
workspaceId: string,
|
|
3274
4126
|
rawPayload: unknown,
|
|
3275
4127
|
authorization?: AccessGrantAuthorization,
|
|
4128
|
+
requestOptions?: SessionCreateRequestOptions,
|
|
3276
4129
|
): Promise<CreateSessionResponse> {
|
|
3277
4130
|
return (
|
|
3278
|
-
await createSessionForRequestWithOutcome(
|
|
4131
|
+
await createSessionForRequestWithOutcome(
|
|
4132
|
+
deps,
|
|
4133
|
+
grant,
|
|
4134
|
+
workspaceId,
|
|
4135
|
+
rawPayload,
|
|
4136
|
+
authorization,
|
|
4137
|
+
undefined,
|
|
4138
|
+
requestOptions,
|
|
4139
|
+
)
|
|
3279
4140
|
).session;
|
|
3280
4141
|
}
|
|
3281
4142
|
|
|
3282
|
-
/** Snapshot the selected destination, then recheck live actor, binding and grant
|
|
3283
|
-
* inside the transaction which accepts the turn. Neither selection nor a
|
|
3284
|
-
* registered binding alone is execution authority. Shared by create/send/steer. */
|
|
3285
|
-
function prepareSelectedHostTurnAuthority(
|
|
3286
|
-
settings: Settings,
|
|
3287
|
-
tools: ToolRef[],
|
|
3288
|
-
grant: AccessGrant,
|
|
3289
|
-
workspaceId: string,
|
|
3290
|
-
selections: HostMcpCreateSelection[],
|
|
3291
|
-
authorization?: AccessGrantAuthorization,
|
|
3292
|
-
): ((tx: Database, sessionId: string, turnId: string) => Promise<void>) | undefined {
|
|
3293
|
-
if (!selections.length) return undefined;
|
|
3294
|
-
if (
|
|
3295
|
-
!authorization ||
|
|
3296
|
-
authorization.grant.accountId !== grant.accountId ||
|
|
3297
|
-
authorization.grant.subjectId !== grant.subjectId ||
|
|
3298
|
-
authorization?.grant.workspaceId !== workspaceId ||
|
|
3299
|
-
!hasPermission(authorization?.grant.permissions ?? [], "connections:read") ||
|
|
3300
|
-
!hasPermission(grant.permissions, "connections:read") ||
|
|
3301
|
-
grant.metadata?.["sessionId"]
|
|
3302
|
-
)
|
|
3303
|
-
throw new HTTPException(403, {
|
|
3304
|
-
message: "Host selection requires a verified direct owner",
|
|
3305
|
-
});
|
|
3306
|
-
const beforeCommit = prepareHostMcpOwnerAuthorization(
|
|
3307
|
-
authorization,
|
|
3308
|
-
workspaceId,
|
|
3309
|
-
"connections:read",
|
|
3310
|
-
);
|
|
3311
|
-
const configs = new Map(
|
|
3312
|
-
selections.map((selection) => {
|
|
3313
|
-
const configured = settings.mcpServers.find((server) => server.id === selection.serverId);
|
|
3314
|
-
if (
|
|
3315
|
-
!configured ||
|
|
3316
|
-
!tools.some((tool) => tool.kind === "mcp" && tool.id === selection.serverId) ||
|
|
3317
|
-
configured.connectionRef?.authoritySource !== "host" ||
|
|
3318
|
-
!configured.connectionRef.hostBinding ||
|
|
3319
|
-
!configured.url
|
|
3320
|
-
)
|
|
3321
|
-
throw new HTTPException(422, {
|
|
3322
|
-
message: "Host delegation must match a selected configured host server",
|
|
3323
|
-
});
|
|
3324
|
-
assertHostMcpAuthoritySourceAdmissionEnabled(settings, configured.connectionRef);
|
|
3325
|
-
return [selection.serverId, structuredClone(configured)] as const;
|
|
3326
|
-
}),
|
|
3327
|
-
);
|
|
3328
|
-
return async (tx, sessionId, turnId) => {
|
|
3329
|
-
const owner = await beforeCommit(tx);
|
|
3330
|
-
for (const selection of selections) {
|
|
3331
|
-
const delegation = await getHostMcpDelegation(tx, owner, selection.delegationId);
|
|
3332
|
-
const binding = delegation ? await getHostMcpBinding(tx, owner, delegation.bindingId) : null;
|
|
3333
|
-
const configured = configs.get(selection.serverId)!;
|
|
3334
|
-
if (
|
|
3335
|
-
!delegation ||
|
|
3336
|
-
!binding ||
|
|
3337
|
-
delegation.status !== "active" ||
|
|
3338
|
-
binding.status !== "active" ||
|
|
3339
|
-
delegation.generation !== selection.generation ||
|
|
3340
|
-
!hostMcpBindingMatchesSelection(binding, {
|
|
3341
|
-
serverId: selection.serverId,
|
|
3342
|
-
destinationUrl: configured.url!,
|
|
3343
|
-
connectionRef: configured.connectionRef!,
|
|
3344
|
-
})
|
|
3345
|
-
)
|
|
3346
|
-
throw new HTTPException(403, { message: "Host delegation selection changed" });
|
|
3347
|
-
try {
|
|
3348
|
-
await captureDirectHostMcpAuthority(tx, owner, {
|
|
3349
|
-
sessionId,
|
|
3350
|
-
turnId,
|
|
3351
|
-
delegationId: delegation.id,
|
|
3352
|
-
expectedDelegationGeneration: selection.generation,
|
|
3353
|
-
});
|
|
3354
|
-
} catch (error) {
|
|
3355
|
-
if (error instanceof HostMcpDelegationAuthorityError)
|
|
3356
|
-
throw new HTTPException(403, { message: "Host delegation authority unavailable" });
|
|
3357
|
-
if (error instanceof HostMcpBindingConflictError)
|
|
3358
|
-
throw new HTTPException(409, { message: "Host delegation selection conflicts" });
|
|
3359
|
-
throw error;
|
|
3360
|
-
}
|
|
3361
|
-
}
|
|
3362
|
-
};
|
|
3363
|
-
}
|
|
3364
|
-
|
|
3365
4143
|
function sessionPromptBoundaryRequestHash(input: {
|
|
3366
4144
|
delivery: "send" | "steer";
|
|
3367
4145
|
controlEtag: string | null;
|
|
@@ -3376,8 +4154,7 @@ function sessionPromptBoundaryRequestHash(input: {
|
|
|
3376
4154
|
latencyMode: "standard" | "priority" | "fast" | null;
|
|
3377
4155
|
source: "user" | "api";
|
|
3378
4156
|
mcpCredentialUpdates: SessionMcpCredentialUpdateInput[];
|
|
3379
|
-
|
|
3380
|
-
selectedHostMcpDelegations?: HostMcpCreateSelection[];
|
|
4157
|
+
connectionAccounts?: McpConnectionAccountSelection[];
|
|
3381
4158
|
personalResourceAttachment?: PersonalResourceAttachmentIntent;
|
|
3382
4159
|
commandActor: SessionCommandActor;
|
|
3383
4160
|
}): string {
|
|
@@ -3396,10 +4173,7 @@ function sessionPromptBoundaryRequestHash(input: {
|
|
|
3396
4173
|
latencyMode: input.latencyMode,
|
|
3397
4174
|
source: input.source,
|
|
3398
4175
|
mcpCredentialUpdates: input.mcpCredentialUpdates,
|
|
3399
|
-
|
|
3400
|
-
...(input.selectedHostMcpDelegations?.length
|
|
3401
|
-
? { selectedHostMcpDelegations: input.selectedHostMcpDelegations }
|
|
3402
|
-
: {}),
|
|
4176
|
+
connectionAccounts: input.connectionAccounts ?? [],
|
|
3403
4177
|
personalResourceAttachment: input.personalResourceAttachment ?? null,
|
|
3404
4178
|
...(input.commandActor.type === "service"
|
|
3405
4179
|
? {
|
|
@@ -3437,10 +4211,14 @@ async function acceptSessionUserMessageInFileScope(
|
|
|
3437
4211
|
latencyMode?: "standard" | "priority" | "fast" | null;
|
|
3438
4212
|
clientEventId?: string;
|
|
3439
4213
|
mcpCredentialUpdates?: SessionMcpCredentialUpdateInput[];
|
|
3440
|
-
|
|
3441
|
-
selectedHostMcpDelegations?: HostMcpCreateSelection[];
|
|
4214
|
+
connectionAccounts?: McpConnectionAccountSelection[];
|
|
3442
4215
|
delivery?: "send" | "steer";
|
|
3443
4216
|
origin?: "human" | "operator";
|
|
4217
|
+
/**
|
|
4218
|
+
* Product surface when the entry point knows it (for example Slack or an
|
|
4219
|
+
* in-process embedding host). Omitted derives it from the grant.
|
|
4220
|
+
*/
|
|
4221
|
+
surface?: SessionTurnSurface;
|
|
3444
4222
|
controlEtag?: string | null;
|
|
3445
4223
|
expectedDraftRevision?: number | null;
|
|
3446
4224
|
personalResourceAttachment?: PersonalResourceAttachmentIntent;
|
|
@@ -3456,25 +4234,12 @@ async function acceptSessionUserMessageInFileScope(
|
|
|
3456
4234
|
replay: boolean;
|
|
3457
4235
|
}> {
|
|
3458
4236
|
const { db, bus, workflowClient, objectStorage } = deps;
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
input.authorization.grant.accountId !== grant.accountId ||
|
|
3466
|
-
input.authorization.grant.subjectId !== grant.subjectId ||
|
|
3467
|
-
input.authorization?.grant.workspaceId !== workspaceId ||
|
|
3468
|
-
!hasPermission(input.authorization?.grant.permissions ?? [], "connections:read") ||
|
|
3469
|
-
!hasPermission(grant.permissions, "connections:read") ||
|
|
3470
|
-
grant.metadata?.["sessionId"])
|
|
3471
|
-
)
|
|
3472
|
-
throw new HTTPException(403, {
|
|
3473
|
-
message: "Host selection requires a verified direct owner",
|
|
3474
|
-
});
|
|
3475
|
-
if (hostSelections.length)
|
|
3476
|
-
prepareHostMcpOwnerAuthorization(input.authorization!, workspaceId, "connections:read");
|
|
3477
|
-
const delegatedServiceInitiator = serviceInitiatorForGrant(grant);
|
|
4237
|
+
|
|
4238
|
+
const promptInitiator = creationInitiatorForGrant(grant);
|
|
4239
|
+
const delegatedServiceInitiator =
|
|
4240
|
+
promptInitiator.initiator?.kind === "service"
|
|
4241
|
+
? { initiator: promptInitiator.initiator, context: promptInitiator.context ?? {} }
|
|
4242
|
+
: null;
|
|
3478
4243
|
const delivery = input.delivery ?? "send";
|
|
3479
4244
|
const source = delegatedServiceInitiator || input.origin === "operator" ? "api" : "user";
|
|
3480
4245
|
const commandActor: SessionCommandActor = delegatedServiceInitiator
|
|
@@ -3486,7 +4251,7 @@ async function acceptSessionUserMessageInFileScope(
|
|
|
3486
4251
|
: {}),
|
|
3487
4252
|
context: delegatedServiceInitiator.context,
|
|
3488
4253
|
}
|
|
3489
|
-
: { type: "human", subjectId: grant.subjectId };
|
|
4254
|
+
: (promptInitiator.actor ?? { type: "human", subjectId: grant.subjectId });
|
|
3490
4255
|
await requireSessionAuthorization(deps, grant, {
|
|
3491
4256
|
sessionId,
|
|
3492
4257
|
operation: delivery === "steer" ? "session.steer" : "session.append",
|
|
@@ -3511,10 +4276,7 @@ async function acceptSessionUserMessageInFileScope(
|
|
|
3511
4276
|
latencyMode: input.latencyMode ?? null,
|
|
3512
4277
|
source,
|
|
3513
4278
|
mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
|
|
3514
|
-
|
|
3515
|
-
...(input.connectionAuthorities
|
|
3516
|
-
? { connectionAuthorities: input.connectionAuthorities }
|
|
3517
|
-
: {}),
|
|
4279
|
+
...(input.connectionAccounts ? { connectionAccounts: input.connectionAccounts } : {}),
|
|
3518
4280
|
...(input.personalResourceAttachment
|
|
3519
4281
|
? { personalResourceAttachment: input.personalResourceAttachment }
|
|
3520
4282
|
: {}),
|
|
@@ -3568,6 +4330,14 @@ async function acceptSessionUserMessageInFileScope(
|
|
|
3568
4330
|
// turn's effective model (a follow-up turn inherits the session's model). A
|
|
3569
4331
|
// pure read with no side effects.
|
|
3570
4332
|
const existingSession = await requireSession(db, workspaceId, sessionId);
|
|
4333
|
+
assertSessionIsNotImported(existingSession);
|
|
4334
|
+
// a reached task usage limit refuses a fresh Send/Steer here,
|
|
4335
|
+
// after exact replays returned above and before any queue/interrupt write.
|
|
4336
|
+
await requireSessionUsageLimitAllows(
|
|
4337
|
+
db,
|
|
4338
|
+
{ accountId: grant.accountId, workspaceId, sessionId, runtime: true },
|
|
4339
|
+
{ sink: deps.observability, boundary: delivery === "steer" ? "steer" : "send" },
|
|
4340
|
+
);
|
|
3571
4341
|
const settings = await resolveWorkspaceModelBoundarySettings(
|
|
3572
4342
|
deps,
|
|
3573
4343
|
grant,
|
|
@@ -3601,15 +4371,18 @@ async function acceptSessionUserMessageInFileScope(
|
|
|
3601
4371
|
const effectiveReasoningEffort = input.reasoningEffort ?? sessionReasoningEffort;
|
|
3602
4372
|
const sessionLatencyMode = existingSession.latencyMode;
|
|
3603
4373
|
const effectiveLatencyMode = input.latencyMode ?? sessionLatencyMode;
|
|
3604
|
-
const turnExecutionPolicy =
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
4374
|
+
const turnExecutionPolicy = withDeveloperSetupCredentialRestriction(
|
|
4375
|
+
resolveTurnExecutionPolicyV1(settings, {
|
|
4376
|
+
modelId: effectiveModel,
|
|
4377
|
+
requestedModelId: input.model ?? null,
|
|
4378
|
+
modelSource: input.model == null ? "session" : "explicit",
|
|
4379
|
+
reasoningEffort: effectiveReasoningEffort,
|
|
4380
|
+
reasoningSource: input.reasoningEffort == null ? "session" : "explicit",
|
|
4381
|
+
latencyMode: effectiveLatencyMode,
|
|
4382
|
+
latencyModeSource: input.latencyMode == null ? "session" : "explicit",
|
|
4383
|
+
}),
|
|
4384
|
+
{ grant, authorization: input.authorization, trustedMetadata: [existingSession.metadata] },
|
|
4385
|
+
);
|
|
3613
4386
|
if (composerDraftResources) {
|
|
3614
4387
|
const acceptedResources = new Set(requestedResources.map((resource) => stableJson(resource)));
|
|
3615
4388
|
const unacceptedDraftResource = composerDraftResources.find(
|
|
@@ -3627,33 +4400,69 @@ async function acceptSessionUserMessageInFileScope(
|
|
|
3627
4400
|
sessionId,
|
|
3628
4401
|
input.annotations ?? [],
|
|
3629
4402
|
);
|
|
3630
|
-
|
|
3631
|
-
|
|
4403
|
+
const admissionActor = commandActor;
|
|
4404
|
+
let frozenAdmissionInitiator = await withWorkspaceSessionActivityRls(
|
|
4405
|
+
db,
|
|
3632
4406
|
workspaceId,
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
4407
|
+
(scopedDb) =>
|
|
4408
|
+
frozenInitiatorForCommandActor(scopedDb, workspaceId, admissionActor, grant.subjectLabel),
|
|
4409
|
+
);
|
|
4410
|
+
// Re-submitting a checked-out queue edit retains the original turn's
|
|
4411
|
+
// causal human, just as the locked prompt transaction does.
|
|
4412
|
+
if (delivery === "send" && input.expectedDraftRevision != null) {
|
|
4413
|
+
const draft = await withWorkspaceSubjectSessionActivityRls(
|
|
4414
|
+
db,
|
|
4415
|
+
workspaceId,
|
|
4416
|
+
grant.subjectId,
|
|
4417
|
+
async (scopedDb) =>
|
|
4418
|
+
await getComposerDraftInTransaction(scopedDb, {
|
|
4419
|
+
workspaceId,
|
|
4420
|
+
sessionId,
|
|
4421
|
+
subjectId: grant.subjectId,
|
|
4422
|
+
}),
|
|
4423
|
+
);
|
|
4424
|
+
if (draft?.revision === input.expectedDraftRevision && draft.sourceTurnId) {
|
|
4425
|
+
const sourceTurn = await getSessionTurn(db, workspaceId, draft.sourceTurnId);
|
|
4426
|
+
if (sourceTurn?.sessionId === sessionId) {
|
|
4427
|
+
frozenAdmissionInitiator = {
|
|
4428
|
+
initiator: sourceTurn.initiator,
|
|
4429
|
+
context: sourceTurn.initiatorContext,
|
|
4430
|
+
initiatingHumanSubjectId: await getSessionTurnInitiatingHumanSubjectId(
|
|
4431
|
+
db,
|
|
4432
|
+
workspaceId,
|
|
4433
|
+
sourceTurn.id,
|
|
4434
|
+
),
|
|
4435
|
+
};
|
|
4436
|
+
}
|
|
4437
|
+
}
|
|
4438
|
+
}
|
|
4439
|
+
if (!effectiveModel.startsWith("codex/")) {
|
|
4440
|
+
await requireLimit(deps, {
|
|
4441
|
+
accountId: grant.accountId,
|
|
4442
|
+
workspaceId,
|
|
4443
|
+
initiatingHumanSubjectId: initiatingHumanForAllowance(frozenAdmissionInitiator),
|
|
4444
|
+
action: "agent_run:create",
|
|
4445
|
+
quantity: 1,
|
|
4446
|
+
model: effectiveModel,
|
|
4447
|
+
});
|
|
4448
|
+
}
|
|
3637
4449
|
if (requestedResources.some((resource) => resource.kind === "file") && !objectStorage) {
|
|
3638
4450
|
throw new HTTPException(503, {
|
|
3639
4451
|
message: "object storage is not configured",
|
|
3640
4452
|
});
|
|
3641
4453
|
}
|
|
4454
|
+
const attachmentOwnerContext = input.authorization
|
|
4455
|
+
? await fileOwnerContextForAccess({ db }, input.authorization, "sessions:control")
|
|
4456
|
+
: grant.principalKind === "agent_attempt"
|
|
4457
|
+
? await fileOwnerContextForAgent({ db }, grant, "sessions:control")
|
|
4458
|
+
: undefined;
|
|
3642
4459
|
await validateFileResources(
|
|
3643
4460
|
db,
|
|
3644
4461
|
grant.accountId,
|
|
3645
4462
|
workspaceId,
|
|
3646
4463
|
grant.subjectId,
|
|
3647
4464
|
requestedResources,
|
|
3648
|
-
|
|
3649
|
-
((await getSessionAuthorityProjection(db, workspaceId, sessionId))?.visibility ===
|
|
3650
|
-
"user_private" ||
|
|
3651
|
-
existingSession.memoryScope === "user" ||
|
|
3652
|
-
(await requireWorkspace(db, workspaceId)).kind === "personal")
|
|
3653
|
-
? input.authorization
|
|
3654
|
-
? await fileOwnerContextForAccess({ db }, input.authorization, "sessions:control")
|
|
3655
|
-
: await fileOwnerContextForAgent({ db }, grant, "sessions:control")
|
|
3656
|
-
: undefined,
|
|
4465
|
+
attachmentOwnerContext,
|
|
3657
4466
|
);
|
|
3658
4467
|
await validateGitHubRepositorySelection(db, workspaceId, [
|
|
3659
4468
|
...existingSession.resources,
|
|
@@ -3675,7 +4484,7 @@ async function acceptSessionUserMessageInFileScope(
|
|
|
3675
4484
|
connectionDelegationSource.turnId,
|
|
3676
4485
|
)
|
|
3677
4486
|
: null;
|
|
3678
|
-
const
|
|
4487
|
+
const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
3679
4488
|
db,
|
|
3680
4489
|
workspaceId,
|
|
3681
4490
|
settings,
|
|
@@ -3685,37 +4494,60 @@ async function acceptSessionUserMessageInFileScope(
|
|
|
3685
4494
|
}
|
|
3686
4495
|
: { subjectId: grant.subjectId },
|
|
3687
4496
|
);
|
|
3688
|
-
const
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
existingSession.firstPartyMcpPermissions.includes("connections:read")),
|
|
3706
|
-
...(input.connectionAuthorities ? { authoritySelections: input.connectionAuthorities } : {}),
|
|
4497
|
+
const runtimeSettings = settingsWithSessionMcpServerMetadata(
|
|
4498
|
+
capabilityRuntimeSettings,
|
|
4499
|
+
existingSession.mcpServers,
|
|
4500
|
+
);
|
|
4501
|
+
// A `workspace_default` session stores only its creation-time tool
|
|
4502
|
+
// snapshot and resolves the current workspace defaults at run time, so the
|
|
4503
|
+
// raw column is not the connector allow-list this follow-up executes with.
|
|
4504
|
+
// Freeze accounts against the same resolved list the composer and the
|
|
4505
|
+
// worker see.
|
|
4506
|
+
const connectionWorkspace = await requireWorkspace(db, workspaceId);
|
|
4507
|
+
const connectionAccountTools = sessionToolsForConnectionAccounts({
|
|
4508
|
+
session: existingSession,
|
|
4509
|
+
runtimeMcpServers: runtimeSettings.mcpServers,
|
|
4510
|
+
defaultMcpServerIds: workspaceSessionToolPolicyDefaultServerIdsFor(
|
|
4511
|
+
capabilityRuntimeSettings.mcpServers,
|
|
4512
|
+
connectionWorkspace.settings,
|
|
4513
|
+
),
|
|
3707
4514
|
});
|
|
3708
|
-
const
|
|
3709
|
-
|
|
3710
|
-
existingSession
|
|
3711
|
-
|
|
3712
|
-
workspaceId,
|
|
3713
|
-
hostSelections,
|
|
3714
|
-
input.authorization,
|
|
4515
|
+
const existingEffectiveFirstPartyTools = resolveSessionFirstPartyMcpTools(
|
|
4516
|
+
settings,
|
|
4517
|
+
existingSession,
|
|
4518
|
+
connectionWorkspace.settings,
|
|
3715
4519
|
);
|
|
3716
|
-
const
|
|
3717
|
-
|
|
3718
|
-
|
|
4520
|
+
const { personalConnectionDelegations, mcpAccountBindings, connectionAccountIntent } =
|
|
4521
|
+
await freezeConnectionAccounts({
|
|
4522
|
+
db,
|
|
4523
|
+
accountId: grant.accountId,
|
|
4524
|
+
workspaceId,
|
|
4525
|
+
settings: runtimeSettings,
|
|
4526
|
+
tools: connectionAccountTools,
|
|
4527
|
+
resources: [...existingSession.resources, ...requestedResources],
|
|
4528
|
+
source: connectionDelegationSource,
|
|
4529
|
+
targetSessionId: sessionId,
|
|
4530
|
+
googleDrivePublicationEnabled:
|
|
4531
|
+
existingEffectiveFirstPartyTools.includes("editable_artifact_export") &&
|
|
4532
|
+
existingEffectiveFirstPartyTools.includes("editable_artifact_export_status") &&
|
|
4533
|
+
(!existingSession.firstPartyMcpPermissions?.length ||
|
|
4534
|
+
(existingSession.firstPartyMcpPermissions.includes("artifacts:read") &&
|
|
4535
|
+
existingSession.firstPartyMcpPermissions.includes("artifacts:publish"))),
|
|
4536
|
+
atlassianEnabled:
|
|
4537
|
+
existingSession.firstPartyMcpTools.some((tool) => tool.startsWith("atlassian_")) &&
|
|
4538
|
+
(!existingSession.firstPartyMcpPermissions?.length ||
|
|
4539
|
+
existingSession.firstPartyMcpPermissions.includes("connections:read")),
|
|
4540
|
+
...(input.connectionAccounts ? { authoritySelections: input.connectionAccounts } : {}),
|
|
4541
|
+
});
|
|
4542
|
+
|
|
4543
|
+
const captureLinkedAuthority = prepareExternalLinkTurnAdmission(input.authorization);
|
|
4544
|
+
const surface = resolveTurnSurface({
|
|
4545
|
+
grant,
|
|
4546
|
+
authorization: input.authorization,
|
|
4547
|
+
requested: input.surface,
|
|
4548
|
+
});
|
|
4549
|
+
const { accepted, turn, draft, receipt, routing, interruptionCount, replay } =
|
|
4550
|
+
await postUserMessageTurn({
|
|
3719
4551
|
db,
|
|
3720
4552
|
bus,
|
|
3721
4553
|
workflowClient,
|
|
@@ -3735,20 +4567,33 @@ async function acceptSessionUserMessageInFileScope(
|
|
|
3735
4567
|
turnExecutionPolicy,
|
|
3736
4568
|
mcpCredentialUpdates,
|
|
3737
4569
|
personalConnectionDelegations,
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
4570
|
+
mcpAccountBindings,
|
|
4571
|
+
connectionAccountIntent,
|
|
4572
|
+
|
|
4573
|
+
captureTurnAuthority: async (tx: Database, turnId: string) => {
|
|
4574
|
+
await captureLinkedAuthority?.(tx, sessionId, turnId);
|
|
4575
|
+
// The prompt transaction has frozen the exact authority at this
|
|
4576
|
+
// point. A refusal rolls back both prompt and receipt atomically.
|
|
4577
|
+
if (effectiveModel.startsWith("codex/"))
|
|
4578
|
+
await requireLimit(
|
|
4579
|
+
{ ...deps, db: tx },
|
|
4580
|
+
{
|
|
4581
|
+
accountId: grant.accountId,
|
|
4582
|
+
workspaceId,
|
|
4583
|
+
initiatingHumanSubjectId: initiatingHumanForAllowance(frozenAdmissionInitiator),
|
|
4584
|
+
action: "agent_run:create",
|
|
4585
|
+
quantity: 1,
|
|
4586
|
+
model: effectiveModel,
|
|
4587
|
+
acceptedTurn: { sessionId, turnId },
|
|
3744
4588
|
},
|
|
3745
|
-
|
|
3746
|
-
|
|
4589
|
+
);
|
|
4590
|
+
},
|
|
3747
4591
|
...(input.personalResourceAttachment
|
|
3748
4592
|
? { personalResourceAttachment: input.personalResourceAttachment }
|
|
3749
4593
|
: {}),
|
|
3750
4594
|
delivery,
|
|
3751
4595
|
origin: source === "api" ? "operator" : "human",
|
|
4596
|
+
surface,
|
|
3752
4597
|
actor: grant.subjectId,
|
|
3753
4598
|
...(grant.subjectLabel ? { actorLabel: grant.subjectLabel } : {}),
|
|
3754
4599
|
commandActor,
|
|
@@ -3763,6 +4608,7 @@ async function acceptSessionUserMessageInFileScope(
|
|
|
3763
4608
|
? { schedulePostCommit: deps.schedulePromptPostCommit }
|
|
3764
4609
|
: {}),
|
|
3765
4610
|
});
|
|
4611
|
+
if (!replay) recordUserMessageAccepted(deps.observability, { surface });
|
|
3766
4612
|
return {
|
|
3767
4613
|
accepted,
|
|
3768
4614
|
turn,
|
|
@@ -3880,6 +4726,131 @@ export async function updateSessionTitle(
|
|
|
3880
4726
|
};
|
|
3881
4727
|
}
|
|
3882
4728
|
|
|
4729
|
+
/**
|
|
4730
|
+
* Set or clear the session's keep-live exemption from the idle-session archive.
|
|
4731
|
+
* Archived sessions are read-only and reject the change.
|
|
4732
|
+
*/
|
|
4733
|
+
export async function setSessionRetention(
|
|
4734
|
+
deps: {
|
|
4735
|
+
db: Database;
|
|
4736
|
+
sessionAuthorization?: SessionAuthorizationPort | null;
|
|
4737
|
+
},
|
|
4738
|
+
grant: AccessGrant,
|
|
4739
|
+
sessionId: string,
|
|
4740
|
+
keepLive: boolean,
|
|
4741
|
+
): Promise<{
|
|
4742
|
+
result: SetSessionKeepLiveResult;
|
|
4743
|
+
relatedSessionAccess: "target" | "root";
|
|
4744
|
+
}> {
|
|
4745
|
+
const authorization = await requireSessionAuthorization(deps, grant, {
|
|
4746
|
+
sessionId,
|
|
4747
|
+
operation: "session.retention.write",
|
|
4748
|
+
surface: "core",
|
|
4749
|
+
});
|
|
4750
|
+
const result = await setSessionKeepLive(deps.db, {
|
|
4751
|
+
workspaceId: grant.workspaceId,
|
|
4752
|
+
sessionId,
|
|
4753
|
+
keepLive,
|
|
4754
|
+
});
|
|
4755
|
+
return { result, relatedSessionAccess: authorization?.relatedSessionAccess ?? "root" };
|
|
4756
|
+
}
|
|
4757
|
+
|
|
4758
|
+
/** Change future defaults without accepting a prompt or touching accepted work. */
|
|
4759
|
+
export async function setSessionModel(
|
|
4760
|
+
deps: Pick<
|
|
4761
|
+
ApiRouteDeps,
|
|
4762
|
+
"db" | "bus" | "settings" | "catalogSourceSettings" | "sessionAuthorization"
|
|
4763
|
+
>,
|
|
4764
|
+
grant: AccessGrant,
|
|
4765
|
+
sessionId: string,
|
|
4766
|
+
request: SetSessionModelRequest,
|
|
4767
|
+
surface: SessionAuthorizationSurface = "core",
|
|
4768
|
+
) {
|
|
4769
|
+
requirePermission(grant, "sessions:control");
|
|
4770
|
+
const input = SetSessionModelRequest.parse(request);
|
|
4771
|
+
const authorization = await requireSessionAuthorization(deps, grant, {
|
|
4772
|
+
sessionId,
|
|
4773
|
+
operation: "session.model.write",
|
|
4774
|
+
surface,
|
|
4775
|
+
});
|
|
4776
|
+
const settings = await resolveWorkspaceModelBoundarySettings(deps, grant, grant.workspaceId, [
|
|
4777
|
+
input.model,
|
|
4778
|
+
]);
|
|
4779
|
+
const model = canonicalConfiguredModel(settings, input.model)!;
|
|
4780
|
+
await assertWorkspaceModelPolicyAllows(deps.db, settings, grant.workspaceId, model);
|
|
4781
|
+
const caller = authorization?.actor;
|
|
4782
|
+
const service = serviceInitiatorForGrant(grant);
|
|
4783
|
+
const actor: SessionCommandActor =
|
|
4784
|
+
caller?.kind === "agent_attempt"
|
|
4785
|
+
? {
|
|
4786
|
+
type: "agent_attempt",
|
|
4787
|
+
sessionId: caller.callerSessionId,
|
|
4788
|
+
turnId: caller.turnId,
|
|
4789
|
+
attemptId: caller.attemptId,
|
|
4790
|
+
executionGeneration: caller.executionGeneration,
|
|
4791
|
+
}
|
|
4792
|
+
: service
|
|
4793
|
+
? { type: "service", subjectId: service.initiator.subjectId, context: service.context }
|
|
4794
|
+
: { type: "human", subjectId: grant.subjectId };
|
|
4795
|
+
const result = await runIdempotentPersistenceTransaction(
|
|
4796
|
+
{
|
|
4797
|
+
stage: "session.model_settings",
|
|
4798
|
+
eventTypes: ["session.model_settings.updated"],
|
|
4799
|
+
maxAttempts: 3,
|
|
4800
|
+
},
|
|
4801
|
+
async () => {
|
|
4802
|
+
const write = async (tx: Parameters<typeof setSessionModelInTransaction>[0]) =>
|
|
4803
|
+
await setSessionModelInTransaction(tx, {
|
|
4804
|
+
accountId: grant.accountId,
|
|
4805
|
+
workspaceId: grant.workspaceId,
|
|
4806
|
+
sessionId,
|
|
4807
|
+
actor,
|
|
4808
|
+
operationKey: input.idempotencyKey,
|
|
4809
|
+
model,
|
|
4810
|
+
reasoningEffort: input.reasoningEffort,
|
|
4811
|
+
validate: (session) => {
|
|
4812
|
+
try {
|
|
4813
|
+
assertSessionAllowsProductModel(
|
|
4814
|
+
{
|
|
4815
|
+
codexCompactionMode:
|
|
4816
|
+
session.codexCompactionMode as Session["codexCompactionMode"],
|
|
4817
|
+
},
|
|
4818
|
+
model,
|
|
4819
|
+
);
|
|
4820
|
+
// Validate fresh selection/provider and preserved latency exactly as
|
|
4821
|
+
// prompt admission does; never silently clamp requested settings.
|
|
4822
|
+
resolveTurnExecutionPolicyV1(settings, {
|
|
4823
|
+
modelId: model,
|
|
4824
|
+
requestedModelId: input.model,
|
|
4825
|
+
modelSource: "explicit",
|
|
4826
|
+
reasoningEffort: input.reasoningEffort,
|
|
4827
|
+
reasoningSource: "explicit",
|
|
4828
|
+
latencyMode: session.latencyMode as Session["latencyMode"],
|
|
4829
|
+
latencyModeSource: "session",
|
|
4830
|
+
});
|
|
4831
|
+
} catch (error) {
|
|
4832
|
+
throw new HTTPException(422, {
|
|
4833
|
+
message: error instanceof Error ? error.message : "Invalid model settings",
|
|
4834
|
+
cause: error,
|
|
4835
|
+
});
|
|
4836
|
+
}
|
|
4837
|
+
},
|
|
4838
|
+
});
|
|
4839
|
+
return actor.type === "agent_attempt"
|
|
4840
|
+
? await withWorkspaceSessionActivityRls(deps.db, grant.workspaceId, write)
|
|
4841
|
+
: await withWorkspaceSubjectSessionActivityRls(
|
|
4842
|
+
deps.db,
|
|
4843
|
+
grant.workspaceId,
|
|
4844
|
+
grant.subjectId,
|
|
4845
|
+
write,
|
|
4846
|
+
);
|
|
4847
|
+
},
|
|
4848
|
+
);
|
|
4849
|
+
const event = await getSessionEvent(deps.db, grant.workspaceId, result.eventId);
|
|
4850
|
+
if (event) await publishDurableSessionEvents(deps.bus, grant.workspaceId, sessionId, [event]);
|
|
4851
|
+
return { sessionId, ...result, effectiveFrom: "future_turns" as const };
|
|
4852
|
+
}
|
|
4853
|
+
|
|
3883
4854
|
/**
|
|
3884
4855
|
* Update one existing session MCP server's approval policy. The database
|
|
3885
4856
|
* serializes this write with attempt claim under the session lock: an already
|
|
@@ -3905,7 +4876,7 @@ export async function updateSessionMcpApprovalPolicy(
|
|
|
3905
4876
|
});
|
|
3906
4877
|
requirePermission(grant, "sessions:control");
|
|
3907
4878
|
|
|
3908
|
-
const outcome: { server?:
|
|
4879
|
+
const outcome: { server?: SessionMcpApprovalPolicyTarget } = {};
|
|
3909
4880
|
const events = await appendSessionEventsWithLockedSessionUpdate(
|
|
3910
4881
|
deps.db,
|
|
3911
4882
|
grant.workspaceId,
|
|
@@ -3975,7 +4946,17 @@ function toolPolicyAuditSnapshot(
|
|
|
3975
4946
|
const toolRefs = allToolRefs.slice(0, maxToolPolicyAuditRefs);
|
|
3976
4947
|
return {
|
|
3977
4948
|
mode: policy.mode,
|
|
4949
|
+
...(policy.firstPartyMode ? { firstPartyMode: policy.firstPartyMode } : {}),
|
|
4950
|
+
...(policy.firstPartyAdditions?.length
|
|
4951
|
+
? { firstPartyAdditions: [...policy.firstPartyAdditions] }
|
|
4952
|
+
: {}),
|
|
3978
4953
|
inheritedFromSessionId: policy.inheritedFromSessionId,
|
|
4954
|
+
...(policy.excludedMcpServerIds?.length
|
|
4955
|
+
? {
|
|
4956
|
+
excludedMcpServerIds: policy.excludedMcpServerIds.slice(0, maxToolPolicyAuditRefs),
|
|
4957
|
+
excludedMcpServerCount: policy.excludedMcpServerIds.length,
|
|
4958
|
+
}
|
|
4959
|
+
: {}),
|
|
3979
4960
|
// IDs only: no MCP URLs, names, headers, credentials, schemas, or args.
|
|
3980
4961
|
toolIds: [...toolRefs]
|
|
3981
4962
|
.sort((left, right) => `${left.kind}:${left.id}`.localeCompare(`${right.kind}:${right.id}`))
|
|
@@ -3984,7 +4965,37 @@ function toolPolicyAuditSnapshot(
|
|
|
3984
4965
|
toolCount: allToolRefs.length,
|
|
3985
4966
|
firstPartyMcpTools: [...firstPartyMcpTools].sort(),
|
|
3986
4967
|
firstPartyMcpToolCount: firstPartyMcpTools.length,
|
|
3987
|
-
truncated:
|
|
4968
|
+
truncated:
|
|
4969
|
+
allToolRefs.length > toolRefs.length ||
|
|
4970
|
+
(policy.excludedMcpServerIds?.length ?? 0) > maxToolPolicyAuditRefs,
|
|
4971
|
+
};
|
|
4972
|
+
}
|
|
4973
|
+
|
|
4974
|
+
/** Exact parent authority shared by tool-policy and agent-configuration writes. */
|
|
4975
|
+
function sessionToolPolicyCeiling(
|
|
4976
|
+
session: Session,
|
|
4977
|
+
settings: Settings,
|
|
4978
|
+
runtimeSettings: Settings,
|
|
4979
|
+
workspaceDefaults: ReturnType<typeof resolveWorkspaceSessionToolDefaults>,
|
|
4980
|
+
): { tools: ToolRef[]; firstPartyMcpTools: FirstPartyMcpToolName[] } {
|
|
4981
|
+
return {
|
|
4982
|
+
tools: withFirstPartyTools(
|
|
4983
|
+
session.toolPolicy.mode === "workspace_default"
|
|
4984
|
+
? withWorkspaceDefaultMcpTools(
|
|
4985
|
+
availableToolRefs(session.tools, runtimeSettings),
|
|
4986
|
+
settings,
|
|
4987
|
+
runtimeSettings,
|
|
4988
|
+
workspaceDefaults,
|
|
4989
|
+
)
|
|
4990
|
+
: session.tools,
|
|
4991
|
+
runtimeSettings,
|
|
4992
|
+
).filter(
|
|
4993
|
+
(tool) =>
|
|
4994
|
+
tool.id === "opengeni" || !session.toolPolicy.excludedMcpServerIds?.includes(tool.id),
|
|
4995
|
+
),
|
|
4996
|
+
firstPartyMcpTools: resolveSessionFirstPartyMcpTools(settings, session, {
|
|
4997
|
+
sessionToolDefaults: workspaceDefaults,
|
|
4998
|
+
}),
|
|
3988
4999
|
};
|
|
3989
5000
|
}
|
|
3990
5001
|
|
|
@@ -4028,9 +5039,31 @@ export async function updateSessionToolPolicy(
|
|
|
4028
5039
|
);
|
|
4029
5040
|
const explicitRequest = request.mode === "workspace_default" ? null : request;
|
|
4030
5041
|
const requestedMode = explicitRequest ? "explicit" : "workspace_default";
|
|
5042
|
+
const connectorOnlyEdit =
|
|
5043
|
+
request.mode === "workspace_default" && request.excludedMcpServerIds !== undefined;
|
|
5044
|
+
const requestedExclusions =
|
|
5045
|
+
request.mode === "workspace_default" ? (request.excludedMcpServerIds ?? []) : [];
|
|
4031
5046
|
const explicitRequestedTools = explicitRequest
|
|
4032
5047
|
? (() => {
|
|
4033
|
-
|
|
5048
|
+
// A disconnected stored selection remains policy truth, not executable
|
|
5049
|
+
// authority. Preserve exact existing refs while validating every new or
|
|
5050
|
+
// changed ref against the current runtime registry.
|
|
5051
|
+
const availableIds = new Set(runtimeSettings.mcpServers.map((server) => server.id));
|
|
5052
|
+
const retainedUnavailableRefs = explicitRequest.tools.filter(
|
|
5053
|
+
(tool) =>
|
|
5054
|
+
!availableIds.has(tool.id) &&
|
|
5055
|
+
existingSession.tools.some((existing) => stableJson(existing) === stableJson(tool)),
|
|
5056
|
+
);
|
|
5057
|
+
const retainedIds = new Set(retainedUnavailableRefs.map((tool) => tool.id));
|
|
5058
|
+
const validatedCurrentRefs = validateToolRefs(
|
|
5059
|
+
explicitRequest.tools.filter((tool) => !retainedIds.has(tool.id)),
|
|
5060
|
+
runtimeSettings,
|
|
5061
|
+
);
|
|
5062
|
+
const validatedTools = explicitRequest.tools.filter(
|
|
5063
|
+
(tool) =>
|
|
5064
|
+
retainedUnavailableRefs.includes(tool) ||
|
|
5065
|
+
validatedCurrentRefs.some((validated) => validated.id === tool.id),
|
|
5066
|
+
);
|
|
4034
5067
|
const validatedIds = new Set(validatedTools.map((tool) => `${tool.kind}:${tool.id}`));
|
|
4035
5068
|
const unknown = explicitRequest.tools.find(
|
|
4036
5069
|
(tool) => !validatedIds.has(`${tool.kind}:${tool.id}`),
|
|
@@ -4043,18 +5076,28 @@ export async function updateSessionToolPolicy(
|
|
|
4043
5076
|
return withFirstPartyTools(validatedTools, runtimeSettings);
|
|
4044
5077
|
})()
|
|
4045
5078
|
: null;
|
|
4046
|
-
const explicitRequestedFirstPartyTools = explicitRequest
|
|
4047
|
-
? [...explicitRequest.firstPartyMcpTools]
|
|
4048
|
-
: null;
|
|
4049
5079
|
const deploymentFirstPartyMcpToolPolicy = resolveFirstPartyMcpToolPolicy(deps.settings);
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
5080
|
+
// Echoing the session's stored catalog (or create defaults) can include a
|
|
5081
|
+
// tool the deployment later removed. Runtime already strips those; failing
|
|
5082
|
+
// the whole connector save would block unrelated MCP toggles. A newly added
|
|
5083
|
+
// disabled tool, however, is explicit intent and fails rather than vanishing.
|
|
5084
|
+
if (explicitRequest) {
|
|
5085
|
+
requireDeploymentFirstPartyMcpTools(
|
|
5086
|
+
explicitRequest.firstPartyMcpTools,
|
|
5087
|
+
deploymentFirstPartyMcpToolPolicy.allowed,
|
|
5088
|
+
[
|
|
5089
|
+
// A session without a stored selection was shown the unfiltered defaults.
|
|
5090
|
+
...(existingSession.firstPartyMcpTools ??
|
|
5091
|
+
deps.settings.defaultFirstPartyMcpTools ??
|
|
5092
|
+
DEFAULT_FIRST_PARTY_MCP_TOOLS),
|
|
5093
|
+
...(workspaceSessionToolDefaults?.firstPartyMcpTools ?? []),
|
|
5094
|
+
...(existingSession.toolPolicy.firstPartyAdditions ?? []),
|
|
5095
|
+
],
|
|
5096
|
+
);
|
|
4057
5097
|
}
|
|
5098
|
+
const explicitRequestedFirstPartyTools = explicitRequest
|
|
5099
|
+
? allowedFirstPartyMcpToolsForSession(deps.settings, explicitRequest.firstPartyMcpTools)
|
|
5100
|
+
: null;
|
|
4058
5101
|
const workspaceDefaultTools = withFirstPartyTools(
|
|
4059
5102
|
withWorkspaceDefaultMcpTools(
|
|
4060
5103
|
[],
|
|
@@ -4082,6 +5125,11 @@ export async function updateSessionToolPolicy(
|
|
|
4082
5125
|
let nextTools: ToolRef[];
|
|
4083
5126
|
let nextFirstPartyMcpTools: FirstPartyMcpToolName[];
|
|
4084
5127
|
let nextPolicy: SessionToolPolicy;
|
|
5128
|
+
// Guaranteed additions belong to the session, not to one tool choice: an
|
|
5129
|
+
// explicit edit stops applying them and a reset to defaults restores them.
|
|
5130
|
+
const sessionFirstPartyAdditions = session.toolPolicy.firstPartyAdditions?.length
|
|
5131
|
+
? { firstPartyAdditions: [...session.toolPolicy.firstPartyAdditions] }
|
|
5132
|
+
: {};
|
|
4085
5133
|
if (session.parentSessionId) {
|
|
4086
5134
|
const parent = await context.getLockedSession(session.parentSessionId);
|
|
4087
5135
|
if (!parent) {
|
|
@@ -4090,23 +5138,13 @@ export async function updateSessionToolPolicy(
|
|
|
4090
5138
|
});
|
|
4091
5139
|
}
|
|
4092
5140
|
const parentTracksWorkspaceDefaults = parent.toolPolicy?.mode === "workspace_default";
|
|
4093
|
-
const parentEffective =
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
)
|
|
4101
|
-
: parent.tools,
|
|
4102
|
-
runtimeSettings,
|
|
4103
|
-
);
|
|
4104
|
-
const deploymentAllowedFirstPartyMcpTools = new Set(
|
|
4105
|
-
deploymentFirstPartyMcpToolPolicy.allowed,
|
|
4106
|
-
);
|
|
4107
|
-
const parentFirstPartyMcpTools = [
|
|
4108
|
-
...(parent.firstPartyMcpTools ?? deploymentFirstPartyMcpToolPolicy.default),
|
|
4109
|
-
].filter((tool) => deploymentAllowedFirstPartyMcpTools.has(tool));
|
|
5141
|
+
const { tools: parentEffective, firstPartyMcpTools: parentFirstPartyMcpTools } =
|
|
5142
|
+
sessionToolPolicyCeiling(
|
|
5143
|
+
parent,
|
|
5144
|
+
deps.settings,
|
|
5145
|
+
runtimeSettings,
|
|
5146
|
+
workspaceSessionToolDefaults,
|
|
5147
|
+
);
|
|
4110
5148
|
if (requestedMode === "workspace_default") {
|
|
4111
5149
|
if (!parentTracksWorkspaceDefaults) {
|
|
4112
5150
|
throw new HTTPException(403, {
|
|
@@ -4119,6 +5157,18 @@ export async function updateSessionToolPolicy(
|
|
|
4119
5157
|
nextPolicy = {
|
|
4120
5158
|
mode: "workspace_default",
|
|
4121
5159
|
inheritedFromSessionId: parent.id,
|
|
5160
|
+
...(parent.toolPolicy.firstPartyMode === "workspace_default"
|
|
5161
|
+
? {
|
|
5162
|
+
firstPartyMode: "workspace_default" as const,
|
|
5163
|
+
...(parent.toolPolicy.firstPartyAdditions?.length
|
|
5164
|
+
? { firstPartyAdditions: [...parent.toolPolicy.firstPartyAdditions] }
|
|
5165
|
+
: {}),
|
|
5166
|
+
}
|
|
5167
|
+
: {}),
|
|
5168
|
+
...defaultPolicyExclusions([
|
|
5169
|
+
...(parent.toolPolicy.excludedMcpServerIds ?? []),
|
|
5170
|
+
...requestedExclusions,
|
|
5171
|
+
]),
|
|
4122
5172
|
};
|
|
4123
5173
|
} else {
|
|
4124
5174
|
nextTools = explicitRequestedTools!;
|
|
@@ -4133,7 +5183,7 @@ export async function updateSessionToolPolicy(
|
|
|
4133
5183
|
);
|
|
4134
5184
|
if (widenedFirstPartyTool) {
|
|
4135
5185
|
throw new HTTPException(403, {
|
|
4136
|
-
message: `session
|
|
5186
|
+
message: `session Opengeni tools may only narrow the parent policy: ${widenedFirstPartyTool}`,
|
|
4137
5187
|
});
|
|
4138
5188
|
}
|
|
4139
5189
|
nextFirstPartyMcpTools = explicitRequestedFirstPartyTools!;
|
|
@@ -4147,43 +5197,131 @@ export async function updateSessionToolPolicy(
|
|
|
4147
5197
|
requestedMode === "workspace_default" ? workspaceDefaultTools : explicitRequestedTools!;
|
|
4148
5198
|
nextFirstPartyMcpTools =
|
|
4149
5199
|
requestedMode === "workspace_default"
|
|
4150
|
-
?
|
|
5200
|
+
? [
|
|
5201
|
+
...new Set([
|
|
5202
|
+
...workspaceDefaultFirstPartyTools,
|
|
5203
|
+
...(session.toolPolicy.firstPartyAdditions ?? []).filter((tool) =>
|
|
5204
|
+
deploymentFirstPartyMcpToolPolicy.allowed.includes(tool),
|
|
5205
|
+
),
|
|
5206
|
+
]),
|
|
5207
|
+
]
|
|
4151
5208
|
: explicitRequestedFirstPartyTools!;
|
|
4152
|
-
nextPolicy = {
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
5209
|
+
nextPolicy = {
|
|
5210
|
+
mode: requestedMode,
|
|
5211
|
+
inheritedFromSessionId: null,
|
|
5212
|
+
...sessionFirstPartyAdditions,
|
|
5213
|
+
...(requestedMode === "workspace_default"
|
|
5214
|
+
? {
|
|
5215
|
+
firstPartyMode: "workspace_default" as const,
|
|
5216
|
+
...defaultPolicyExclusions(requestedExclusions),
|
|
5217
|
+
}
|
|
5218
|
+
: {}),
|
|
5219
|
+
};
|
|
5220
|
+
}
|
|
5221
|
+
if (connectorOnlyEdit) {
|
|
5222
|
+
if (session.toolPolicy.mode !== "workspace_default") {
|
|
5223
|
+
throw new HTTPException(409, {
|
|
5224
|
+
message: "adopt workspace defaults before editing connector exclusions",
|
|
5225
|
+
});
|
|
5226
|
+
}
|
|
5227
|
+
// A connector switch never rewrites built-in tool choices or selected refs.
|
|
5228
|
+
nextTools = session.tools;
|
|
5229
|
+
nextFirstPartyMcpTools = [
|
|
5230
|
+
...(session.firstPartyMcpTools ?? deploymentFirstPartyMcpToolPolicy.default),
|
|
5231
|
+
];
|
|
5232
|
+
delete nextPolicy.firstPartyMode;
|
|
5233
|
+
if (session.toolPolicy.firstPartyMode) {
|
|
5234
|
+
nextPolicy.firstPartyMode = session.toolPolicy.firstPartyMode;
|
|
5235
|
+
}
|
|
5236
|
+
}
|
|
5237
|
+
if (!connectorOnlyEdit) {
|
|
5238
|
+
nextTools = withoutExcludedMcpServers(nextTools, nextPolicy.excludedMcpServerIds);
|
|
5239
|
+
}
|
|
5240
|
+
if (session.agent) {
|
|
5241
|
+
// The agent configuration is the ceiling of a configured session's
|
|
5242
|
+
// legacy tool columns: a tool-policy edit can narrow within it but
|
|
5243
|
+
// never re-add a tool of a capability the configuration turns off.
|
|
5244
|
+
const clamped = applySessionAgentConfigWriteThrough({
|
|
5245
|
+
config: session.agent,
|
|
5246
|
+
firstPartyMcpTools: nextFirstPartyMcpTools,
|
|
5247
|
+
tools: nextTools,
|
|
5248
|
+
toolPolicy: nextPolicy,
|
|
5249
|
+
productServerIds: [
|
|
5250
|
+
...session.mcpServers.map((server) => server.id),
|
|
5251
|
+
...(explicitRequest ? explicitRequest.tools.map((tool) => tool.id) : []),
|
|
5252
|
+
],
|
|
5253
|
+
});
|
|
5254
|
+
nextFirstPartyMcpTools = clamped.firstPartyMcpTools;
|
|
5255
|
+
nextTools = clamped.tools;
|
|
5256
|
+
nextPolicy = clamped.toolPolicy;
|
|
5257
|
+
}
|
|
5258
|
+
if (agentAttemptCaller && !session.parentSessionId) {
|
|
5259
|
+
if (
|
|
5260
|
+
nextPolicy.firstPartyMode === "workspace_default" &&
|
|
5261
|
+
session.toolPolicy.firstPartyMode !== "workspace_default"
|
|
5262
|
+
) {
|
|
5263
|
+
throw new HTTPException(403, {
|
|
5264
|
+
message: "an agent may not replace a pinned built-in selection with future defaults",
|
|
5265
|
+
});
|
|
5266
|
+
}
|
|
5267
|
+
// A human or API key may widen a top-level session; a live agent
|
|
5268
|
+
// attempt may only narrow relative to the session's CURRENT
|
|
5269
|
+
// effective policy, in either mode. Adopting workspace defaults is a
|
|
5270
|
+
// widen whenever it adds a server or tool the session does not hold.
|
|
5271
|
+
const sessionTracksWorkspaceDefaults = session.toolPolicy?.mode === "workspace_default";
|
|
5272
|
+
const currentEffectiveTools = withFirstPartyTools(
|
|
5273
|
+
sessionTracksWorkspaceDefaults
|
|
5274
|
+
? withWorkspaceDefaultMcpTools(
|
|
5275
|
+
availableToolRefs(session.tools, runtimeSettings),
|
|
5276
|
+
deps.settings,
|
|
5277
|
+
runtimeSettings,
|
|
5278
|
+
workspaceSessionToolDefaults,
|
|
5279
|
+
)
|
|
5280
|
+
: session.tools,
|
|
5281
|
+
runtimeSettings,
|
|
5282
|
+
);
|
|
5283
|
+
const currentAllowedTools = withoutExcludedMcpServers(
|
|
5284
|
+
currentEffectiveTools,
|
|
5285
|
+
session.toolPolicy.excludedMcpServerIds,
|
|
5286
|
+
);
|
|
5287
|
+
const nextEffectiveTools =
|
|
5288
|
+
nextPolicy.mode === "workspace_default"
|
|
5289
|
+
? withFirstPartyTools(
|
|
5290
|
+
withWorkspaceDefaultMcpTools(
|
|
5291
|
+
availableToolRefs(nextTools, runtimeSettings),
|
|
4163
5292
|
deps.settings,
|
|
4164
5293
|
runtimeSettings,
|
|
4165
5294
|
workspaceSessionToolDefaults,
|
|
4166
|
-
)
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
)
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
);
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
)
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
5295
|
+
),
|
|
5296
|
+
runtimeSettings,
|
|
5297
|
+
)
|
|
5298
|
+
: nextTools;
|
|
5299
|
+
if (
|
|
5300
|
+
nextPolicy.mode === "workspace_default" &&
|
|
5301
|
+
(session.toolPolicy.excludedMcpServerIds ?? []).some(
|
|
5302
|
+
(id) => !nextPolicy.excludedMcpServerIds?.includes(id),
|
|
5303
|
+
)
|
|
5304
|
+
) {
|
|
5305
|
+
throw new HTTPException(403, {
|
|
5306
|
+
message: "an agent may not remove session connector exclusions",
|
|
5307
|
+
});
|
|
5308
|
+
}
|
|
5309
|
+
assertToolRefsSubset(
|
|
5310
|
+
withoutExcludedMcpServers(nextEffectiveTools, nextPolicy.excludedMcpServerIds),
|
|
5311
|
+
currentAllowedTools,
|
|
5312
|
+
"an agent may only narrow its session tool policy",
|
|
5313
|
+
);
|
|
5314
|
+
const currentFirstPartyCeiling = effectiveFirstPartyMcpToolCeiling(
|
|
5315
|
+
resolveSessionFirstPartyMcpTools(deps.settings, session, workspace.settings),
|
|
5316
|
+
deploymentFirstPartyMcpToolPolicy,
|
|
5317
|
+
);
|
|
5318
|
+
const widenedFirstPartyTool = nextFirstPartyMcpTools.find(
|
|
5319
|
+
(tool) => !currentFirstPartyCeiling.has(tool),
|
|
5320
|
+
);
|
|
5321
|
+
if (widenedFirstPartyTool) {
|
|
5322
|
+
throw new HTTPException(403, {
|
|
5323
|
+
message: `an agent may only narrow its session Opengeni tools: ${widenedFirstPartyTool}`,
|
|
5324
|
+
});
|
|
4187
5325
|
}
|
|
4188
5326
|
}
|
|
4189
5327
|
|
|
@@ -4248,6 +5386,353 @@ export async function updateSessionToolPolicy(
|
|
|
4248
5386
|
return await requireSession(deps.db, grant.workspaceId, sessionId);
|
|
4249
5387
|
}
|
|
4250
5388
|
|
|
5389
|
+
/**
|
|
5390
|
+
* Replace a running session's agent configuration (`PUT .../agent`). Omitted
|
|
5391
|
+
* request fields keep their current values; a legacy (null) session converts
|
|
5392
|
+
* from its current effective state without widening. The configuration and
|
|
5393
|
+
* its write-through to the legacy tool columns (and the instructions alias)
|
|
5394
|
+
* commit under the same tool-policy version CAS, so tool-policy and agent
|
|
5395
|
+
* updates cannot race. Effective from the next attempt. Agent callers may only
|
|
5396
|
+
* narrow; a child never widens past its parent.
|
|
5397
|
+
*/
|
|
5398
|
+
export async function updateSessionAgent(
|
|
5399
|
+
deps: {
|
|
5400
|
+
db: Database;
|
|
5401
|
+
bus: EventBus;
|
|
5402
|
+
settings: Settings;
|
|
5403
|
+
sessionAuthorization?: SessionAuthorizationPort | null;
|
|
5404
|
+
},
|
|
5405
|
+
grant: AccessGrant,
|
|
5406
|
+
sessionId: string,
|
|
5407
|
+
request: UpdateSessionAgentRequest,
|
|
5408
|
+
): Promise<Session> {
|
|
5409
|
+
await requireSessionAuthorization(deps, grant, {
|
|
5410
|
+
sessionId,
|
|
5411
|
+
operation: "session.tool_policy.write",
|
|
5412
|
+
surface: "core",
|
|
5413
|
+
});
|
|
5414
|
+
requirePermission(grant, "sessions:control");
|
|
5415
|
+
const agentAttemptCaller = grantHasAgentAttemptAuthority(grant);
|
|
5416
|
+
const workspace = await requireWorkspace(deps.db, grant.workspaceId);
|
|
5417
|
+
const goal = await getSessionGoal(deps.db, grant.workspaceId, sessionId);
|
|
5418
|
+
const goalOpen = goal !== null && goal.status !== "completed";
|
|
5419
|
+
const deploymentPolicy = agentConfigDeploymentPolicy(deps.settings);
|
|
5420
|
+
const deploymentFirstPartyMcpToolPolicy = resolveFirstPartyMcpToolPolicy(deps.settings);
|
|
5421
|
+
const workspaceSessionToolDefaults = resolveWorkspaceSessionToolDefaults(workspace.settings);
|
|
5422
|
+
const defaultFirstPartyTools = [
|
|
5423
|
+
...(workspaceSessionToolDefaults?.firstPartyMcpTools?.filter((tool) =>
|
|
5424
|
+
deploymentFirstPartyMcpToolPolicy.allowed.includes(tool),
|
|
5425
|
+
) ?? deploymentFirstPartyMcpToolPolicy.default),
|
|
5426
|
+
];
|
|
5427
|
+
const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
5428
|
+
deps.db,
|
|
5429
|
+
grant.workspaceId,
|
|
5430
|
+
deps.settings,
|
|
5431
|
+
{
|
|
5432
|
+
subjectId: grant.subjectId,
|
|
5433
|
+
},
|
|
5434
|
+
);
|
|
5435
|
+
const runtimeMcpServers = capabilityRuntimeSettings.mcpServers;
|
|
5436
|
+
const runtimeServerIds = new Set(runtimeMcpServers.map((server) => server.id));
|
|
5437
|
+
const workspaceDefaultServerIds = workspaceSessionToolPolicyDefaultServerIdsFor(
|
|
5438
|
+
runtimeMcpServers,
|
|
5439
|
+
workspace.settings,
|
|
5440
|
+
);
|
|
5441
|
+
const events = await appendSessionEventsWithLockedSessionUpdate(
|
|
5442
|
+
deps.db,
|
|
5443
|
+
grant.workspaceId,
|
|
5444
|
+
sessionId,
|
|
5445
|
+
async (session, context) => {
|
|
5446
|
+
const currentVersion = session.toolPolicyVersion ?? 1;
|
|
5447
|
+
if (request.expectedVersion !== currentVersion) {
|
|
5448
|
+
throw new SessionToolPolicyVersionConflictError(currentVersion);
|
|
5449
|
+
}
|
|
5450
|
+
let parentCeiling: ResolvedAgentCapabilities | undefined;
|
|
5451
|
+
let parent: Session | null = null;
|
|
5452
|
+
if (session.parentSessionId) {
|
|
5453
|
+
parent = await context.getLockedSession(session.parentSessionId);
|
|
5454
|
+
if (!parent) {
|
|
5455
|
+
throw new HTTPException(409, { message: "parent session is no longer available" });
|
|
5456
|
+
}
|
|
5457
|
+
parentCeiling =
|
|
5458
|
+
parent.agent?.capabilities ??
|
|
5459
|
+
legacySessionAgentCapabilities(deps.settings, parent, workspace.settings);
|
|
5460
|
+
}
|
|
5461
|
+
const parentTools = parent
|
|
5462
|
+
? sessionToolPolicyCeiling(
|
|
5463
|
+
parent,
|
|
5464
|
+
deps.settings,
|
|
5465
|
+
settingsWithSessionMcpServerMetadata(capabilityRuntimeSettings, session.mcpServers),
|
|
5466
|
+
workspaceSessionToolDefaults,
|
|
5467
|
+
)
|
|
5468
|
+
: null;
|
|
5469
|
+
const legacyCeiling = legacySessionAgentCapabilities(
|
|
5470
|
+
deps.settings,
|
|
5471
|
+
session,
|
|
5472
|
+
workspace.settings,
|
|
5473
|
+
workspaceDefaultServerIds,
|
|
5474
|
+
);
|
|
5475
|
+
const previousCapabilities = session.agent?.capabilities ?? legacyCeiling;
|
|
5476
|
+
const next = withAgentConfigHttpErrors(() =>
|
|
5477
|
+
resolveAgentConfigUpdate({
|
|
5478
|
+
current: session.agent,
|
|
5479
|
+
legacyCeiling,
|
|
5480
|
+
request: request.agent,
|
|
5481
|
+
deployment: deploymentPolicy,
|
|
5482
|
+
onlyNarrow: agentAttemptCaller,
|
|
5483
|
+
...(parentCeiling ? { parentCeiling } : {}),
|
|
5484
|
+
goal: goalOpen,
|
|
5485
|
+
}),
|
|
5486
|
+
);
|
|
5487
|
+
const currentFirstPartyMcpTools = resolveSessionFirstPartyMcpTools(
|
|
5488
|
+
deps.settings,
|
|
5489
|
+
session,
|
|
5490
|
+
workspace.settings,
|
|
5491
|
+
);
|
|
5492
|
+
const added = agentAttemptCaller
|
|
5493
|
+
? []
|
|
5494
|
+
: agentConfigAddedFirstPartyMcpTools(
|
|
5495
|
+
previousCapabilities,
|
|
5496
|
+
next,
|
|
5497
|
+
parentTools?.firstPartyMcpTools ?? defaultFirstPartyTools,
|
|
5498
|
+
);
|
|
5499
|
+
let tools = [...session.tools];
|
|
5500
|
+
let toolPolicy: SessionToolPolicy = { ...session.toolPolicy };
|
|
5501
|
+
if (!agentAttemptCaller) {
|
|
5502
|
+
// Re-enabled built-ins come back; re-enabled connectors track the
|
|
5503
|
+
// workspace defaults again for a top-level session.
|
|
5504
|
+
const reEnabled = (id: "workspaceFiles" | "knowledge" | "workspaceConnectors") =>
|
|
5505
|
+
!previousCapabilities[id] && next.capabilities[id];
|
|
5506
|
+
for (const [capability, serverId] of [
|
|
5507
|
+
["workspaceFiles", "files"],
|
|
5508
|
+
["knowledge", "docs"],
|
|
5509
|
+
] as const) {
|
|
5510
|
+
if (!reEnabled(capability)) continue;
|
|
5511
|
+
if (toolPolicy.excludedMcpServerIds?.includes(serverId)) {
|
|
5512
|
+
const excluded = toolPolicy.excludedMcpServerIds.filter((id) => id !== serverId);
|
|
5513
|
+
const { excludedMcpServerIds: _removed, ...rest } = toolPolicy;
|
|
5514
|
+
toolPolicy = excluded.length ? { ...rest, excludedMcpServerIds: excluded } : rest;
|
|
5515
|
+
} else if (
|
|
5516
|
+
toolPolicy.mode !== "workspace_default" &&
|
|
5517
|
+
runtimeServerIds.has(serverId) &&
|
|
5518
|
+
!tools.some((tool) => tool.id === serverId)
|
|
5519
|
+
) {
|
|
5520
|
+
tools.push({ kind: "mcp", id: serverId, optional: true });
|
|
5521
|
+
}
|
|
5522
|
+
const parentTool = parentTools?.tools.find((tool) => tool.id === serverId);
|
|
5523
|
+
if (parentTool && !tools.some((tool) => tool.id === serverId)) tools.push(parentTool);
|
|
5524
|
+
}
|
|
5525
|
+
if (
|
|
5526
|
+
reEnabled("workspaceConnectors") &&
|
|
5527
|
+
!session.parentSessionId &&
|
|
5528
|
+
toolPolicy.mode === "explicit"
|
|
5529
|
+
) {
|
|
5530
|
+
toolPolicy = { ...toolPolicy, mode: "workspace_default", inheritedFromSessionId: null };
|
|
5531
|
+
}
|
|
5532
|
+
}
|
|
5533
|
+
let firstPartyMcpTools = [...new Set([...currentFirstPartyMcpTools, ...added])];
|
|
5534
|
+
if (parent && parentTools) {
|
|
5535
|
+
// Family booleans are not exact tool authority: re-enabling a family
|
|
5536
|
+
// restores only selections the locked parent actually permits.
|
|
5537
|
+
const runtimeSettings = settingsWithSessionMcpServerMetadata(
|
|
5538
|
+
capabilityRuntimeSettings,
|
|
5539
|
+
session.mcpServers,
|
|
5540
|
+
);
|
|
5541
|
+
const parentFirstPartyTools = new Set(parentTools.firstPartyMcpTools);
|
|
5542
|
+
firstPartyMcpTools = firstPartyMcpTools.filter((tool) => parentFirstPartyTools.has(tool));
|
|
5543
|
+
if (toolPolicy.mode === "workspace_default") {
|
|
5544
|
+
tools = withoutExcludedMcpServers(
|
|
5545
|
+
withFirstPartyTools(
|
|
5546
|
+
withWorkspaceDefaultMcpTools(
|
|
5547
|
+
availableToolRefs(tools, runtimeSettings),
|
|
5548
|
+
deps.settings,
|
|
5549
|
+
runtimeSettings,
|
|
5550
|
+
workspaceSessionToolDefaults,
|
|
5551
|
+
),
|
|
5552
|
+
runtimeSettings,
|
|
5553
|
+
),
|
|
5554
|
+
toolPolicy.excludedMcpServerIds,
|
|
5555
|
+
);
|
|
5556
|
+
if (parent.toolPolicy.mode !== "workspace_default") {
|
|
5557
|
+
// An explicit parent cannot authorize future workspace additions.
|
|
5558
|
+
toolPolicy = { mode: "inherited", inheritedFromSessionId: parent.id };
|
|
5559
|
+
} else {
|
|
5560
|
+
toolPolicy = {
|
|
5561
|
+
...toolPolicy,
|
|
5562
|
+
...defaultPolicyExclusions([
|
|
5563
|
+
...(parent.toolPolicy.excludedMcpServerIds ?? []),
|
|
5564
|
+
...(toolPolicy.excludedMcpServerIds ?? []),
|
|
5565
|
+
]),
|
|
5566
|
+
};
|
|
5567
|
+
}
|
|
5568
|
+
}
|
|
5569
|
+
const parentToolIds = new Set(parentTools.tools.map((tool) => `${tool.kind}:${tool.id}`));
|
|
5570
|
+
tools = tools.filter((tool) => parentToolIds.has(`${tool.kind}:${tool.id}`));
|
|
5571
|
+
}
|
|
5572
|
+
const writeThrough = applySessionAgentConfigWriteThrough({
|
|
5573
|
+
config: next,
|
|
5574
|
+
firstPartyMcpTools,
|
|
5575
|
+
tools,
|
|
5576
|
+
toolPolicy,
|
|
5577
|
+
productServerIds: [
|
|
5578
|
+
...session.mcpServers.map((server) => server.id),
|
|
5579
|
+
...(session.toolPolicy.mode !== "workspace_default"
|
|
5580
|
+
? session.tools.map((tool) => tool.id)
|
|
5581
|
+
: []),
|
|
5582
|
+
],
|
|
5583
|
+
});
|
|
5584
|
+
tools = writeThrough.tools;
|
|
5585
|
+
toolPolicy = writeThrough.toolPolicy;
|
|
5586
|
+
const nextInstructions =
|
|
5587
|
+
request.agent.instructions !== undefined
|
|
5588
|
+
? request.agent.instructions
|
|
5589
|
+
: (session.instructions ?? null);
|
|
5590
|
+
if (goalOpen && !writeThrough.firstPartyMcpTools.includes("goal_complete")) {
|
|
5591
|
+
throw new HTTPException(422, {
|
|
5592
|
+
message: "a session with an open goal must keep its goal tools",
|
|
5593
|
+
});
|
|
5594
|
+
}
|
|
5595
|
+
const unchanged =
|
|
5596
|
+
stableJson({
|
|
5597
|
+
agent: session.agent,
|
|
5598
|
+
instructions: session.instructions ?? null,
|
|
5599
|
+
firstPartyMcpTools: currentFirstPartyMcpTools,
|
|
5600
|
+
tools: session.tools,
|
|
5601
|
+
policy: session.toolPolicy,
|
|
5602
|
+
}) ===
|
|
5603
|
+
stableJson({
|
|
5604
|
+
agent: next,
|
|
5605
|
+
instructions: nextInstructions,
|
|
5606
|
+
firstPartyMcpTools: writeThrough.firstPartyMcpTools,
|
|
5607
|
+
tools,
|
|
5608
|
+
policy: toolPolicy,
|
|
5609
|
+
});
|
|
5610
|
+
if (unchanged) return { events: [] };
|
|
5611
|
+
const nextVersion = currentVersion + 1;
|
|
5612
|
+
return {
|
|
5613
|
+
events: [
|
|
5614
|
+
{
|
|
5615
|
+
type: "session.agent.updated" as const,
|
|
5616
|
+
payload: {
|
|
5617
|
+
before: session.agent,
|
|
5618
|
+
after: next,
|
|
5619
|
+
instructionsChanged: nextInstructions !== (session.instructions ?? null),
|
|
5620
|
+
toolPolicy: {
|
|
5621
|
+
before: toolPolicyAuditSnapshot(
|
|
5622
|
+
session,
|
|
5623
|
+
session.tools,
|
|
5624
|
+
currentFirstPartyMcpTools,
|
|
5625
|
+
session.toolPolicy,
|
|
5626
|
+
),
|
|
5627
|
+
after: toolPolicyAuditSnapshot(
|
|
5628
|
+
session,
|
|
5629
|
+
tools,
|
|
5630
|
+
writeThrough.firstPartyMcpTools,
|
|
5631
|
+
toolPolicy,
|
|
5632
|
+
),
|
|
5633
|
+
},
|
|
5634
|
+
version: nextVersion,
|
|
5635
|
+
effectiveFrom: "next_attempt",
|
|
5636
|
+
},
|
|
5637
|
+
},
|
|
5638
|
+
],
|
|
5639
|
+
update: {
|
|
5640
|
+
agentConfig: next,
|
|
5641
|
+
...(nextInstructions !== (session.instructions ?? null)
|
|
5642
|
+
? { instructions: nextInstructions }
|
|
5643
|
+
: {}),
|
|
5644
|
+
tools,
|
|
5645
|
+
firstPartyMcpTools: writeThrough.firstPartyMcpTools,
|
|
5646
|
+
toolPolicy,
|
|
5647
|
+
toolPolicyVersion: nextVersion,
|
|
5648
|
+
expectedToolPolicyVersion: request.expectedVersion,
|
|
5649
|
+
},
|
|
5650
|
+
};
|
|
5651
|
+
},
|
|
5652
|
+
{ activity: "semantic", lockParentSession: true },
|
|
5653
|
+
);
|
|
5654
|
+
if (events.length > 0) {
|
|
5655
|
+
await publishDurableSessionEvents(deps.bus, grant.workspaceId, sessionId, events);
|
|
5656
|
+
}
|
|
5657
|
+
return await requireSession(deps.db, grant.workspaceId, sessionId);
|
|
5658
|
+
}
|
|
5659
|
+
|
|
5660
|
+
/**
|
|
5661
|
+
* Replace the Skills a session carries itself. The worker reads them at the
|
|
5662
|
+
* start of every attempt, so the change applies from the next attempt; work
|
|
5663
|
+
* already running keeps the Skill index it started with. Shares the
|
|
5664
|
+
* tool-policy version (409 when stale) with the other agent-configuration
|
|
5665
|
+
* writes. An agent may only remove Skills, never add or change one.
|
|
5666
|
+
*/
|
|
5667
|
+
export async function updateSessionSkills(
|
|
5668
|
+
deps: {
|
|
5669
|
+
db: Database;
|
|
5670
|
+
bus: EventBus;
|
|
5671
|
+
sessionAuthorization?: SessionAuthorizationPort | null;
|
|
5672
|
+
},
|
|
5673
|
+
grant: AccessGrant,
|
|
5674
|
+
sessionId: string,
|
|
5675
|
+
request: UpdateSessionSkillsRequest,
|
|
5676
|
+
): Promise<Session> {
|
|
5677
|
+
await requireSessionAuthorization(deps, grant, {
|
|
5678
|
+
sessionId,
|
|
5679
|
+
operation: "session.tool_policy.write",
|
|
5680
|
+
surface: "core",
|
|
5681
|
+
});
|
|
5682
|
+
requirePermission(grant, "sessions:control");
|
|
5683
|
+
let skills: SessionSkill[];
|
|
5684
|
+
try {
|
|
5685
|
+
skills = SessionSkills.parse(request.skills);
|
|
5686
|
+
} catch (error) {
|
|
5687
|
+
throw new HTTPException(422, {
|
|
5688
|
+
message: error instanceof Error ? error.message : "invalid session Skill selection",
|
|
5689
|
+
});
|
|
5690
|
+
}
|
|
5691
|
+
const agentAttemptCaller = grantHasAgentAttemptAuthority(grant);
|
|
5692
|
+
const events = await appendSessionEventsWithLockedSessionUpdate(
|
|
5693
|
+
deps.db,
|
|
5694
|
+
grant.workspaceId,
|
|
5695
|
+
sessionId,
|
|
5696
|
+
(session) => {
|
|
5697
|
+
const currentVersion = session.toolPolicyVersion ?? 1;
|
|
5698
|
+
if (request.expectedVersion !== currentVersion) {
|
|
5699
|
+
throw new SessionToolPolicyVersionConflictError(currentVersion);
|
|
5700
|
+
}
|
|
5701
|
+
const current = new Set(session.skills.map((skill) => stableJson(skill)));
|
|
5702
|
+
if (agentAttemptCaller && skills.some((skill) => !current.has(stableJson(skill)))) {
|
|
5703
|
+
throw new HTTPException(403, {
|
|
5704
|
+
message: "An agent can only remove Skills from a session.",
|
|
5705
|
+
});
|
|
5706
|
+
}
|
|
5707
|
+
if (stableJson(session.skills) === stableJson(skills)) return { events: [] };
|
|
5708
|
+
const nextVersion = currentVersion + 1;
|
|
5709
|
+
return {
|
|
5710
|
+
events: [
|
|
5711
|
+
{
|
|
5712
|
+
type: "session.skills.updated" as const,
|
|
5713
|
+
payload: {
|
|
5714
|
+
before: session.skills.map((skill) => skill.name),
|
|
5715
|
+
after: skills.map((skill) => skill.name),
|
|
5716
|
+
version: nextVersion,
|
|
5717
|
+
effectiveFrom: "next_attempt",
|
|
5718
|
+
},
|
|
5719
|
+
},
|
|
5720
|
+
],
|
|
5721
|
+
update: {
|
|
5722
|
+
skills,
|
|
5723
|
+
toolPolicyVersion: nextVersion,
|
|
5724
|
+
expectedToolPolicyVersion: request.expectedVersion,
|
|
5725
|
+
},
|
|
5726
|
+
};
|
|
5727
|
+
},
|
|
5728
|
+
{ activity: "semantic" },
|
|
5729
|
+
);
|
|
5730
|
+
if (events.length > 0) {
|
|
5731
|
+
await publishDurableSessionEvents(deps.bus, grant.workspaceId, sessionId, events);
|
|
5732
|
+
}
|
|
5733
|
+
return await requireSession(deps.db, grant.workspaceId, sessionId);
|
|
5734
|
+
}
|
|
5735
|
+
|
|
4251
5736
|
export async function readSessionLineage(
|
|
4252
5737
|
deps: Pick<ApiRouteDeps, "db" | "sessionAuthorization">,
|
|
4253
5738
|
grant: AccessGrant,
|
|
@@ -4272,6 +5757,30 @@ export async function readSessionLineage(
|
|
|
4272
5757
|
return lineage;
|
|
4273
5758
|
}
|
|
4274
5759
|
|
|
5760
|
+
/**
|
|
5761
|
+
* A server attached in the same root create request is selected by that
|
|
5762
|
+
* attachment. Neither `tools` (an allow-list over the workspace/deployment
|
|
5763
|
+
* registry) nor the workspace-default expansion can name it, so without this
|
|
5764
|
+
* the attached endpoint was stored but never contacted and the model saw no
|
|
5765
|
+
* tools. This grants nothing beyond the attach itself: the caller already
|
|
5766
|
+
* passed `mcp_servers:attach` for exactly this endpoint, and the server's own
|
|
5767
|
+
* approval policy still governs every call. An explicit ref for the same id
|
|
5768
|
+
* wins unchanged so a caller can still mark it `eager` or `optional`; the
|
|
5769
|
+
* added ref is strict, exactly like listing `{ kind: "mcp", id }` explicitly.
|
|
5770
|
+
* Child creates never reach here: an inherited snapshot keeps the parent's
|
|
5771
|
+
* selection and a child may only narrow it.
|
|
5772
|
+
*/
|
|
5773
|
+
export function withSameRequestSessionMcpServerTools(
|
|
5774
|
+
tools: ToolRef[],
|
|
5775
|
+
attachedServers: ReadonlyArray<{ id: string }>,
|
|
5776
|
+
): ToolRef[] {
|
|
5777
|
+
const selectedIds = new Set(tools.map((tool) => tool.id));
|
|
5778
|
+
const missing = attachedServers
|
|
5779
|
+
.filter((server) => !selectedIds.has(server.id))
|
|
5780
|
+
.map((server) => ({ kind: "mcp" as const, id: server.id }));
|
|
5781
|
+
return missing.length === 0 ? tools : mergeToolRefs(tools, missing);
|
|
5782
|
+
}
|
|
5783
|
+
|
|
4275
5784
|
function withFirstPartyTools(
|
|
4276
5785
|
tools: ToolRef[],
|
|
4277
5786
|
runtimeSettings: { mcpServers: Array<{ id: string }> },
|
|
@@ -4282,6 +5791,34 @@ function withFirstPartyTools(
|
|
|
4282
5791
|
return mergeToolRefs(tools, [{ kind: "mcp", id: "opengeni" }]);
|
|
4283
5792
|
}
|
|
4284
5793
|
|
|
5794
|
+
/**
|
|
5795
|
+
* The executable MCP tool list a follow-up on an existing session freezes
|
|
5796
|
+
* connector accounts against.
|
|
5797
|
+
*
|
|
5798
|
+
* A `workspace_default` session stores only the tool snapshot taken when it
|
|
5799
|
+
* was created and expands the current workspace defaults at run time, so a
|
|
5800
|
+
* connector enabled after creation never enters the stored column. The
|
|
5801
|
+
* composer projects that same expansion and submits account selections for
|
|
5802
|
+
* it, so freezing against the stored column rejected every such selection as
|
|
5803
|
+
* unmatched and never froze its personal delegation. Resolve through the one
|
|
5804
|
+
* ID-only policy resolver the API projection and the worker use, with the
|
|
5805
|
+
* same omitted-tools default, so all three agree on the executable set.
|
|
5806
|
+
*/
|
|
5807
|
+
export function sessionToolsForConnectionAccounts(input: {
|
|
5808
|
+
session: Pick<Session, "tools" | "toolPolicy">;
|
|
5809
|
+
/** The resolved runtime registry, including session-local servers. */
|
|
5810
|
+
runtimeMcpServers: Iterable<{ id: string }>;
|
|
5811
|
+
/** The current omitted-tools default for this workspace. */
|
|
5812
|
+
defaultMcpServerIds: Iterable<string>;
|
|
5813
|
+
}): ToolRef[] {
|
|
5814
|
+
return resolveSessionToolPolicy({
|
|
5815
|
+
toolPolicy: input.session.toolPolicy,
|
|
5816
|
+
sessionTools: input.session.tools,
|
|
5817
|
+
availableMcpServerIds: [...input.runtimeMcpServers].map((server) => server.id),
|
|
5818
|
+
defaultMcpServerIds: input.defaultMcpServerIds,
|
|
5819
|
+
}).toolRefs;
|
|
5820
|
+
}
|
|
5821
|
+
|
|
4285
5822
|
function hasOwnProperty(value: unknown, key: string): boolean {
|
|
4286
5823
|
return Boolean(
|
|
4287
5824
|
value && typeof value === "object" && Object.prototype.hasOwnProperty.call(value, key),
|