@opengeni/core 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +64 -5
- package/dist/index.js +343 -35
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
- package/src/application/session-commands.ts +2 -0
- package/src/dependencies.ts +2 -0
- package/src/domain/resources.ts +32 -1
- package/src/domain/scheduled-tasks.ts +4 -8
- package/src/domain/session-tool-policy.ts +211 -0
- package/src/domain/sessions.ts +255 -35
- package/src/index.ts +1 -0
package/src/domain/sessions.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { CODEX_MODEL_ID_PREFIX } from "@opengeni/codex";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
canonicalizeConfiguredModelId,
|
|
4
|
+
configuredAllowedModels,
|
|
5
|
+
policyProviderIdForModel,
|
|
6
|
+
resolveTurnExecutionPolicyV1,
|
|
7
|
+
type Settings,
|
|
8
|
+
} from "@opengeni/config";
|
|
3
9
|
import {
|
|
4
10
|
CreateSessionRequest,
|
|
11
|
+
DEFAULT_FIRST_PARTY_MCP_PERMISSIONS,
|
|
5
12
|
ServiceTurnInitiator,
|
|
6
13
|
ServiceTurnInitiatorContext,
|
|
7
14
|
evaluateWorkspaceModelPolicy,
|
|
@@ -14,14 +21,18 @@ import {
|
|
|
14
21
|
type ResourceRef,
|
|
15
22
|
type Session,
|
|
16
23
|
type SessionEvent,
|
|
24
|
+
SessionMcpApprovalPolicy,
|
|
17
25
|
type SessionMcpCredentialUpdateInput,
|
|
18
26
|
type SessionMcpServerInput,
|
|
19
27
|
type SessionMcpServerMetadata,
|
|
28
|
+
type UpdateSessionMcpApprovalPolicyResponse,
|
|
20
29
|
type SessionAuthorizationPort,
|
|
30
|
+
type SessionToolPolicy,
|
|
21
31
|
type SessionTurn,
|
|
22
32
|
type ToolRef,
|
|
23
33
|
type TurnInitiator,
|
|
24
34
|
type TurnInitiatorContext,
|
|
35
|
+
type TurnExecutionPolicyV1,
|
|
25
36
|
} from "@opengeni/contracts";
|
|
26
37
|
import {
|
|
27
38
|
createSession,
|
|
@@ -47,6 +58,7 @@ import {
|
|
|
47
58
|
listSessionMcpServersForChildInheritance,
|
|
48
59
|
requireSession,
|
|
49
60
|
submitHumanPromptInTransaction,
|
|
61
|
+
appendSessionEventsWithLockedSessionUpdate,
|
|
50
62
|
updateSessionTitle as updateSessionTitleRow,
|
|
51
63
|
withWorkspaceSubjectRls,
|
|
52
64
|
type CreateSessionMcpServerInput,
|
|
@@ -76,11 +88,14 @@ import { swapActiveSandbox, type FleetContext } from "../sandbox/fleet";
|
|
|
76
88
|
import { settingsWithEnabledCapabilityMcpServers } from "./capabilities";
|
|
77
89
|
import { requireVariableSetEncryption, validateVariableSetAttachment } from "./environments";
|
|
78
90
|
import {
|
|
91
|
+
assertToolRefsSubset,
|
|
92
|
+
availableToolRefs,
|
|
79
93
|
mergeToolRefs,
|
|
80
94
|
normalizeResources,
|
|
81
95
|
validateFileResources,
|
|
82
96
|
validateGitHubRepositorySelection,
|
|
83
97
|
validateToolRefs,
|
|
98
|
+
validateToolRefsForSessionPolicy,
|
|
84
99
|
withDefaultEnabledCapabilityMcpTools,
|
|
85
100
|
} from "./resources";
|
|
86
101
|
|
|
@@ -265,6 +280,7 @@ function mcpServerConfigFromMetadata(
|
|
|
265
280
|
...(server.name ? { name: server.name } : {}),
|
|
266
281
|
url: server.url,
|
|
267
282
|
cacheToolsList: false,
|
|
283
|
+
requireApproval: server.requireApproval,
|
|
268
284
|
...(server.connectionRef ? { connectionRef: server.connectionRef } : {}),
|
|
269
285
|
};
|
|
270
286
|
}
|
|
@@ -340,6 +356,7 @@ function validateSessionMcpServersForCreate(
|
|
|
340
356
|
url: server.url,
|
|
341
357
|
headerNames: Object.keys(headersEncrypted).sort(),
|
|
342
358
|
credentialVersion: 1,
|
|
359
|
+
requireApproval: server.requireApproval ?? false,
|
|
343
360
|
connectionRef: server.connectionRef ?? null,
|
|
344
361
|
});
|
|
345
362
|
}
|
|
@@ -383,6 +400,7 @@ function validateInheritedSessionMcpServersForCreate(
|
|
|
383
400
|
url: server.url,
|
|
384
401
|
headerNames: Object.keys(server.headersEncrypted ?? {}).sort(),
|
|
385
402
|
credentialVersion: 1,
|
|
403
|
+
requireApproval: server.requireApproval ?? false,
|
|
386
404
|
connectionRef: server.connectionRef ?? null,
|
|
387
405
|
})),
|
|
388
406
|
};
|
|
@@ -436,9 +454,14 @@ export async function createAndStartSession(input: {
|
|
|
436
454
|
turnInstructions?: string | null;
|
|
437
455
|
resources: ResourceRef[];
|
|
438
456
|
tools: ToolRef[];
|
|
457
|
+
// Public admission always supplies provenance; optional keeps internal
|
|
458
|
+
// callers that predate durable tool-policy provenance source-compatible
|
|
459
|
+
// during the rolling deploy.
|
|
460
|
+
toolPolicy?: SessionToolPolicy;
|
|
439
461
|
clientEventId?: string;
|
|
440
462
|
model: string;
|
|
441
463
|
reasoningEffort: Settings["openaiReasoningEffort"];
|
|
464
|
+
turnExecutionPolicy: TurnExecutionPolicyV1;
|
|
442
465
|
sandboxBackend: Settings["sandboxBackend"];
|
|
443
466
|
metadata: Record<string, unknown>;
|
|
444
467
|
createdBy?: TurnInitiator;
|
|
@@ -527,6 +550,7 @@ export async function createAndStartSession(input: {
|
|
|
527
550
|
initialTurnInstructions: input.turnInstructions ?? null,
|
|
528
551
|
resources: input.resources,
|
|
529
552
|
tools: input.tools,
|
|
553
|
+
...(input.toolPolicy ? { toolPolicy: input.toolPolicy } : {}),
|
|
530
554
|
metadata: sessionMetadata,
|
|
531
555
|
...(input.createdBy ? { createdBy: input.createdBy } : {}),
|
|
532
556
|
...(input.createdByContext ? { createdByContext: input.createdByContext } : {}),
|
|
@@ -560,6 +584,7 @@ export async function createAndStartSession(input: {
|
|
|
560
584
|
initialTurnInstructions: input.turnInstructions ?? null,
|
|
561
585
|
resources: input.resources,
|
|
562
586
|
tools: input.tools,
|
|
587
|
+
...(input.toolPolicy ? { toolPolicy: input.toolPolicy } : {}),
|
|
563
588
|
metadata: sessionMetadata,
|
|
564
589
|
...(input.createdBy ? { createdBy: input.createdBy } : {}),
|
|
565
590
|
...(input.createdByContext ? { createdByContext: input.createdByContext } : {}),
|
|
@@ -594,9 +619,11 @@ async function finishStartSession(
|
|
|
594
619
|
turnInstructions?: string | null;
|
|
595
620
|
resources: ResourceRef[];
|
|
596
621
|
tools: ToolRef[];
|
|
622
|
+
toolPolicy?: SessionToolPolicy;
|
|
597
623
|
clientEventId?: string;
|
|
598
624
|
model: string;
|
|
599
625
|
reasoningEffort: Settings["openaiReasoningEffort"];
|
|
626
|
+
turnExecutionPolicy: TurnExecutionPolicyV1;
|
|
600
627
|
sandboxBackend: Settings["sandboxBackend"];
|
|
601
628
|
variableSet?: { id: string; name: string } | null;
|
|
602
629
|
goal?: GoalSpec | null;
|
|
@@ -647,7 +674,9 @@ async function finishStartSession(
|
|
|
647
674
|
sessionId: session.id,
|
|
648
675
|
...(input.clientEventId ? { clientEventId: input.clientEventId } : {}),
|
|
649
676
|
reasoningEffortFallback: input.reasoningEffort,
|
|
677
|
+
turnExecutionPolicy: input.turnExecutionPolicy,
|
|
650
678
|
createdEventPayload: {
|
|
679
|
+
...(input.toolPolicy ? { toolPolicy: input.toolPolicy } : {}),
|
|
651
680
|
...(input.variableSet
|
|
652
681
|
? { variableSetId: input.variableSet.id, variableSetName: input.variableSet.name }
|
|
653
682
|
: {}),
|
|
@@ -703,12 +732,16 @@ export function workflowIdForSession(sessionId: string): string {
|
|
|
703
732
|
* later) and the MCP surfaces that share them validate identically and cannot
|
|
704
733
|
* drift.
|
|
705
734
|
*/
|
|
706
|
-
export function
|
|
735
|
+
export function canonicalConfiguredModel(
|
|
736
|
+
settings: Settings,
|
|
737
|
+
model: string | null | undefined,
|
|
738
|
+
): string | null | undefined {
|
|
707
739
|
if (model === null || model === undefined) {
|
|
708
|
-
return;
|
|
740
|
+
return model;
|
|
709
741
|
}
|
|
710
|
-
|
|
711
|
-
|
|
742
|
+
const canonicalModel = canonicalizeConfiguredModelId(settings, model);
|
|
743
|
+
if (configuredAllowedModels(settings).includes(canonicalModel)) {
|
|
744
|
+
return canonicalModel;
|
|
712
745
|
}
|
|
713
746
|
// Codex subscription models (codex/<slug>) are injected per-workspace by the
|
|
714
747
|
// worker overlay at turn time, so they are never in the deployment-global
|
|
@@ -716,12 +749,16 @@ export function assertConfiguredModel(settings: Settings, model: string | null |
|
|
|
716
749
|
// only surfaces them for a connected workspace, and the worker enforces the
|
|
717
750
|
// actual connection (an unconnected workspace fails the turn with a clear
|
|
718
751
|
// "no Codex subscription connected" error rather than a misleading 422 here).
|
|
719
|
-
if (settings.codexSubscriptionEnabled &&
|
|
720
|
-
return;
|
|
752
|
+
if (settings.codexSubscriptionEnabled && canonicalModel.startsWith(CODEX_MODEL_ID_PREFIX)) {
|
|
753
|
+
return canonicalModel;
|
|
721
754
|
}
|
|
722
755
|
throw new HTTPException(422, { message: `model is not available: ${model}` });
|
|
723
756
|
}
|
|
724
757
|
|
|
758
|
+
export function assertConfiguredModel(settings: Settings, model: string | null | undefined): void {
|
|
759
|
+
canonicalConfiguredModel(settings, model);
|
|
760
|
+
}
|
|
761
|
+
|
|
725
762
|
/**
|
|
726
763
|
* Reject a model the WORKSPACE's model policy blocks, at the same choke points
|
|
727
764
|
* as assertConfiguredModel — a 422 at the edge instead of a queued turn the
|
|
@@ -742,18 +779,25 @@ export async function assertWorkspaceModelPolicyAllows(
|
|
|
742
779
|
if (model === null || model === undefined) {
|
|
743
780
|
return;
|
|
744
781
|
}
|
|
782
|
+
const canonicalModel = canonicalConfiguredModel(settings, model);
|
|
783
|
+
if (canonicalModel === null || canonicalModel === undefined) {
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
745
786
|
const policy = await getWorkspaceModelPolicy(db, workspaceId);
|
|
746
787
|
if (!policy) {
|
|
747
788
|
return;
|
|
748
789
|
}
|
|
749
|
-
const providerId = policyProviderIdForModel(settings,
|
|
750
|
-
const verdict = evaluateWorkspaceModelPolicy(policy, {
|
|
790
|
+
const providerId = policyProviderIdForModel(settings, canonicalModel);
|
|
791
|
+
const verdict = evaluateWorkspaceModelPolicy(policy, {
|
|
792
|
+
providerId,
|
|
793
|
+
modelId: canonicalModel,
|
|
794
|
+
});
|
|
751
795
|
if (!verdict.allowed) {
|
|
752
796
|
throw new HTTPException(422, {
|
|
753
797
|
message:
|
|
754
798
|
verdict.reason === "provider"
|
|
755
|
-
? `model "${
|
|
756
|
-
: `model "${
|
|
799
|
+
? `model "${canonicalModel}" is not allowed by this workspace's model policy: provider "${providerId}" is not in the allowed providers`
|
|
800
|
+
: `model "${canonicalModel}" is not allowed by this workspace's model policy`,
|
|
757
801
|
});
|
|
758
802
|
}
|
|
759
803
|
}
|
|
@@ -802,6 +846,7 @@ export async function postUserMessageTurn(input: {
|
|
|
802
846
|
turnInstructions?: string | null;
|
|
803
847
|
resources: ResourceRef[];
|
|
804
848
|
tools: ToolRef[];
|
|
849
|
+
toolsProvided: boolean;
|
|
805
850
|
model?: string | null;
|
|
806
851
|
reasoningEffort?: Settings["openaiReasoningEffort"] | null;
|
|
807
852
|
clientEventId?: string;
|
|
@@ -813,9 +858,11 @@ export async function postUserMessageTurn(input: {
|
|
|
813
858
|
commandActor?: SessionCommandActor;
|
|
814
859
|
controlEtag?: string | null;
|
|
815
860
|
expectedDraftRevision?: number | null;
|
|
861
|
+
reasoningEffortFallback?: Settings["openaiReasoningEffort"];
|
|
862
|
+
turnExecutionPolicy: TurnExecutionPolicyV1;
|
|
816
863
|
}): Promise<{ accepted: SessionEvent; turn: SessionTurn }> {
|
|
817
864
|
const { db, bus, workflowClient, settings, accountId, workspaceId, sessionId } = input;
|
|
818
|
-
const requestedModel = input.model ?? null;
|
|
865
|
+
const requestedModel = canonicalConfiguredModel(settings, input.model ?? null) ?? null;
|
|
819
866
|
const requestedReasoningEffort = input.reasoningEffort ?? null;
|
|
820
867
|
// Reject an explicit per-message model the host does not expose; an omitted
|
|
821
868
|
// model inherits the session's model downstream (always a configured id).
|
|
@@ -844,9 +891,11 @@ export async function postUserMessageTurn(input: {
|
|
|
844
891
|
turnInstructions: input.turnInstructions ?? null,
|
|
845
892
|
resources: input.resources,
|
|
846
893
|
tools: input.tools,
|
|
894
|
+
toolsProvided: input.toolsProvided,
|
|
847
895
|
model: requestedModel,
|
|
848
896
|
reasoningEffort: requestedReasoningEffort,
|
|
849
|
-
reasoningEffortFallback: settings.openaiReasoningEffort,
|
|
897
|
+
reasoningEffortFallback: input.reasoningEffortFallback ?? settings.openaiReasoningEffort,
|
|
898
|
+
turnExecutionPolicy: input.turnExecutionPolicy,
|
|
850
899
|
source: input.origin === "operator" ? "api" : "user",
|
|
851
900
|
mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
|
|
852
901
|
}),
|
|
@@ -973,21 +1022,58 @@ export async function createSessionForRequest(
|
|
|
973
1022
|
? payload.resources
|
|
974
1023
|
: (parentSession?.resources ?? payload.resources),
|
|
975
1024
|
);
|
|
1025
|
+
const toolsProvided = hasOwnProperty(rawPayload, "tools");
|
|
976
1026
|
const requestedTools = validateToolRefs(
|
|
977
|
-
|
|
1027
|
+
toolsProvided ? payload.tools : (parentSession?.tools ?? payload.tools),
|
|
978
1028
|
runtimeSettings,
|
|
979
1029
|
);
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1030
|
+
let selectedTools: ToolRef[];
|
|
1031
|
+
let toolPolicy: SessionToolPolicy;
|
|
1032
|
+
if (parentSession) {
|
|
1033
|
+
const parentTracksWorkspaceDefaults = parentSession.toolPolicy?.mode === "workspace_default";
|
|
1034
|
+
const parentEffective = withFirstPartyTools(
|
|
1035
|
+
parentTracksWorkspaceDefaults
|
|
1036
|
+
? withDefaultEnabledCapabilityMcpTools(
|
|
1037
|
+
availableToolRefs(parentSession.tools, runtimeSettings),
|
|
1038
|
+
settings,
|
|
1039
|
+
runtimeSettings,
|
|
1040
|
+
)
|
|
1041
|
+
: parentSession.tools,
|
|
1042
|
+
runtimeSettings,
|
|
1043
|
+
);
|
|
1044
|
+
if (toolsProvided) {
|
|
1045
|
+
assertToolRefsSubset(
|
|
1046
|
+
requestedTools,
|
|
1047
|
+
parentEffective,
|
|
1048
|
+
"child tools may only narrow the parent session tool policy",
|
|
1049
|
+
);
|
|
1050
|
+
selectedTools = requestedTools;
|
|
1051
|
+
toolPolicy = { mode: "explicit", inheritedFromSessionId: parentSession.id };
|
|
1052
|
+
} else {
|
|
1053
|
+
selectedTools = parentEffective;
|
|
1054
|
+
toolPolicy = {
|
|
1055
|
+
mode: parentTracksWorkspaceDefaults ? "workspace_default" : "inherited",
|
|
1056
|
+
inheritedFromSessionId: parentSession.id,
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
} else if (toolsProvided) {
|
|
1060
|
+
selectedTools = requestedTools;
|
|
1061
|
+
toolPolicy = { mode: "explicit", inheritedFromSessionId: null };
|
|
1062
|
+
} else {
|
|
1063
|
+
selectedTools = withDefaultEnabledCapabilityMcpTools(
|
|
1064
|
+
requestedTools,
|
|
1065
|
+
settings,
|
|
1066
|
+
capabilityRuntimeSettings,
|
|
1067
|
+
);
|
|
1068
|
+
toolPolicy = { mode: "workspace_default", inheritedFromSessionId: null };
|
|
1069
|
+
}
|
|
984
1070
|
// The first-party MCP server is attached to EVERY session. It hosts the
|
|
985
1071
|
// session's own metadata tool (set_session_title) + goal tools, and — only
|
|
986
1072
|
// when the grant carries the permission — the orchestration/variableSet/
|
|
987
1073
|
// github tools. Capability is gated per-tool by permission, never by whether
|
|
988
1074
|
// the server is attached, so a bare chat still gets titling while the
|
|
989
1075
|
// dangerous tools stay off by default.
|
|
990
|
-
const tools = withFirstPartyTools(
|
|
1076
|
+
const tools = withFirstPartyTools(selectedTools, runtimeSettings);
|
|
991
1077
|
await validateGitHubRepositorySelection(db, workspaceId, resources);
|
|
992
1078
|
if (resources.some((resource) => resource.kind === "file") && !objectStorage) {
|
|
993
1079
|
throw new HTTPException(503, { message: "object storage is not configured" });
|
|
@@ -1033,19 +1119,23 @@ export async function createSessionForRequest(
|
|
|
1033
1119
|
frozenRigVersionId = rig.activeVersion.id;
|
|
1034
1120
|
}
|
|
1035
1121
|
}
|
|
1036
|
-
|
|
1122
|
+
const model = canonicalConfiguredModel(settings, payload.model ?? settings.openaiModel);
|
|
1123
|
+
if (model === null || model === undefined) {
|
|
1124
|
+
throw new Error("effective session model unexpectedly resolved to null");
|
|
1125
|
+
}
|
|
1037
1126
|
// Session creation persists the EFFECTIVE model — an omitted payload.model
|
|
1038
1127
|
// stamps the deployment default onto the session — so the policy must vet
|
|
1039
1128
|
// that effective value, not just explicit ones (a restricted workspace's
|
|
1040
1129
|
// default-model session would otherwise be born blocked).
|
|
1041
|
-
await assertWorkspaceModelPolicyAllows(
|
|
1042
|
-
db,
|
|
1043
|
-
settings,
|
|
1044
|
-
workspaceId,
|
|
1045
|
-
payload.model ?? settings.openaiModel,
|
|
1046
|
-
);
|
|
1047
|
-
const model = payload.model ?? settings.openaiModel;
|
|
1130
|
+
await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, model);
|
|
1048
1131
|
const reasoningEffort = payload.reasoningEffort ?? settings.openaiReasoningEffort;
|
|
1132
|
+
const turnExecutionPolicy = resolveTurnExecutionPolicyV1(settings, {
|
|
1133
|
+
modelId: model,
|
|
1134
|
+
requestedModelId: payload.model ?? null,
|
|
1135
|
+
modelSource: payload.model === undefined ? "deployment" : "explicit",
|
|
1136
|
+
reasoningEffort,
|
|
1137
|
+
reasoningSource: payload.reasoningEffort === undefined ? "deployment" : "explicit",
|
|
1138
|
+
});
|
|
1049
1139
|
// Parent linkage was resolved above, before context validation. A child with
|
|
1050
1140
|
// no explicit permission override inherits the creating session's effective
|
|
1051
1141
|
// grant instead of silently expanding to standalone worker defaults.
|
|
@@ -1056,8 +1146,30 @@ export async function createSessionForRequest(
|
|
|
1056
1146
|
// normal worker defaults. A child omission inherits its creator's exact
|
|
1057
1147
|
// effective grant, preserving a host/operator's narrowed capability boundary
|
|
1058
1148
|
// through the whole session tree.
|
|
1149
|
+
const parentFirstPartyMcpPermissions = parentSession
|
|
1150
|
+
? [...(parentSession.firstPartyMcpPermissions ?? DEFAULT_FIRST_PARTY_MCP_PERMISSIONS)]
|
|
1151
|
+
: null;
|
|
1152
|
+
if (
|
|
1153
|
+
parentFirstPartyMcpPermissions &&
|
|
1154
|
+
payload.firstPartyMcpPermissions?.some(
|
|
1155
|
+
(permission) => !hasPermission(parentFirstPartyMcpPermissions, permission),
|
|
1156
|
+
)
|
|
1157
|
+
) {
|
|
1158
|
+
throw new HTTPException(403, {
|
|
1159
|
+
message: "child first-party MCP permissions may only narrow the parent session grant",
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
// A worker-signed creator may itself carry less authority than its parent
|
|
1163
|
+
// session (for example a narrowly delegated spawn token). Inherit the
|
|
1164
|
+
// intersection in the shared canonical default order so null/default parent
|
|
1165
|
+
// policies cannot expand when runtime signing resolves them.
|
|
1059
1166
|
let firstPartyMcpPermissions =
|
|
1060
|
-
payload.firstPartyMcpPermissions ??
|
|
1167
|
+
payload.firstPartyMcpPermissions ??
|
|
1168
|
+
(parentFirstPartyMcpPermissions
|
|
1169
|
+
? parentFirstPartyMcpPermissions.filter((permission) =>
|
|
1170
|
+
hasPermission(grant.permissions, permission),
|
|
1171
|
+
)
|
|
1172
|
+
: null);
|
|
1061
1173
|
if (firstPartyMcpPermissions && firstPartyMcpPermissions.length === 0) {
|
|
1062
1174
|
// An empty set would sign an unusable zero-permission token; the default
|
|
1063
1175
|
// worker set is expressed by omitting the field.
|
|
@@ -1322,9 +1434,11 @@ export async function createSessionForRequest(
|
|
|
1322
1434
|
turnInstructions: payload.turnInstructions ?? null,
|
|
1323
1435
|
resources,
|
|
1324
1436
|
tools,
|
|
1437
|
+
toolPolicy,
|
|
1325
1438
|
...(payload.clientEventId ? { clientEventId: payload.clientEventId } : {}),
|
|
1326
1439
|
model,
|
|
1327
1440
|
reasoningEffort,
|
|
1441
|
+
turnExecutionPolicy,
|
|
1328
1442
|
// A shared spawn inherits the box's backend; a caller-supplied
|
|
1329
1443
|
// sandboxBackend on a shared spawn is ignored (it is the same box). A
|
|
1330
1444
|
// machine-targeted top-level create labels the home "selfhosted"
|
|
@@ -1396,8 +1510,9 @@ export async function createSessionForRequest(
|
|
|
1396
1510
|
* Full accept-user-message flow shared by the `user.message` branch of
|
|
1397
1511
|
* `POST /sessions/:id/events` and the first-party MCP `session_send_message`
|
|
1398
1512
|
* tool: resource/tool validation, usage limits, the locked append + turn
|
|
1399
|
-
* enqueue, and usage recording. `toolsProvided: false`
|
|
1400
|
-
*
|
|
1513
|
+
* enqueue, and usage recording. `toolsProvided: false` durably preserves an
|
|
1514
|
+
* absent `tools` key so execution inherits the session policy; an explicit
|
|
1515
|
+
* empty array is a deliberate per-turn narrowing.
|
|
1401
1516
|
*/
|
|
1402
1517
|
export async function acceptSessionUserMessage(
|
|
1403
1518
|
deps: AcceptSessionUserMessageDependencies,
|
|
@@ -1420,6 +1535,12 @@ export async function acceptSessionUserMessage(
|
|
|
1420
1535
|
expectedDraftRevision?: number | null;
|
|
1421
1536
|
},
|
|
1422
1537
|
): Promise<{ accepted: SessionEvent; turn: SessionTurn }> {
|
|
1538
|
+
if (input.toolsProvided && !deps.settings.sessionTurnToolReplacementEnabled) {
|
|
1539
|
+
throw new HTTPException(503, {
|
|
1540
|
+
message:
|
|
1541
|
+
"explicit follow-up tool replacement is temporarily unavailable until provenance-aware turn workers finish rolling out; omit tools to inherit the session policy and retry",
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1423
1544
|
const { settings, db, bus, workflowClient, objectStorage } = deps;
|
|
1424
1545
|
await requireSessionAuthorization(deps, grant, {
|
|
1425
1546
|
sessionId,
|
|
@@ -1435,21 +1556,55 @@ export async function acceptSessionUserMessage(
|
|
|
1435
1556
|
// turn's effective model (a follow-up turn inherits the session's model). A
|
|
1436
1557
|
// pure read with no side effects.
|
|
1437
1558
|
const existingSession = await requireSession(db, workspaceId, sessionId);
|
|
1559
|
+
const requestedModel = canonicalConfiguredModel(settings, input.model ?? null) ?? null;
|
|
1560
|
+
const effectiveModel =
|
|
1561
|
+
canonicalConfiguredModel(settings, requestedModel ?? existingSession.model) ?? null;
|
|
1562
|
+
if (effectiveModel === null) {
|
|
1563
|
+
throw new Error("effective follow-up model unexpectedly resolved to null");
|
|
1564
|
+
}
|
|
1565
|
+
const sessionReasoningEffort = reasoningEffortForSession(
|
|
1566
|
+
existingSession.metadata,
|
|
1567
|
+
settings.openaiReasoningEffort,
|
|
1568
|
+
);
|
|
1569
|
+
const effectiveReasoningEffort = input.reasoningEffort ?? sessionReasoningEffort;
|
|
1570
|
+
const turnExecutionPolicy = resolveTurnExecutionPolicyV1(settings, {
|
|
1571
|
+
modelId: effectiveModel,
|
|
1572
|
+
requestedModelId: input.model ?? null,
|
|
1573
|
+
modelSource: input.model == null ? "session" : "explicit",
|
|
1574
|
+
reasoningEffort: effectiveReasoningEffort,
|
|
1575
|
+
reasoningSource: input.reasoningEffort == null ? "session" : "explicit",
|
|
1576
|
+
});
|
|
1438
1577
|
const runtimeSettings = settingsWithSessionMcpServerMetadata(
|
|
1439
1578
|
capabilityRuntimeSettings,
|
|
1440
1579
|
existingSession.mcpServers,
|
|
1441
1580
|
);
|
|
1442
1581
|
const requestedResources = normalizeResources(input.resources ?? []);
|
|
1443
|
-
const
|
|
1444
|
-
const
|
|
1445
|
-
|
|
1446
|
-
|
|
1582
|
+
const tracksWorkspaceDefaults = existingSession.toolPolicy?.mode === "workspace_default";
|
|
1583
|
+
const sessionPolicyTools = withFirstPartyTools(
|
|
1584
|
+
tracksWorkspaceDefaults
|
|
1585
|
+
? withDefaultEnabledCapabilityMcpTools(
|
|
1586
|
+
availableToolRefs(existingSession.tools, runtimeSettings),
|
|
1587
|
+
settings,
|
|
1588
|
+
capabilityRuntimeSettings,
|
|
1589
|
+
)
|
|
1590
|
+
: existingSession.tools,
|
|
1591
|
+
runtimeSettings,
|
|
1592
|
+
);
|
|
1593
|
+
const validatedTools = input.toolsProvided
|
|
1594
|
+
? validateToolRefsForSessionPolicy({
|
|
1595
|
+
requested: input.tools ?? [],
|
|
1596
|
+
settings: runtimeSettings,
|
|
1597
|
+
allowedTools: sessionPolicyTools,
|
|
1598
|
+
message: "message tools may only narrow the session tool policy",
|
|
1599
|
+
})
|
|
1600
|
+
: [];
|
|
1601
|
+
const requestedTools = input.toolsProvided ? validatedTools : [];
|
|
1447
1602
|
await requireLimit(deps, {
|
|
1448
1603
|
accountId: grant.accountId,
|
|
1449
1604
|
workspaceId,
|
|
1450
1605
|
action: "agent_run:create",
|
|
1451
1606
|
quantity: 1,
|
|
1452
|
-
model:
|
|
1607
|
+
model: effectiveModel,
|
|
1453
1608
|
});
|
|
1454
1609
|
if (requestedResources.some((resource) => resource.kind === "file") && !objectStorage) {
|
|
1455
1610
|
throw new HTTPException(503, { message: "object storage is not configured" });
|
|
@@ -1478,8 +1633,11 @@ export async function acceptSessionUserMessage(
|
|
|
1478
1633
|
turnInstructions: input.turnInstructions ?? null,
|
|
1479
1634
|
resources: requestedResources,
|
|
1480
1635
|
tools: requestedTools,
|
|
1636
|
+
toolsProvided: input.toolsProvided,
|
|
1481
1637
|
model: input.model ?? null,
|
|
1482
1638
|
reasoningEffort: input.reasoningEffort ?? null,
|
|
1639
|
+
reasoningEffortFallback: sessionReasoningEffort,
|
|
1640
|
+
turnExecutionPolicy,
|
|
1483
1641
|
mcpCredentialUpdates,
|
|
1484
1642
|
delivery: input.delivery ?? "send",
|
|
1485
1643
|
origin: delegatedServiceInitiator ? "operator" : (input.origin ?? "human"),
|
|
@@ -1571,6 +1729,68 @@ export async function updateSessionTitle(
|
|
|
1571
1729
|
};
|
|
1572
1730
|
}
|
|
1573
1731
|
|
|
1732
|
+
/**
|
|
1733
|
+
* Update one existing session MCP server's approval policy. The database
|
|
1734
|
+
* serializes this write with attempt claim under the session lock: an already
|
|
1735
|
+
* claimed attempt retains its immutable snapshot, while the next claim captures
|
|
1736
|
+
* this value. No attempt is cancelled, restarted, or reinterpreted.
|
|
1737
|
+
*/
|
|
1738
|
+
export async function updateSessionMcpApprovalPolicy(
|
|
1739
|
+
deps: {
|
|
1740
|
+
db: Database;
|
|
1741
|
+
bus: EventBus;
|
|
1742
|
+
sessionAuthorization?: SessionAuthorizationPort | null;
|
|
1743
|
+
},
|
|
1744
|
+
grant: AccessGrant,
|
|
1745
|
+
sessionId: string,
|
|
1746
|
+
serverId: string,
|
|
1747
|
+
requireApproval: SessionMcpApprovalPolicy,
|
|
1748
|
+
): Promise<UpdateSessionMcpApprovalPolicyResponse> {
|
|
1749
|
+
const normalizedPolicy = SessionMcpApprovalPolicy.parse(requireApproval);
|
|
1750
|
+
await requireSessionAuthorization(deps, grant, {
|
|
1751
|
+
sessionId,
|
|
1752
|
+
operation: "session.mcp.approval_policy.write",
|
|
1753
|
+
surface: "core",
|
|
1754
|
+
});
|
|
1755
|
+
requirePermission(grant, "sessions:control");
|
|
1756
|
+
|
|
1757
|
+
const outcome: { server?: SessionMcpServerMetadata } = {};
|
|
1758
|
+
const events = await appendSessionEventsWithLockedSessionUpdate(
|
|
1759
|
+
deps.db,
|
|
1760
|
+
grant.workspaceId,
|
|
1761
|
+
sessionId,
|
|
1762
|
+
async (_session, context) => {
|
|
1763
|
+
const result = await context.updateSessionMcpApprovalPolicy(serverId, normalizedPolicy);
|
|
1764
|
+
if (!result.server) {
|
|
1765
|
+
throw new HTTPException(404, { message: "session MCP server not found" });
|
|
1766
|
+
}
|
|
1767
|
+
outcome.server = result.server;
|
|
1768
|
+
return {
|
|
1769
|
+
events: result.changed
|
|
1770
|
+
? [
|
|
1771
|
+
{
|
|
1772
|
+
type: "session.mcp.approval_policy.updated" as const,
|
|
1773
|
+
payload: {
|
|
1774
|
+
serverId,
|
|
1775
|
+
effectiveFrom: "next_attempt",
|
|
1776
|
+
},
|
|
1777
|
+
},
|
|
1778
|
+
]
|
|
1779
|
+
: [],
|
|
1780
|
+
};
|
|
1781
|
+
},
|
|
1782
|
+
);
|
|
1783
|
+
const updatedServer = outcome.server;
|
|
1784
|
+
if (!updatedServer) {
|
|
1785
|
+
throw new Error("session MCP approval policy update returned no server");
|
|
1786
|
+
}
|
|
1787
|
+
await publishDurableSessionEvents(deps.bus, grant.workspaceId, sessionId, events);
|
|
1788
|
+
return {
|
|
1789
|
+
server: updatedServer,
|
|
1790
|
+
effectiveFrom: "next_attempt",
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1574
1794
|
export async function readSessionLineage(
|
|
1575
1795
|
deps: Pick<ApiRouteDeps, "db" | "sessionAuthorization">,
|
|
1576
1796
|
grant: AccessGrant,
|
package/src/index.ts
CHANGED
|
@@ -57,6 +57,7 @@ export * from "./domain/environments";
|
|
|
57
57
|
export * from "./rigs";
|
|
58
58
|
export * from "./domain/packs";
|
|
59
59
|
export * from "./domain/resources";
|
|
60
|
+
export * from "./domain/session-tool-policy";
|
|
60
61
|
export * from "./domain/scheduled-tasks";
|
|
61
62
|
export * from "./domain/sessions";
|
|
62
63
|
export * from "./domain/workspace-members";
|