@opengeni/core 2.7.2-canary.0 → 2.7.5-canary.0

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": "2.7.2-canary.0",
3
+ "version": "2.7.5-canary.0",
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": {
@@ -54,15 +54,17 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "@modelcontextprotocol/sdk": "^1.29.0",
57
- "@opengeni/codex": "^0.2.20-canary.2",
58
- "@opengeni/config": "^0.23.2-canary.0",
59
- "@opengeni/contracts": "^2.11.1-canary.0",
60
- "@opengeni/db": "^3.8.2-canary.0",
61
- "@opengeni/documents": "^0.8.17-canary.0",
62
- "@opengeni/events": "^0.4.15-canary.0",
63
- "@opengeni/observability": "^0.8.17-canary.0",
64
- "@opengeni/runtime": "^2.1.2-canary.0",
65
- "@opengeni/storage": "^0.2.118-canary.0",
57
+ "@opengeni/capabilities": "^0.3.2-canary.0",
58
+ "@opengeni/codex": "^0.2.21-canary.0",
59
+ "@opengeni/config": "^1.0.0-canary.0",
60
+ "@opengeni/contracts": "^2.13.0-canary.0",
61
+ "@opengeni/db": "^4.0.0-canary.0",
62
+ "@opengeni/documents": "^0.8.19-canary.0",
63
+ "@opengeni/events": "^0.4.17-canary.0",
64
+ "@opengeni/network": "^0.3.0-canary.0",
65
+ "@opengeni/observability": "^0.8.19-canary.0",
66
+ "@opengeni/runtime": "^2.3.0-canary.0",
67
+ "@opengeni/storage": "^0.2.120-canary.0",
66
68
  "hono": "^4.12.18",
67
69
  "zod": "^4.2.1"
68
70
  },
@@ -243,6 +243,31 @@ export function requireAccountAdminAuthorizationStamp(
243
243
  });
244
244
  }
245
245
 
