@opengeni/core 4.0.0 → 4.0.1

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.
@@ -68,7 +68,7 @@ export type FleetSandboxEntry = {
68
68
  /** The sandbox id used as the attach/swap/run_on `target`. For the session's
69
69
  * own group box this is the group id (a null active pointer == this box). */
70
70
  id: string;
71
- kind: "modal" | "selfhosted" | "opensandbox";
71
+ kind: Exclude<SandboxBackend, "none">;
72
72
  name: string;
73
73
  liveness: FleetLiveness;
74
74
  /** True for the session's currently-active sandbox (the routing target). */
@@ -14,6 +14,13 @@ export type ManagedSessionGroupBackend = Exclude<SandboxBackend, "none" | "selfh
14
14
  * fallback to expose.
15
15
  */
16
16
  export declare function managedSessionGroupBackend(deploymentBackend: SandboxBackend, sessionBackend: SandboxBackend): ManagedSessionGroupBackend | null;
17
+ /** Display name and kind for the session's own managed box. The kind is the
18
+ * real provider. Callers must not collapse local, Docker, or other providers
19
+ * into Modal. */
20
+ export declare function sessionGroupMachinePresentation(backend: ManagedSessionGroupBackend): {
21
+ name: string;
22
+ kind: ManagedSessionGroupBackend;
23
+ };
17
24
  /** A machine-home row carries the machine's host OS; its managed group uses the
18
25
  * platform's canonical managed-sandbox OS instead. Ordinary sessions preserve
19
26
  * their explicitly selected OS. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/core",
3
- "version": "4.0.0",
3
+ "version": "4.0.1",
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": {
@@ -51,16 +51,16 @@
51
51
  "dependencies": {
52
52
  "@modelcontextprotocol/sdk": "^1.29.0",
53
53
  "@opengeni/capabilities": "^0.3.4",
54
- "@opengeni/codex": "^0.2.24",
55
- "@opengeni/config": "^2.0.0",
56
- "@opengeni/contracts": "^5.0.0",
57
- "@opengeni/db": "^6.0.0",
58
- "@opengeni/documents": "^0.8.31",
59
- "@opengeni/events": "^0.4.29",
54
+ "@opengeni/codex": "^0.2.25",
55
+ "@opengeni/config": "^2.1.0",
56
+ "@opengeni/contracts": "^5.1.0",
57
+ "@opengeni/db": "^6.0.1",
58
+ "@opengeni/documents": "^0.8.32",
59
+ "@opengeni/events": "^0.4.30",
60
60
  "@opengeni/network": "^0.3.1",
61
- "@opengeni/observability": "^0.8.30",
62
- "@opengeni/runtime": "^4.0.0",
63
- "@opengeni/storage": "^0.2.131",
61
+ "@opengeni/observability": "^0.8.31",
62
+ "@opengeni/runtime": "^4.0.1",
63
+ "@opengeni/storage": "^0.2.132",
64
64
  "hono": "^4.12.18",
65
65
  "zod": "^4.2.1"
66
66
  },
@@ -19,6 +19,7 @@ import {
19
19
  publicNewSessionDraftOptions,
20
20
  requireFileForSubject,
21
21
  saveNewSessionDraftInTransaction,
22
+ reusableNewSessionRepositoryResource,
22
23
  withWorkspaceSubjectRls,
23
24
  } from "@opengeni/db";
24
25
  import { HTTPException } from "hono/http-exception";
@@ -333,3 +334,44 @@ export async function saveActorNewSessionDraft(
333
334
  : { subjectId: grant.subjectId, privateFileOwnerSubjectId: null };
334
335
  return withSessionRlsActorContext(actor, () => saveActorNewSessionDraftInFileScope(...args));
335
336
  }
337
+
338
+ /** Reuse the website's actor/workspace selections without consuming its draft. */
339
+ export async function getActorNewSessionDefaults(
340
+ deps: Parameters<typeof getActorNewSessionDraft>[0],
341
+ grant: AccessGrant,
342
+ workspaceId: string,
343
+ ) {
344
+ const draft = await getActorNewSessionDraft(deps, grant, workspaceId);
345
+ const options = draft.options;
346
+ return {
347
+ // Revision zero is a synthetic empty form, not a user's model preference.
348
+ ...(draft.revision > 0
349
+ ? {
350
+ model: draft.model,
351
+ reasoningEffort: draft.reasoningEffort,
352
+ latencyMode: draft.latencyMode,
353
+ }
354
+ : {}),
355
+ resources: draft.resources.flatMap((resource) =>
356
+ resource.kind === "repository" ? [reusableNewSessionRepositoryResource(resource)] : [],
357
+ ),
358
+ ...(draft.toolsProvided
359
+ ? { tools: draft.tools }
360
+ : options.excludedMcpServerIds
361
+ ? { excludedMcpServerIds: options.excludedMcpServerIds }
362
+ : {}),
363
+ ...(options.firstPartyMcpTools ? { firstPartyMcpTools: options.firstPartyMcpTools } : {}),
364
+ ...(options.firstPartyMcpPermissions
365
+ ? { firstPartyMcpPermissions: options.firstPartyMcpPermissions }
366
+ : {}),
367
+ ...(options.variableSetIds ? { variableSetIds: options.variableSetIds } : {}),
368
+ ...(options.rigId ? { rigId: options.rigId } : {}),
369
+ ...(options.sandboxBackend ? { sandboxBackend: options.sandboxBackend } : {}),
370
+ ...(options.targetSandboxId
371
+ ? {
372
+ targetSandboxId: options.targetSandboxId,
373
+ ...(options.workingDir ? { workingDir: options.workingDir } : {}),
374
+ }
375
+ : {}),
376
+ };
377
+ }
@@ -4,6 +4,7 @@ import type { GitHubSkillSourceClient, GitHubSkillTreeEntry } from "./skill-impo
4
4
  import { pinnedFetch, readResponseJsonBounded, readResponseTextBounded } from "@opengeni/network";
5
5
  import {
6
6
  PORTABLE_SKILL_MAX_FILES,
7
+ PORTABLE_SKILL_MAX_FILE_BYTES,
7
8
  PORTABLE_SKILL_MAX_TOTAL_BYTES,
8
9
  type SkillLibraryFile,
9
10
  } from "@opengeni/runtime/skill-library";
@@ -11,7 +12,8 @@ import {
11
12
  const githubApiBase = "https://api.github.com";
12
13
  const githubRequestTimeoutMs = 15_000;
13
14
  const githubMetadataMaxBytes = 4 * 1024 * 1024;
14
- const githubBlobResponseMaxBytes = 512 * 1024;
15
+ // GitHub blobs are base64-encoded JSON; allow framing and line wrapping.
16
+ const githubBlobResponseMaxBytes = PORTABLE_SKILL_MAX_FILE_BYTES * 2;
15
17
 
16
18
  type GitHubJsonRequest = (path: string, maxBytes: number, label: string) => Promise<unknown>;
17
19
 
@@ -188,9 +188,25 @@ export async function workspaceSessionToolPolicyDefaultServerIds(
188
188
  const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(db, workspaceId, settings, {
189
189
  ...(subjectId ? { subjectId } : {}),
190
190
  });
191
- const availableDefaults = defaultSessionMcpServerIds(runtimeSettings.mcpServers);
192
191
  const workspace = await requireWorkspace(db, workspaceId);
193
- const configured = resolveWorkspaceSessionToolDefaults(workspace.settings);
192
+ return workspaceSessionToolPolicyDefaultServerIdsFor(
193
+ runtimeSettings.mcpServers,
194
+ workspace.settings,
195
+ );
196
+ }
197
+
198
+ /**
199
+ * The omitted-tools default for one resolved runtime registry and one
200
+ * workspace settings bag. Pure so a caller that already holds both never has
201
+ * to re-query the capability registry to agree with the worker and the
202
+ * composer on which connectors a `workspace_default` session executes with.
203
+ */
204
+ export function workspaceSessionToolPolicyDefaultServerIdsFor(
205
+ runtimeMcpServers: Iterable<{ id: string }>,
206
+ workspaceSettings: unknown,
207
+ ): string[] {
208
+ const availableDefaults = defaultSessionMcpServerIds(runtimeMcpServers);
209
+ const configured = resolveWorkspaceSessionToolDefaults(workspaceSettings);
194
210
  if (!configured?.mcpServerIds) return availableDefaults;
195
211
  const available = new Set(availableDefaults);
196
212
  return sortedIds([
@@ -16,6 +16,7 @@ import { sessionCreationMetadata } from "../site-session-origin";
16
16
  import {
17
17
  canonicalizeConfiguredModelId,
18
18
  configuredAllowedModels,
19
+ withCodexCatalogProvider,
19
20
  ORGANIZATION_GATEWAY_MODEL_ID_PREFIX,
20
21
  ORGANIZATION_OPENROUTER_MODEL_ID_PREFIX,
21
22
  resolveFirstPartyMcpToolPolicy,
@@ -181,6 +182,10 @@ import {
181
182
  workspaceCustomModelReference,
182
183
  } from "../model-catalog";
183
184
  import { settingsWithEnabledCapabilityMcpServers } from "./capabilities";
185
+ import {
186
+ resolveSessionToolPolicy,
187
+ workspaceSessionToolPolicyDefaultServerIdsFor,
188
+ } from "./session-tool-policy";
184
189
  import { validateSubmittedTimelineAnnotations } from "./timeline-annotations";
185
190
  import { requireVariableSetEncryption, validateVariableSetAttachment } from "./environments";
186
191
  import {
@@ -1420,6 +1425,18 @@ export function canonicalConfiguredModel(
1420
1425
  return model;
1421
1426
  }
1422
1427
  const canonicalModel = canonicalizeConfiguredModelId(settings, model);
1428
+ if (
1429
+ canonicalModel.startsWith(CODEX_MODEL_ID_PREFIX) &&
1430
+ settings.resolvedCodexModelsJson !== undefined
1431
+ ) {
1432
+ if (
1433
+ settings.codexSubscriptionEnabled &&
1434
+ configuredAllowedModels(withCodexCatalogProvider(settings)).includes(canonicalModel)
1435
+ ) {
1436
+ return canonicalModel;
1437
+ }
1438
+ throw new HTTPException(422, { message: `model is not available: ${model}` });
1439
+ }
1423
1440
  if (configuredAllowedModels(settings).includes(canonicalModel)) {
1424
1441
  return canonicalModel;
1425
1442
  }
@@ -3724,25 +3741,39 @@ async function acceptSessionUserMessageInFileScope(
3724
3741
  connectionDelegationSource.turnId,
3725
3742
  )
3726
3743
  : null;
3744
+ const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
3745
+ db,
3746
+ workspaceId,
3747
+ settings,
3748
+ inheritedPersonalConnectionDelegations
3749
+ ? {
3750
+ personalConnectionDelegations: inheritedPersonalConnectionDelegations,
3751
+ }
3752
+ : { subjectId: grant.subjectId },
3753
+ );
3727
3754
  const runtimeSettings = settingsWithSessionMcpServerMetadata(
3728
- await settingsWithEnabledCapabilityMcpServers(
3729
- db,
3730
- workspaceId,
3731
- settings,
3732
- inheritedPersonalConnectionDelegations
3733
- ? {
3734
- personalConnectionDelegations: inheritedPersonalConnectionDelegations,
3735
- }
3736
- : { subjectId: grant.subjectId },
3737
- ),
3755
+ capabilityRuntimeSettings,
3738
3756
  existingSession.mcpServers,
3739
3757
  );
3758
+ // A `workspace_default` session stores only its creation-time tool
3759
+ // snapshot and resolves the current workspace defaults at run time, so the
3760
+ // raw column is not the connector allow-list this follow-up executes with.
3761
+ // Freeze accounts against the same resolved list the composer and the
3762
+ // worker see.
3763
+ const connectionAccountTools = sessionToolsForConnectionAccounts({
3764
+ session: existingSession,
3765
+ runtimeMcpServers: runtimeSettings.mcpServers,
3766
+ defaultMcpServerIds: workspaceSessionToolPolicyDefaultServerIdsFor(
3767
+ capabilityRuntimeSettings.mcpServers,
3768
+ (await requireWorkspace(db, workspaceId)).settings,
3769
+ ),
3770
+ });
3740
3771
  const { personalConnectionDelegations, mcpAccountBindings } = await freezeConnectionAccounts({
3741
3772
  db,
3742
3773
  accountId: grant.accountId,
3743
3774
  workspaceId,
3744
3775
  settings: runtimeSettings,
3745
- tools: existingSession.tools,
3776
+ tools: connectionAccountTools,
3746
3777
  resources: [...existingSession.resources, ...requestedResources],
3747
3778
  source: connectionDelegationSource,
3748
3779
  targetSessionId: sessionId,
@@ -4411,6 +4442,34 @@ function withFirstPartyTools(
4411
4442
  return mergeToolRefs(tools, [{ kind: "mcp", id: "opengeni" }]);
4412
4443
  }
4413
4444
 
4445
+ /**
4446
+ * The executable MCP tool list a follow-up on an existing session freezes
4447
+ * connector accounts against.
4448
+ *
4449
+ * A `workspace_default` session stores only the tool snapshot taken when it
4450
+ * was created and expands the current workspace defaults at run time, so a
4451
+ * connector enabled after creation never enters the stored column. The
4452
+ * composer projects that same expansion and submits account selections for
4453
+ * it, so freezing against the stored column rejected every such selection as
4454
+ * unmatched and never froze its personal delegation. Resolve through the one
4455
+ * ID-only policy resolver the API projection and the worker use, with the
4456
+ * same omitted-tools default, so all three agree on the executable set.
4457
+ */
4458
+ export function sessionToolsForConnectionAccounts(input: {
4459
+ session: Pick<Session, "tools" | "toolPolicy">;
4460
+ /** The resolved runtime registry, including session-local servers. */
4461
+ runtimeMcpServers: Iterable<{ id: string }>;
4462
+ /** The current omitted-tools default for this workspace. */
4463
+ defaultMcpServerIds: Iterable<string>;
4464
+ }): ToolRef[] {
4465
+ return resolveSessionToolPolicy({
4466
+ toolPolicy: input.session.toolPolicy,
4467
+ sessionTools: input.session.tools,
4468
+ availableMcpServerIds: [...input.runtimeMcpServers].map((server) => server.id),
4469
+ defaultMcpServerIds: input.defaultMcpServerIds,
4470
+ }).toolRefs;
4471
+ }
4472
+
4414
4473
  function hasOwnProperty(value: unknown, key: string): boolean {
4415
4474
  return Boolean(
4416
4475
  value && typeof value === "object" && Object.prototype.hasOwnProperty.call(value, key),
@@ -4,6 +4,7 @@ import {
4
4
  configuredGatewayWorkspaceProductModelIds,
5
5
  configuredGatewayOrganizationProductModelIds,
6
6
  configuredModels,
7
+ isModelAvailableForNewSelection,
7
8
  configuredModelNotes,
8
9
  configuredOpenRouterWorkspaceProductModelIds,
9
10
  configuredOpenRouterOrganizationProductModelIds,
@@ -708,38 +709,41 @@ export function resolveWorkspaceModelSelection(
708
709
  ? input.credentialReadinessMaxAgeMs
709
710
  : MODEL_CREDENTIAL_READINESS_OBSERVATION_MAX_AGE_MS;
710
711
 
711
- return configuredModels(catalogSettings).map((model) => {
712
- const provider = providers.get(model.providerId);
713
- const policyAllowed =
714
- evaluateWorkspaceModelPolicy(input.policy, {
715
- providerId: model.providerId,
716
- modelId: model.id,
717
- }).allowed && modelAllowedByConnections(input.connectionModelRestrictions ?? {}, model.id);
718
- const credentialReadiness = credentialReadinessFor({
719
- model,
720
- provider,
721
- codexSubscriptionActive: input.codexSubscriptionActive,
722
- xaiSubscriptionActive: input.xaiSubscriptionActive === true,
723
- workspaceGatewayConnectionActive: input.workspaceGatewayConnectionActive === true,
724
- workspaceOpenRouterConnectionActive: input.workspaceOpenRouterConnectionActive === true,
725
- organizationGatewayConnectionActive: input.organizationGatewayConnectionActive === true,
726
- organizationOpenRouterConnectionActive: input.organizationOpenRouterConnectionActive === true,
727
- observation: input.credentialReadinessObservations?.[model.definitionVersion],
728
- nowMs,
729
- maxAgeMs,
730
- });
731
- return {
732
- model,
733
- credentialReadiness,
734
- policyAllowed,
735
- availability: availabilityFor({
712
+ return configuredModels(catalogSettings)
713
+ .filter((model) => isModelAvailableForNewSelection(catalogSettings, model.id))
714
+ .map((model) => {
715
+ const provider = providers.get(model.providerId);
716
+ const policyAllowed =
717
+ evaluateWorkspaceModelPolicy(input.policy, {
718
+ providerId: model.providerId,
719
+ modelId: model.id,
720
+ }).allowed && modelAllowedByConnections(input.connectionModelRestrictions ?? {}, model.id);
721
+ const credentialReadiness = credentialReadinessFor({
736
722
  model,
737
- credentialReadiness,
738
- policyAllowed,
739
- observation: input.observations?.[model.definitionVersion],
723
+ provider,
724
+ codexSubscriptionActive: input.codexSubscriptionActive,
725
+ xaiSubscriptionActive: input.xaiSubscriptionActive === true,
726
+ workspaceGatewayConnectionActive: input.workspaceGatewayConnectionActive === true,
727
+ workspaceOpenRouterConnectionActive: input.workspaceOpenRouterConnectionActive === true,
728
+ organizationGatewayConnectionActive: input.organizationGatewayConnectionActive === true,
729
+ organizationOpenRouterConnectionActive:
730
+ input.organizationOpenRouterConnectionActive === true,
731
+ observation: input.credentialReadinessObservations?.[model.definitionVersion],
740
732
  nowMs,
741
733
  maxAgeMs,
742
- }),
743
- };
744
- });
734
+ });
735
+ return {
736
+ model,
737
+ credentialReadiness,
738
+ policyAllowed,
739
+ availability: availabilityFor({
740
+ model,
741
+ credentialReadiness,
742
+ policyAllowed,
743
+ observation: input.observations?.[model.definitionVersion],
744
+ nowMs,
745
+ maxAgeMs,
746
+ }),
747
+ };
748
+ });
745
749
  }
@@ -46,7 +46,7 @@ import {
46
46
  type SelfhostedOperationResourcePolicy,
47
47
  } from "@opengeni/runtime/sandbox";
48
48
  import { relayConfigFromSettings } from "./routing";
49
- import { managedSessionGroupBackend } from "./runtime-settings";
49
+ import { managedSessionGroupBackend, sessionGroupMachinePresentation } from "./runtime-settings";
50
50
 
51
51
  export type FleetServices = {
52
52
  db: Database;
@@ -130,7 +130,7 @@ export type FleetSandboxEntry = {
130
130
  /** The sandbox id used as the attach/swap/run_on `target`. For the session's
131
131
  * own group box this is the group id (a null active pointer == this box). */
132
132
  id: string;
133
- kind: "modal" | "selfhosted" | "opensandbox";
133
+ kind: Exclude<SandboxBackend, "none">;
134
134
  name: string;
135
135
  liveness: FleetLiveness;
136
136
  /** True for the session's currently-active sandbox (the routing target). */
@@ -325,10 +325,11 @@ export async function listFleet(
325
325
  : groupRecovering
326
326
  ? "recovering"
327
327
  : "wakeable";
328
+ const presentation = sessionGroupMachinePresentation(groupBackend);
328
329
  entries.push({
329
330
  id: ctx.sessionGroupId,
330
- kind: groupBackend === "opensandbox" ? "opensandbox" : "modal",
331
- name: "session sandbox",
331
+ kind: presentation.kind,
332
+ name: presentation.name,
332
333
  liveness: groupOnline ? "online" : groupRecovering ? "reconnecting" : "offline",
333
334
  active: groupActive,
334
335
  isSessionGroup: true,
@@ -34,6 +34,29 @@ export function managedSessionGroupBackend(
34
34
  return backend === "none" || backend === "selfhosted" ? null : backend;
35
35
  }
36
36
 
37
+ const SESSION_GROUP_MACHINE_NAMES: Record<ManagedSessionGroupBackend, string> = {
38
+ local: "this computer",
39
+ docker: "Docker",
40
+ modal: "Modal",
41
+ daytona: "Daytona",
42
+ runloop: "Runloop",
43
+ e2b: "E2B",
44
+ blaxel: "Blaxel",
45
+ cloudflare: "Cloudflare",
46
+ vercel: "Vercel",
47
+ opensandbox: "OpenSandbox",
48
+ };
49
+
50
+ /** Display name and kind for the session's own managed box. The kind is the
51
+ * real provider. Callers must not collapse local, Docker, or other providers
52
+ * into Modal. */
53
+ export function sessionGroupMachinePresentation(backend: ManagedSessionGroupBackend): {
54
+ name: string;
55
+ kind: ManagedSessionGroupBackend;
56
+ } {
57
+ return { name: SESSION_GROUP_MACHINE_NAMES[backend], kind: backend };
58
+ }
59
+
37
60
  /** A machine-home row carries the machine's host OS; its managed group uses the
38
61
  * platform's canonical managed-sandbox OS instead. Ordinary sessions preserve
39
62
  * their explicitly selected OS. */