@opengeni/core 0.12.5 → 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.5",
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.10",
39
- "@opengeni/contracts": "^0.21.0",
40
- "@opengeni/db": "^0.13.4",
41
- "@opengeni/documents": "^0.2.41",
42
- "@opengeni/events": "^0.3.32",
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.0",
45
- "@opengeni/storage": "^0.2.34",
44
+ "@opengeni/runtime": "^0.14.3",
45
+ "@opengeni/storage": "^0.2.37",
46
46
  "hono": "^4.12.18"
47
47
  },
48
48
  "engines": {
@@ -236,6 +236,9 @@ async function delegatedAccessContext(
236
236
  metadata: {
237
237
  delegated: true,
238
238
  ...(payload.sessionId ? { sessionId: payload.sessionId } : {}),
239
+ ...(payload.firstPartyMcpTools !== undefined
240
+ ? { firstPartyMcpTools: payload.firstPartyMcpTools }
241
+ : {}),
239
242
  // Caller identity: the turn that minted this token. Tools classify the
240
243
  // CALLER from this instead of re-reading the live active pointer.
241
244
  ...(payload.turnId ? { turnId: payload.turnId } : {}),
@@ -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,
@@ -5,6 +5,7 @@ import type {
5
5
  GitHubAppApiPort,
6
6
  ScheduledTask,
7
7
  SessionAuthorizationPort,
8
+ TurnInitiator,
8
9
  } from "@opengeni/contracts";
9
10
  import type { Database } from "@opengeni/db";
10
11
  import type { DocumentServices } from "@opengeni/documents";
@@ -53,8 +54,9 @@ export type SessionWorkflowClient = {
53
54
  deleteScheduledTaskSchedule: (input: { temporalScheduleId: string }) => Promise<void>;
54
55
  triggerScheduledTask: (input: {
55
56
  task: ScheduledTask;
56
- agentRunUsageIdempotencyKey?: string;
57
- triggerWorkflowId?: string;
57
+ agentRunUsageIdempotencyKey: string;
58
+ triggerWorkflowId: string;
59
+ initiator: TurnInitiator;
58
60
  }) => Promise<void>;
59
61
  startRigVerification: (input: {
60
62
  workspaceId: string;
@@ -12,6 +12,12 @@ import {
12
12
  type EnableCapabilityRequest,
13
13
  type McpServerConnectionRef,
14
14
  } from "@opengeni/contracts";
15
+ import {
16
+ CODEX_APPS_MCP_SERVER_ID,
17
+ CODEX_APPS_MCP_SERVER_NAME,
18
+ CODEX_APPS_MCP_URL,
19
+ CODEX_APPS_STARTUP_TIMEOUT_MS,
20
+ } from "@opengeni/codex";
15
21
  import {
16
22
  decryptVariableSetValue,
17
23
  decryptedCapabilityHeaders,
@@ -711,7 +717,36 @@ export async function settingsWithEnabledCapabilityMcpServers(
711
717
  settings: Settings,
712
718
  ): Promise<Settings> {
713
719
  const enabled = await listEnabledMcpCapabilityServers(db, workspaceId);
714
- return settingsWithMcpCapabilityServers(settings, enabled);
720
+ return settingsWithCodexAppsMcpServer(settingsWithMcpCapabilityServers(settings, enabled));
721
+ }
722
+
723
+ /**
724
+ * Register Codex Apps as an optional runtime MCP when the deployment enables
725
+ * it. Registration only makes the server selectable; the session tool policy
726
+ * decides whether the model sees it, and Codex credential resolution
727
+ * independently decides whether calls can authenticate.
728
+ */
729
+ export function settingsWithCodexAppsMcpServer(settings: Settings): Settings {
730
+ if (
731
+ !settings.codexConnectedAppsEnabled ||
732
+ settings.mcpServers.some((server) => server.id === CODEX_APPS_MCP_SERVER_ID)
733
+ ) {
734
+ return settings;
735
+ }
736
+ return {
737
+ ...settings,
738
+ mcpServers: [
739
+ ...settings.mcpServers,
740
+ {
741
+ id: CODEX_APPS_MCP_SERVER_ID,
742
+ name: CODEX_APPS_MCP_SERVER_NAME,
743
+ url: CODEX_APPS_MCP_URL,
744
+ timeoutMs: CODEX_APPS_STARTUP_TIMEOUT_MS,
745
+ // Availability is credential-specific, so discover on every run.
746
+ cacheToolsList: false,
747
+ },
748
+ ],
749
+ };
715
750
  }
716
751
 
717
752
  export function settingsWithMcpCapabilityServers(
@@ -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,
@@ -9,6 +9,8 @@ import {
9
9
  import {
10
10
  CreateSessionRequest,
11
11
  DEFAULT_FIRST_PARTY_MCP_PERMISSIONS,
12
+ DEFAULT_FIRST_PARTY_MCP_TOOLS,
13
+ FIRST_PARTY_MCP_TOOL_NAMES,
12
14
  OPENGENI_SLACK_BOT_SESSION_METADATA_KEY,
13
15
  SessionSpawnDenial,
14
16
  ServiceTurnInitiator,
@@ -19,6 +21,7 @@ import {
19
21
  type AccessGrant,
20
22
  type CreateSessionResponse,
21
23
  type GoalSpec,
24
+ type FirstPartyMcpToolName,
22
25
  type Permission,
23
26
  type ReasoningEffort,
24
27
  type ResourceRef,
@@ -103,7 +106,6 @@ import {
103
106
  validateFileResources,
104
107
  validateGitHubRepositorySelection,
105
108
  validateToolRefs,
106
- validateToolRefsForSessionPolicy,
107
109
  withDefaultEnabledCapabilityMcpTools,
108
110
  } from "./resources";
109
111
 
@@ -127,6 +129,21 @@ export class SessionSpawnDeniedError extends Error {
127
129
  }
128
130
  }
129
131
 
132
+ /**
133
+ * Resolve per-session first-party tool visibility without consulting
134
+ * authorization. Top-level omission uses the minimal runtime default (stored
135
+ * as null); child omission snapshots the parent's exact effective selection.
136
+ * Explicit [] is authoritative and must never widen.
137
+ */
138
+ export function resolveFirstPartyMcpToolsForCreate(
139
+ requested: FirstPartyMcpToolName[] | undefined,
140
+ parentStored: FirstPartyMcpToolName[] | null | undefined,
141
+ ): FirstPartyMcpToolName[] {
142
+ if (requested !== undefined) return [...requested];
143
+ if (parentStored === undefined) return [...DEFAULT_FIRST_PARTY_MCP_TOOLS];
144
+ return [...(parentStored ?? DEFAULT_FIRST_PARTY_MCP_TOOLS)];
145
+ }
146
+
130
147
  function sessionSpawnDeniedMessage(denial: SessionSpawnDenial): string {
131
148
  if (denial.code === "nested_agent_depth_override_forbidden") {
132
149
  return `requested nested-agent depth limit ${denial.requestedMaxNestedAgentDepthOverride ?? "unknown"} exceeds inherited limit ${denial.effectiveMaxNestedAgentDepth}; workspace:admin is required to increase it`;
@@ -497,7 +514,7 @@ export async function createAndStartSession(input: {
497
514
  // Public admission always supplies provenance; optional keeps internal
498
515
  // callers that predate durable tool-policy provenance source-compatible
499
516
  // during the rolling deploy.
500
- toolPolicy?: SessionToolPolicy;
517
+ toolPolicy: SessionToolPolicy;
501
518
  clientEventId?: string;
502
519
  model: string;
503
520
  reasoningEffort: Settings["openaiReasoningEffort"];
@@ -522,6 +539,9 @@ export async function createAndStartSession(input: {
522
539
  instructions?: string | null;
523
540
  // Validated against the creating grant before this is called.
524
541
  firstPartyMcpPermissions?: Permission[] | null;
542
+ // Model-visible first-party tool names. Authorization remains controlled by
543
+ // firstPartyMcpPermissions and the target resource checks.
544
+ firstPartyMcpTools: FirstPartyMcpToolName[];
525
545
  // Encrypted DB rows plus matching safe metadata for create-time per-session
526
546
  // MCP servers. Metadata is the only shape emitted in events/responses.
527
547
  mcpServers?: CreateSessionMcpServerInput[];
@@ -582,7 +602,7 @@ export async function createAndStartSession(input: {
582
602
  resources: input.resources,
583
603
  skills: input.skills ?? [],
584
604
  tools: input.tools,
585
- ...(input.toolPolicy ? { toolPolicy: input.toolPolicy } : {}),
605
+ toolPolicy: input.toolPolicy,
586
606
  metadata: sessionMetadata,
587
607
  ...(input.createdBy ? { createdBy: input.createdBy } : {}),
588
608
  ...(input.createdByContext ? { createdByContext: input.createdByContext } : {}),
@@ -593,6 +613,7 @@ export async function createAndStartSession(input: {
593
613
  rigId: input.rigId ?? null,
594
614
  rigVersionId: input.rigVersionId ?? null,
595
615
  firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
616
+ firstPartyMcpTools: input.firstPartyMcpTools,
596
617
  instructions: input.instructions ?? null,
597
618
  parentSessionId: input.parentSessionId ?? null,
598
619
  createIdempotencyKey: input.createIdempotencyKey,
@@ -626,7 +647,7 @@ export async function createAndStartSession(input: {
626
647
  resources: input.resources,
627
648
  skills: input.skills ?? [],
628
649
  tools: input.tools,
629
- ...(input.toolPolicy ? { toolPolicy: input.toolPolicy } : {}),
650
+ toolPolicy: input.toolPolicy,
630
651
  metadata: sessionMetadata,
631
652
  ...(input.createdBy ? { createdBy: input.createdBy } : {}),
632
653
  ...(input.createdByContext ? { createdByContext: input.createdByContext } : {}),
@@ -637,6 +658,7 @@ export async function createAndStartSession(input: {
637
658
  rigId: input.rigId ?? null,
638
659
  rigVersionId: input.rigVersionId ?? null,
639
660
  firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
661
+ firstPartyMcpTools: input.firstPartyMcpTools,
640
662
  instructions: input.instructions ?? null,
641
663
  parentSessionId: input.parentSessionId ?? null,
642
664
  sandboxGroupId: input.sandboxGroupId ?? null,
@@ -670,7 +692,7 @@ async function finishStartSession(
670
692
  turnInstructions?: string | null;
671
693
  resources: ResourceRef[];
672
694
  tools: ToolRef[];
673
- toolPolicy?: SessionToolPolicy;
695
+ toolPolicy: SessionToolPolicy;
674
696
  clientEventId?: string;
675
697
  model: string;
676
698
  reasoningEffort: Settings["openaiReasoningEffort"];
@@ -728,7 +750,7 @@ async function finishStartSession(
728
750
  reasoningEffortFallback: input.reasoningEffort,
729
751
  turnExecutionPolicy: input.turnExecutionPolicy,
730
752
  createdEventPayload: {
731
- ...(input.toolPolicy ? { toolPolicy: input.toolPolicy } : {}),
753
+ toolPolicy: input.toolPolicy,
732
754
  ...(input.variableSet
733
755
  ? { variableSetId: input.variableSet.id, variableSetName: input.variableSet.name }
734
756
  : {}),
@@ -898,8 +920,6 @@ export async function postUserMessageTurn(input: {
898
920
  text: string;
899
921
  turnInstructions?: string | null;
900
922
  resources: ResourceRef[];
901
- tools: ToolRef[];
902
- toolsProvided: boolean;
903
923
  model?: string | null;
904
924
  reasoningEffort?: Settings["openaiReasoningEffort"] | null;
905
925
  clientEventId?: string;
@@ -943,8 +963,6 @@ export async function postUserMessageTurn(input: {
943
963
  text: input.text,
944
964
  turnInstructions: input.turnInstructions ?? null,
945
965
  resources: input.resources,
946
- tools: input.tools,
947
- toolsProvided: input.toolsProvided,
948
966
  model: requestedModel,
949
967
  reasoningEffort: requestedReasoningEffort,
950
968
  reasoningEffortFallback: input.reasoningEffortFallback ?? settings.openaiReasoningEffort,
@@ -1142,12 +1160,10 @@ export async function createSessionForRequest(
1142
1160
  );
1143
1161
  toolPolicy = { mode: "workspace_default", inheritedFromSessionId: null };
1144
1162
  }
1145
- // The first-party MCP server is attached to EVERY session. It hosts the
1146
- // session's own metadata tool (set_session_title) + goal tools, and — only
1147
- // when the grant carries the permission — the orchestration/variableSet/
1148
- // github tools. Capability is gated per-tool by permission, never by whether
1149
- // the server is attached, so a bare chat still gets titling while the
1150
- // dangerous tools stay off by default.
1163
+ // The first-party MCP server is attached to EVERY session. Registration is
1164
+ // independently intersected with the exact model-visible selection and the
1165
+ // tool's permission/target authorization predicate, so attachment alone
1166
+ // exposes nothing.
1151
1167
  const tools = withFirstPartyTools(selectedTools, runtimeSettings);
1152
1168
  await validateGitHubRepositorySelection(db, workspaceId, resources);
1153
1169
  if (resources.some((resource) => resource.kind === "file") && !objectStorage) {
@@ -1276,6 +1292,23 @@ export async function createSessionForRequest(
1276
1292
  "goal-bearing sessions require goals:manage in the resulting first-party MCP permission set",
1277
1293
  });
1278
1294
  }
1295
+ // Tool visibility is independent from permission authority. A child that
1296
+ // omits the field inherits the parent's exact effective selection; a
1297
+ // top-level omission selects the complete catalog.
1298
+ const firstPartyMcpTools = resolveFirstPartyMcpToolsForCreate(
1299
+ payload.firstPartyMcpTools,
1300
+ parentSession ? parentSession.firstPartyMcpTools : undefined,
1301
+ );
1302
+ if (payload.goal) {
1303
+ const missingGoalTools = ["goal_update", "goal_complete", "goal_pause"].filter(
1304
+ (name) => !firstPartyMcpTools.includes(name as FirstPartyMcpToolName),
1305
+ );
1306
+ if (missingGoalTools.length > 0) {
1307
+ throw new HTTPException(422, {
1308
+ message: `goal-bearing sessions require first-party MCP tools: ${missingGoalTools.join(", ")}`,
1309
+ });
1310
+ }
1311
+ }
1279
1312
  // Parent linkage: a worker is linked to its manager ONLY from the
1280
1313
  // worker-signed sessionId claim on the creating grant — the manager
1281
1314
  // session's own id, signed into the delegated token by the worker and never
@@ -1541,6 +1574,7 @@ export async function createSessionForRequest(
1541
1574
  // time. Not surfaced as an event.
1542
1575
  instructions: payload.instructions ?? null,
1543
1576
  firstPartyMcpPermissions,
1577
+ firstPartyMcpTools,
1544
1578
  mcpServers: sessionMcpServers.dbServers,
1545
1579
  sessionMcpServers: sessionMcpServers.metadata,
1546
1580
  parentSessionId,
@@ -1597,8 +1631,7 @@ export async function createSessionForRequest(
1597
1631
  * `POST /sessions/:id/events` and the first-party MCP `session_send_message`
1598
1632
  * tool: resource/tool validation, usage limits, the locked append + turn
1599
1633
  * enqueue, and usage recording. `toolsProvided: false` durably preserves an
1600
- * absent `tools` key so execution inherits the session policy; an explicit
1601
- * empty array is a deliberate per-turn narrowing.
1634
+ * Tool selection is durable session state and never rides a follow-up prompt.
1602
1635
  */
1603
1636
  export async function acceptSessionUserMessage(
1604
1637
  deps: AcceptSessionUserMessageDependencies,
@@ -1609,8 +1642,6 @@ export async function acceptSessionUserMessage(
1609
1642
  text: string;
1610
1643
  turnInstructions?: string | null;
1611
1644
  resources?: ResourceRef[];
1612
- tools?: ToolRef[];
1613
- toolsProvided: boolean;
1614
1645
  model?: string | null;
1615
1646
  reasoningEffort?: ReasoningEffort | null;
1616
1647
  clientEventId?: string;
@@ -1621,23 +1652,12 @@ export async function acceptSessionUserMessage(
1621
1652
  expectedDraftRevision?: number | null;
1622
1653
  },
1623
1654
  ): Promise<{ accepted: SessionEvent; turn: SessionTurn }> {
1624
- if (input.toolsProvided && !deps.settings.sessionTurnToolReplacementEnabled) {
1625
- throw new HTTPException(503, {
1626
- message:
1627
- "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",
1628
- });
1629
- }
1630
1655
  const { settings, db, bus, workflowClient, objectStorage } = deps;
1631
1656
  await requireSessionAuthorization(deps, grant, {
1632
1657
  sessionId,
1633
1658
  operation: input.delivery === "steer" ? "session.steer" : "session.append",
1634
1659
  surface: "core",
1635
1660
  });
1636
- const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
1637
- db,
1638
- workspaceId,
1639
- settings,
1640
- );
1641
1661
  // Hoisted above requireLimit so the codex-billed predicate can resolve the
1642
1662
  // turn's effective model (a follow-up turn inherits the session's model). A
1643
1663
  // pure read with no side effects.
@@ -1660,31 +1680,7 @@ export async function acceptSessionUserMessage(
1660
1680
  reasoningEffort: effectiveReasoningEffort,
1661
1681
  reasoningSource: input.reasoningEffort == null ? "session" : "explicit",
1662
1682
  });
1663
- const runtimeSettings = settingsWithSessionMcpServerMetadata(
1664
- capabilityRuntimeSettings,
1665
- existingSession.mcpServers,
1666
- );
1667
1683
  const requestedResources = normalizeResources(input.resources ?? []);
1668
- const tracksWorkspaceDefaults = existingSession.toolPolicy?.mode === "workspace_default";
1669
- const sessionPolicyTools = withFirstPartyTools(
1670
- tracksWorkspaceDefaults
1671
- ? withDefaultEnabledCapabilityMcpTools(
1672
- availableToolRefs(existingSession.tools, runtimeSettings),
1673
- settings,
1674
- capabilityRuntimeSettings,
1675
- )
1676
- : existingSession.tools,
1677
- runtimeSettings,
1678
- );
1679
- const validatedTools = input.toolsProvided
1680
- ? validateToolRefsForSessionPolicy({
1681
- requested: input.tools ?? [],
1682
- settings: runtimeSettings,
1683
- allowedTools: sessionPolicyTools,
1684
- message: "message tools may only narrow the session tool policy",
1685
- })
1686
- : [];
1687
- const requestedTools = input.toolsProvided ? validatedTools : [];
1688
1684
  await requireLimit(deps, {
1689
1685
  accountId: grant.accountId,
1690
1686
  workspaceId,
@@ -1718,8 +1714,6 @@ export async function acceptSessionUserMessage(
1718
1714
  text: input.text,
1719
1715
  turnInstructions: input.turnInstructions ?? null,
1720
1716
  resources: requestedResources,
1721
- tools: requestedTools,
1722
- toolsProvided: input.toolsProvided,
1723
1717
  model: input.model ?? null,
1724
1718
  reasoningEffort: input.reasoningEffort ?? null,
1725
1719
  reasoningEffortFallback: sessionReasoningEffort,
@@ -1880,7 +1874,8 @@ export async function updateSessionMcpApprovalPolicy(
1880
1874
  function toolPolicyAuditSnapshot(
1881
1875
  session: Session,
1882
1876
  tools: ToolRef[],
1883
- policy = session.toolPolicy ?? { mode: "legacy" as const, inheritedFromSessionId: null },
1877
+ firstPartyMcpTools: FirstPartyMcpToolName[],
1878
+ policy = session.toolPolicy,
1884
1879
  ) {
1885
1880
  // Tool policy refs contain only public server ids and the optional/strict
1886
1881
  // execution mode; they never carry URLs, names, headers, credentials,
@@ -1912,6 +1907,8 @@ function toolPolicyAuditSnapshot(
1912
1907
  .map((tool) => tool.id),
1913
1908
  toolRefs,
1914
1909
  toolCount: allToolRefs.length,
1910
+ firstPartyMcpTools: [...firstPartyMcpTools].sort(),
1911
+ firstPartyMcpToolCount: firstPartyMcpTools.length,
1915
1912
  truncated: allToolRefs.length > toolRefs.length,
1916
1913
  };
1917
1914
  }
@@ -1965,10 +1962,14 @@ export async function updateSessionToolPolicy(
1965
1962
  return withFirstPartyTools(validatedTools, runtimeSettings);
1966
1963
  })()
1967
1964
  : null;
1965
+ const explicitRequestedFirstPartyTools = explicitRequest
1966
+ ? [...explicitRequest.firstPartyMcpTools]
1967
+ : null;
1968
1968
  const workspaceDefaultTools = withFirstPartyTools(
1969
1969
  withDefaultEnabledCapabilityMcpTools([], deps.settings, capabilityRuntimeSettings),
1970
1970
  runtimeSettings,
1971
1971
  );
1972
+ const workspaceDefaultFirstPartyTools = [...FIRST_PARTY_MCP_TOOL_NAMES];
1972
1973
  const events = await appendSessionEventsWithLockedSessionUpdate(
1973
1974
  deps.db,
1974
1975
  grant.workspaceId,
@@ -1980,6 +1981,7 @@ export async function updateSessionToolPolicy(
1980
1981
  }
1981
1982
 
1982
1983
  let nextTools: ToolRef[];
1984
+ let nextFirstPartyMcpTools: FirstPartyMcpToolName[];
1983
1985
  let nextPolicy: SessionToolPolicy;
1984
1986
  if (session.parentSessionId) {
1985
1987
  const parent = await context.getLockedSession(session.parentSessionId);
@@ -1997,6 +1999,9 @@ export async function updateSessionToolPolicy(
1997
1999
  : parent.tools,
1998
2000
  runtimeSettings,
1999
2001
  );
2002
+ const parentFirstPartyMcpTools = [
2003
+ ...(parent.firstPartyMcpTools ?? DEFAULT_FIRST_PARTY_MCP_TOOLS),
2004
+ ];
2000
2005
  if (requestedMode === "workspace_default") {
2001
2006
  if (!parentTracksWorkspaceDefaults) {
2002
2007
  throw new HTTPException(403, {
@@ -2005,6 +2010,7 @@ export async function updateSessionToolPolicy(
2005
2010
  });
2006
2011
  }
2007
2012
  nextTools = parentEffective;
2013
+ nextFirstPartyMcpTools = parentFirstPartyMcpTools;
2008
2014
  nextPolicy = {
2009
2015
  mode: "workspace_default",
2010
2016
  inheritedFromSessionId: parent.id,
@@ -2016,6 +2022,16 @@ export async function updateSessionToolPolicy(
2016
2022
  parentEffective,
2017
2023
  "session tools may only narrow the parent session tool policy",
2018
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!;
2019
2035
  nextPolicy = {
2020
2036
  mode: "explicit",
2021
2037
  inheritedFromSessionId: parent.id,
@@ -2024,20 +2040,29 @@ export async function updateSessionToolPolicy(
2024
2040
  } else {
2025
2041
  nextTools =
2026
2042
  requestedMode === "workspace_default" ? workspaceDefaultTools : explicitRequestedTools!;
2043
+ nextFirstPartyMcpTools =
2044
+ requestedMode === "workspace_default"
2045
+ ? workspaceDefaultFirstPartyTools
2046
+ : explicitRequestedFirstPartyTools!;
2027
2047
  nextPolicy = { mode: requestedMode, inheritedFromSessionId: null };
2028
2048
  }
2029
2049
 
2030
- const currentPolicy = session.toolPolicy ?? {
2031
- mode: "legacy" as const,
2032
- inheritedFromSessionId: null,
2033
- };
2050
+ const currentPolicy = session.toolPolicy;
2034
2051
  // JSONB normalizes object-key order on the round trip, so plain
2035
2052
  // JSON.stringify would turn an identical retry into a second mutation
2036
2053
  // (and version bump) merely because the persisted key order differs from
2037
2054
  // the request object. Compare canonical JSON instead.
2038
2055
  const unchanged =
2039
- stableJson({ tools: session.tools, policy: currentPolicy }) ===
2040
- 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
+ });
2041
2066
  if (unchanged) {
2042
2067
  return { events: [] };
2043
2068
  }
@@ -2048,8 +2073,18 @@ export async function updateSessionToolPolicy(
2048
2073
  {
2049
2074
  type: "session.tool_policy.updated" as const,
2050
2075
  payload: {
2051
- before: toolPolicyAuditSnapshot(session, session.tools, currentPolicy),
2052
- 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
+ ),
2053
2088
  version: nextVersion,
2054
2089
  effectiveFrom: "next_attempt",
2055
2090
  },
@@ -2057,6 +2092,7 @@ export async function updateSessionToolPolicy(
2057
2092
  ],
2058
2093
  update: {
2059
2094
  tools: nextTools,
2095
+ firstPartyMcpTools: nextFirstPartyMcpTools,
2060
2096
  toolPolicy: nextPolicy,
2061
2097
  toolPolicyVersion: nextVersion,
2062
2098
  expectedToolPolicyVersion: request.expectedVersion,