@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/core",
3
- "version": "0.12.7",
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.11",
39
- "@opengeni/contracts": "^0.22.0",
40
- "@opengeni/db": "^0.14.0",
41
- "@opengeni/documents": "^0.2.42",
42
- "@opengeni/events": "^0.3.33",
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.1",
45
- "@opengeni/storage": "^0.2.35",
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(deps, context, input.targetSessionId, "session.control");
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?: SessionToolPolicy | null;
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 ?? { mode: "legacy" as const, inheritedFromSessionId: null };
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
- input.turnToolsProvided === true
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
- ...(session.toolPolicy ? { toolPolicy: session.toolPolicy } : {}),
208
+ toolPolicy: session.toolPolicy,
218
209
  sessionTools: session.tools,
219
210
  availableMcpServerIds: availableIds,
220
211
  defaultMcpServerIds: workspaceDefaultServerIds,
@@ -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[] | null {
141
+ ): FirstPartyMcpToolName[] {
142
142
  if (requested !== undefined) return [...requested];
143
- if (parentStored === undefined) return null;
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?: SessionToolPolicy;
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?: FirstPartyMcpToolName[] | null;
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
- ...(input.toolPolicy ? { toolPolicy: input.toolPolicy } : {}),
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 ?? null,
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
- ...(input.toolPolicy ? { toolPolicy: input.toolPolicy } : {}),
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 ?? null,
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?: SessionToolPolicy;
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
- ...(input.toolPolicy ? { toolPolicy: input.toolPolicy } : {}),
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 resolves to the fixed minimal default at execution.
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) => !effectiveTools.includes(name as FirstPartyMcpToolName),
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
- * absent `tools` key so execution inherits the session policy; an explicit
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
- policy = session.toolPolicy ?? { mode: "legacy" as const, inheritedFromSessionId: null },
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({ tools: session.tools, policy: currentPolicy }) ===
2079
- stableJson({ tools: nextTools, policy: nextPolicy });
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(session, session.tools, currentPolicy),
2091
- after: toolPolicyAuditSnapshot(session, nextTools, nextPolicy),
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,