246
+ /**
247
+ * Verify that an access authorization was minted by the canonical request
248
+ * resolver for this exact subject and workspace.
249
+ *
250
+ * This is the protocol-neutral boundary for request-local services that need
251
+ * the authenticated grant rather than a caller-supplied grant-shaped object.
252
+ * Object identity is intentional: matching fields alone are not proof that the
253
+ * request authenticated the named subject.
254
+ */
255
+ export function requireResolvedAccessGrantAuthorization(
256
+ authorization: AccessGrantAuthorization,
257
+ workspaceId: string,
258
+ ): AccessGrant {
259
+ const { grant } = authorization;
260
+ if (
261
+ !resolvedAccessGrantAuthorizations.has(authorization) ||
262
+ !authorization.contextIntegrity ||
263
+ authorization.authenticatedSubjectId !== grant.subjectId ||
264
+ grant.workspaceId !== workspaceId
265
+ ) {
266
+ throw new HTTPException(403, { message: "workspace access authorization is invalid" });
267
+ }
268
+ return grant;
269
+ }
270
+
246
271
  /**
247
272
  * Resolve the exact built-in single-user local administrator for an account.
248
273
  *
@@ -0,0 +1,215 @@
1
+ import {
2
+ createGraphqlMcpServer,
3
+ createOpenApiMcpServer,
4
+ createPinnedIntegrationTransport,
5
+ directIntegrationTransport,
6
+ IntegrationInvocationError,
7
+ type IntegrationCredentialResolver,
8
+ type IntegrationInvocationAuthority,
9
+ type IntegrationTransport,
10
+ } from "@opengeni/capabilities";
11
+ import type { Settings } from "@opengeni/config";
12
+ import type { ToolAuthNeededPayload } from "@opengeni/contracts";
13
+ import type {
14
+ ApiIntegrationRuntime,
15
+ ResolveConnectionCredentialInput,
16
+ ResolveConnectionCredentialResult,
17
+ } from "@opengeni/db";
18
+ import type { FetchLike } from "@opengeni/network";
19
+ import type { LocalMcpServerRegistration } from "@opengeni/runtime";
20
+
21
+ export type BuildApiIntegrationServersInput = {
22
+ settings: Settings;
23
+ integrations: readonly ApiIntegrationRuntime[];
24
+ authority: Omit<IntegrationInvocationAuthority, "connectionRef">;
25
+ resolveCredential: (
26
+ input: ResolveConnectionCredentialInput,
27
+ ) => Promise<ResolveConnectionCredentialResult>;
28
+ onAuthNeeded?: (payload: ToolAuthNeededPayload) => Promise<void> | void;
29
+ fetchImpl?: FetchLike;
30
+ };
31
+
32
+ /**
33
+ * Compile active persisted API facets into ordinary in-process MCP providers.
34
+ * The caller supplies its authority resolver, so agent attempts and current
35
+ * humans use the same provider assembly without sharing credentials or policy.
36
+ */
37
+ export function buildApiIntegrationMcpServers(
38
+ input: BuildApiIntegrationServersInput,
39
+ ): LocalMcpServerRegistration[] {
40
+ const transport = createPinnedIntegrationTransport({
41
+ network: input.settings,
42
+ ...(input.fetchImpl ? { fetchImpl: input.fetchImpl } : {}),
43
+ });
44
+ return input.integrations.map((integration) => {
45
+ const authority: IntegrationInvocationAuthority = {
46
+ ...input.authority,
47
+ ...(integration.connectionRef?.connectionId
48
+ ? { connectionRef: integration.connectionRef.connectionId }
49
+ : {}),
50
+ };
51
+ const credentialResolver = integration.connectionRef
52
+ ? integrationCredentialResolver(input, integration, "execution")
53
+ : undefined;
54
+ const buildServer = (
55
+ serverTransport: IntegrationTransport,
56
+ serverCredentialResolver = credentialResolver,
57
+ ) =>
58
+ integration.revision.protocol === "openapi"
59
+ ? createOpenApiMcpServer({
60
+ revision: integration.revision,
61
+ transport: serverTransport,
62
+ authority,
63
+ ...(serverCredentialResolver ? { credentialResolver: serverCredentialResolver } : {}),
64
+ })
65
+ : createGraphqlMcpServer({
66
+ revision: integration.revision,
67
+ endpoint: integration.baseUrl,
68
+ transport: serverTransport,
69
+ authority,
70
+ ...(serverCredentialResolver ? { credentialResolver: serverCredentialResolver } : {}),
71
+ });
72
+ const server = buildServer(transport);
73
+ const preflightCredentialResolver = integration.connectionRef
74
+ ? integrationCredentialResolver(input, integration, "preflight")
75
+ : undefined;
76
+ const preflightServer = preflightCredentialResolver
77
+ ? buildServer(providerBlockingPreflightTransport, preflightCredentialResolver)
78
+ : null;
79
+ return {
80
+ id: integration.serverId,
81
+ server,
82
+ approvalAuthority: {
83
+ kind: "api_integration",
84
+ capabilityId: integration.capabilityId,
85
+ pluginKey: integration.pluginKey,
86
+ pluginInstallationId: integration.pluginInstallationId,
87
+ installationVersion: integration.installationVersion,
88
+ instanceId: integration.instanceId,
89
+ instanceKey: integration.instanceKey,
90
+ instanceVersion: integration.instanceVersion,
91
+ definitionId: integration.definitionId,
92
+ definitionProvenance: integration.definitionProvenance,
93
+ revisionId: integration.revision.id,
94
+ baseUrl: integration.baseUrl,
95
+ providerDomain: integration.providerDomain,
96
+ connectionRef: integration.connectionRef,
97
+ connectionAuthorityGeneration: integration.connectionAuthorityGeneration,
98
+ },
99
+ ...(integration.connectionRef?.connectionId
100
+ ? { resolvedConnectionId: integration.connectionRef.connectionId }
101
+ : {}),
102
+ ...(credentialResolver
103
+ ? {
104
+ preflightCall: async (
105
+ toolName: string,
106
+ args: Record<string, unknown>,
107
+ options?: { signal?: AbortSignal },
108
+ ) => await preflightApiIntegrationCall(preflightServer!, toolName, args, options),
109
+ }
110
+ : {}),
111
+ };
112
+ });
113
+ }
114
+
115
+ const providerBlockingPreflightTransport = directIntegrationTransport(async () => {
116
+ throw new Error("integration provider request blocked by preflight");
117
+ });
118
+
119
+ async function preflightApiIntegrationCall(
120
+ server: LocalMcpServerRegistration["server"],
121
+ toolName: string,
122
+ args: Record<string, unknown>,
123
+ options?: { signal?: AbortSignal },
124
+ ): Promise<void> {
125
+ try {
126
+ await server.callTool(toolName, args, null, options);
127
+ } catch (error) {
128
+ if (error instanceof IntegrationInvocationError && error.code === "request_failed") return;
129
+ throw error;
130
+ }
131
+ throw new Error("integration preflight unexpectedly crossed the provider request boundary");
132
+ }
133
+
134
+ function integrationCredentialResolver(
135
+ input: BuildApiIntegrationServersInput,
136
+ integration: ApiIntegrationRuntime,
137
+ credentialResolutionMode: "execution" | "preflight",
138
+ ): IntegrationCredentialResolver {
139
+ const connectionRef = integration.connectionRef;
140
+ if (!connectionRef) throw new Error("Integration credential resolver requires a connection");
141
+ const expectedAuthorityGeneration = integration.connectionAuthorityGeneration;
142
+ if (expectedAuthorityGeneration === null) {
143
+ throw new Error("Integration credential resolver requires a connection authority generation");
144
+ }
145
+ return {
146
+ resolve: async (request) => {
147
+ const result = await input.resolveCredential({
148
+ workspaceId: input.authority.workspaceId,
149
+ serverId: integration.serverId,
150
+ toolName: request.operationKey,
151
+ connectionRef,
152
+ destinationUrl: request.destinationUrl,
153
+ credentialTarget: "http_api",
154
+ forceRefresh: request.forceRefresh === true,
155
+ credentialResolutionMode,
156
+ expectedAuthorityGeneration,
157
+ });
158
+ if (result.status === "auth_needed") {
159
+ await publishAuthNeeded(
160
+ input,
161
+ integration.serverId,
162
+ request.operationKey,
163
+ result,
164
+ connectionRef,
165
+ );
166
+ return null;
167
+ }
168
+ const destination = new URL(request.destinationUrl);
169
+ return {
170
+ audience: { origin: destination.origin, pathPrefix: "/" },
171
+ placements:
172
+ result.placements ??
173
+ Object.entries(result.headers).map(([name, value]) => ({
174
+ carrier: "header" as const,
175
+ name,
176
+ value,
177
+ })),
178
+ ...(result.authorizeProviderRequest
179
+ ? { authorizeProviderRequest: result.authorizeProviderRequest }
180
+ : {}),
181
+ ...(result.expiresAt ? { expiresAt: result.expiresAt.toISOString() } : {}),
182
+ ...(connectionRef.scopes ? { scope: [...connectionRef.scopes] } : {}),
183
+ };
184
+ },
185
+ };
186
+ }
187
+
188
+ async function publishAuthNeeded(
189
+ input: BuildApiIntegrationServersInput,
190
+ serverId: string,
191
+ toolName: string,
192
+ result: Extract<ResolveConnectionCredentialResult, { status: "auth_needed" }>,
193
+ connectionRef: NonNullable<ApiIntegrationRuntime["connectionRef"]>,
194
+ ): Promise<void> {
195
+ try {
196
+ await input.onAuthNeeded?.({
197
+ serverId,
198
+ toolName,
199
+ providerDomain: result.providerDomain,
200
+ ...(result.provider ? { provider: result.provider } : {}),
201
+ reason: result.reason,
202
+ ...(result.connectionId ? { connectionId: result.connectionId } : {}),
203
+ ...(result.authoritySource === "host" || connectionRef.authoritySource === "host"
204
+ ? { authoritySource: "host" as const }
205
+ : {}),
206
+ ...(result.scopes ? { scopes: result.scopes } : {}),
207
+ ...(result.resource ? { resource: result.resource } : {}),
208
+ ...(result.selectedResources ? { selectedResources: result.selectedResources } : {}),
209
+ ...(result.authorizationUrl ? { authorizationUrl: result.authorizationUrl } : {}),
210
+ });
211
+ } catch {
212
+ // Authentication notices are advisory UI/audit signals. The local tool
213
+ // still returns the fixed connection-required result when publication fails.
214
+ }
215
+ }
@@ -555,7 +555,7 @@ export async function controlAgentSessionWorkstream(
555
555
  wakeRevision: result.workflowWake.wakeRevision,
556
556
  shouldSignal: true,
557
557
  interruptionCount: result.interruptionCount,
558
- controlRequested: true,
558
+ controlRequested: input.action === "pause",
559
559
  });
