@opengeni/core 0.8.0 → 0.11.1
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 +148 -6
- package/dist/index.js +827 -127
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
- package/src/application/new-session-drafts.ts +126 -0
- 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 +27 -9
- package/src/domain/session-tool-policy.ts +211 -0
- package/src/domain/sessions.ts +369 -84
- package/src/index.ts +2 -0
- package/src/sandbox/fleet.ts +149 -39
- package/src/sandbox/routing.ts +261 -3
package/src/domain/sessions.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
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,
|
|
12
|
+
SessionSpawnDenial,
|
|
5
13
|
ServiceTurnInitiator,
|
|
6
14
|
ServiceTurnInitiatorContext,
|
|
7
15
|
evaluateWorkspaceModelPolicy,
|
|
@@ -14,18 +22,22 @@ import {
|
|
|
14
22
|
type ResourceRef,
|
|
15
23
|
type Session,
|
|
16
24
|
type SessionEvent,
|
|
25
|
+
SessionMcpApprovalPolicy,
|
|
17
26
|
type SessionMcpCredentialUpdateInput,
|
|
18
27
|
type SessionMcpServerInput,
|
|
19
28
|
type SessionMcpServerMetadata,
|
|
29
|
+
type UpdateSessionMcpApprovalPolicyResponse,
|
|
20
30
|
type SessionAuthorizationPort,
|
|
31
|
+
type SessionToolPolicy,
|
|
21
32
|
type SessionTurn,
|
|
22
33
|
type ToolRef,
|
|
23
34
|
type TurnInitiator,
|
|
24
35
|
type TurnInitiatorContext,
|
|
36
|
+
type TurnExecutionPolicyV1,
|
|
25
37
|
} from "@opengeni/contracts";
|
|
26
38
|
import {
|
|
27
39
|
createSession,
|
|
28
|
-
|
|
40
|
+
createSessionWithIdempotencyKeyResult,
|
|
29
41
|
encryptVariableSetValue,
|
|
30
42
|
getAnySessionInGroup,
|
|
31
43
|
getEnrollment,
|
|
@@ -36,7 +48,7 @@ import {
|
|
|
36
48
|
getSandbox,
|
|
37
49
|
getSession,
|
|
38
50
|
SessionIdConflictError,
|
|
39
|
-
|
|
51
|
+
getSessionSpawnDenialByIdempotencyKey,
|
|
40
52
|
getSessionEvent,
|
|
41
53
|
getWorkspaceControlEvent,
|
|
42
54
|
getSessionLineage,
|
|
@@ -47,6 +59,7 @@ import {
|
|
|
47
59
|
listSessionMcpServersForChildInheritance,
|
|
48
60
|
requireSession,
|
|
49
61
|
submitHumanPromptInTransaction,
|
|
62
|
+
appendSessionEventsWithLockedSessionUpdate,
|
|
50
63
|
updateSessionTitle as updateSessionTitleRow,
|
|
51
64
|
withWorkspaceSubjectRls,
|
|
52
65
|
type CreateSessionMcpServerInput,
|
|
@@ -54,6 +67,7 @@ import {
|
|
|
54
67
|
type UpdateSessionMcpServerCredentialsInput,
|
|
55
68
|
QueueCommandConflictError,
|
|
56
69
|
AgentCommandAuthorityError,
|
|
70
|
+
SessionSpawnDeniedDbError,
|
|
57
71
|
SessionControlConflictError,
|
|
58
72
|
type SessionCommandActor,
|
|
59
73
|
} from "@opengeni/db";
|
|
@@ -76,11 +90,14 @@ import { swapActiveSandbox, type FleetContext } from "../sandbox/fleet";
|
|
|
76
90
|
import { settingsWithEnabledCapabilityMcpServers } from "./capabilities";
|
|
77
91
|
import { requireVariableSetEncryption, validateVariableSetAttachment } from "./environments";
|
|
78
92
|
import {
|
|
93
|
+
assertToolRefsSubset,
|
|
94
|
+
availableToolRefs,
|
|
79
95
|
mergeToolRefs,
|
|
80
96
|
normalizeResources,
|
|
81
97
|
validateFileResources,
|
|
82
98
|
validateGitHubRepositorySelection,
|
|
83
99
|
validateToolRefs,
|
|
100
|
+
validateToolRefsForSessionPolicy,
|
|
84
101
|
withDefaultEnabledCapabilityMcpTools,
|
|
85
102
|
} from "./resources";
|
|
86
103
|
|
|
@@ -90,6 +107,34 @@ const maxSessionMcpCredentialHeaderValueLength = 4096;
|
|
|
90
107
|
// RFC 9110 field-name token characters.
|
|
91
108
|
const sessionMcpCredentialHeaderName = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
|
|
92
109
|
|
|
110
|
+
/** Transport-neutral typed denial raised only after its audit row committed. */
|
|
111
|
+
export class SessionSpawnDeniedError extends Error {
|
|
112
|
+
readonly denial: SessionSpawnDenial;
|
|
113
|
+
|
|
114
|
+
constructor(denial: SessionSpawnDenial) {
|
|
115
|
+
super(sessionSpawnDeniedMessage(denial));
|
|
116
|
+
this.name = "SessionSpawnDeniedError";
|
|
117
|
+
this.denial = denial;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function sessionSpawnDeniedMessage(denial: SessionSpawnDenial): string {
|
|
122
|
+
if (denial.code === "nested_agent_depth_override_forbidden") {
|
|
123
|
+
return `requested nested-agent depth limit ${denial.requestedMaxNestedAgentDepthOverride ?? "unknown"} exceeds inherited limit ${denial.effectiveMaxNestedAgentDepth}; workspace:admin is required to increase it`;
|
|
124
|
+
}
|
|
125
|
+
return `nested-agent depth ${denial.attemptedDepth} exceeds effective limit ${denial.effectiveMaxNestedAgentDepth} (current parent depth ${denial.currentDepth})`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function sessionSpawnDenialEnvelope(error: SessionSpawnDeniedError) {
|
|
129
|
+
return {
|
|
130
|
+
error: {
|
|
131
|
+
code: error.denial.code,
|
|
132
|
+
message: error.message,
|
|
133
|
+
details: { denial: error.denial },
|
|
134
|
+
},
|
|
135
|
+
} as const;
|
|
136
|
+
}
|
|
137
|
+
|
|
93
138
|
type ValidatedSessionMcpServers = {
|
|
94
139
|
runtimeServers: Settings["mcpServers"];
|
|
95
140
|
dbServers: CreateSessionMcpServerInput[];
|
|
@@ -265,6 +310,7 @@ function mcpServerConfigFromMetadata(
|
|
|
265
310
|
...(server.name ? { name: server.name } : {}),
|
|
266
311
|
url: server.url,
|
|
267
312
|
cacheToolsList: false,
|
|
313
|
+
requireApproval: server.requireApproval,
|
|
268
314
|
...(server.connectionRef ? { connectionRef: server.connectionRef } : {}),
|
|
269
315
|
};
|
|
270
316
|
}
|
|
@@ -340,6 +386,7 @@ function validateSessionMcpServersForCreate(
|
|
|
340
386
|
url: server.url,
|
|
341
387
|
headerNames: Object.keys(headersEncrypted).sort(),
|
|
342
388
|
credentialVersion: 1,
|
|
389
|
+
requireApproval: server.requireApproval ?? false,
|
|
343
390
|
connectionRef: server.connectionRef ?? null,
|
|
344
391
|
});
|
|
345
392
|
}
|
|
@@ -383,6 +430,7 @@ function validateInheritedSessionMcpServersForCreate(
|
|
|
383
430
|
url: server.url,
|
|
384
431
|
headerNames: Object.keys(server.headersEncrypted ?? {}).sort(),
|
|
385
432
|
credentialVersion: 1,
|
|
433
|
+
requireApproval: server.requireApproval ?? false,
|
|
386
434
|
connectionRef: server.connectionRef ?? null,
|
|
387
435
|
})),
|
|
388
436
|
};
|
|
@@ -436,9 +484,14 @@ export async function createAndStartSession(input: {
|
|
|
436
484
|
turnInstructions?: string | null;
|
|
437
485
|
resources: ResourceRef[];
|
|
438
486
|
tools: ToolRef[];
|
|
487
|
+
// Public admission always supplies provenance; optional keeps internal
|
|
488
|
+
// callers that predate durable tool-policy provenance source-compatible
|
|
489
|
+
// during the rolling deploy.
|
|
490
|
+
toolPolicy?: SessionToolPolicy;
|
|
439
491
|
clientEventId?: string;
|
|
440
492
|
model: string;
|
|
441
493
|
reasoningEffort: Settings["openaiReasoningEffort"];
|
|
494
|
+
turnExecutionPolicy: TurnExecutionPolicyV1;
|
|
442
495
|
sandboxBackend: Settings["sandboxBackend"];
|
|
443
496
|
metadata: Record<string, unknown>;
|
|
444
497
|
createdBy?: TurnInitiator;
|
|
@@ -490,36 +543,27 @@ export async function createAndStartSession(input: {
|
|
|
490
543
|
// `workingDir` (optional) is the path/cwd base the chosen machine runs under,
|
|
491
544
|
// seeded alongside the pointer through the epoch-fenced CAS.
|
|
492
545
|
seedTargetSandbox?: { sandboxId: string; settings: Settings; workingDir?: string | null } | null;
|
|
546
|
+
// Exact actor-private pre-session draft represented by this create. The
|
|
547
|
+
// initializer consumes it only after the first durable runnable unit commits.
|
|
548
|
+
consumeNewSessionDraft?: { subjectId: string; expectedRevision: number } | null;
|
|
549
|
+
// A child may lower its inherited nested-agent depth limit freely; increases
|
|
550
|
+
// are authorized by the caller's workspace:admin grant and checked again by
|
|
551
|
+
// the database admission transaction.
|
|
552
|
+
maxNestedAgentDepthOverride?: number | null;
|
|
553
|
+
allowNestedAgentDepthIncrease?: boolean;
|
|
554
|
+
subjectId?: string | null;
|
|
493
555
|
}): Promise<CreateSessionResponse> {
|
|
494
556
|
const sessionMetadata = {
|
|
495
557
|
...input.metadata,
|
|
496
558
|
model: input.model,
|
|
497
559
|
reasoningEffort: input.reasoningEffort,
|
|
498
560
|
};
|
|
499
|
-
//
|
|
500
|
-
//
|
|
561
|
+
// Keyed creation is intentionally handled only by the database admission
|
|
562
|
+
// transaction below. Its workspace/key lock replays either the successful
|
|
563
|
+
// session or the committed denial atomically; an application-side lookup
|
|
564
|
+
// cannot serialize those two source tables against an older writer.
|
|
501
565
|
if (input.createIdempotencyKey) {
|
|
502
|
-
const
|
|
503
|
-
input.db,
|
|
504
|
-
input.workspaceId,
|
|
505
|
-
input.createIdempotencyKey,
|
|
506
|
-
);
|
|
507
|
-
if (existing) {
|
|
508
|
-
if (input.requestedSessionId && existing.id !== input.requestedSessionId) {
|
|
509
|
-
throw new SessionIdConflictError(input.requestedSessionId);
|
|
510
|
-
}
|
|
511
|
-
return await finishStartSession(
|
|
512
|
-
existing.temporalWorkflowId ? { ...input, seedTargetSandbox: null } : input,
|
|
513
|
-
existing,
|
|
514
|
-
);
|
|
515
|
-
}
|
|
516
|
-
// No prior session: insert under the key, racing concurrent creates. The
|
|
517
|
-
// partial unique index lets exactly one insert win; a loser gets back the
|
|
518
|
-
// winner's row with created=false. Both callers may enter the idempotent
|
|
519
|
-
// initializer; exactly one creates the first events/turn. Each retry
|
|
520
|
-
// advances the coalesced wake revision so an in-flight stale delivery can
|
|
521
|
-
// never acknowledge work committed by the other caller.
|
|
522
|
-
const { session: keyed, created } = await createSessionWithIdempotencyKey(input.db, {
|
|
566
|
+
const keyedResult = await createSessionWithIdempotencyKeyResult(input.db, {
|
|
523
567
|
...(input.requestedSessionId ? { requestedSessionId: input.requestedSessionId } : {}),
|
|
524
568
|
accountId: input.accountId,
|
|
525
569
|
workspaceId: input.workspaceId,
|
|
@@ -527,6 +571,7 @@ export async function createAndStartSession(input: {
|
|
|
527
571
|
initialTurnInstructions: input.turnInstructions ?? null,
|
|
528
572
|
resources: input.resources,
|
|
529
573
|
tools: input.tools,
|
|
574
|
+
...(input.toolPolicy ? { toolPolicy: input.toolPolicy } : {}),
|
|
530
575
|
metadata: sessionMetadata,
|
|
531
576
|
...(input.createdBy ? { createdBy: input.createdBy } : {}),
|
|
532
577
|
...(input.createdByContext ? { createdByContext: input.createdByContext } : {}),
|
|
@@ -543,7 +588,14 @@ export async function createAndStartSession(input: {
|
|
|
543
588
|
sandboxGroupId: input.sandboxGroupId ?? null,
|
|
544
589
|
...(input.sandboxOs ? { sandboxOs: input.sandboxOs } : {}),
|
|
545
590
|
mcpServers: input.mcpServers ?? [],
|
|
591
|
+
maxNestedAgentDepthOverride: input.maxNestedAgentDepthOverride ?? null,
|
|
592
|
+
allowNestedAgentDepthIncrease: input.allowNestedAgentDepthIncrease ?? false,
|
|
593
|
+
subjectId: input.subjectId ?? null,
|
|
546
594
|
});
|
|
595
|
+
if (keyedResult.denied) {
|
|
596
|
+
throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(keyedResult.denial));
|
|
597
|
+
}
|
|
598
|
+
const { session: keyed, created } = keyedResult;
|
|
547
599
|
if (!created) {
|
|
548
600
|
return await finishStartSession(
|
|
549
601
|
keyed.temporalWorkflowId ? { ...input, seedTargetSandbox: null } : input,
|
|
@@ -552,30 +604,42 @@ export async function createAndStartSession(input: {
|
|
|
552
604
|
}
|
|
553
605
|
return await finishStartSession(input, keyed);
|
|
554
606
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
607
|
+
let session: Session;
|
|
608
|
+
try {
|
|
609
|
+
session = await createSession(input.db, {
|
|
610
|
+
...(input.requestedSessionId ? { requestedSessionId: input.requestedSessionId } : {}),
|
|
611
|
+
accountId: input.accountId,
|
|
612
|
+
workspaceId: input.workspaceId,
|
|
613
|
+
initialMessage: input.initialMessage,
|
|
614
|
+
initialTurnInstructions: input.turnInstructions ?? null,
|
|
615
|
+
resources: input.resources,
|
|
616
|
+
tools: input.tools,
|
|
617
|
+
...(input.toolPolicy ? { toolPolicy: input.toolPolicy } : {}),
|
|
618
|
+
metadata: sessionMetadata,
|
|
619
|
+
...(input.createdBy ? { createdBy: input.createdBy } : {}),
|
|
620
|
+
...(input.createdByContext ? { createdByContext: input.createdByContext } : {}),
|
|
621
|
+
createdByActor: input.createdByActor ?? null,
|
|
622
|
+
model: input.model,
|
|
623
|
+
sandboxBackend: input.sandboxBackend,
|
|
624
|
+
variableSetId: input.variableSet?.id ?? null,
|
|
625
|
+
rigId: input.rigId ?? null,
|
|
626
|
+
rigVersionId: input.rigVersionId ?? null,
|
|
627
|
+
firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
|
|
628
|
+
instructions: input.instructions ?? null,
|
|
629
|
+
parentSessionId: input.parentSessionId ?? null,
|
|
630
|
+
sandboxGroupId: input.sandboxGroupId ?? null,
|
|
631
|
+
...(input.sandboxOs ? { sandboxOs: input.sandboxOs } : {}),
|
|
632
|
+
mcpServers: input.mcpServers ?? [],
|
|
633
|
+
maxNestedAgentDepthOverride: input.maxNestedAgentDepthOverride ?? null,
|
|
634
|
+
allowNestedAgentDepthIncrease: input.allowNestedAgentDepthIncrease ?? false,
|
|
635
|
+
subjectId: input.subjectId ?? null,
|
|
636
|
+
});
|
|
637
|
+
} catch (error) {
|
|
638
|
+
if (error instanceof SessionSpawnDeniedDbError) {
|
|
639
|
+
throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(error.denial));
|
|
640
|
+
}
|
|
641
|
+
throw error;
|
|
642
|
+
}
|
|
579
643
|
return await finishStartSession(input, session);
|
|
580
644
|
}
|
|
581
645
|
|
|
@@ -594,9 +658,11 @@ async function finishStartSession(
|
|
|
594
658
|
turnInstructions?: string | null;
|
|
595
659
|
resources: ResourceRef[];
|
|
596
660
|
tools: ToolRef[];
|
|
661
|
+
toolPolicy?: SessionToolPolicy;
|
|
597
662
|
clientEventId?: string;
|
|
598
663
|
model: string;
|
|
599
664
|
reasoningEffort: Settings["openaiReasoningEffort"];
|
|
665
|
+
turnExecutionPolicy: TurnExecutionPolicyV1;
|
|
600
666
|
sandboxBackend: Settings["sandboxBackend"];
|
|
601
667
|
variableSet?: { id: string; name: string } | null;
|
|
602
668
|
goal?: GoalSpec | null;
|
|
@@ -606,6 +672,7 @@ async function finishStartSession(
|
|
|
606
672
|
settings: Settings;
|
|
607
673
|
workingDir?: string | null;
|
|
608
674
|
} | null;
|
|
675
|
+
consumeNewSessionDraft?: { subjectId: string; expectedRevision: number } | null;
|
|
609
676
|
},
|
|
610
677
|
session: Session,
|
|
611
678
|
): Promise<CreateSessionResponse> {
|
|
@@ -647,7 +714,9 @@ async function finishStartSession(
|
|
|
647
714
|
sessionId: session.id,
|
|
648
715
|
...(input.clientEventId ? { clientEventId: input.clientEventId } : {}),
|
|
649
716
|
reasoningEffortFallback: input.reasoningEffort,
|
|
717
|
+
turnExecutionPolicy: input.turnExecutionPolicy,
|
|
650
718
|
createdEventPayload: {
|
|
719
|
+
...(input.toolPolicy ? { toolPolicy: input.toolPolicy } : {}),
|
|
651
720
|
...(input.variableSet
|
|
652
721
|
? { variableSetId: input.variableSet.id, variableSetName: input.variableSet.name }
|
|
653
722
|
: {}),
|
|
@@ -664,6 +733,7 @@ async function finishStartSession(
|
|
|
664
733
|
: {}),
|
|
665
734
|
}
|
|
666
735
|
: null,
|
|
736
|
+
consumeNewSessionDraft: input.consumeNewSessionDraft ?? null,
|
|
667
737
|
});
|
|
668
738
|
await publishDurableSessionEvents(input.bus, session.workspaceId, session.id, started.events);
|
|
669
739
|
if (started.workflowWakeRevision !== null) {
|
|
@@ -703,12 +773,16 @@ export function workflowIdForSession(sessionId: string): string {
|
|
|
703
773
|
* later) and the MCP surfaces that share them validate identically and cannot
|
|
704
774
|
* drift.
|
|
705
775
|
*/
|
|
706
|
-
export function
|
|
776
|
+
export function canonicalConfiguredModel(
|
|
777
|
+
settings: Settings,
|
|
778
|
+
model: string | null | undefined,
|
|
779
|
+
): string | null | undefined {
|
|
707
780
|
if (model === null || model === undefined) {
|
|
708
|
-
return;
|
|
781
|
+
return model;
|
|
709
782
|
}
|
|
710
|
-
|
|
711
|
-
|
|
783
|
+
const canonicalModel = canonicalizeConfiguredModelId(settings, model);
|
|
784
|
+
if (configuredAllowedModels(settings).includes(canonicalModel)) {
|
|
785
|
+
return canonicalModel;
|
|
712
786
|
}
|
|
713
787
|
// Codex subscription models (codex/<slug>) are injected per-workspace by the
|
|
714
788
|
// worker overlay at turn time, so they are never in the deployment-global
|
|
@@ -716,12 +790,16 @@ export function assertConfiguredModel(settings: Settings, model: string | null |
|
|
|
716
790
|
// only surfaces them for a connected workspace, and the worker enforces the
|
|
717
791
|
// actual connection (an unconnected workspace fails the turn with a clear
|
|
718
792
|
// "no Codex subscription connected" error rather than a misleading 422 here).
|
|
719
|
-
if (settings.codexSubscriptionEnabled &&
|
|
720
|
-
return;
|
|
793
|
+
if (settings.codexSubscriptionEnabled && canonicalModel.startsWith(CODEX_MODEL_ID_PREFIX)) {
|
|
794
|
+
return canonicalModel;
|
|
721
795
|
}
|
|
722
796
|
throw new HTTPException(422, { message: `model is not available: ${model}` });
|
|
723
797
|
}
|
|
724
798
|
|
|
799
|
+
export function assertConfiguredModel(settings: Settings, model: string | null | undefined): void {
|
|
800
|
+
canonicalConfiguredModel(settings, model);
|
|
801
|
+
}
|
|
802
|
+
|
|
725
803
|
/**
|
|
726
804
|
* Reject a model the WORKSPACE's model policy blocks, at the same choke points
|
|
727
805
|
* as assertConfiguredModel — a 422 at the edge instead of a queued turn the
|
|
@@ -742,18 +820,25 @@ export async function assertWorkspaceModelPolicyAllows(
|
|
|
742
820
|
if (model === null || model === undefined) {
|
|
743
821
|
return;
|
|
744
822
|
}
|
|
823
|
+
const canonicalModel = canonicalConfiguredModel(settings, model);
|
|
824
|
+
if (canonicalModel === null || canonicalModel === undefined) {
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
745
827
|
const policy = await getWorkspaceModelPolicy(db, workspaceId);
|
|
746
828
|
if (!policy) {
|
|
747
829
|
return;
|
|
748
830
|
}
|
|
749
|
-
const providerId = policyProviderIdForModel(settings,
|
|
750
|
-
const verdict = evaluateWorkspaceModelPolicy(policy, {
|
|
831
|
+
const providerId = policyProviderIdForModel(settings, canonicalModel);
|
|
832
|
+
const verdict = evaluateWorkspaceModelPolicy(policy, {
|
|
833
|
+
providerId,
|
|
834
|
+
modelId: canonicalModel,
|
|
835
|
+
});
|
|
751
836
|
if (!verdict.allowed) {
|
|
752
837
|
throw new HTTPException(422, {
|
|
753
838
|
message:
|
|
754
839
|
verdict.reason === "provider"
|
|
755
|
-
? `model "${
|
|
756
|
-
: `model "${
|
|
840
|
+
? `model "${canonicalModel}" is not allowed by this workspace's model policy: provider "${providerId}" is not in the allowed providers`
|
|
841
|
+
: `model "${canonicalModel}" is not allowed by this workspace's model policy`,
|
|
757
842
|
});
|
|
758
843
|
}
|
|
759
844
|
}
|
|
@@ -802,6 +887,7 @@ export async function postUserMessageTurn(input: {
|
|
|
802
887
|
turnInstructions?: string | null;
|
|
803
888
|
resources: ResourceRef[];
|
|
804
889
|
tools: ToolRef[];
|
|
890
|
+
toolsProvided: boolean;
|
|
805
891
|
model?: string | null;
|
|
806
892
|
reasoningEffort?: Settings["openaiReasoningEffort"] | null;
|
|
807
893
|
clientEventId?: string;
|
|
@@ -813,9 +899,11 @@ export async function postUserMessageTurn(input: {
|
|
|
813
899
|
commandActor?: SessionCommandActor;
|
|
814
900
|
controlEtag?: string | null;
|
|
815
901
|
expectedDraftRevision?: number | null;
|
|
902
|
+
reasoningEffortFallback?: Settings["openaiReasoningEffort"];
|
|
903
|
+
turnExecutionPolicy: TurnExecutionPolicyV1;
|
|
816
904
|
}): Promise<{ accepted: SessionEvent; turn: SessionTurn }> {
|
|
817
905
|
const { db, bus, workflowClient, settings, accountId, workspaceId, sessionId } = input;
|
|
818
|
-
const requestedModel = input.model ?? null;
|
|
906
|
+
const requestedModel = canonicalConfiguredModel(settings, input.model ?? null) ?? null;
|
|
819
907
|
const requestedReasoningEffort = input.reasoningEffort ?? null;
|
|
820
908
|
// Reject an explicit per-message model the host does not expose; an omitted
|
|
821
909
|
// model inherits the session's model downstream (always a configured id).
|
|
@@ -844,9 +932,11 @@ export async function postUserMessageTurn(input: {
|
|
|
844
932
|
turnInstructions: input.turnInstructions ?? null,
|
|
845
933
|
resources: input.resources,
|
|
846
934
|
tools: input.tools,
|
|
935
|
+
toolsProvided: input.toolsProvided,
|
|
847
936
|
model: requestedModel,
|
|
848
937
|
reasoningEffort: requestedReasoningEffort,
|
|
849
|
-
reasoningEffortFallback: settings.openaiReasoningEffort,
|
|
938
|
+
reasoningEffortFallback: input.reasoningEffortFallback ?? settings.openaiReasoningEffort,
|
|
939
|
+
turnExecutionPolicy: input.turnExecutionPolicy,
|
|
850
940
|
source: input.origin === "operator" ? "api" : "user",
|
|
851
941
|
mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
|
|
852
942
|
}),
|
|
@@ -931,6 +1021,20 @@ export async function createSessionForRequest(
|
|
|
931
1021
|
): Promise<Session> {
|
|
932
1022
|
const { settings, db, bus, workflowClient, objectStorage } = deps;
|
|
933
1023
|
const payload = CreateSessionRequest.parse(rawPayload);
|
|
1024
|
+
// A committed keyed denial is the idempotent outcome even if mutable
|
|
1025
|
+
// resources, policy, authorization, or budget have changed since the first
|
|
1026
|
+
// attempt. Replay it before any of those checks, just as a keyed successful
|
|
1027
|
+
// session is returned rather than recreated later in createAndStartSession.
|
|
1028
|
+
if (payload.idempotencyKey) {
|
|
1029
|
+
const denial = await getSessionSpawnDenialByIdempotencyKey(
|
|
1030
|
+
db,
|
|
1031
|
+
workspaceId,
|
|
1032
|
+
payload.idempotencyKey,
|
|
1033
|
+
);
|
|
1034
|
+
if (denial) {
|
|
1035
|
+
throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(denial));
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
934
1038
|
// Parent linkage and execution-context inheritance come ONLY from the
|
|
935
1039
|
// worker-signed sessionId claim. A caller cannot nominate a parent in the
|
|
936
1040
|
// payload, so inheriting an existing repository/tool/credential snapshot does
|
|
@@ -973,21 +1077,58 @@ export async function createSessionForRequest(
|
|
|
973
1077
|
? payload.resources
|
|
974
1078
|
: (parentSession?.resources ?? payload.resources),
|
|
975
1079
|
);
|
|
1080
|
+
const toolsProvided = hasOwnProperty(rawPayload, "tools");
|
|
976
1081
|
const requestedTools = validateToolRefs(
|
|
977
|
-
|
|
1082
|
+
toolsProvided ? payload.tools : (parentSession?.tools ?? payload.tools),
|
|
978
1083
|
runtimeSettings,
|
|
979
1084
|
);
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1085
|
+
let selectedTools: ToolRef[];
|
|
1086
|
+
let toolPolicy: SessionToolPolicy;
|
|
1087
|
+
if (parentSession) {
|
|
1088
|
+
const parentTracksWorkspaceDefaults = parentSession.toolPolicy?.mode === "workspace_default";
|
|
1089
|
+
const parentEffective = withFirstPartyTools(
|
|
1090
|
+
parentTracksWorkspaceDefaults
|
|
1091
|
+
? withDefaultEnabledCapabilityMcpTools(
|
|
1092
|
+
availableToolRefs(parentSession.tools, runtimeSettings),
|
|
1093
|
+
settings,
|
|
1094
|
+
runtimeSettings,
|
|
1095
|
+
)
|
|
1096
|
+
: parentSession.tools,
|
|
1097
|
+
runtimeSettings,
|
|
1098
|
+
);
|
|
1099
|
+
if (toolsProvided) {
|
|
1100
|
+
assertToolRefsSubset(
|
|
1101
|
+
requestedTools,
|
|
1102
|
+
parentEffective,
|
|
1103
|
+
"child tools may only narrow the parent session tool policy",
|
|
1104
|
+
);
|
|
1105
|
+
selectedTools = requestedTools;
|
|
1106
|
+
toolPolicy = { mode: "explicit", inheritedFromSessionId: parentSession.id };
|
|
1107
|
+
} else {
|
|
1108
|
+
selectedTools = parentEffective;
|
|
1109
|
+
toolPolicy = {
|
|
1110
|
+
mode: parentTracksWorkspaceDefaults ? "workspace_default" : "inherited",
|
|
1111
|
+
inheritedFromSessionId: parentSession.id,
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
} else if (toolsProvided) {
|
|
1115
|
+
selectedTools = requestedTools;
|
|
1116
|
+
toolPolicy = { mode: "explicit", inheritedFromSessionId: null };
|
|
1117
|
+
} else {
|
|
1118
|
+
selectedTools = withDefaultEnabledCapabilityMcpTools(
|
|
1119
|
+
requestedTools,
|
|
1120
|
+
settings,
|
|
1121
|
+
capabilityRuntimeSettings,
|
|
1122
|
+
);
|
|
1123
|
+
toolPolicy = { mode: "workspace_default", inheritedFromSessionId: null };
|
|
1124
|
+
}
|
|
984
1125
|
// The first-party MCP server is attached to EVERY session. It hosts the
|
|
985
1126
|
// session's own metadata tool (set_session_title) + goal tools, and — only
|
|
986
1127
|
// when the grant carries the permission — the orchestration/variableSet/
|
|
987
1128
|
// github tools. Capability is gated per-tool by permission, never by whether
|
|
988
1129
|
// the server is attached, so a bare chat still gets titling while the
|
|
989
1130
|
// dangerous tools stay off by default.
|
|
990
|
-
const tools = withFirstPartyTools(
|
|
1131
|
+
const tools = withFirstPartyTools(selectedTools, runtimeSettings);
|
|
991
1132
|
await validateGitHubRepositorySelection(db, workspaceId, resources);
|
|
992
1133
|
if (resources.some((resource) => resource.kind === "file") && !objectStorage) {
|
|
993
1134
|
throw new HTTPException(503, { message: "object storage is not configured" });
|
|
@@ -1033,19 +1174,23 @@ export async function createSessionForRequest(
|
|
|
1033
1174
|
frozenRigVersionId = rig.activeVersion.id;
|
|
1034
1175
|
}
|
|
1035
1176
|
}
|
|
1036
|
-
|
|
1177
|
+
const model = canonicalConfiguredModel(settings, payload.model ?? settings.openaiModel);
|
|
1178
|
+
if (model === null || model === undefined) {
|
|
1179
|
+
throw new Error("effective session model unexpectedly resolved to null");
|
|
1180
|
+
}
|
|
1037
1181
|
// Session creation persists the EFFECTIVE model — an omitted payload.model
|
|
1038
1182
|
// stamps the deployment default onto the session — so the policy must vet
|
|
1039
1183
|
// that effective value, not just explicit ones (a restricted workspace's
|
|
1040
1184
|
// 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;
|
|
1185
|
+
await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, model);
|
|
1048
1186
|
const reasoningEffort = payload.reasoningEffort ?? settings.openaiReasoningEffort;
|
|
1187
|
+
const turnExecutionPolicy = resolveTurnExecutionPolicyV1(settings, {
|
|
1188
|
+
modelId: model,
|
|
1189
|
+
requestedModelId: payload.model ?? null,
|
|
1190
|
+
modelSource: payload.model === undefined ? "deployment" : "explicit",
|
|
1191
|
+
reasoningEffort,
|
|
1192
|
+
reasoningSource: payload.reasoningEffort === undefined ? "deployment" : "explicit",
|
|
1193
|
+
});
|
|
1049
1194
|
// Parent linkage was resolved above, before context validation. A child with
|
|
1050
1195
|
// no explicit permission override inherits the creating session's effective
|
|
1051
1196
|
// grant instead of silently expanding to standalone worker defaults.
|
|
@@ -1056,8 +1201,30 @@ export async function createSessionForRequest(
|
|
|
1056
1201
|
// normal worker defaults. A child omission inherits its creator's exact
|
|
1057
1202
|
// effective grant, preserving a host/operator's narrowed capability boundary
|
|
1058
1203
|
// through the whole session tree.
|
|
1204
|
+
const parentFirstPartyMcpPermissions = parentSession
|
|
1205
|
+
? [...(parentSession.firstPartyMcpPermissions ?? DEFAULT_FIRST_PARTY_MCP_PERMISSIONS)]
|
|
1206
|
+
: null;
|
|
1207
|
+
if (
|
|
1208
|
+
parentFirstPartyMcpPermissions &&
|
|
1209
|
+
payload.firstPartyMcpPermissions?.some(
|
|
1210
|
+
(permission) => !hasPermission(parentFirstPartyMcpPermissions, permission),
|
|
1211
|
+
)
|
|
1212
|
+
) {
|
|
1213
|
+
throw new HTTPException(403, {
|
|
1214
|
+
message: "child first-party MCP permissions may only narrow the parent session grant",
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
// A worker-signed creator may itself carry less authority than its parent
|
|
1218
|
+
// session (for example a narrowly delegated spawn token). Inherit the
|
|
1219
|
+
// intersection in the shared canonical default order so null/default parent
|
|
1220
|
+
// policies cannot expand when runtime signing resolves them.
|
|
1059
1221
|
let firstPartyMcpPermissions =
|
|
1060
|
-
payload.firstPartyMcpPermissions ??
|
|
1222
|
+
payload.firstPartyMcpPermissions ??
|
|
1223
|
+
(parentFirstPartyMcpPermissions
|
|
1224
|
+
? parentFirstPartyMcpPermissions.filter((permission) =>
|
|
1225
|
+
hasPermission(grant.permissions, permission),
|
|
1226
|
+
)
|
|
1227
|
+
: null);
|
|
1061
1228
|
if (firstPartyMcpPermissions && firstPartyMcpPermissions.length === 0) {
|
|
1062
1229
|
// An empty set would sign an unusable zero-permission token; the default
|
|
1063
1230
|
// worker set is expressed by omitting the field.
|
|
@@ -1322,9 +1489,11 @@ export async function createSessionForRequest(
|
|
|
1322
1489
|
turnInstructions: payload.turnInstructions ?? null,
|
|
1323
1490
|
resources,
|
|
1324
1491
|
tools,
|
|
1492
|
+
toolPolicy,
|
|
1325
1493
|
...(payload.clientEventId ? { clientEventId: payload.clientEventId } : {}),
|
|
1326
1494
|
model,
|
|
1327
1495
|
reasoningEffort,
|
|
1496
|
+
turnExecutionPolicy,
|
|
1328
1497
|
// A shared spawn inherits the box's backend; a caller-supplied
|
|
1329
1498
|
// sandboxBackend on a shared spawn is ignored (it is the same box). A
|
|
1330
1499
|
// machine-targeted top-level create labels the home "selfhosted"
|
|
@@ -1355,6 +1524,9 @@ export async function createSessionForRequest(
|
|
|
1355
1524
|
sessionMcpServers: sessionMcpServers.metadata,
|
|
1356
1525
|
parentSessionId,
|
|
1357
1526
|
createIdempotencyKey: payload.idempotencyKey ?? null,
|
|
1527
|
+
maxNestedAgentDepthOverride: payload.maxNestedAgentDepth ?? null,
|
|
1528
|
+
allowNestedAgentDepthIncrease: hasPermission(grant.permissions, "workspace:admin"),
|
|
1529
|
+
subjectId: grant.subjectId,
|
|
1358
1530
|
// Create-time machine targeting (A-2a): when a target sandbox is named, the
|
|
1359
1531
|
// active-sandbox pointer is seeded race-free inside createAndStartSession
|
|
1360
1532
|
// (after the row exists, before the first turn dispatches). Validation
|
|
@@ -1362,6 +1534,13 @@ export async function createSessionForRequest(
|
|
|
1362
1534
|
seedTargetSandbox: payload.targetSandboxId
|
|
1363
1535
|
? { sandboxId: payload.targetSandboxId, settings, workingDir: payload.workingDir ?? null }
|
|
1364
1536
|
: null,
|
|
1537
|
+
consumeNewSessionDraft:
|
|
1538
|
+
payload.expectedNewSessionDraftRevision !== undefined
|
|
1539
|
+
? {
|
|
1540
|
+
subjectId: grant.subjectId,
|
|
1541
|
+
expectedRevision: payload.expectedNewSessionDraftRevision,
|
|
1542
|
+
}
|
|
1543
|
+
: null,
|
|
1365
1544
|
});
|
|
1366
1545
|
} catch (error) {
|
|
1367
1546
|
if (error instanceof AgentCommandAuthorityError) {
|
|
@@ -1396,8 +1575,9 @@ export async function createSessionForRequest(
|
|
|
1396
1575
|
* Full accept-user-message flow shared by the `user.message` branch of
|
|
1397
1576
|
* `POST /sessions/:id/events` and the first-party MCP `session_send_message`
|
|
1398
1577
|
* tool: resource/tool validation, usage limits, the locked append + turn
|
|
1399
|
-
* enqueue, and usage recording. `toolsProvided: false`
|
|
1400
|
-
*
|
|
1578
|
+
* enqueue, and usage recording. `toolsProvided: false` durably preserves an
|
|
1579
|
+
* absent `tools` key so execution inherits the session policy; an explicit
|
|
1580
|
+
* empty array is a deliberate per-turn narrowing.
|
|
1401
1581
|
*/
|
|
1402
1582
|
export async function acceptSessionUserMessage(
|
|
1403
1583
|
deps: AcceptSessionUserMessageDependencies,
|
|
@@ -1420,6 +1600,12 @@ export async function acceptSessionUserMessage(
|
|
|
1420
1600
|
expectedDraftRevision?: number | null;
|
|
1421
1601
|
},
|
|
1422
1602
|
): Promise<{ accepted: SessionEvent; turn: SessionTurn }> {
|
|
1603
|
+
if (input.toolsProvided && !deps.settings.sessionTurnToolReplacementEnabled) {
|
|
1604
|
+
throw new HTTPException(503, {
|
|
1605
|
+
message:
|
|
1606
|
+
"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",
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1423
1609
|
const { settings, db, bus, workflowClient, objectStorage } = deps;
|
|
1424
1610
|
await requireSessionAuthorization(deps, grant, {
|
|
1425
1611
|
sessionId,
|
|
@@ -1435,21 +1621,55 @@ export async function acceptSessionUserMessage(
|
|
|
1435
1621
|
// turn's effective model (a follow-up turn inherits the session's model). A
|
|
1436
1622
|
// pure read with no side effects.
|
|
1437
1623
|
const existingSession = await requireSession(db, workspaceId, sessionId);
|
|
1624
|
+
const requestedModel = canonicalConfiguredModel(settings, input.model ?? null) ?? null;
|
|
1625
|
+
const effectiveModel =
|
|
1626
|
+
canonicalConfiguredModel(settings, requestedModel ?? existingSession.model) ?? null;
|
|
1627
|
+
if (effectiveModel === null) {
|
|
1628
|
+
throw new Error("effective follow-up model unexpectedly resolved to null");
|
|
1629
|
+
}
|
|
1630
|
+
const sessionReasoningEffort = reasoningEffortForSession(
|
|
1631
|
+
existingSession.metadata,
|
|
1632
|
+
settings.openaiReasoningEffort,
|
|
1633
|
+
);
|
|
1634
|
+
const effectiveReasoningEffort = input.reasoningEffort ?? sessionReasoningEffort;
|
|
1635
|
+
const turnExecutionPolicy = resolveTurnExecutionPolicyV1(settings, {
|
|
1636
|
+
modelId: effectiveModel,
|
|
1637
|
+
requestedModelId: input.model ?? null,
|
|
1638
|
+
modelSource: input.model == null ? "session" : "explicit",
|
|
1639
|
+
reasoningEffort: effectiveReasoningEffort,
|
|
1640
|
+
reasoningSource: input.reasoningEffort == null ? "session" : "explicit",
|
|
1641
|
+
});
|
|
1438
1642
|
const runtimeSettings = settingsWithSessionMcpServerMetadata(
|
|
1439
1643
|
capabilityRuntimeSettings,
|
|
1440
1644
|
existingSession.mcpServers,
|
|
1441
1645
|
);
|
|
1442
1646
|
const requestedResources = normalizeResources(input.resources ?? []);
|
|
1443
|
-
const
|
|
1444
|
-
const
|
|
1445
|
-
|
|
1446
|
-
|
|
1647
|
+
const tracksWorkspaceDefaults = existingSession.toolPolicy?.mode === "workspace_default";
|
|
1648
|
+
const sessionPolicyTools = withFirstPartyTools(
|
|
1649
|
+
tracksWorkspaceDefaults
|
|
1650
|
+
? withDefaultEnabledCapabilityMcpTools(
|
|
1651
|
+
availableToolRefs(existingSession.tools, runtimeSettings),
|
|
1652
|
+
settings,
|
|
1653
|
+
capabilityRuntimeSettings,
|
|
1654
|
+
)
|
|
1655
|
+
: existingSession.tools,
|
|
1656
|
+
runtimeSettings,
|
|
1657
|
+
);
|
|
1658
|
+
const validatedTools = input.toolsProvided
|
|
1659
|
+
? validateToolRefsForSessionPolicy({
|
|
1660
|
+
requested: input.tools ?? [],
|
|
1661
|
+
settings: runtimeSettings,
|
|
1662
|
+
allowedTools: sessionPolicyTools,
|
|
1663
|
+
message: "message tools may only narrow the session tool policy",
|
|
1664
|
+
})
|
|
1665
|
+
: [];
|
|
1666
|
+
const requestedTools = input.toolsProvided ? validatedTools : [];
|
|
1447
1667
|
await requireLimit(deps, {
|
|
1448
1668
|
accountId: grant.accountId,
|
|
1449
1669
|
workspaceId,
|
|
1450
1670
|
action: "agent_run:create",
|
|
1451
1671
|
quantity: 1,
|
|
1452
|
-
model:
|
|
1672
|
+
model: effectiveModel,
|
|
1453
1673
|
});
|
|
1454
1674
|
if (requestedResources.some((resource) => resource.kind === "file") && !objectStorage) {
|
|
1455
1675
|
throw new HTTPException(503, { message: "object storage is not configured" });
|
|
@@ -1478,8 +1698,11 @@ export async function acceptSessionUserMessage(
|
|
|
1478
1698
|
turnInstructions: input.turnInstructions ?? null,
|
|
1479
1699
|
resources: requestedResources,
|
|
1480
1700
|
tools: requestedTools,
|
|
1701
|
+
toolsProvided: input.toolsProvided,
|
|
1481
1702
|
model: input.model ?? null,
|
|
1482
1703
|
reasoningEffort: input.reasoningEffort ?? null,
|
|
1704
|
+
reasoningEffortFallback: sessionReasoningEffort,
|
|
1705
|
+
turnExecutionPolicy,
|
|
1483
1706
|
mcpCredentialUpdates,
|
|
1484
1707
|
delivery: input.delivery ?? "send",
|
|
1485
1708
|
origin: delegatedServiceInitiator ? "operator" : (input.origin ?? "human"),
|
|
@@ -1571,6 +1794,68 @@ export async function updateSessionTitle(
|
|
|
1571
1794
|
};
|
|
1572
1795
|
}
|
|
1573
1796
|
|
|
1797
|
+
/**
|
|
1798
|
+
* Update one existing session MCP server's approval policy. The database
|
|
1799
|
+
* serializes this write with attempt claim under the session lock: an already
|
|
1800
|
+
* claimed attempt retains its immutable snapshot, while the next claim captures
|
|
1801
|
+
* this value. No attempt is cancelled, restarted, or reinterpreted.
|
|
1802
|
+
*/
|
|
1803
|
+
export async function updateSessionMcpApprovalPolicy(
|
|
1804
|
+
deps: {
|
|
1805
|
+
db: Database;
|
|
1806
|
+
bus: EventBus;
|
|
1807
|
+
sessionAuthorization?: SessionAuthorizationPort | null;
|
|
1808
|
+
},
|
|
1809
|
+
grant: AccessGrant,
|
|
1810
|
+
sessionId: string,
|
|
1811
|
+
serverId: string,
|
|
1812
|
+
requireApproval: SessionMcpApprovalPolicy,
|
|
1813
|
+
): Promise<UpdateSessionMcpApprovalPolicyResponse> {
|
|
1814
|
+
const normalizedPolicy = SessionMcpApprovalPolicy.parse(requireApproval);
|
|
1815
|
+
await requireSessionAuthorization(deps, grant, {
|
|
1816
|
+
sessionId,
|
|
1817
|
+
operation: "session.mcp.approval_policy.write",
|
|
1818
|
+
surface: "core",
|
|
1819
|
+
});
|
|
1820
|
+
requirePermission(grant, "sessions:control");
|
|
1821
|
+
|
|
1822
|
+
const outcome: { server?: SessionMcpServerMetadata } = {};
|
|
1823
|
+
const events = await appendSessionEventsWithLockedSessionUpdate(
|
|
1824
|
+
deps.db,
|
|
1825
|
+
grant.workspaceId,
|
|
1826
|
+
sessionId,
|
|
1827
|
+
async (_session, context) => {
|
|
1828
|
+
const result = await context.updateSessionMcpApprovalPolicy(serverId, normalizedPolicy);
|
|
1829
|
+
if (!result.server) {
|
|
1830
|
+
throw new HTTPException(404, { message: "session MCP server not found" });
|
|
1831
|
+
}
|
|
1832
|
+
outcome.server = result.server;
|
|
1833
|
+
return {
|
|
1834
|
+
events: result.changed
|
|
1835
|
+
? [
|
|
1836
|
+
{
|
|
1837
|
+
type: "session.mcp.approval_policy.updated" as const,
|
|
1838
|
+
payload: {
|
|
1839
|
+
serverId,
|
|
1840
|
+
effectiveFrom: "next_attempt",
|
|
1841
|
+
},
|
|
1842
|
+
},
|
|
1843
|
+
]
|
|
1844
|
+
: [],
|
|
1845
|
+
};
|
|
1846
|
+
},
|
|
1847
|
+
);
|
|
1848
|
+
const updatedServer = outcome.server;
|
|
1849
|
+
if (!updatedServer) {
|
|
1850
|
+
throw new Error("session MCP approval policy update returned no server");
|
|
1851
|
+
}
|
|
1852
|
+
await publishDurableSessionEvents(deps.bus, grant.workspaceId, sessionId, events);
|
|
1853
|
+
return {
|
|
1854
|
+
server: updatedServer,
|
|
1855
|
+
effectiveFrom: "next_attempt",
|
|
1856
|
+
};
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1574
1859
|
export async function readSessionLineage(
|
|
1575
1860
|
deps: Pick<ApiRouteDeps, "db" | "sessionAuthorization">,
|
|
1576
1861
|
grant: AccessGrant,
|