@opengeni/core 0.20.13 → 0.21.2

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.
@@ -1,4 +1,13 @@
1
1
  import type { TranscribeAudioResponse, VoiceInputErrorCode } from "@opengeni/contracts";
2
+ /**
3
+ * Server-owned upstream budget for one provider attempt. Resumable recording
4
+ * claims remain fenced for longer than this budget before another worker may
5
+ * reclaim them. Provider adapters must honor the supplied AbortSignal and must
6
+ * not return while their upstream request is still live; OpenGeni does not
7
+ * claim remote-side idempotency or cancellation for vendors that cannot meet
8
+ * that adapter contract.
9
+ */
10
+ export declare const TRANSCRIPTION_PROVIDER_REQUEST_TIMEOUT_MILLISECONDS: number;
2
11
  export type TranscriptionLimits = {
3
12
  maxDurationSeconds: number;
4
13
  maxSizeBytes: number;
@@ -13,6 +22,10 @@ export type TranscriptionRequest = {
13
22
  durationSeconds?: number | undefined;
14
23
  signal?: AbortSignal | undefined;
15
24
  requestId: string;
25
+ /** Absolute server-owned provider deadline persisted for resumable attempts. */
26
+ providerDeadlineAt?: Date | undefined;
27
+ /** Exact provider selected before a resumable segment is first sent upstream. */
28
+ providerId?: string | undefined;
16
29
  };
17
30
  export type TranscriptionResult = TranscribeAudioResponse & {
18
31
  /** Server-private provider id for operational metrics only. Never returned to clients. */
@@ -43,6 +56,8 @@ export type TranscriptionAvailabilityContext = {
43
56
  */
44
57
  export type TranscriptionProvider = {
45
58
  readonly id: string;
59
+ /** The adapter guarantees that its upstream transport honors AbortSignal. */
60
+ readonly supportsServerDeadline: true;
46
61
  readonly experimental?: boolean | undefined;
47
62
  /**
48
63
  * Deployment readiness when called without a workspace. When `workspaceId` is
@@ -54,6 +69,7 @@ export type TranscriptionProvider = {
54
69
  mimeType: string;
55
70
  filename: string;
56
71
  workspaceId: string;
72
+ requestId: string;
57
73
  signal?: AbortSignal | undefined;
58
74
  }): Promise<{
59
75
  text: string;
@@ -64,8 +80,27 @@ export type TranscriptionService = {
64
80
  limits(): TranscriptionLimits;
65
81
  /** True when at least one ready provider can serve requests. */
66
82
  available(context?: TranscriptionAvailabilityContext): boolean | Promise<boolean>;
83
+ /** Select one provider before a durable segment attempt; retries pin this id. */
84
+ selectProvider?(context: TranscriptionAvailabilityContext): string | null | Promise<string | null>;
67
85
  transcribe(request: TranscriptionRequest): Promise<TranscriptionResult>;
68
86
  };
87
+ export type PreparedTranscriptionSegment = {
88
+ segmentNumber: number;
89
+ startMilliseconds: number;
90
+ durationMilliseconds: number;
91
+ mimeType: "audio/wav";
92
+ bytes: Uint8Array;
93
+ };
94
+ export type TranscriptionSegmenter = {
95
+ available(): boolean | Promise<boolean>;
96
+ segment(input: {
97
+ sourceMimeType: string;
98
+ totalDurationMilliseconds: number;
99
+ providerSegmentSeconds: number;
100
+ chunks: AsyncIterable<Uint8Array>;
101
+ signal?: AbortSignal | undefined;
102
+ }): AsyncIterable<PreparedTranscriptionSegment>;
103
+ };
69
104
  export declare function normalizeMimeType(mimeType: string): string;
70
105
  export declare function isAcceptedMimeType(mimeType: string, accepted: readonly string[]): boolean;
71
106
  export declare function filenameForMimeType(mimeType: string): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/core",
3
- "version": "0.20.13",
3
+ "version": "0.21.2",
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": {
@@ -34,15 +34,15 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@modelcontextprotocol/sdk": "^1.29.0",
37
- "@opengeni/codex": "^0.2.10",
38
- "@opengeni/config": "^0.10.12",
39
- "@opengeni/contracts": "^0.38.2",
40
- "@opengeni/db": "^0.27.9",
41
- "@opengeni/documents": "^0.5.5",
42
- "@opengeni/events": "^0.3.75",
43
- "@opengeni/observability": "^0.4.15",
44
- "@opengeni/runtime": "^0.18.12",
45
- "@opengeni/storage": "^0.2.65",
37
+ "@opengeni/codex": "^0.2.11",
38
+ "@opengeni/config": "^0.11.0",
39
+ "@opengeni/contracts": "^0.39.0",
40
+ "@opengeni/db": "^0.28.1",
41
+ "@opengeni/documents": "^0.5.10",
42
+ "@opengeni/events": "^0.3.80",
43
+ "@opengeni/observability": "^0.5.0",
44
+ "@opengeni/runtime": "^0.18.17",
45
+ "@opengeni/storage": "^0.2.68",
46
46
  "hono": "^4.12.18"
47
47
  },
48
48
  "engines": {
@@ -15,7 +15,7 @@ import type { Observability } from "@opengeni/observability";
15
15
  import type { createObjectStorage } from "@opengeni/storage";
16
16
  import type { ManagedAuth } from "./managed-auth-type";
17
17
  import type { ApiSandboxClient, ResumeBoxByIdInput, ResumedSandboxSession } from "./sandbox-types";
18
- import type { TranscriptionService } from "./transcription";
18
+ import type { TranscriptionSegmenter, TranscriptionService } from "./transcription";
19
19
 
20
20
  export type SessionWorkflowClient = {
21
21
  signalUserMessage: (input: {
@@ -123,6 +123,8 @@ export type AppDependencies = {
123
123
  oauthCallbackDeadlineMs?: number;
124
124
  /** Optional host-owned voice-input transcription service. */
125
125
  transcription?: TranscriptionService | null;
126
+ /** Optional host-owned long-form audio normalization/segmentation service. */
127
+ transcriptionSegmenter?: TranscriptionSegmenter | null;
126
128
  // The API process's OWN agent-loop-free sandbox client (constructed from
127
129
  // settings via @opengeni/runtime/sandbox). Undefined when sandboxBackend=none.
128
130
  // This is the foundation of the API-direct control plane: the API resumes
@@ -29,6 +29,8 @@ import {
29
29
  getCapabilityCatalogItem,
30
30
  getCapabilityInstallation,
31
31
  getConnectionMetadata,
32
+ getCodexAppsCredentialAuthorizationForRun,
33
+ getWorkspaceGrant,
32
34
  getPackInstallation,
33
35
  getStoredCapabilityHeaderCiphertext,
34
36
  getVariableSet,
@@ -45,6 +47,7 @@ import {
45
47
  type EnabledMcpCapabilityServer,
46
48
  } from "@opengeni/db";
47
49
  import { HTTPException } from "hono/http-exception";
50
+ import { hasPermission } from "../access";
48
51
  import {
49
52
  getSkillLibraryEntry,
50
53
  listSkillLibraryEntries,
@@ -137,6 +140,15 @@ export async function createCatalogItem(input: {
137
140
  message: "skill ids are managed by the OpenGeni skill library or runtime adapters",
138
141
  });
139
142
  }
143
+ if (
144
+ input.payload.kind === "mcp" &&
145
+ typeof input.payload.metadata.mcpServerId === "string" &&
146
+ input.payload.metadata.mcpServerId.trim() === CODEX_APPS_MCP_SERVER_ID
147
+ ) {
148
+ throw new HTTPException(422, {
149
+ message: `${CODEX_APPS_MCP_SERVER_ID} is reserved for the canonical Codex Apps service`,
150
+ });
151
+ }
140
152
  const source =
141
153
  input.payload.source === "built_in" ||
142
154
  input.payload.source === "library" ||
@@ -721,36 +733,76 @@ export async function settingsWithEnabledCapabilityMcpServers(
721
733
  workspaceId: string,
722
734
  settings: Settings,
723
735
  ): Promise<Settings> {
724
- const enabled = await listEnabledMcpCapabilityServers(db, workspaceId);
725
- return settingsWithCodexAppsMcpServer(settingsWithMcpCapabilityServers(settings, enabled));
736
+ const [enabled, codexAppsCredentialId] = await Promise.all([
737
+ listEnabledMcpCapabilityServers(db, workspaceId),
738
+ resolveCodexAppsCredentialIdForRun(db, workspaceId),
739
+ ]);
740
+ return settingsWithCodexAppsMcpServer(
741
+ settingsWithMcpCapabilityServers(settings, enabled),
742
+ codexAppsCredentialId !== null,
743
+ );
744
+ }
745
+
746
+ /**
747
+ * Resolve executable Apps authority. The connector must remain active and its
748
+ * exact owner must still hold workspace connection-management permission.
749
+ */
750
+ export async function resolveCodexAppsCredentialIdForRun(
751
+ db: Database,
752
+ workspaceId: string,
753
+ ): Promise<string | null> {
754
+ const authorization = await getCodexAppsCredentialAuthorizationForRun(db, workspaceId);
755
+ if (!authorization) return null;
756
+ const grant = await getWorkspaceGrant(db, authorization.ownerSubjectId, workspaceId);
757
+ return grant && hasPermission(grant.permissions, "connections:write")
758
+ ? authorization.credentialId
759
+ : null;
726
760
  }
727
761
 
728
762
  /**
729
- * Register Codex Apps as an optional runtime MCP when the deployment enables
730
- * it. Registration only makes the server selectable; the session tool policy
731
- * decides whether the model sees it, and Codex credential resolution
732
- * independently decides whether calls can authenticate.
763
+ * Register Codex Apps as an optional runtime MCP only when the deployment is
764
+ * enabled and this workspace has an active explicit Apps designation.
765
+ * Registration only makes the server selectable; session policy decides
766
+ * whether the model sees it.
733
767
  */
734
- export function settingsWithCodexAppsMcpServer(settings: Settings): Settings {
768
+ export function settingsWithCodexAppsMcpServer(
769
+ settings: Settings,
770
+ credentialAvailable: boolean,
771
+ ): Settings {
772
+ const canonicalServer = {
773
+ id: CODEX_APPS_MCP_SERVER_ID,
774
+ name: CODEX_APPS_MCP_SERVER_NAME,
775
+ url: CODEX_APPS_MCP_URL,
776
+ timeoutMs: CODEX_APPS_STARTUP_TIMEOUT_MS,
777
+ // Availability is credential-specific, so discover on every run.
778
+ cacheToolsList: false,
779
+ };
780
+ // The id is a credential-routing trust boundary. Discard every configured or
781
+ // capability-provided claimant before optionally appending the one canonical
782
+ // endpoint; preserving an existing id could send the designated bearer to an
783
+ // attacker-controlled URL.
784
+ const withoutReservedId = settings.mcpServers.filter(
785
+ (server) => server.id !== CODEX_APPS_MCP_SERVER_ID,
786
+ );
787
+ if (!settings.codexConnectedAppsEnabled || !credentialAvailable) {
788
+ return withoutReservedId.length === settings.mcpServers.length
789
+ ? settings
790
+ : { ...settings, mcpServers: withoutReservedId };
791
+ }
792
+ const existing = settings.mcpServers.at(-1);
735
793
  if (
736
- !settings.codexConnectedAppsEnabled ||
737
- settings.mcpServers.some((server) => server.id === CODEX_APPS_MCP_SERVER_ID)
794
+ withoutReservedId.length === settings.mcpServers.length - 1 &&
795
+ existing !== undefined &&
796
+ Object.keys(existing).length === Object.keys(canonicalServer).length &&
797
+ Object.entries(canonicalServer).every(
798
+ ([key, value]) => existing[key as keyof typeof existing] === value,
799
+ )
738
800
  ) {
739
801
  return settings;
740
802
  }
741
803
  return {
742
804
  ...settings,
743
- mcpServers: [
744
- ...settings.mcpServers,
745
- {
746
- id: CODEX_APPS_MCP_SERVER_ID,
747
- name: CODEX_APPS_MCP_SERVER_NAME,
748
- url: CODEX_APPS_MCP_URL,
749
- timeoutMs: CODEX_APPS_STARTUP_TIMEOUT_MS,
750
- // Availability is credential-specific, so discover on every run.
751
- cacheToolsList: false,
752
- },
753
- ],
805
+ mcpServers: [...withoutReservedId, canonicalServer],
754
806
  };
755
807
  }
756
808
 
@@ -1,5 +1,15 @@
1
1
  import type { TranscribeAudioResponse, VoiceInputErrorCode } from "@opengeni/contracts";
2
2
 
3
+ /**
4
+ * Server-owned upstream budget for one provider attempt. Resumable recording
5
+ * claims remain fenced for longer than this budget before another worker may
6
+ * reclaim them. Provider adapters must honor the supplied AbortSignal and must
7
+ * not return while their upstream request is still live; OpenGeni does not
8
+ * claim remote-side idempotency or cancellation for vendors that cannot meet
9
+ * that adapter contract.
10
+ */
11
+ export const TRANSCRIPTION_PROVIDER_REQUEST_TIMEOUT_MILLISECONDS = 10 * 60 * 1_000;
12
+
3
13
  export type TranscriptionLimits = {
4
14
  maxDurationSeconds: number;
5
15
  maxSizeBytes: number;
@@ -15,6 +25,10 @@ export type TranscriptionRequest = {
15
25
  durationSeconds?: number | undefined;
16
26
  signal?: AbortSignal | undefined;
17
27
  requestId: string;
28
+ /** Absolute server-owned provider deadline persisted for resumable attempts. */
29
+ providerDeadlineAt?: Date | undefined;
30
+ /** Exact provider selected before a resumable segment is first sent upstream. */
31
+ providerId?: string | undefined;
18
32
  };
19
33
 
20
34
  export type TranscriptionResult = TranscribeAudioResponse & {
@@ -82,6 +96,8 @@ export type TranscriptionAvailabilityContext = {
82
96
  */
83
97
  export type TranscriptionProvider = {
84
98
  readonly id: string;
99
+ /** The adapter guarantees that its upstream transport honors AbortSignal. */
100
+ readonly supportsServerDeadline: true;
85
101
  readonly experimental?: boolean | undefined;
86
102
  /**
87
103
  * Deployment readiness when called without a workspace. When `workspaceId` is
@@ -93,6 +109,7 @@ export type TranscriptionProvider = {
93
109
  mimeType: string;
94
110
  filename: string;
95
111
  workspaceId: string;
112
+ requestId: string;
96
113
  signal?: AbortSignal | undefined;
97
114
  }): Promise<{ text: string; languages: string[] }>;
98
115
  };
@@ -101,9 +118,32 @@ export type TranscriptionService = {
101
118
  limits(): TranscriptionLimits;
102
119
  /** True when at least one ready provider can serve requests. */
103
120
  available(context?: TranscriptionAvailabilityContext): boolean | Promise<boolean>;
121
+ /** Select one provider before a durable segment attempt; retries pin this id. */
122
+ selectProvider?(
123
+ context: TranscriptionAvailabilityContext,
124
+ ): string | null | Promise<string | null>;
104
125
  transcribe(request: TranscriptionRequest): Promise<TranscriptionResult>;
105
126
  };
106
127
 
128
+ export type PreparedTranscriptionSegment = {
129
+ segmentNumber: number;
130
+ startMilliseconds: number;
131
+ durationMilliseconds: number;
132
+ mimeType: "audio/wav";
133
+ bytes: Uint8Array;
134
+ };
135
+
136
+ export type TranscriptionSegmenter = {
137
+ available(): boolean | Promise<boolean>;
138
+ segment(input: {
139
+ sourceMimeType: string;
140
+ totalDurationMilliseconds: number;
141
+ providerSegmentSeconds: number;
142
+ chunks: AsyncIterable<Uint8Array>;
143
+ signal?: AbortSignal | undefined;
144
+ }): AsyncIterable<PreparedTranscriptionSegment>;
145
+ };
146
+
107
147
  export function normalizeMimeType(mimeType: string): string {
108
148
  return mimeType.trim().toLowerCase();
109
149
  }