560
560
  },
561
561
  },
@@ -928,7 +928,7 @@ export async function controlHumanSessionWorkstreamWithOutcome(
928
928
  wakeRevision: result.workflowWake.wakeRevision,
929
929
  shouldSignal: true,
930
930
  interruptionCount: result.interruptionCount,
931
- controlRequested: true,
931
+ controlRequested: input.action === "pause",
932
932
  });
933
933
  },
934
934
  },
package/src/index.ts CHANGED
@@ -106,6 +106,7 @@ export * from "./application/composer-submit";
106
106
  export * from "./application/session-commands";
107
107
  export * from "./application/session-tenancy";
108
108
  export * from "./application/user-resource-grants";
109
+ export * from "./application/api-integration-servers";
109
110
 
110
111
  // Durable editable-artifact live broker, ticket, ports, and projection types.
111
112
  export * from "./editable-artifact-live";
@@ -29,7 +29,6 @@ import {
29
29
  type SandboxRetainedProcess,
30
30
  type SandboxWorkspaceMutationAdmission,
31
31
  } from "@opengeni/db";
32
- import { settleSessionBackgroundCommandForRetainedProcess } from "@opengeni/db/session-background-commands";
33
32
  import { appendAndPublishEvents, type EventBus } from "@opengeni/events";
