@opengeni/core 0.12.7 → 0.14.3
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/access/index.d.ts +22 -0
- package/dist/application/new-session-drafts.d.ts +14 -0
- package/dist/application/session-commands.d.ts +107 -0
- package/dist/billing/limits.d.ts +29 -0
- package/dist/dependencies.d.ts +137 -0
- package/dist/domain/capabilities.d.ts +62 -0
- package/dist/domain/environments.d.ts +33 -0
- package/dist/domain/insights.d.ts +11 -0
- package/dist/domain/packs.d.ts +27 -0
- package/dist/domain/resources.d.ts +32 -0
- package/dist/domain/scheduled-tasks.d.ts +72 -0
- package/dist/domain/session-tool-policy.d.ts +31 -0
- package/dist/domain/sessions.d.ts +256 -0
- package/dist/domain/slack-bot.d.ts +19 -0
- package/dist/domain/workspace-members.d.ts +34 -0
- package/dist/index.d.ts +23 -1189
- package/dist/index.js +758 -115
- package/dist/index.js.map +1 -1
- package/dist/managed-auth-type.d.ts +2 -0
- package/dist/rigs/index.d.ts +57 -0
- package/dist/sandbox/fleet.d.ts +197 -0
- package/dist/sandbox/routing.d.ts +55 -0
- package/dist/sandbox-types.d.ts +52 -0
- package/dist/session-authorization.d.ts +36 -0
- package/dist/transcription.d.ts +71 -0
- package/dist/workflow-wake-contract.d.ts +4 -0
- package/package.json +11 -11
- package/src/access/index.ts +73 -2
- package/src/application/new-session-drafts.ts +3 -0
- package/src/application/session-commands.ts +22 -9
- package/src/dependencies.ts +5 -0
- package/src/domain/insights.ts +480 -0
- package/src/domain/session-tool-policy.ts +22 -39
- package/src/domain/sessions.ts +140 -72
- package/src/domain/slack-bot.ts +2 -4
- package/src/index.ts +2 -0
- package/src/sandbox/fleet.ts +96 -33
- package/src/sandbox/routing.ts +29 -7
- package/src/transcription.ts +142 -0
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
} from "@opengeni/contracts";
|
|
11
11
|
import type { Database } from "@opengeni/db";
|
|
12
12
|
import { settingsWithEnabledCapabilityMcpServers } from "./capabilities";
|
|
13
|
-
import { enabledCapabilityMcpToolRefs } from "./resources";
|
|
14
13
|
|
|
15
14
|
const MANDATORY_SESSION_MCP_SERVER_IDS = ["opengeni"] as const;
|
|
16
15
|
const PROJECTABLE_REGISTRY_ID = /^[A-Za-z0-9_-]+$/;
|
|
@@ -21,11 +20,8 @@ export type ResolvedSessionToolPolicy = {
|
|
|
21
20
|
};
|
|
22
21
|
|
|
23
22
|
export type SessionToolPolicyInput = {
|
|
24
|
-
toolPolicy
|
|
23
|
+
toolPolicy: SessionToolPolicy;
|
|
25
24
|
sessionTools: ToolRef[];
|
|
26
|
-
turnTools?: ToolRef[];
|
|
27
|
-
/** Undefined preserves the legacy merge path for pre-provenance callers. */
|
|
28
|
-
turnToolsProvided?: boolean;
|
|
29
25
|
availableMcpServerIds: Iterable<string>;
|
|
30
26
|
/** Current omitted-tools defaults, intentionally narrower than all servers. */
|
|
31
27
|
defaultMcpServerIds?: Iterable<string>;
|
|
@@ -35,6 +31,12 @@ function sortedIds(ids: Iterable<string>): string[] {
|
|
|
35
31
|
return [...new Set(ids)].sort();
|
|
36
32
|
}
|
|
37
33
|
|
|
34
|
+
/** Every configured runtime MCP defaults on; mandatory carrier IDs are separate. */
|
|
35
|
+
export function defaultSessionMcpServerIds(servers: Iterable<{ id: string }>): string[] {
|
|
36
|
+
const mandatory = new Set<string>(MANDATORY_SESSION_MCP_SERVER_IDS);
|
|
37
|
+
return sortedIds([...servers].map((server) => server.id).filter((id) => !mandatory.has(id)));
|
|
38
|
+
}
|
|
39
|
+
|
|
38
40
|
function projectIds(ids: readonly string[]): { ids: string[]; truncated: boolean } {
|
|
39
41
|
const projectable = ids.filter(
|
|
40
42
|
(id) =>
|
|
@@ -52,33 +54,27 @@ function projectIds(ids: readonly string[]): { ids: string[]; truncated: boolean
|
|
|
52
54
|
* Resolve the same ID-only policy used by API projections and worker turns.
|
|
53
55
|
* This function never receives endpoint URLs, credentials, schemas, or live
|
|
54
56
|
* probe results. `availableMcpServerIds` is the resolved runtime registry;
|
|
55
|
-
* `defaultMcpServerIds` is the
|
|
57
|
+
* `defaultMcpServerIds` is the current configured omitted-tools default.
|
|
56
58
|
*/
|
|
57
59
|
export function resolveSessionToolPolicy(input: SessionToolPolicyInput): ResolvedSessionToolPolicy {
|
|
58
|
-
const policy = input.toolPolicy
|
|
60
|
+
const policy = input.toolPolicy;
|
|
59
61
|
const availableIds = new Set(input.availableMcpServerIds);
|
|
60
|
-
// Never infer omitted-tools defaults from the full runtime registry: static
|
|
61
|
-
// MCPs are explicit-only unless they are capability-derived defaults.
|
|
62
62
|
const defaultIds = new Set(input.defaultMcpServerIds ?? []);
|
|
63
63
|
const mandatoryIds: string[] = MANDATORY_SESSION_MCP_SERVER_IDS.filter((id) =>
|
|
64
64
|
availableIds.has(id),
|
|
65
65
|
);
|
|
66
66
|
const mandatoryIdSet = new Set<string>(mandatoryIds);
|
|
67
|
-
const selectedRefs =
|
|
68
|
-
|
|
69
|
-
? mergeToolRefs([], input.turnTools ?? [])
|
|
70
|
-
: input.turnToolsProvided === false
|
|
71
|
-
? mergeToolRefs([], input.sessionTools)
|
|
72
|
-
: mergeToolRefs(input.sessionTools, input.turnTools ?? []);
|
|
73
|
-
const tracksWorkspaceDefaults =
|
|
74
|
-
policy.mode === "workspace_default" && input.turnToolsProvided !== true;
|
|
67
|
+
const selectedRefs = mergeToolRefs([], input.sessionTools);
|
|
68
|
+
const tracksWorkspaceDefaults = policy.mode === "workspace_default";
|
|
75
69
|
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
|
|
70
|
+
// Persisted refs may outlive a capability installation, deployment config,
|
|
71
|
+
// or its credentials. Admission remains strict for newly requested refs, but
|
|
72
|
+
// turn-time materialization must not hand any no-longer-registered id to the
|
|
73
|
+
// runtime router: doing so fails before the model can respond and traps the
|
|
74
|
+
// session in an "Unknown MCP server id" loop. Keep the stale selection in the
|
|
75
|
+
// effective-policy projection below, while executable refs contain only the
|
|
76
|
+
// registry that is available for this exact turn.
|
|
77
|
+
let toolRefs = selectedRefs.filter((tool) => availableIds.has(tool.id));
|
|
82
78
|
if (tracksWorkspaceDefaults) {
|
|
83
79
|
toolRefs = mergeToolRefs(
|
|
84
80
|
toolRefs,
|
|
@@ -168,19 +164,6 @@ export function resolveSessionToolPolicy(input: SessionToolPolicyInput): Resolve
|
|
|
168
164
|
};
|
|
169
165
|
}
|
|
170
166
|
|
|
171
|
-
/**
|
|
172
|
-
* Native provider tools that belong to the workspace-default capability set
|
|
173
|
-
* follow the same omission/narrowing fence as deferred MCP tools. A durable
|
|
174
|
-
* workspace-default policy receives them; fixed historical policies and an
|
|
175
|
-
* explicit per-turn replacement do not. Provider support remains a separate
|
|
176
|
-
* runtime gate and must also be true before a native tool is attached.
|
|
177
|
-
*/
|
|
178
|
-
export function sessionToolPolicyAllowsDefaultNativeTools(
|
|
179
|
-
policy: SessionEffectiveToolPolicy,
|
|
180
|
-
): boolean {
|
|
181
|
-
return policy.mode === "workspace_default" && policy.lazyRouter.state === "required";
|
|
182
|
-
}
|
|
183
|
-
|
|
184
167
|
/** Current full runtime registry IDs, including configured static servers. */
|
|
185
168
|
export async function workspaceSessionToolPolicyServerIds(
|
|
186
169
|
db: Database,
|
|
@@ -191,14 +174,14 @@ export async function workspaceSessionToolPolicyServerIds(
|
|
|
191
174
|
return sortedIds(runtimeSettings.mcpServers.map((server) => server.id));
|
|
192
175
|
}
|
|
193
176
|
|
|
194
|
-
/** Current omitted-tools defaults
|
|
177
|
+
/** Current omitted-tools defaults: every configured runtime MCP is on. */
|
|
195
178
|
export async function workspaceSessionToolPolicyDefaultServerIds(
|
|
196
179
|
db: Database,
|
|
197
180
|
workspaceId: string,
|
|
198
181
|
settings: Settings,
|
|
199
182
|
): Promise<string[]> {
|
|
200
183
|
const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(db, workspaceId, settings);
|
|
201
|
-
return
|
|
184
|
+
return defaultSessionMcpServerIds(runtimeSettings.mcpServers);
|
|
202
185
|
}
|
|
203
186
|
|
|
204
187
|
/** Add a bounded, secret-safe effective projection to a session response. */
|
|
@@ -214,7 +197,7 @@ export function sessionWithEffectiveToolPolicy(
|
|
|
214
197
|
return {
|
|
215
198
|
...session,
|
|
216
199
|
effectiveToolPolicy: resolveSessionToolPolicy({
|
|
217
|
-
|
|
200
|
+
toolPolicy: session.toolPolicy,
|
|
218
201
|
sessionTools: session.tools,
|
|
219
202
|
availableMcpServerIds: availableIds,
|
|
220
203
|
defaultMcpServerIds: workspaceDefaultServerIds,
|
package/src/domain/sessions.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CODEX_MODEL_ID_PREFIX } from "@opengeni/codex";
|
|
1
|
+
import { CODEX_MODEL_ID_PREFIX, isCodexBilledModel } from "@opengeni/codex";
|
|
2
2
|
import {
|
|
3
3
|
canonicalizeConfiguredModelId,
|
|
4
4
|
configuredAllowedModels,
|
|
@@ -10,11 +10,13 @@ import {
|
|
|
10
10
|
CreateSessionRequest,
|
|
11
11
|
DEFAULT_FIRST_PARTY_MCP_PERMISSIONS,
|
|
12
12
|
DEFAULT_FIRST_PARTY_MCP_TOOLS,
|
|
13
|
+
FIRST_PARTY_MCP_TOOL_NAMES,
|
|
13
14
|
OPENGENI_SLACK_BOT_SESSION_METADATA_KEY,
|
|
14
15
|
SessionSpawnDenial,
|
|
15
16
|
ServiceTurnInitiator,
|
|
16
17
|
ServiceTurnInitiatorContext,
|
|
17
18
|
evaluateWorkspaceModelPolicy,
|
|
19
|
+
latencyModeForMetadata,
|
|
18
20
|
reasoningEffortForMetadata,
|
|
19
21
|
stableJson,
|
|
20
22
|
type AccessGrant,
|
|
@@ -105,7 +107,6 @@ import {
|
|
|
105
107
|
validateFileResources,
|
|
106
108
|
validateGitHubRepositorySelection,
|
|
107
109
|
validateToolRefs,
|
|
108
|
-
validateToolRefsForSessionPolicy,
|
|
109
110
|
withDefaultEnabledCapabilityMcpTools,
|
|
110
111
|
} from "./resources";
|
|
111
112
|
|
|
@@ -131,16 +132,16 @@ export class SessionSpawnDeniedError extends Error {
|
|
|
131
132
|
|
|
132
133
|
/**
|
|
133
134
|
* Resolve per-session first-party tool visibility without consulting
|
|
134
|
-
* authorization. Top-level omission
|
|
135
|
-
*
|
|
136
|
-
*
|
|
135
|
+
* authorization. Top-level omission snapshots the complete runtime default;
|
|
136
|
+
* child omission snapshots the parent's exact effective selection. Explicit
|
|
137
|
+
* [] is authoritative and must never widen.
|
|
137
138
|
*/
|
|
138
139
|
export function resolveFirstPartyMcpToolsForCreate(
|
|
139
140
|
requested: FirstPartyMcpToolName[] | undefined,
|
|
140
141
|
parentStored: FirstPartyMcpToolName[] | null | undefined,
|
|
141
|
-
): FirstPartyMcpToolName[]
|
|
142
|
+
): FirstPartyMcpToolName[] {
|
|
142
143
|
if (requested !== undefined) return [...requested];
|
|
143
|
-
if (parentStored === undefined) return
|
|
144
|
+
if (parentStored === undefined) return [...DEFAULT_FIRST_PARTY_MCP_TOOLS];
|
|
144
145
|
return [...(parentStored ?? DEFAULT_FIRST_PARTY_MCP_TOOLS)];
|
|
145
146
|
}
|
|
146
147
|
|
|
@@ -514,10 +515,12 @@ export async function createAndStartSession(input: {
|
|
|
514
515
|
// Public admission always supplies provenance; optional keeps internal
|
|
515
516
|
// callers that predate durable tool-policy provenance source-compatible
|
|
516
517
|
// during the rolling deploy.
|
|
517
|
-
toolPolicy
|
|
518
|
+
toolPolicy: SessionToolPolicy;
|
|
518
519
|
clientEventId?: string;
|
|
519
520
|
model: string;
|
|
520
521
|
reasoningEffort: Settings["openaiReasoningEffort"];
|
|
522
|
+
/** Session default Fast/standard; mirrored into metadata when set. */
|
|
523
|
+
latencyMode?: "standard" | "priority" | "fast";
|
|
521
524
|
turnExecutionPolicy: TurnExecutionPolicyV1;
|
|
522
525
|
sandboxBackend: Settings["sandboxBackend"];
|
|
523
526
|
metadata: Record<string, unknown>;
|
|
@@ -541,7 +544,7 @@ export async function createAndStartSession(input: {
|
|
|
541
544
|
firstPartyMcpPermissions?: Permission[] | null;
|
|
542
545
|
// Model-visible first-party tool names. Authorization remains controlled by
|
|
543
546
|
// firstPartyMcpPermissions and the target resource checks.
|
|
544
|
-
firstPartyMcpTools
|
|
547
|
+
firstPartyMcpTools: FirstPartyMcpToolName[];
|
|
545
548
|
// Encrypted DB rows plus matching safe metadata for create-time per-session
|
|
546
549
|
// MCP servers. Metadata is the only shape emitted in events/responses.
|
|
547
550
|
mcpServers?: CreateSessionMcpServerInput[];
|
|
@@ -587,6 +590,7 @@ export async function createAndStartSession(input: {
|
|
|
587
590
|
...input.metadata,
|
|
588
591
|
model: input.model,
|
|
589
592
|
reasoningEffort: input.reasoningEffort,
|
|
593
|
+
...(input.latencyMode !== undefined ? { latencyMode: input.latencyMode } : {}),
|
|
590
594
|
};
|
|
591
595
|
// Keyed creation is intentionally handled only by the database admission
|
|
592
596
|
// transaction below. Its workspace/key lock replays either the successful
|
|
@@ -602,7 +606,7 @@ export async function createAndStartSession(input: {
|
|
|
602
606
|
resources: input.resources,
|
|
603
607
|
skills: input.skills ?? [],
|
|
604
608
|
tools: input.tools,
|
|
605
|
-
|
|
609
|
+
toolPolicy: input.toolPolicy,
|
|
606
610
|
metadata: sessionMetadata,
|
|
607
611
|
...(input.createdBy ? { createdBy: input.createdBy } : {}),
|
|
608
612
|
...(input.createdByContext ? { createdByContext: input.createdByContext } : {}),
|
|
@@ -613,7 +617,7 @@ export async function createAndStartSession(input: {
|
|
|
613
617
|
rigId: input.rigId ?? null,
|
|
614
618
|
rigVersionId: input.rigVersionId ?? null,
|
|
615
619
|
firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
|
|
616
|
-
firstPartyMcpTools: input.firstPartyMcpTools
|
|
620
|
+
firstPartyMcpTools: input.firstPartyMcpTools,
|
|
617
621
|
instructions: input.instructions ?? null,
|
|
618
622
|
parentSessionId: input.parentSessionId ?? null,
|
|
619
623
|
createIdempotencyKey: input.createIdempotencyKey,
|
|
@@ -647,7 +651,7 @@ export async function createAndStartSession(input: {
|
|
|
647
651
|
resources: input.resources,
|
|
648
652
|
skills: input.skills ?? [],
|
|
649
653
|
tools: input.tools,
|
|
650
|
-
|
|
654
|
+
toolPolicy: input.toolPolicy,
|
|
651
655
|
metadata: sessionMetadata,
|
|
652
656
|
...(input.createdBy ? { createdBy: input.createdBy } : {}),
|
|
653
657
|
...(input.createdByContext ? { createdByContext: input.createdByContext } : {}),
|
|
@@ -658,7 +662,7 @@ export async function createAndStartSession(input: {
|
|
|
658
662
|
rigId: input.rigId ?? null,
|
|
659
663
|
rigVersionId: input.rigVersionId ?? null,
|
|
660
664
|
firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
|
|
661
|
-
firstPartyMcpTools: input.firstPartyMcpTools
|
|
665
|
+
firstPartyMcpTools: input.firstPartyMcpTools,
|
|
662
666
|
instructions: input.instructions ?? null,
|
|
663
667
|
parentSessionId: input.parentSessionId ?? null,
|
|
664
668
|
sandboxGroupId: input.sandboxGroupId ?? null,
|
|
@@ -692,7 +696,7 @@ async function finishStartSession(
|
|
|
692
696
|
turnInstructions?: string | null;
|
|
693
697
|
resources: ResourceRef[];
|
|
694
698
|
tools: ToolRef[];
|
|
695
|
-
toolPolicy
|
|
699
|
+
toolPolicy: SessionToolPolicy;
|
|
696
700
|
clientEventId?: string;
|
|
697
701
|
model: string;
|
|
698
702
|
reasoningEffort: Settings["openaiReasoningEffort"];
|
|
@@ -750,7 +754,7 @@ async function finishStartSession(
|
|
|
750
754
|
reasoningEffortFallback: input.reasoningEffort,
|
|
751
755
|
turnExecutionPolicy: input.turnExecutionPolicy,
|
|
752
756
|
createdEventPayload: {
|
|
753
|
-
|
|
757
|
+
toolPolicy: input.toolPolicy,
|
|
754
758
|
...(input.variableSet
|
|
755
759
|
? { variableSetId: input.variableSet.id, variableSetName: input.variableSet.name }
|
|
756
760
|
: {}),
|
|
@@ -834,6 +838,36 @@ export function assertConfiguredModel(settings: Settings, model: string | null |
|
|
|
834
838
|
canonicalConfiguredModel(settings, model);
|
|
835
839
|
}
|
|
836
840
|
|
|
841
|
+
export const CODEX_COMPACTION_V2_PROVIDER_LOCKED = "codex_compaction_v2_provider_locked" as const;
|
|
842
|
+
|
|
843
|
+
/** Session is frozen on Codex remote compaction v2; non-Codex models are refused. */
|
|
844
|
+
export class CodexCompactionV2ProviderLockedError extends Error {
|
|
845
|
+
readonly code = CODEX_COMPACTION_V2_PROVIDER_LOCKED;
|
|
846
|
+
readonly productModelId: string;
|
|
847
|
+
|
|
848
|
+
constructor(productModelId: string) {
|
|
849
|
+
super(
|
|
850
|
+
`session is locked to Codex remote compaction v2; model "${productModelId}" is not a Codex subscription model`,
|
|
851
|
+
);
|
|
852
|
+
this.name = "CodexCompactionV2ProviderLockedError";
|
|
853
|
+
this.productModelId = productModelId;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Fail closed when a remote_v2 session would run a non-Codex product model.
|
|
859
|
+
* Portable sessions and non-Codex sessions keep free mid-session provider swap.
|
|
860
|
+
*/
|
|
861
|
+
export function assertSessionAllowsProductModel(
|
|
862
|
+
session: Pick<Session, "codexCompactionMode">,
|
|
863
|
+
productModelId: string | null | undefined,
|
|
864
|
+
): void {
|
|
865
|
+
if (productModelId === null || productModelId === undefined) return;
|
|
866
|
+
if (session.codexCompactionMode !== "remote_v2") return;
|
|
867
|
+
if (isCodexBilledModel(productModelId)) return;
|
|
868
|
+
throw new CodexCompactionV2ProviderLockedError(productModelId);
|
|
869
|
+
}
|
|
870
|
+
|
|
837
871
|
/**
|
|
838
872
|
* Reject a model the WORKSPACE's model policy blocks, at the same choke points
|
|
839
873
|
* as assertConfiguredModel — a 422 at the edge instead of a queued turn the
|
|
@@ -902,6 +936,13 @@ export function reasoningEffortForSession(
|
|
|
902
936
|
return reasoningEffortForMetadata(metadata, fallback);
|
|
903
937
|
}
|
|
904
938
|
|
|
939
|
+
export function latencyModeForSession(
|
|
940
|
+
metadata: Record<string, unknown>,
|
|
941
|
+
fallback: "standard" | "priority" | "fast" = "standard",
|
|
942
|
+
): "standard" | "priority" | "fast" {
|
|
943
|
+
return latencyModeForMetadata(metadata, fallback);
|
|
944
|
+
}
|
|
945
|
+
|
|
905
946
|
/**
|
|
906
947
|
* Appends a `user.message` to an existing session and enqueues the resulting
|
|
907
948
|
* turn, merging requested resources/tools into the session and waking the
|
|
@@ -920,10 +961,9 @@ export async function postUserMessageTurn(input: {
|
|
|
920
961
|
text: string;
|
|
921
962
|
turnInstructions?: string | null;
|
|
922
963
|
resources: ResourceRef[];
|
|
923
|
-
tools: ToolRef[];
|
|
924
|
-
toolsProvided: boolean;
|
|
925
964
|
model?: string | null;
|
|
926
965
|
reasoningEffort?: Settings["openaiReasoningEffort"] | null;
|
|
966
|
+
latencyMode?: "standard" | "priority" | "fast" | null;
|
|
927
967
|
clientEventId?: string;
|
|
928
968
|
mcpCredentialUpdates?: UpdateSessionMcpServerCredentialsInput[];
|
|
929
969
|
delivery?: "send" | "steer";
|
|
@@ -943,6 +983,16 @@ export async function postUserMessageTurn(input: {
|
|
|
943
983
|
// model inherits the session's model downstream (always a configured id).
|
|
944
984
|
assertConfiguredModel(settings, requestedModel);
|
|
945
985
|
await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, requestedModel);
|
|
986
|
+
const sessionForModelGate = await requireSession(db, workspaceId, sessionId);
|
|
987
|
+
const effectiveModelForGate = requestedModel ?? sessionForModelGate.model;
|
|
988
|
+
try {
|
|
989
|
+
assertSessionAllowsProductModel(sessionForModelGate, effectiveModelForGate);
|
|
990
|
+
} catch (error) {
|
|
991
|
+
if (error instanceof CodexCompactionV2ProviderLockedError) {
|
|
992
|
+
throw new HTTPException(422, { message: error.message, cause: error });
|
|
993
|
+
}
|
|
994
|
+
throw error;
|
|
995
|
+
}
|
|
946
996
|
const operationKey = input.clientEventId ?? crypto.randomUUID();
|
|
947
997
|
let result;
|
|
948
998
|
try {
|
|
@@ -965,10 +1015,9 @@ export async function postUserMessageTurn(input: {
|
|
|
965
1015
|
text: input.text,
|
|
966
1016
|
turnInstructions: input.turnInstructions ?? null,
|
|
967
1017
|
resources: input.resources,
|
|
968
|
-
tools: input.tools,
|
|
969
|
-
toolsProvided: input.toolsProvided,
|
|
970
1018
|
model: requestedModel,
|
|
971
1019
|
reasoningEffort: requestedReasoningEffort,
|
|
1020
|
+
latencyMode: input.latencyMode ?? null,
|
|
972
1021
|
reasoningEffortFallback: input.reasoningEffortFallback ?? settings.openaiReasoningEffort,
|
|
973
1022
|
turnExecutionPolicy: input.turnExecutionPolicy,
|
|
974
1023
|
source: input.origin === "operator" ? "api" : "user",
|
|
@@ -1224,12 +1273,15 @@ export async function createSessionForRequest(
|
|
|
1224
1273
|
// default-model session would otherwise be born blocked).
|
|
1225
1274
|
await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, model);
|
|
1226
1275
|
const reasoningEffort = payload.reasoningEffort ?? settings.openaiReasoningEffort;
|
|
1276
|
+
const latencyMode = payload.latencyMode ?? "standard";
|
|
1227
1277
|
const turnExecutionPolicy = resolveTurnExecutionPolicyV1(settings, {
|
|
1228
1278
|
modelId: model,
|
|
1229
1279
|
requestedModelId: payload.model ?? null,
|
|
1230
1280
|
modelSource: payload.model === undefined ? "deployment" : "explicit",
|
|
1231
1281
|
reasoningEffort,
|
|
1232
1282
|
reasoningSource: payload.reasoningEffort === undefined ? "deployment" : "explicit",
|
|
1283
|
+
latencyMode,
|
|
1284
|
+
latencyModeSource: payload.latencyMode === undefined ? "deployment" : "explicit",
|
|
1233
1285
|
});
|
|
1234
1286
|
// Parent linkage was resolved above, before context validation. A child with
|
|
1235
1287
|
// no explicit permission override inherits the creating session's effective
|
|
@@ -1298,15 +1350,14 @@ export async function createSessionForRequest(
|
|
|
1298
1350
|
}
|
|
1299
1351
|
// Tool visibility is independent from permission authority. A child that
|
|
1300
1352
|
// omits the field inherits the parent's exact effective selection; a
|
|
1301
|
-
// top-level omission
|
|
1353
|
+
// top-level omission selects the complete catalog.
|
|
1302
1354
|
const firstPartyMcpTools = resolveFirstPartyMcpToolsForCreate(
|
|
1303
1355
|
payload.firstPartyMcpTools,
|
|
1304
1356
|
parentSession ? parentSession.firstPartyMcpTools : undefined,
|
|
1305
1357
|
);
|
|
1306
1358
|
if (payload.goal) {
|
|
1307
|
-
const effectiveTools = firstPartyMcpTools ?? [...DEFAULT_FIRST_PARTY_MCP_TOOLS];
|
|
1308
1359
|
const missingGoalTools = ["goal_update", "goal_complete", "goal_pause"].filter(
|
|
1309
|
-
(name) => !
|
|
1360
|
+
(name) => !firstPartyMcpTools.includes(name as FirstPartyMcpToolName),
|
|
1310
1361
|
);
|
|
1311
1362
|
if (missingGoalTools.length > 0) {
|
|
1312
1363
|
throw new HTTPException(422, {
|
|
@@ -1552,6 +1603,7 @@ export async function createSessionForRequest(
|
|
|
1552
1603
|
...(payload.clientEventId ? { clientEventId: payload.clientEventId } : {}),
|
|
1553
1604
|
model,
|
|
1554
1605
|
reasoningEffort,
|
|
1606
|
+
latencyMode,
|
|
1555
1607
|
turnExecutionPolicy,
|
|
1556
1608
|
// A shared spawn inherits the box's backend; a caller-supplied
|
|
1557
1609
|
// sandboxBackend on a shared spawn is ignored (it is the same box). A
|
|
@@ -1636,8 +1688,7 @@ export async function createSessionForRequest(
|
|
|
1636
1688
|
* `POST /sessions/:id/events` and the first-party MCP `session_send_message`
|
|
1637
1689
|
* tool: resource/tool validation, usage limits, the locked append + turn
|
|
1638
1690
|
* enqueue, and usage recording. `toolsProvided: false` durably preserves an
|
|
1639
|
-
*
|
|
1640
|
-
* empty array is a deliberate per-turn narrowing.
|
|
1691
|
+
* Tool selection is durable session state and never rides a follow-up prompt.
|
|
1641
1692
|
*/
|
|
1642
1693
|
export async function acceptSessionUserMessage(
|
|
1643
1694
|
deps: AcceptSessionUserMessageDependencies,
|
|
@@ -1648,10 +1699,9 @@ export async function acceptSessionUserMessage(
|
|
|
1648
1699
|
text: string;
|
|
1649
1700
|
turnInstructions?: string | null;
|
|
1650
1701
|
resources?: ResourceRef[];
|
|
1651
|
-
tools?: ToolRef[];
|
|
1652
|
-
toolsProvided: boolean;
|
|
1653
1702
|
model?: string | null;
|
|
1654
1703
|
reasoningEffort?: ReasoningEffort | null;
|
|
1704
|
+
latencyMode?: "standard" | "priority" | "fast" | null;
|
|
1655
1705
|
clientEventId?: string;
|
|
1656
1706
|
mcpCredentialUpdates?: SessionMcpCredentialUpdateInput[];
|
|
1657
1707
|
delivery?: "send" | "steer";
|
|
@@ -1660,23 +1710,12 @@ export async function acceptSessionUserMessage(
|
|
|
1660
1710
|
expectedDraftRevision?: number | null;
|
|
1661
1711
|
},
|
|
1662
1712
|
): Promise<{ accepted: SessionEvent; turn: SessionTurn }> {
|
|
1663
|
-
if (input.toolsProvided && !deps.settings.sessionTurnToolReplacementEnabled) {
|
|
1664
|
-
throw new HTTPException(503, {
|
|
1665
|
-
message:
|
|
1666
|
-
"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",
|
|
1667
|
-
});
|
|
1668
|
-
}
|
|
1669
1713
|
const { settings, db, bus, workflowClient, objectStorage } = deps;
|
|
1670
1714
|
await requireSessionAuthorization(deps, grant, {
|
|
1671
1715
|
sessionId,
|
|
1672
1716
|
operation: input.delivery === "steer" ? "session.steer" : "session.append",
|
|
1673
1717
|
surface: "core",
|
|
1674
1718
|
});
|
|
1675
|
-
const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
1676
|
-
db,
|
|
1677
|
-
workspaceId,
|
|
1678
|
-
settings,
|
|
1679
|
-
);
|
|
1680
1719
|
// Hoisted above requireLimit so the codex-billed predicate can resolve the
|
|
1681
1720
|
// turn's effective model (a follow-up turn inherits the session's model). A
|
|
1682
1721
|
// pure read with no side effects.
|
|
@@ -1687,43 +1726,31 @@ export async function acceptSessionUserMessage(
|
|
|
1687
1726
|
if (effectiveModel === null) {
|
|
1688
1727
|
throw new Error("effective follow-up model unexpectedly resolved to null");
|
|
1689
1728
|
}
|
|
1729
|
+
try {
|
|
1730
|
+
assertSessionAllowsProductModel(existingSession, effectiveModel);
|
|
1731
|
+
} catch (error) {
|
|
1732
|
+
if (error instanceof CodexCompactionV2ProviderLockedError) {
|
|
1733
|
+
throw new HTTPException(422, { message: error.message, cause: error });
|
|
1734
|
+
}
|
|
1735
|
+
throw error;
|
|
1736
|
+
}
|
|
1690
1737
|
const sessionReasoningEffort = reasoningEffortForSession(
|
|
1691
1738
|
existingSession.metadata,
|
|
1692
1739
|
settings.openaiReasoningEffort,
|
|
1693
1740
|
);
|
|
1694
1741
|
const effectiveReasoningEffort = input.reasoningEffort ?? sessionReasoningEffort;
|
|
1742
|
+
const sessionLatencyMode = latencyModeForSession(existingSession.metadata, "standard");
|
|
1743
|
+
const effectiveLatencyMode = input.latencyMode ?? sessionLatencyMode;
|
|
1695
1744
|
const turnExecutionPolicy = resolveTurnExecutionPolicyV1(settings, {
|
|
1696
1745
|
modelId: effectiveModel,
|
|
1697
1746
|
requestedModelId: input.model ?? null,
|
|
1698
1747
|
modelSource: input.model == null ? "session" : "explicit",
|
|
1699
1748
|
reasoningEffort: effectiveReasoningEffort,
|
|
1700
1749
|
reasoningSource: input.reasoningEffort == null ? "session" : "explicit",
|
|
1750
|
+
latencyMode: effectiveLatencyMode,
|
|
1751
|
+
latencyModeSource: input.latencyMode == null ? "session" : "explicit",
|
|
1701
1752
|
});
|
|
1702
|
-
const runtimeSettings = settingsWithSessionMcpServerMetadata(
|
|
1703
|
-
capabilityRuntimeSettings,
|
|
1704
|
-
existingSession.mcpServers,
|
|
1705
|
-
);
|
|
1706
1753
|
const requestedResources = normalizeResources(input.resources ?? []);
|
|
1707
|
-
const tracksWorkspaceDefaults = existingSession.toolPolicy?.mode === "workspace_default";
|
|
1708
|
-
const sessionPolicyTools = withFirstPartyTools(
|
|
1709
|
-
tracksWorkspaceDefaults
|
|
1710
|
-
? withDefaultEnabledCapabilityMcpTools(
|
|
1711
|
-
availableToolRefs(existingSession.tools, runtimeSettings),
|
|
1712
|
-
settings,
|
|
1713
|
-
capabilityRuntimeSettings,
|
|
1714
|
-
)
|
|
1715
|
-
: existingSession.tools,
|
|
1716
|
-
runtimeSettings,
|
|
1717
|
-
);
|
|
1718
|
-
const validatedTools = input.toolsProvided
|
|
1719
|
-
? validateToolRefsForSessionPolicy({
|
|
1720
|
-
requested: input.tools ?? [],
|
|
1721
|
-
settings: runtimeSettings,
|
|
1722
|
-
allowedTools: sessionPolicyTools,
|
|
1723
|
-
message: "message tools may only narrow the session tool policy",
|
|
1724
|
-
})
|
|
1725
|
-
: [];
|
|
1726
|
-
const requestedTools = input.toolsProvided ? validatedTools : [];
|
|
1727
1754
|
await requireLimit(deps, {
|
|
1728
1755
|
accountId: grant.accountId,
|
|
1729
1756
|
workspaceId,
|
|
@@ -1757,10 +1784,9 @@ export async function acceptSessionUserMessage(
|
|
|
1757
1784
|
text: input.text,
|
|
1758
1785
|
turnInstructions: input.turnInstructions ?? null,
|
|
1759
1786
|
resources: requestedResources,
|
|
1760
|
-
tools: requestedTools,
|
|
1761
|
-
toolsProvided: input.toolsProvided,
|
|
1762
1787
|
model: input.model ?? null,
|
|
1763
1788
|
reasoningEffort: input.reasoningEffort ?? null,
|
|
1789
|
+
latencyMode: input.latencyMode ?? null,
|
|
1764
1790
|
reasoningEffortFallback: sessionReasoningEffort,
|
|
1765
1791
|
turnExecutionPolicy,
|
|
1766
1792
|
mcpCredentialUpdates,
|
|
@@ -1919,7 +1945,8 @@ export async function updateSessionMcpApprovalPolicy(
|
|
|
1919
1945
|
function toolPolicyAuditSnapshot(
|
|
1920
1946
|
session: Session,
|
|
1921
1947
|
tools: ToolRef[],
|
|
1922
|
-
|
|
1948
|
+
firstPartyMcpTools: FirstPartyMcpToolName[],
|
|
1949
|
+
policy = session.toolPolicy,
|
|
1923
1950
|
) {
|
|
1924
1951
|
// Tool policy refs contain only public server ids and the optional/strict
|
|
1925
1952
|
// execution mode; they never carry URLs, names, headers, credentials,
|
|
@@ -1951,6 +1978,8 @@ function toolPolicyAuditSnapshot(
|
|
|
1951
1978
|
.map((tool) => tool.id),
|
|
1952
1979
|
toolRefs,
|
|
1953
1980
|
toolCount: allToolRefs.length,
|
|
1981
|
+
firstPartyMcpTools: [...firstPartyMcpTools].sort(),
|
|
1982
|
+
firstPartyMcpToolCount: firstPartyMcpTools.length,
|
|
1954
1983
|
truncated: allToolRefs.length > toolRefs.length,
|
|
1955
1984
|
};
|
|
1956
1985
|
}
|
|
@@ -2004,10 +2033,14 @@ export async function updateSessionToolPolicy(
|
|
|
2004
2033
|
return withFirstPartyTools(validatedTools, runtimeSettings);
|
|
2005
2034
|
})()
|
|
2006
2035
|
: null;
|
|
2036
|
+
const explicitRequestedFirstPartyTools = explicitRequest
|
|
2037
|
+
? [...explicitRequest.firstPartyMcpTools]
|
|
2038
|
+
: null;
|
|
2007
2039
|
const workspaceDefaultTools = withFirstPartyTools(
|
|
2008
2040
|
withDefaultEnabledCapabilityMcpTools([], deps.settings, capabilityRuntimeSettings),
|
|
2009
2041
|
runtimeSettings,
|
|
2010
2042
|
);
|
|
2043
|
+
const workspaceDefaultFirstPartyTools = [...FIRST_PARTY_MCP_TOOL_NAMES];
|
|
2011
2044
|
const events = await appendSessionEventsWithLockedSessionUpdate(
|
|
2012
2045
|
deps.db,
|
|
2013
2046
|
grant.workspaceId,
|
|
@@ -2019,6 +2052,7 @@ export async function updateSessionToolPolicy(
|
|
|
2019
2052
|
}
|
|
2020
2053
|
|
|
2021
2054
|
let nextTools: ToolRef[];
|
|
2055
|
+
let nextFirstPartyMcpTools: FirstPartyMcpToolName[];
|
|
2022
2056
|
let nextPolicy: SessionToolPolicy;
|
|
2023
2057
|
if (session.parentSessionId) {
|
|
2024
2058
|
const parent = await context.getLockedSession(session.parentSessionId);
|
|
@@ -2036,6 +2070,9 @@ export async function updateSessionToolPolicy(
|
|
|
2036
2070
|
: parent.tools,
|
|
2037
2071
|
runtimeSettings,
|
|
2038
2072
|
);
|
|
2073
|
+
const parentFirstPartyMcpTools = [
|
|
2074
|
+
...(parent.firstPartyMcpTools ?? DEFAULT_FIRST_PARTY_MCP_TOOLS),
|
|
2075
|
+
];
|
|
2039
2076
|
if (requestedMode === "workspace_default") {
|
|
2040
2077
|
if (!parentTracksWorkspaceDefaults) {
|
|
2041
2078
|
throw new HTTPException(403, {
|
|
@@ -2044,6 +2081,7 @@ export async function updateSessionToolPolicy(
|
|
|
2044
2081
|
});
|
|
2045
2082
|
}
|
|
2046
2083
|
nextTools = parentEffective;
|
|
2084
|
+
nextFirstPartyMcpTools = parentFirstPartyMcpTools;
|
|
2047
2085
|
nextPolicy = {
|
|
2048
2086
|
mode: "workspace_default",
|
|
2049
2087
|
inheritedFromSessionId: parent.id,
|
|
@@ -2055,6 +2093,16 @@ export async function updateSessionToolPolicy(
|
|
|
2055
2093
|
parentEffective,
|
|
2056
2094
|
"session tools may only narrow the parent session tool policy",
|
|
2057
2095
|
);
|
|
2096
|
+
const parentFirstPartySet = new Set(parentFirstPartyMcpTools);
|
|
2097
|
+
const widenedFirstPartyTool = explicitRequestedFirstPartyTools!.find(
|
|
2098
|
+
(tool) => !parentFirstPartySet.has(tool),
|
|
2099
|
+
);
|
|
2100
|
+
if (widenedFirstPartyTool) {
|
|
2101
|
+
throw new HTTPException(403, {
|
|
2102
|
+
message: `session OpenGeni tools may only narrow the parent policy: ${widenedFirstPartyTool}`,
|
|
2103
|
+
});
|
|
2104
|
+
}
|
|
2105
|
+
nextFirstPartyMcpTools = explicitRequestedFirstPartyTools!;
|
|
2058
2106
|
nextPolicy = {
|
|
2059
2107
|
mode: "explicit",
|
|
2060
2108
|
inheritedFromSessionId: parent.id,
|
|
@@ -2063,20 +2111,29 @@ export async function updateSessionToolPolicy(
|
|
|
2063
2111
|
} else {
|
|
2064
2112
|
nextTools =
|
|
2065
2113
|
requestedMode === "workspace_default" ? workspaceDefaultTools : explicitRequestedTools!;
|
|
2114
|
+
nextFirstPartyMcpTools =
|
|
2115
|
+
requestedMode === "workspace_default"
|
|
2116
|
+
? workspaceDefaultFirstPartyTools
|
|
2117
|
+
: explicitRequestedFirstPartyTools!;
|
|
2066
2118
|
nextPolicy = { mode: requestedMode, inheritedFromSessionId: null };
|
|
2067
2119
|
}
|
|
2068
2120
|
|
|
2069
|
-
const currentPolicy = session.toolPolicy
|
|
2070
|
-
mode: "legacy" as const,
|
|
2071
|
-
inheritedFromSessionId: null,
|
|
2072
|
-
};
|
|
2121
|
+
const currentPolicy = session.toolPolicy;
|
|
2073
2122
|
// JSONB normalizes object-key order on the round trip, so plain
|
|
2074
2123
|
// JSON.stringify would turn an identical retry into a second mutation
|
|
2075
2124
|
// (and version bump) merely because the persisted key order differs from
|
|
2076
2125
|
// the request object. Compare canonical JSON instead.
|
|
2077
2126
|
const unchanged =
|
|
2078
|
-
stableJson({
|
|
2079
|
-
|
|
2127
|
+
stableJson({
|
|
2128
|
+
tools: session.tools,
|
|
2129
|
+
firstPartyMcpTools: session.firstPartyMcpTools ?? DEFAULT_FIRST_PARTY_MCP_TOOLS,
|
|
2130
|
+
policy: currentPolicy,
|
|
2131
|
+
}) ===
|
|
2132
|
+
stableJson({
|
|
2133
|
+
tools: nextTools,
|
|
2134
|
+
firstPartyMcpTools: nextFirstPartyMcpTools,
|
|
2135
|
+
policy: nextPolicy,
|
|
2136
|
+
});
|
|
2080
2137
|
if (unchanged) {
|
|
2081
2138
|
return { events: [] };
|
|
2082
2139
|
}
|
|
@@ -2087,8 +2144,18 @@ export async function updateSessionToolPolicy(
|
|
|
2087
2144
|
{
|
|
2088
2145
|
type: "session.tool_policy.updated" as const,
|
|
2089
2146
|
payload: {
|
|
2090
|
-
before: toolPolicyAuditSnapshot(
|
|
2091
|
-
|
|
2147
|
+
before: toolPolicyAuditSnapshot(
|
|
2148
|
+
session,
|
|
2149
|
+
session.tools,
|
|
2150
|
+
[...(session.firstPartyMcpTools ?? DEFAULT_FIRST_PARTY_MCP_TOOLS)],
|
|
2151
|
+
currentPolicy,
|
|
2152
|
+
),
|
|
2153
|
+
after: toolPolicyAuditSnapshot(
|
|
2154
|
+
session,
|
|
2155
|
+
nextTools,
|
|
2156
|
+
nextFirstPartyMcpTools,
|
|
2157
|
+
nextPolicy,
|
|
2158
|
+
),
|
|
2092
2159
|
version: nextVersion,
|
|
2093
2160
|
effectiveFrom: "next_attempt",
|
|
2094
2161
|
},
|
|
@@ -2096,6 +2163,7 @@ export async function updateSessionToolPolicy(
|
|
|
2096
2163
|
],
|
|
2097
2164
|
update: {
|
|
2098
2165
|
tools: nextTools,
|
|
2166
|
+
firstPartyMcpTools: nextFirstPartyMcpTools,
|
|
2099
2167
|
toolPolicy: nextPolicy,
|
|
2100
2168
|
toolPolicyVersion: nextVersion,
|
|
2101
2169
|
expectedToolPolicyVersion: request.expectedVersion,
|
package/src/domain/slack-bot.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
|
|
3
|
-
OPENGENI_SLACK_BOT_REQUIRED_SCOPES,
|
|
4
3
|
OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
|
|
5
4
|
OPENGENI_SLACK_BOT_SESSION_METADATA_KEY,
|
|
6
5
|
OpenGeniSlackBotConnectionMetadata,
|
|
6
|
+
areOpenGeniSlackBotScopesAccepted,
|
|
7
7
|
type AccessGrant,
|
|
8
8
|
type ConnectionMetadata,
|
|
9
9
|
type OpenGeniSlackBotConnectionMetadata as OpenGeniSlackBotMetadata,
|
|
@@ -30,15 +30,13 @@ export function isOpenGeniSlackBotConnection(
|
|
|
30
30
|
Pick<ConnectionMetadataWithVerification, "verifiedInstallAt" | "verifiedInstallVersion">
|
|
31
31
|
>,
|
|
32
32
|
): boolean {
|
|
33
|
-
const granted = new Set(connection.grantedScopes);
|
|
34
33
|
return (
|
|
35
34
|
connection.verifiedInstallAt != null &&
|
|
36
35
|
connection.verifiedInstallVersion === connection.version &&
|
|
37
36
|
connection.subjectId === null &&
|
|
38
37
|
connection.providerDomain === "slack.com" &&
|
|
39
38
|
connection.kind === "app_install" &&
|
|
40
|
-
|
|
41
|
-
OPENGENI_SLACK_BOT_REQUIRED_SCOPES.every((scope) => granted.has(scope)) &&
|
|
39
|
+
areOpenGeniSlackBotScopesAccepted(connection.grantedScopes) &&
|
|
42
40
|
openGeniSlackBotMetadata(connection.metadata)?.credentialRole ===
|
|
43
41
|
OPENGENI_SLACK_BOT_CREDENTIAL_ROLE
|
|
44
42
|
);
|