@opengeni/core 0.12.2 → 0.12.7

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.2",
3
+ "version": "0.12.7",
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.8",
39
- "@opengeni/contracts": "^0.20.1",
40
- "@opengeni/db": "^0.13.1",
41
- "@opengeni/documents": "^0.2.38",
42
- "@opengeni/events": "^0.3.29",
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",
43
43
  "@opengeni/observability": "^0.3.0",
44
- "@opengeni/runtime": "^0.13.13",
45
- "@opengeni/storage": "^0.2.32",
44
+ "@opengeni/runtime": "^0.14.1",
45
+ "@opengeni/storage": "^0.2.35",
46
46
  "hono": "^4.12.18"
47
47
  },
48
48
  "engines": {
@@ -86,6 +86,10 @@ export function hasPermission(permissions: Permission[], permission: Permission)
86
86
 
87
87
  async function resolveAccessContext(c: Context, deps: AccessDeps): Promise<AccessContext | null> {
88
88
  if (deps.settings.productAccessMode === "local") {
89
+ const delegated = await delegatedAccessContext(c, deps, "local");
90
+ if (delegated) {
91
+ return delegated;
92
+ }
89
93
  return await bootstrapWorkspace(deps.db, {
90
94
  accountExternalSource: "opengeni:local",
91
95
  accountExternalId: "default",
@@ -198,7 +202,7 @@ async function apiKeyAccessContext(
198
202
  async function delegatedAccessContext(
199
203
  c: Context,
200
204
  deps: AccessDeps,
201
- mode: "configured" | "managed",
205
+ mode: "local" | "configured" | "managed",
202
206
  token = bearerToken(c),
203
207
  ): Promise<AccessContext | null> {
204
208
  if (!token || !deps.settings.delegationSecret) {
@@ -232,6 +236,9 @@ async function delegatedAccessContext(
232
236
  metadata: {
233
237
  delegated: true,
234
238
  ...(payload.sessionId ? { sessionId: payload.sessionId } : {}),
239
+ ...(payload.firstPartyMcpTools !== undefined
240
+ ? { firstPartyMcpTools: payload.firstPartyMcpTools }
241
+ : {}),
235
242
  // Caller identity: the turn that minted this token. Tools classify the
236
243
  // CALLER from this instead of re-reading the live active pointer.
237
244
  ...(payload.turnId ? { turnId: payload.turnId } : {}),
@@ -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,
@@ -27,6 +33,7 @@ import {
27
33
  getVariableSet,
28
34
  listCapabilityCatalogItems,
29
35
  listCapabilityInstallations,
36
+ listConnectionsMetadata,
30
37
  listEnabledMcpCapabilityServers,
31
38
  listPackInstallations,
32
39
  mcpServerIdForCapability,
@@ -424,14 +431,10 @@ async function validateMcpCapabilityConnectionRef(
424
431
  item: CapabilityCatalogItem,
425
432
  ref: McpServerConnectionRef,
426
433
  ): Promise<McpServerConnectionRef> {
427
- if (ref.subjectScope === "subject") {
428
- throw new HTTPException(422, {
429
- message: "subject-owned connection refs are not supported for agent runtime use yet",
430
- });
431
- }
434
+ const subjectScope = ref.subjectScope ?? "workspace";
432
435
  const normalized: McpServerConnectionRef = {
433
436
  providerDomain: ref.providerDomain.trim(),
434
- subjectScope: "workspace",
437
+ subjectScope,
435
438
  ...(ref.connectionId ? { connectionId: ref.connectionId } : {}),
436
439
  ...(ref.provider ? { provider: ref.provider.trim() } : {}),
437
440
  ...(ref.kind ? { kind: ref.kind } : {}),
@@ -450,23 +453,41 @@ async function validateMcpCapabilityConnectionRef(
450
453
  "MCP capabilities need a remote streamable HTTP endpoint before they can use a connectionRef",
451
454
  });
452
455
  }
453
- if (!normalized.connectionId) {
454
- return normalized;
456
+
457
+ let connection = normalized.connectionId
458
+ ? await getConnectionMetadata(
459
+ input.db,
460
+ input.workspaceId,
461
+ normalized.connectionId,
462
+ input.grant.subjectId,
463
+ )
464
+ : null;
465
+ if (!connection && subjectScope === "subject" && !normalized.connectionId) {
466
+ const visible = await listConnectionsMetadata(
467
+ input.db,
468
+ input.workspaceId,
469
+ input.grant.subjectId,
470
+ );
471
+ connection =
472
+ visible.find(
473
+ (candidate) =>
474
+ candidate.subjectId === input.grant.subjectId &&
475
+ candidate.providerDomain === normalized.providerDomain &&
476
+ (!normalized.kind || candidate.kind === normalized.kind) &&
477
+ candidate.status === "active",
478
+ ) ?? null;
455
479
  }
456
- const connection = await getConnectionMetadata(
457
- input.db,
458
- input.workspaceId,
459
- normalized.connectionId,
460
- input.grant.subjectId,
461
- );
462
480
  if (!connection) {
463
481
  throw new HTTPException(422, {
464
- message: "connectionRef.connectionId does not reference a visible connection",
482
+ message: "connectionRef does not reference a visible active connection",
465
483
  });
466
484
  }
467
- if (connection.subjectId !== null) {
485
+ if (
486
+ (subjectScope === "subject" && connection.subjectId !== input.grant.subjectId) ||
487
+ (subjectScope === "workspace" && connection.subjectId !== null)
488
+ ) {
468
489
  throw new HTTPException(422, {
469
- message: "agent runtime connection refs must reference workspace-shared connections in I1",
490
+ message: `connectionRef does not reference a ${subjectScope}-owned connection`,
470
491
  });
471
492
  }
472
493
  if (connection.status !== "active") {
@@ -484,6 +505,11 @@ async function validateMcpCapabilityConnectionRef(
484
505
  message: "connectionRef.kind does not match the referenced connection",
485
506
  });
486
507
  }
508
+ if (subjectScope === "subject") {
509
+ const genericSubjectRef = { ...normalized, kind: connection.kind };
510
+ delete genericSubjectRef.connectionId;
511
+ return genericSubjectRef;
512
+ }
487
513
  return normalized;
488
514
  }
489
515
 
@@ -691,7 +717,36 @@ export async function settingsWithEnabledCapabilityMcpServers(
691
717
  settings: Settings,
692
718
  ): Promise<Settings> {
693
719
  const enabled = await listEnabledMcpCapabilityServers(db, workspaceId);
694
- 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
+ };
695
750
  }
696
751
 
697
752
  export function settingsWithMcpCapabilityServers(
@@ -1157,12 +1212,16 @@ function installationConnectionRef(
1157
1212
  if (!ref || typeof ref !== "object") {
1158
1213
  return null;
1159
1214
  }
1160
- const { connectionId, providerDomain, kind } = ref as Record<string, unknown>;
1161
- if (
1162
- typeof connectionId !== "string" ||
1163
- typeof providerDomain !== "string" ||
1164
- typeof kind !== "string"
1165
- ) {
1215
+ const { connectionId, providerDomain, kind, subjectScope } = ref as Record<string, unknown>;
1216
+ if (typeof providerDomain !== "string" || typeof kind !== "string") {
1217
+ return null;
1218
+ }
1219
+ if (subjectScope === "subject") {
1220
+ // Never project a personal connection UUID through workspace-visible
1221
+ // capability configuration, including legacy rows that still contain one.
1222
+ return { providerDomain, kind, subjectScope: "subject" };
1223
+ }
1224
+ if (typeof connectionId !== "string") {
1166
1225
  return null;
1167
1226
  }
1168
1227
  return { connectionId, providerDomain, kind };
@@ -9,6 +9,7 @@ import {
9
9
  import {
10
10
  CreateSessionRequest,
11
11
  DEFAULT_FIRST_PARTY_MCP_PERMISSIONS,
12
+ DEFAULT_FIRST_PARTY_MCP_TOOLS,
12
13
  OPENGENI_SLACK_BOT_SESSION_METADATA_KEY,
13
14
  SessionSpawnDenial,
14
15
  ServiceTurnInitiator,
@@ -19,10 +20,12 @@ import {
19
20
  type AccessGrant,
20
21
  type CreateSessionResponse,
21
22
  type GoalSpec,
23
+ type FirstPartyMcpToolName,
22
24
  type Permission,
23
25
  type ReasoningEffort,
24
26
  type ResourceRef,
25
27
  type Session,
28
+ type SessionSkill,
26
29
  type SessionEvent,
27
30
  SessionMcpApprovalPolicy,
28
31
  type SessionMcpCredentialUpdateInput,
@@ -126,6 +129,21 @@ export class SessionSpawnDeniedError extends Error {
126
129
  }
127
130
  }
128
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[] | null {
142
+ if (requested !== undefined) return [...requested];
143
+ if (parentStored === undefined) return null;
144
+ return [...(parentStored ?? DEFAULT_FIRST_PARTY_MCP_TOOLS)];
145
+ }
146
+
129
147
  function sessionSpawnDeniedMessage(denial: SessionSpawnDenial): string {
130
148
  if (denial.code === "nested_agent_depth_override_forbidden") {
131
149
  return `requested nested-agent depth limit ${denial.requestedMaxNestedAgentDepthOverride ?? "unknown"} exceeds inherited limit ${denial.effectiveMaxNestedAgentDepth}; workspace:admin is required to increase it`;
@@ -491,6 +509,7 @@ export async function createAndStartSession(input: {
491
509
  initialMessage: string;
492
510
  turnInstructions?: string | null;
493
511
  resources: ResourceRef[];
512
+ skills?: SessionSkill[];
494
513
  tools: ToolRef[];
495
514
  // Public admission always supplies provenance; optional keeps internal
496
515
  // callers that predate durable tool-policy provenance source-compatible
@@ -520,6 +539,9 @@ export async function createAndStartSession(input: {
520
539
  instructions?: string | null;
521
540
  // Validated against the creating grant before this is called.
522
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[] | null;
523
545
  // Encrypted DB rows plus matching safe metadata for create-time per-session
524
546
  // MCP servers. Metadata is the only shape emitted in events/responses.
525
547
  mcpServers?: CreateSessionMcpServerInput[];
@@ -578,6 +600,7 @@ export async function createAndStartSession(input: {
578
600
  initialMessage: input.initialMessage,
579
601
  initialTurnInstructions: input.turnInstructions ?? null,
580
602
  resources: input.resources,
603
+ skills: input.skills ?? [],
581
604
  tools: input.tools,
582
605
  ...(input.toolPolicy ? { toolPolicy: input.toolPolicy } : {}),
583
606
  metadata: sessionMetadata,
@@ -590,6 +613,7 @@ export async function createAndStartSession(input: {
590
613
  rigId: input.rigId ?? null,
591
614
  rigVersionId: input.rigVersionId ?? null,
592
615
  firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
616
+ firstPartyMcpTools: input.firstPartyMcpTools ?? null,
593
617
  instructions: input.instructions ?? null,
594
618
  parentSessionId: input.parentSessionId ?? null,
595
619
  createIdempotencyKey: input.createIdempotencyKey,
@@ -621,6 +645,7 @@ export async function createAndStartSession(input: {
621
645
  initialMessage: input.initialMessage,
622
646
  initialTurnInstructions: input.turnInstructions ?? null,
623
647
  resources: input.resources,
648
+ skills: input.skills ?? [],
624
649
  tools: input.tools,
625
650
  ...(input.toolPolicy ? { toolPolicy: input.toolPolicy } : {}),
626
651
  metadata: sessionMetadata,
@@ -633,6 +658,7 @@ export async function createAndStartSession(input: {
633
658
  rigId: input.rigId ?? null,
634
659
  rigVersionId: input.rigVersionId ?? null,
635
660
  firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
661
+ firstPartyMcpTools: input.firstPartyMcpTools ?? null,
636
662
  instructions: input.instructions ?? null,
637
663
  parentSessionId: input.parentSessionId ?? null,
638
664
  sandboxGroupId: input.sandboxGroupId ?? null,
@@ -1090,6 +1116,9 @@ export async function createSessionForRequest(
1090
1116
  ? payload.resources
1091
1117
  : (parentSession?.resources ?? payload.resources),
1092
1118
  );
1119
+ const skills = hasOwnProperty(rawPayload, "skills")
1120
+ ? payload.skills
1121
+ : (parentSession?.skills ?? payload.skills);
1093
1122
  const toolsProvided = hasOwnProperty(rawPayload, "tools");
1094
1123
  const requestedTools = validateToolRefs(
1095
1124
  toolsProvided ? payload.tools : (parentSession?.tools ?? payload.tools),
@@ -1135,12 +1164,10 @@ export async function createSessionForRequest(
1135
1164
  );
1136
1165
  toolPolicy = { mode: "workspace_default", inheritedFromSessionId: null };
1137
1166
  }
1138
- // The first-party MCP server is attached to EVERY session. It hosts the
1139
- // session's own metadata tool (set_session_title) + goal tools, and — only
1140
- // when the grant carries the permission — the orchestration/variableSet/
1141
- // github tools. Capability is gated per-tool by permission, never by whether
1142
- // the server is attached, so a bare chat still gets titling while the
1143
- // dangerous tools stay off by default.
1167
+ // The first-party MCP server is attached to EVERY session. Registration is
1168
+ // independently intersected with the exact model-visible selection and the
1169
+ // tool's permission/target authorization predicate, so attachment alone
1170
+ // exposes nothing.
1144
1171
  const tools = withFirstPartyTools(selectedTools, runtimeSettings);
1145
1172
  await validateGitHubRepositorySelection(db, workspaceId, resources);
1146
1173
  if (resources.some((resource) => resource.kind === "file") && !objectStorage) {
@@ -1269,6 +1296,24 @@ export async function createSessionForRequest(
1269
1296
  "goal-bearing sessions require goals:manage in the resulting first-party MCP permission set",
1270
1297
  });
1271
1298
  }
1299
+ // Tool visibility is independent from permission authority. A child that
1300
+ // omits the field inherits the parent's exact effective selection; a
1301
+ // top-level omission resolves to the fixed minimal default at execution.
1302
+ const firstPartyMcpTools = resolveFirstPartyMcpToolsForCreate(
1303
+ payload.firstPartyMcpTools,
1304
+ parentSession ? parentSession.firstPartyMcpTools : undefined,
1305
+ );
1306
+ if (payload.goal) {
1307
+ const effectiveTools = firstPartyMcpTools ?? [...DEFAULT_FIRST_PARTY_MCP_TOOLS];
1308
+ const missingGoalTools = ["goal_update", "goal_complete", "goal_pause"].filter(
1309
+ (name) => !effectiveTools.includes(name as FirstPartyMcpToolName),
1310
+ );
1311
+ if (missingGoalTools.length > 0) {
1312
+ throw new HTTPException(422, {
1313
+ message: `goal-bearing sessions require first-party MCP tools: ${missingGoalTools.join(", ")}`,
1314
+ });
1315
+ }
1316
+ }
1272
1317
  // Parent linkage: a worker is linked to its manager ONLY from the
1273
1318
  // worker-signed sessionId claim on the creating grant — the manager
1274
1319
  // session's own id, signed into the delegated token by the worker and never
@@ -1501,6 +1546,7 @@ export async function createSessionForRequest(
1501
1546
  initialMessage: payload.initialMessage,
1502
1547
  turnInstructions: payload.turnInstructions ?? null,
1503
1548
  resources,
1549
+ skills,
1504
1550
  tools,
1505
1551
  toolPolicy,
1506
1552
  ...(payload.clientEventId ? { clientEventId: payload.clientEventId } : {}),
@@ -1533,6 +1579,7 @@ export async function createSessionForRequest(
1533
1579
  // time. Not surfaced as an event.
1534
1580
  instructions: payload.instructions ?? null,
1535
1581
  firstPartyMcpPermissions,
1582
+ firstPartyMcpTools,
1536
1583
  mcpServers: sessionMcpServers.dbServers,
1537
1584
  sessionMcpServers: sessionMcpServers.metadata,
1538
1585
  parentSessionId,