34
33
  import {
35
34
  isProviderSandboxGoneDuringRoutedOperation,
@@ -449,14 +448,11 @@ export function wrapChannelABoxWithRouting(
449
448
  reason: proof.reason,
450
449
  idleGraceMs: settings.sandboxIdleGraceMs,
451
450
  });
452
- const backgroundSettlement = retainedProcessBackgroundSettlement(settlement.process, proof);
453
- await settleSessionBackgroundCommandForRetainedProcess(db, {
454
- accountId: ids.accountId,
455
- workspaceId: ids.workspaceId,
456
- sessionId: ids.sessionId,
457
- retainedProcessId: process.id,
458
- ...backgroundSettlement,
459
- });
451
+ if (settlement.backgroundCommandEvents.length > 0 && bus) {
452
+ await bus
453
+ .publish(ids.workspaceId, ids.sessionId, settlement.backgroundCommandEvents)
454
+ .catch(() => undefined);
455
+ }
460
456
  }
461
457
  : undefined;
462
458
  const resolver = makeActiveBackendResolver({
@@ -520,6 +516,7 @@ export function wrapChannelABoxWithRouting(
520
516
  });
521
517
 
522
518
  const proxy = new RoutingSandboxSession({
519
+ bindActiveRouteOnFirstResolve: true,
523
520
  defaultResolved: {
524
521
  session: established.session as RoutableBackendSession,
525
522
  sandboxId: null,