@opengeni/core 0.12.7 → 0.12.10
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 +25 -15
- package/dist/index.js +65 -62
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/application/session-commands.ts +19 -8
- package/src/domain/session-tool-policy.ts +5 -14
- package/src/domain/sessions.ts +65 -68
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/core",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.10",
|
|
4
4
|
"description": "OpenGeni framework-agnostic core: the domain, access, and billing layers (neutral access, off-HTTP V2 surface). Behavior-preserving extraction from apps/api — keeps Hono's HTTPException for error throwing (typed-errors cleanup deferred).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -35,14 +35,14 @@
|
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
37
37
|
"@opengeni/codex": "^0.2.7",
|
|
38
|
-
"@opengeni/config": "^0.7.
|
|
39
|
-
"@opengeni/contracts": "^0.
|
|
40
|
-
"@opengeni/db": "^0.14.
|
|
41
|
-
"@opengeni/documents": "^0.2.
|
|
42
|
-
"@opengeni/events": "^0.3.
|
|
38
|
+
"@opengeni/config": "^0.7.13",
|
|
39
|
+
"@opengeni/contracts": "^0.23.0",
|
|
40
|
+
"@opengeni/db": "^0.14.3",
|
|
41
|
+
"@opengeni/documents": "^0.2.45",
|
|
42
|
+
"@opengeni/events": "^0.3.36",
|
|
43
43
|
"@opengeni/observability": "^0.3.0",
|
|
44
|
-
"@opengeni/runtime": "^0.14.
|
|
45
|
-
"@opengeni/storage": "^0.2.
|
|
44
|
+
"@opengeni/runtime": "^0.14.3",
|
|
45
|
+
"@opengeni/storage": "^0.2.37",
|
|
46
46
|
"hono": "^4.12.18"
|
|
47
47
|
},
|
|
48
48
|
"engines": {
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
AccessGrant,
|
|
8
8
|
SessionAuthorizationOperation,
|
|
9
9
|
SessionAuthorizationPort,
|
|
10
|
+
SessionAuthorizationSurface,
|
|
10
11
|
SessionCommandReceipt,
|
|
11
12
|
SessionControlRequest,
|
|
12
13
|
SessionControlResponse,
|
|
@@ -55,6 +56,8 @@ export type HumanSessionCommandContext = {
|
|
|
55
56
|
workspaceId: string;
|
|
56
57
|
sessionId: string;
|
|
57
58
|
subjectId: string;
|
|
59
|
+
/** See AgentSessionCommandContext.authorizationSurface. */
|
|
60
|
+
authorizationSurface?: SessionAuthorizationSurface;
|
|
58
61
|
};
|
|
59
62
|
|
|
60
63
|
export type AgentSessionCommandContext = {
|
|
@@ -65,6 +68,13 @@ export type AgentSessionCommandContext = {
|
|
|
65
68
|
callerTurnId: string;
|
|
66
69
|
callerAttemptId: string;
|
|
67
70
|
callerExecutionGeneration: number;
|
|
71
|
+
/**
|
|
72
|
+
* The trusted adapter surface that owns this command's one authorization
|
|
73
|
+
* decision. Direct core callers omit it and retain the canonical `core`
|
|
74
|
+
* surface; adapters that delegate the complete command set it explicitly so
|
|
75
|
+
* they do not authorize once at the edge and then repeat the host call here.
|
|
76
|
+
*/
|
|
77
|
+
authorizationSurface?: SessionAuthorizationSurface;
|
|
68
78
|
};
|
|
69
79
|
|
|
70
80
|
type SessionAuthorizationCommandDeps = {
|
|
@@ -104,7 +114,7 @@ async function authorizeHumanSessionCommand(
|
|
|
104
114
|
return await requireSessionAuthorization(deps, humanAccessGrant(context), {
|
|
105
115
|
sessionId: context.sessionId,
|
|
106
116
|
operation,
|
|
107
|
-
surface: "core",
|
|
117
|
+
surface: context.authorizationSurface ?? "core",
|
|
108
118
|
});
|
|
109
119
|
}
|
|
110
120
|
|
|
@@ -117,7 +127,7 @@ async function authorizeAgentSessionCommand(
|
|
|
117
127
|
return await requireSessionAuthorization(deps, agentAccessGrant(context), {
|
|
118
128
|
sessionId: targetSessionId,
|
|
119
129
|
operation,
|
|
120
|
-
surface: "core",
|
|
130
|
+
surface: context.authorizationSurface ?? "core",
|
|
121
131
|
});
|
|
122
132
|
}
|
|
123
133
|
|
|
@@ -339,7 +349,12 @@ export async function controlAgentSessionWorkstream(
|
|
|
339
349
|
reason?: string | null;
|
|
340
350
|
},
|
|
341
351
|
) {
|
|
342
|
-
await authorizeAgentSessionCommand(
|
|
352
|
+
const authorization = await authorizeAgentSessionCommand(
|
|
353
|
+
deps,
|
|
354
|
+
context,
|
|
355
|
+
input.targetSessionId,
|
|
356
|
+
"session.control",
|
|
357
|
+
);
|
|
343
358
|
const result = await withWorkspaceRls(deps.db, context.workspaceId, (scoped) =>
|
|
344
359
|
scoped.transaction((tx) =>
|
|
345
360
|
mutateSessionControlInTransaction(tx as unknown as Database, {
|
|
@@ -358,7 +373,7 @@ export async function controlAgentSessionWorkstream(
|
|
|
358
373
|
]);
|
|
359
374
|
await publishWorkspaceControlEvent(deps, context.workspaceId, result.workspaceControlEventId);
|
|
360
375
|
await requestControlWakeDispatch(deps, result.wakeCount);
|
|
361
|
-
return result;
|
|
376
|
+
return { ...result, authorization };
|
|
362
377
|
}
|
|
363
378
|
|
|
364
379
|
function receipt(row: SessionCommandReceiptRow): SessionCommandReceipt {
|
|
@@ -384,8 +399,6 @@ function composerDraft(
|
|
|
384
399
|
revision: row.revision,
|
|
385
400
|
text: row.text,
|
|
386
401
|
resources: row.resources as ComposerDraft["resources"],
|
|
387
|
-
tools: row.tools as ComposerDraft["tools"],
|
|
388
|
-
toolsProvided: row.toolsProvided,
|
|
389
402
|
model: row.model,
|
|
390
403
|
reasoningEffort: row.reasoningEffort as ComposerDraft["reasoningEffort"],
|
|
391
404
|
sourceTurnId: row.sourceTurnId,
|
|
@@ -648,8 +661,6 @@ export async function getHumanComposerDraft(
|
|
|
648
661
|
revision: 0,
|
|
649
662
|
text: "",
|
|
650
663
|
resources: [],
|
|
651
|
-
tools: [],
|
|
652
|
-
toolsProvided: false,
|
|
653
664
|
model: session.model,
|
|
654
665
|
reasoningEffort: reasoningEffortForMetadata(session.metadata, "medium"),
|
|
655
666
|
sourceTurnId: null,
|
|
@@ -21,11 +21,8 @@ export type ResolvedSessionToolPolicy = {
|
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
export type SessionToolPolicyInput = {
|
|
24
|
-
toolPolicy
|
|
24
|
+
toolPolicy: SessionToolPolicy;
|
|
25
25
|
sessionTools: ToolRef[];
|
|
26
|
-
turnTools?: ToolRef[];
|
|
27
|
-
/** Undefined preserves the legacy merge path for pre-provenance callers. */
|
|
28
|
-
turnToolsProvided?: boolean;
|
|
29
26
|
availableMcpServerIds: Iterable<string>;
|
|
30
27
|
/** Current omitted-tools defaults, intentionally narrower than all servers. */
|
|
31
28
|
defaultMcpServerIds?: Iterable<string>;
|
|
@@ -55,7 +52,7 @@ function projectIds(ids: readonly string[]): { ids: string[]; truncated: boolean
|
|
|
55
52
|
* `defaultMcpServerIds` is the capability-only omitted-tools set.
|
|
56
53
|
*/
|
|
57
54
|
export function resolveSessionToolPolicy(input: SessionToolPolicyInput): ResolvedSessionToolPolicy {
|
|
58
|
-
const policy = input.toolPolicy
|
|
55
|
+
const policy = input.toolPolicy;
|
|
59
56
|
const availableIds = new Set(input.availableMcpServerIds);
|
|
60
57
|
// Never infer omitted-tools defaults from the full runtime registry: static
|
|
61
58
|
// MCPs are explicit-only unless they are capability-derived defaults.
|
|
@@ -64,14 +61,8 @@ export function resolveSessionToolPolicy(input: SessionToolPolicyInput): Resolve
|
|
|
64
61
|
availableIds.has(id),
|
|
65
62
|
);
|
|
66
63
|
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;
|
|
64
|
+
const selectedRefs = mergeToolRefs([], input.sessionTools);
|
|
65
|
+
const tracksWorkspaceDefaults = policy.mode === "workspace_default";
|
|
75
66
|
|
|
76
67
|
// Optional capability refs are a historical materialization of a
|
|
77
68
|
// workspace-default selection. They may outlive an installation or its
|
|
@@ -214,7 +205,7 @@ export function sessionWithEffectiveToolPolicy(
|
|
|
214
205
|
return {
|
|
215
206
|
...session,
|
|
216
207
|
effectiveToolPolicy: resolveSessionToolPolicy({
|
|
217
|
-
|
|
208
|
+
toolPolicy: session.toolPolicy,
|
|
218
209
|
sessionTools: session.tools,
|
|
219
210
|
availableMcpServerIds: availableIds,
|
|
220
211
|
defaultMcpServerIds: workspaceDefaultServerIds,
|
package/src/domain/sessions.ts
CHANGED
|
@@ -10,6 +10,7 @@ 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,
|
|
@@ -105,7 +106,6 @@ import {
|
|
|
105
106
|
validateFileResources,
|
|
106
107
|
validateGitHubRepositorySelection,
|
|
107
108
|
validateToolRefs,
|
|
108
|
-
validateToolRefsForSessionPolicy,
|
|
109
109
|
withDefaultEnabledCapabilityMcpTools,
|
|
110
110
|
} from "./resources";
|
|
111
111
|
|
|
@@ -138,9 +138,9 @@ export class SessionSpawnDeniedError extends Error {
|
|
|
138
138
|
export function resolveFirstPartyMcpToolsForCreate(
|
|
139
139
|
requested: FirstPartyMcpToolName[] | undefined,
|
|
140
140
|
parentStored: FirstPartyMcpToolName[] | null | undefined,
|
|
141
|
-
): FirstPartyMcpToolName[]
|
|
141
|
+
): FirstPartyMcpToolName[] {
|
|
142
142
|
if (requested !== undefined) return [...requested];
|
|
143
|
-
if (parentStored === undefined) return
|
|
143
|
+
if (parentStored === undefined) return [...DEFAULT_FIRST_PARTY_MCP_TOOLS];
|
|
144
144
|
return [...(parentStored ?? DEFAULT_FIRST_PARTY_MCP_TOOLS)];
|
|
145
145
|
}
|
|
146
146
|
|
|
@@ -514,7 +514,7 @@ export async function createAndStartSession(input: {
|
|
|
514
514
|
// Public admission always supplies provenance; optional keeps internal
|
|
515
515
|
// callers that predate durable tool-policy provenance source-compatible
|
|
516
516
|
// during the rolling deploy.
|
|
517
|
-
toolPolicy
|
|
517
|
+
toolPolicy: SessionToolPolicy;
|
|
518
518
|
clientEventId?: string;
|
|
519
519
|
model: string;
|
|
520
520
|
reasoningEffort: Settings["openaiReasoningEffort"];
|
|
@@ -541,7 +541,7 @@ export async function createAndStartSession(input: {
|
|
|
541
541
|
firstPartyMcpPermissions?: Permission[] | null;
|
|
542
542
|
// Model-visible first-party tool names. Authorization remains controlled by
|
|
543
543
|
// firstPartyMcpPermissions and the target resource checks.
|
|
544
|
-
firstPartyMcpTools
|
|
544
|
+
firstPartyMcpTools: FirstPartyMcpToolName[];
|
|
545
545
|
// Encrypted DB rows plus matching safe metadata for create-time per-session
|
|
546
546
|
// MCP servers. Metadata is the only shape emitted in events/responses.
|
|
547
547
|
mcpServers?: CreateSessionMcpServerInput[];
|
|
@@ -602,7 +602,7 @@ export async function createAndStartSession(input: {
|
|
|
602
602
|
resources: input.resources,
|
|
603
603
|
skills: input.skills ?? [],
|
|
604
604
|
tools: input.tools,
|
|
605
|
-
|
|
605
|
+
toolPolicy: input.toolPolicy,
|
|
606
606
|
metadata: sessionMetadata,
|
|
607
607
|
...(input.createdBy ? { createdBy: input.createdBy } : {}),
|
|
608
608
|
...(input.createdByContext ? { createdByContext: input.createdByContext } : {}),
|
|
@@ -613,7 +613,7 @@ export async function createAndStartSession(input: {
|
|
|
613
613
|
rigId: input.rigId ?? null,
|
|
614
614
|
rigVersionId: input.rigVersionId ?? null,
|
|
615
615
|
firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
|
|
616
|
-
firstPartyMcpTools: input.firstPartyMcpTools
|
|
616
|
+
firstPartyMcpTools: input.firstPartyMcpTools,
|
|
617
617
|
instructions: input.instructions ?? null,
|
|
618
618
|
parentSessionId: input.parentSessionId ?? null,
|
|
619
619
|
createIdempotencyKey: input.createIdempotencyKey,
|
|
@@ -647,7 +647,7 @@ export async function createAndStartSession(input: {
|
|
|
647
647
|
resources: input.resources,
|
|
648
648
|
skills: input.skills ?? [],
|
|
649
649
|
tools: input.tools,
|
|
650
|
-
|
|
650
|
+
toolPolicy: input.toolPolicy,
|
|
651
651
|
metadata: sessionMetadata,
|
|
652
652
|
...(input.createdBy ? { createdBy: input.createdBy } : {}),
|
|
653
653
|
...(input.createdByContext ? { createdByContext: input.createdByContext } : {}),
|
|
@@ -658,7 +658,7 @@ export async function createAndStartSession(input: {
|
|
|
658
658
|
rigId: input.rigId ?? null,
|
|
659
659
|
rigVersionId: input.rigVersionId ?? null,
|
|
660
660
|
firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
|
|
661
|
-
firstPartyMcpTools: input.firstPartyMcpTools
|
|
661
|
+
firstPartyMcpTools: input.firstPartyMcpTools,
|
|
662
662
|
instructions: input.instructions ?? null,
|
|
663
663
|
parentSessionId: input.parentSessionId ?? null,
|
|
664
664
|
sandboxGroupId: input.sandboxGroupId ?? null,
|
|
@@ -692,7 +692,7 @@ async function finishStartSession(
|
|
|
692
692
|
turnInstructions?: string | null;
|
|
693
693
|
resources: ResourceRef[];
|
|
694
694
|
tools: ToolRef[];
|
|
695
|
-
toolPolicy
|
|
695
|
+
toolPolicy: SessionToolPolicy;
|
|
696
696
|
clientEventId?: string;
|
|
697
697
|
model: string;
|
|
698
698
|
reasoningEffort: Settings["openaiReasoningEffort"];
|
|
@@ -750,7 +750,7 @@ async function finishStartSession(
|
|
|
750
750
|
reasoningEffortFallback: input.reasoningEffort,
|
|
751
751
|
turnExecutionPolicy: input.turnExecutionPolicy,
|
|
752
752
|
createdEventPayload: {
|
|
753
|
-
|
|
753
|
+
toolPolicy: input.toolPolicy,
|
|
754
754
|
...(input.variableSet
|
|
755
755
|
? { variableSetId: input.variableSet.id, variableSetName: input.variableSet.name }
|
|
756
756
|
: {}),
|
|
@@ -920,8 +920,6 @@ export async function postUserMessageTurn(input: {
|
|
|
920
920
|
text: string;
|
|
921
921
|
turnInstructions?: string | null;
|
|
922
922
|
resources: ResourceRef[];
|
|
923
|
-
tools: ToolRef[];
|
|
924
|
-
toolsProvided: boolean;
|
|
925
923
|
model?: string | null;
|
|
926
924
|
reasoningEffort?: Settings["openaiReasoningEffort"] | null;
|
|
927
925
|
clientEventId?: string;
|
|
@@ -965,8 +963,6 @@ export async function postUserMessageTurn(input: {
|
|
|
965
963
|
text: input.text,
|
|
966
964
|
turnInstructions: input.turnInstructions ?? null,
|
|
967
965
|
resources: input.resources,
|
|
968
|
-
tools: input.tools,
|
|
969
|
-
toolsProvided: input.toolsProvided,
|
|
970
966
|
model: requestedModel,
|
|
971
967
|
reasoningEffort: requestedReasoningEffort,
|
|
972
968
|
reasoningEffortFallback: input.reasoningEffortFallback ?? settings.openaiReasoningEffort,
|
|
@@ -1298,15 +1294,14 @@ export async function createSessionForRequest(
|
|
|
1298
1294
|
}
|
|
1299
1295
|
// Tool visibility is independent from permission authority. A child that
|
|
1300
1296
|
// omits the field inherits the parent's exact effective selection; a
|
|
1301
|
-
// top-level omission
|
|
1297
|
+
// top-level omission selects the complete catalog.
|
|
1302
1298
|
const firstPartyMcpTools = resolveFirstPartyMcpToolsForCreate(
|
|
1303
1299
|
payload.firstPartyMcpTools,
|
|
1304
1300
|
parentSession ? parentSession.firstPartyMcpTools : undefined,
|
|
1305
1301
|
);
|
|
1306
1302
|
if (payload.goal) {
|
|
1307
|
-
const effectiveTools = firstPartyMcpTools ?? [...DEFAULT_FIRST_PARTY_MCP_TOOLS];
|
|
1308
1303
|
const missingGoalTools = ["goal_update", "goal_complete", "goal_pause"].filter(
|
|
1309
|
-
(name) => !
|
|
1304
|
+
(name) => !firstPartyMcpTools.includes(name as FirstPartyMcpToolName),
|
|
1310
1305
|
);
|
|
1311
1306
|
if (missingGoalTools.length > 0) {
|
|
1312
1307
|
throw new HTTPException(422, {
|
|
@@ -1636,8 +1631,7 @@ export async function createSessionForRequest(
|
|
|
1636
1631
|
* `POST /sessions/:id/events` and the first-party MCP `session_send_message`
|
|
1637
1632
|
* tool: resource/tool validation, usage limits, the locked append + turn
|
|
1638
1633
|
* enqueue, and usage recording. `toolsProvided: false` durably preserves an
|
|
1639
|
-
*
|
|
1640
|
-
* empty array is a deliberate per-turn narrowing.
|
|
1634
|
+
* Tool selection is durable session state and never rides a follow-up prompt.
|
|
1641
1635
|
*/
|
|
1642
1636
|
export async function acceptSessionUserMessage(
|
|
1643
1637
|
deps: AcceptSessionUserMessageDependencies,
|
|
@@ -1648,8 +1642,6 @@ export async function acceptSessionUserMessage(
|
|
|
1648
1642
|
text: string;
|
|
1649
1643
|
turnInstructions?: string | null;
|
|
1650
1644
|
resources?: ResourceRef[];
|
|
1651
|
-
tools?: ToolRef[];
|
|
1652
|
-
toolsProvided: boolean;
|
|
1653
1645
|
model?: string | null;
|
|
1654
1646
|
reasoningEffort?: ReasoningEffort | null;
|
|
1655
1647
|
clientEventId?: string;
|
|
@@ -1660,23 +1652,12 @@ export async function acceptSessionUserMessage(
|
|
|
1660
1652
|
expectedDraftRevision?: number | null;
|
|
1661
1653
|
},
|
|
1662
1654
|
): 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
1655
|
const { settings, db, bus, workflowClient, objectStorage } = deps;
|
|
1670
1656
|
await requireSessionAuthorization(deps, grant, {
|
|
1671
1657
|
sessionId,
|
|
1672
1658
|
operation: input.delivery === "steer" ? "session.steer" : "session.append",
|
|
1673
1659
|
surface: "core",
|
|
1674
1660
|
});
|
|
1675
|
-
const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
1676
|
-
db,
|
|
1677
|
-
workspaceId,
|
|
1678
|
-
settings,
|
|
1679
|
-
);
|
|
1680
1661
|
// Hoisted above requireLimit so the codex-billed predicate can resolve the
|
|
1681
1662
|
// turn's effective model (a follow-up turn inherits the session's model). A
|
|
1682
1663
|
// pure read with no side effects.
|
|
@@ -1699,31 +1680,7 @@ export async function acceptSessionUserMessage(
|
|
|
1699
1680
|
reasoningEffort: effectiveReasoningEffort,
|
|
1700
1681
|
reasoningSource: input.reasoningEffort == null ? "session" : "explicit",
|
|
1701
1682
|
});
|
|
1702
|
-
const runtimeSettings = settingsWithSessionMcpServerMetadata(
|
|
1703
|
-
capabilityRuntimeSettings,
|
|
1704
|
-
existingSession.mcpServers,
|
|
1705
|
-
);
|
|
1706
1683
|
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
1684
|
await requireLimit(deps, {
|
|
1728
1685
|
accountId: grant.accountId,
|
|
1729
1686
|
workspaceId,
|
|
@@ -1757,8 +1714,6 @@ export async function acceptSessionUserMessage(
|
|
|
1757
1714
|
text: input.text,
|
|
1758
1715
|
turnInstructions: input.turnInstructions ?? null,
|
|
1759
1716
|
resources: requestedResources,
|
|
1760
|
-
tools: requestedTools,
|
|
1761
|
-
toolsProvided: input.toolsProvided,
|
|
1762
1717
|
model: input.model ?? null,
|
|
1763
1718
|
reasoningEffort: input.reasoningEffort ?? null,
|
|
1764
1719
|
reasoningEffortFallback: sessionReasoningEffort,
|
|
@@ -1919,7 +1874,8 @@ export async function updateSessionMcpApprovalPolicy(
|
|
|
1919
1874
|
function toolPolicyAuditSnapshot(
|
|
1920
1875
|
session: Session,
|
|
1921
1876
|
tools: ToolRef[],
|
|
1922
|
-
|
|
1877
|
+
firstPartyMcpTools: FirstPartyMcpToolName[],
|
|
1878
|
+
policy = session.toolPolicy,
|
|
1923
1879
|
) {
|
|
1924
1880
|
// Tool policy refs contain only public server ids and the optional/strict
|
|
1925
1881
|
// execution mode; they never carry URLs, names, headers, credentials,
|
|
@@ -1951,6 +1907,8 @@ function toolPolicyAuditSnapshot(
|
|
|
1951
1907
|
.map((tool) => tool.id),
|
|
1952
1908
|
toolRefs,
|
|
1953
1909
|
toolCount: allToolRefs.length,
|
|
1910
|
+
firstPartyMcpTools: [...firstPartyMcpTools].sort(),
|
|
1911
|
+
firstPartyMcpToolCount: firstPartyMcpTools.length,
|
|
1954
1912
|
truncated: allToolRefs.length > toolRefs.length,
|
|
1955
1913
|
};
|
|
1956
1914
|
}
|
|
@@ -2004,10 +1962,14 @@ export async function updateSessionToolPolicy(
|
|
|
2004
1962
|
return withFirstPartyTools(validatedTools, runtimeSettings);
|
|
2005
1963
|
})()
|
|
2006
1964
|
: null;
|
|
1965
|
+
const explicitRequestedFirstPartyTools = explicitRequest
|
|
1966
|
+
? [...explicitRequest.firstPartyMcpTools]
|
|
1967
|
+
: null;
|
|
2007
1968
|
const workspaceDefaultTools = withFirstPartyTools(
|
|
2008
1969
|
withDefaultEnabledCapabilityMcpTools([], deps.settings, capabilityRuntimeSettings),
|
|
2009
1970
|
runtimeSettings,
|
|
2010
1971
|
);
|
|
1972
|
+
const workspaceDefaultFirstPartyTools = [...FIRST_PARTY_MCP_TOOL_NAMES];
|
|
2011
1973
|
const events = await appendSessionEventsWithLockedSessionUpdate(
|
|
2012
1974
|
deps.db,
|
|
2013
1975
|
grant.workspaceId,
|
|
@@ -2019,6 +1981,7 @@ export async function updateSessionToolPolicy(
|
|
|
2019
1981
|
}
|
|
2020
1982
|
|
|
2021
1983
|
let nextTools: ToolRef[];
|
|
1984
|
+
let nextFirstPartyMcpTools: FirstPartyMcpToolName[];
|
|
2022
1985
|
let nextPolicy: SessionToolPolicy;
|
|
2023
1986
|
if (session.parentSessionId) {
|
|
2024
1987
|
const parent = await context.getLockedSession(session.parentSessionId);
|
|
@@ -2036,6 +1999,9 @@ export async function updateSessionToolPolicy(
|
|
|
2036
1999
|
: parent.tools,
|
|
2037
2000
|
runtimeSettings,
|
|
2038
2001
|
);
|
|
2002
|
+
const parentFirstPartyMcpTools = [
|
|
2003
|
+
...(parent.firstPartyMcpTools ?? DEFAULT_FIRST_PARTY_MCP_TOOLS),
|
|
2004
|
+
];
|
|
2039
2005
|
if (requestedMode === "workspace_default") {
|
|
2040
2006
|
if (!parentTracksWorkspaceDefaults) {
|
|
2041
2007
|
throw new HTTPException(403, {
|
|
@@ -2044,6 +2010,7 @@ export async function updateSessionToolPolicy(
|
|
|
2044
2010
|
});
|
|
2045
2011
|
}
|
|
2046
2012
|
nextTools = parentEffective;
|
|
2013
|
+
nextFirstPartyMcpTools = parentFirstPartyMcpTools;
|
|
2047
2014
|
nextPolicy = {
|
|
2048
2015
|
mode: "workspace_default",
|
|
2049
2016
|
inheritedFromSessionId: parent.id,
|
|
@@ -2055,6 +2022,16 @@ export async function updateSessionToolPolicy(
|
|
|
2055
2022
|
parentEffective,
|
|
2056
2023
|
"session tools may only narrow the parent session tool policy",
|
|
2057
2024
|
);
|
|
2025
|
+
const parentFirstPartySet = new Set(parentFirstPartyMcpTools);
|
|
2026
|
+
const widenedFirstPartyTool = explicitRequestedFirstPartyTools!.find(
|
|
2027
|
+
(tool) => !parentFirstPartySet.has(tool),
|
|
2028
|
+
);
|
|
2029
|
+
if (widenedFirstPartyTool) {
|
|
2030
|
+
throw new HTTPException(403, {
|
|
2031
|
+
message: `session OpenGeni tools may only narrow the parent policy: ${widenedFirstPartyTool}`,
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
nextFirstPartyMcpTools = explicitRequestedFirstPartyTools!;
|
|
2058
2035
|
nextPolicy = {
|
|
2059
2036
|
mode: "explicit",
|
|
2060
2037
|
inheritedFromSessionId: parent.id,
|
|
@@ -2063,20 +2040,29 @@ export async function updateSessionToolPolicy(
|
|
|
2063
2040
|
} else {
|
|
2064
2041
|
nextTools =
|
|
2065
2042
|
requestedMode === "workspace_default" ? workspaceDefaultTools : explicitRequestedTools!;
|
|
2043
|
+
nextFirstPartyMcpTools =
|
|
2044
|
+
requestedMode === "workspace_default"
|
|
2045
|
+
? workspaceDefaultFirstPartyTools
|
|
2046
|
+
: explicitRequestedFirstPartyTools!;
|
|
2066
2047
|
nextPolicy = { mode: requestedMode, inheritedFromSessionId: null };
|
|
2067
2048
|
}
|
|
2068
2049
|
|
|
2069
|
-
const currentPolicy = session.toolPolicy
|
|
2070
|
-
mode: "legacy" as const,
|
|
2071
|
-
inheritedFromSessionId: null,
|
|
2072
|
-
};
|
|
2050
|
+
const currentPolicy = session.toolPolicy;
|
|
2073
2051
|
// JSONB normalizes object-key order on the round trip, so plain
|
|
2074
2052
|
// JSON.stringify would turn an identical retry into a second mutation
|
|
2075
2053
|
// (and version bump) merely because the persisted key order differs from
|
|
2076
2054
|
// the request object. Compare canonical JSON instead.
|
|
2077
2055
|
const unchanged =
|
|
2078
|
-
stableJson({
|
|
2079
|
-
|
|
2056
|
+
stableJson({
|
|
2057
|
+
tools: session.tools,
|
|
2058
|
+
firstPartyMcpTools: session.firstPartyMcpTools ?? DEFAULT_FIRST_PARTY_MCP_TOOLS,
|
|
2059
|
+
policy: currentPolicy,
|
|
2060
|
+
}) ===
|
|
2061
|
+
stableJson({
|
|
2062
|
+
tools: nextTools,
|
|
2063
|
+
firstPartyMcpTools: nextFirstPartyMcpTools,
|
|
2064
|
+
policy: nextPolicy,
|
|
2065
|
+
});
|
|
2080
2066
|
if (unchanged) {
|
|
2081
2067
|
return { events: [] };
|
|
2082
2068
|
}
|
|
@@ -2087,8 +2073,18 @@ export async function updateSessionToolPolicy(
|
|
|
2087
2073
|
{
|
|
2088
2074
|
type: "session.tool_policy.updated" as const,
|
|
2089
2075
|
payload: {
|
|
2090
|
-
before: toolPolicyAuditSnapshot(
|
|
2091
|
-
|
|
2076
|
+
before: toolPolicyAuditSnapshot(
|
|
2077
|
+
session,
|
|
2078
|
+
session.tools,
|
|
2079
|
+
[...(session.firstPartyMcpTools ?? DEFAULT_FIRST_PARTY_MCP_TOOLS)],
|
|
2080
|
+
currentPolicy,
|
|
2081
|
+
),
|
|
2082
|
+
after: toolPolicyAuditSnapshot(
|
|
2083
|
+
session,
|
|
2084
|
+
nextTools,
|
|
2085
|
+
nextFirstPartyMcpTools,
|
|
2086
|
+
nextPolicy,
|
|
2087
|
+
),
|
|
2092
2088
|
version: nextVersion,
|
|
2093
2089
|
effectiveFrom: "next_attempt",
|
|
2094
2090
|
},
|
|
@@ -2096,6 +2092,7 @@ export async function updateSessionToolPolicy(
|
|
|
2096
2092
|
],
|
|
2097
2093
|
update: {
|
|
2098
2094
|
tools: nextTools,
|
|
2095
|
+
firstPartyMcpTools: nextFirstPartyMcpTools,
|
|
2099
2096
|
toolPolicy: nextPolicy,
|
|
2100
2097
|
toolPolicyVersion: nextVersion,
|
|
2101
2098
|
expectedToolPolicyVersion: request.expectedVersion,
|