@opengeni/api-router 2.6.4 → 2.10.0-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/dist/access-grant-rls.d.ts +3 -0
- package/dist/app.d.ts +2 -1
- package/dist/app.js +3 -1
- package/dist/auth/organization-user-setup.d.ts +23 -0
- package/dist/{chunk-XTLI3CBH.js → chunk-XXH7DW34.js} +6866 -3554
- package/dist/chunk-XXH7DW34.js.map +1 -0
- package/dist/codemode.d.ts +6 -0
- package/dist/github-access.d.ts +25 -2
- package/dist/http/request-source.d.ts +14 -0
- package/dist/index.js +19 -3
- package/dist/index.js.map +1 -1
- package/dist/integrations/oauth-client.d.ts +2 -11
- package/dist/integrations/personal-github-repositories.d.ts +41 -2
- package/dist/integrations/slack-interactions.d.ts +1 -1
- package/dist/mcp/server.d.ts +158 -1
- package/dist/mcp/session-view.d.ts +56 -2
- package/dist/mcp/session-wait.d.ts +1 -1
- package/dist/mcp-oauth.d.ts +19 -0
- package/dist/model-catalog.d.ts +5 -31
- package/dist/routes/codex.d.ts +27 -2
- package/dist/routes/github.d.ts +2 -0
- package/dist/routes/organization-model-providers.d.ts +3 -0
- package/dist/routes/workspace-artifacts.d.ts +2 -1
- package/dist/work-discovery-observability.d.ts +1 -1
- package/dist/workspace-artifact-content.d.ts +28 -0
- package/dist/workspace-artifact-provenance.d.ts +7 -0
- package/dist/workspace-deletion.d.ts +6 -0
- package/dist/workspace-tool-gateway-observability.d.ts +17 -0
- package/dist/workspace-tool-gateway.d.ts +118 -0
- package/package.json +19 -18
- package/src/access-grant-rls.ts +40 -0
- package/src/app.ts +296 -53
- package/src/auth/organization-user-setup.ts +95 -5
- package/src/codemode.ts +33 -4
- package/src/github-access.ts +117 -1
- package/src/http/auth.ts +11 -0
- package/src/http/request-source.ts +37 -0
- package/src/http/sse.ts +15 -7
- package/src/index.ts +18 -2
- package/src/integrations/oauth-client.ts +168 -203
- package/src/integrations/personal-github-repositories.ts +238 -9
- package/src/integrations/slack-app-home.ts +2 -2
- package/src/integrations/slack-interactions.ts +77 -37
- package/src/mcp/company-brain-governed-writes.ts +2 -2
- package/src/mcp/company-profile-agent-admin.ts +1 -1
- package/src/mcp/remember.ts +1 -1
- package/src/mcp/server.ts +504 -133
- package/src/mcp/session-view.ts +20 -1
- package/src/mcp/session-wait.ts +3 -0
- package/src/mcp-oauth.ts +539 -0
- package/src/model-catalog.ts +45 -337
- package/src/routes/automations.ts +178 -19
- package/src/routes/codex.ts +242 -73
- package/src/routes/connections.ts +271 -16
- package/src/routes/documents.ts +67 -19
- package/src/routes/files.ts +54 -6
- package/src/routes/github.ts +116 -15
- package/src/routes/organization-memberships.ts +59 -16
- package/src/routes/organization-model-providers.ts +231 -0
- package/src/routes/packs.ts +1 -0
- package/src/routes/personal-github.ts +39 -0
- package/src/routes/pr-review.ts +72 -4
- package/src/routes/scheduled-tasks.ts +40 -13
- package/src/routes/sessions.ts +153 -65
- package/src/routes/supergrok.ts +3 -2
- package/src/routes/workspace-artifacts.ts +176 -67
- package/src/routes/workspaces.ts +405 -62
- package/src/sandbox/channel-a.ts +17 -4
- package/src/work-discovery-observability.ts +3 -3
- package/src/workspace-artifact-content.ts +163 -0
- package/src/workspace-artifact-provenance.ts +104 -0
- package/src/workspace-deletion.ts +64 -0
- package/src/workspace-tool-gateway-observability.ts +100 -0
- package/src/workspace-tool-gateway.ts +691 -0
- package/dist/chunk-XTLI3CBH.js.map +0 -1
package/src/model-catalog.ts
CHANGED
|
@@ -1,62 +1,55 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
withCodexCatalogProvider,
|
|
5
|
-
withXaiSubscriptionCatalogProvider,
|
|
6
|
-
withWorkspaceGatewayCatalogProvider,
|
|
2
|
+
OPENROUTER_PROVIDER_ID,
|
|
3
|
+
WORKSPACE_OPENROUTER_PROVIDER_ID,
|
|
7
4
|
type ConfiguredModel,
|
|
8
|
-
type Settings,
|
|
9
5
|
} from "@opengeni/config";
|
|
10
6
|
import {
|
|
11
7
|
ClientModel,
|
|
12
8
|
WorkspaceModelCatalogResponse,
|
|
13
|
-
evaluateWorkspaceModelPolicy,
|
|
14
|
-
type ModelAvailabilityV1,
|
|
15
|
-
type ModelCredentialReadinessV1,
|
|
16
9
|
type WorkspaceModelCatalogResponse as WorkspaceModelCatalogResponseType,
|
|
17
|
-
type WorkspaceModelPolicyContract,
|
|
18
10
|
} from "@opengeni/contracts";
|
|
11
|
+
import { resolveWorkspaceModelSelection, type WorkspaceModelSelectionInput } from "@opengeni/core";
|
|
19
12
|
|
|
20
|
-
export
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export type ModelCredentialReadinessObservation =
|
|
27
|
-
| { status: "ready"; checkedAt: string }
|
|
28
|
-
| {
|
|
29
|
-
status: "not_ready";
|
|
30
|
-
reason: "prerequisites_missing" | "needs_reauth";
|
|
31
|
-
checkedAt: string;
|
|
32
|
-
}
|
|
33
|
-
| { status: "error"; reason: "resolver_error"; checkedAt: string };
|
|
34
|
-
|
|
35
|
-
export const MODEL_CREDENTIAL_READINESS_OBSERVATION_MAX_AGE_MS = 5 * 60_000;
|
|
13
|
+
export {
|
|
14
|
+
MODEL_CREDENTIAL_READINESS_OBSERVATION_MAX_AGE_MS,
|
|
15
|
+
type ModelAvailabilityObservation,
|
|
16
|
+
type ModelCredentialReadinessObservation,
|
|
17
|
+
} from "@opengeni/core";
|
|
36
18
|
|
|
37
19
|
/** Static, client-safe definition projection. No provider secret is reachable. */
|
|
38
20
|
export function projectClientModel(model: ConfiguredModel): ClientModel {
|
|
39
21
|
const anonymousProvider =
|
|
40
22
|
model.credentialSource.kind === "deployment" && model.credentialSource.mechanism === "none";
|
|
23
|
+
// Keep the established closed `source` enum compatible for older same-major
|
|
24
|
+
// clients. OpenRouter remains truthfully identified by its public provider
|
|
25
|
+
// id/label and billing metadata; omitting this optional legacy grouping field
|
|
26
|
+
// lets tolerant older contracts parse the additive provider.
|
|
41
27
|
const source =
|
|
42
|
-
model.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
28
|
+
model.providerId === OPENROUTER_PROVIDER_ID ||
|
|
29
|
+
model.providerId === WORKSPACE_OPENROUTER_PROVIDER_ID
|
|
30
|
+
? undefined
|
|
31
|
+
: model.credentialSource.kind === "connected_subscription"
|
|
32
|
+
? model.credentialSource.provider === "xai"
|
|
33
|
+
? "supergrok"
|
|
34
|
+
: "codex"
|
|
35
|
+
: model.credentialSource.kind === "workspace_connection"
|
|
36
|
+
? "workspace_gateway"
|
|
37
|
+
: anonymousProvider
|
|
38
|
+
? undefined
|
|
39
|
+
: "opengeni";
|
|
51
40
|
const publicProvider = anonymousProvider
|
|
52
41
|
? { provider: model.providerId, providerLabel: model.providerLabel }
|
|
53
|
-
:
|
|
54
|
-
? { provider: "
|
|
55
|
-
:
|
|
56
|
-
? { provider: "
|
|
57
|
-
: source === "
|
|
58
|
-
? { provider: "
|
|
59
|
-
:
|
|
42
|
+
: model.providerId === OPENROUTER_PROVIDER_ID
|
|
43
|
+
? { provider: "openrouter", providerLabel: "OpenRouter" }
|
|
44
|
+
: model.providerId === WORKSPACE_OPENROUTER_PROVIDER_ID
|
|
45
|
+
? { provider: "workspace-openrouter", providerLabel: "Your OpenRouter" }
|
|
46
|
+
: source === "codex"
|
|
47
|
+
? { provider: "codex", providerLabel: "Codex" }
|
|
48
|
+
: source === "supergrok"
|
|
49
|
+
? { provider: "supergrok", providerLabel: "SuperGrok" }
|
|
50
|
+
: source === "workspace_gateway"
|
|
51
|
+
? { provider: "workspace-gateway", providerLabel: "Your Gateway" }
|
|
52
|
+
: { provider: "opengeni", providerLabel: "OpenGeni" };
|
|
60
53
|
return ClientModel.parse({
|
|
61
54
|
id: model.id,
|
|
62
55
|
label: model.label,
|
|
@@ -71,243 +64,13 @@ export function projectClientModel(model: ConfiguredModel): ClientModel {
|
|
|
71
64
|
aliases: model.aliases,
|
|
72
65
|
executionLimits: model.executionLimits,
|
|
73
66
|
billing: model.billing,
|
|
67
|
+
cost: model.cost,
|
|
74
68
|
capabilities: model.capabilities,
|
|
75
69
|
...(model.pricing === undefined ? {} : { pricing: model.pricing }),
|
|
76
70
|
definitionVersion: model.definitionVersion,
|
|
77
71
|
});
|
|
78
72
|
}
|
|
79
73
|
|
|
80
|
-
function modelDefinitionRunnable(model: ConfiguredModel): boolean {
|
|
81
|
-
return (
|
|
82
|
-
model.capabilities.inputModalities.includes("text") &&
|
|
83
|
-
model.capabilities.outputModalities.includes("text") &&
|
|
84
|
-
model.capabilities.transports.sse.runnable
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function observedCredentialReadiness(input: {
|
|
89
|
-
observation: ModelCredentialReadinessObservation | undefined;
|
|
90
|
-
basis: "connection" | "resolver";
|
|
91
|
-
nowMs: number;
|
|
92
|
-
maxAgeMs: number;
|
|
93
|
-
}): ModelCredentialReadinessV1 {
|
|
94
|
-
if (!input.observation) {
|
|
95
|
-
return {
|
|
96
|
-
status: "not_ready",
|
|
97
|
-
reason: "prerequisites_missing",
|
|
98
|
-
basis: input.basis,
|
|
99
|
-
checkedAt: null,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
const checkedAtMs = Date.parse(input.observation.checkedAt);
|
|
103
|
-
if (!Number.isFinite(checkedAtMs)) {
|
|
104
|
-
return {
|
|
105
|
-
status: "error",
|
|
106
|
-
reason: "resolver_error",
|
|
107
|
-
basis: input.basis,
|
|
108
|
-
checkedAt: null,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
const checkedAt = new Date(checkedAtMs).toISOString();
|
|
112
|
-
if (Math.abs(input.nowMs - checkedAtMs) > input.maxAgeMs) {
|
|
113
|
-
return {
|
|
114
|
-
status: "not_ready",
|
|
115
|
-
reason: "observation_stale",
|
|
116
|
-
basis: input.basis,
|
|
117
|
-
checkedAt,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
if (input.observation.status === "ready") {
|
|
121
|
-
return { status: "ready", reason: null, basis: input.basis, checkedAt };
|
|
122
|
-
}
|
|
123
|
-
if (input.observation.status === "not_ready") {
|
|
124
|
-
return {
|
|
125
|
-
status: "not_ready",
|
|
126
|
-
reason:
|
|
127
|
-
input.observation.reason === "needs_reauth" ? "needs_reauth" : "prerequisites_missing",
|
|
128
|
-
basis: input.basis,
|
|
129
|
-
checkedAt,
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
return {
|
|
133
|
-
status: "error",
|
|
134
|
-
reason: "resolver_error",
|
|
135
|
-
basis: input.basis,
|
|
136
|
-
checkedAt,
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function credentialReadinessFor(input: {
|
|
141
|
-
model: ConfiguredModel;
|
|
142
|
-
provider: ReturnType<typeof configuredProviders>[number] | undefined;
|
|
143
|
-
codexSubscriptionActive: boolean;
|
|
144
|
-
xaiSubscriptionActive: boolean;
|
|
145
|
-
workspaceGatewayConnectionActive: boolean;
|
|
146
|
-
observation: ModelCredentialReadinessObservation | undefined;
|
|
147
|
-
nowMs: number;
|
|
148
|
-
maxAgeMs: number;
|
|
149
|
-
}): ModelCredentialReadinessV1 {
|
|
150
|
-
const source = input.model.credentialSource;
|
|
151
|
-
if (source.kind === "connected_subscription") {
|
|
152
|
-
const active =
|
|
153
|
-
source.provider === "xai" ? input.xaiSubscriptionActive : input.codexSubscriptionActive;
|
|
154
|
-
return active
|
|
155
|
-
? { status: "ready", reason: null, basis: "connection", checkedAt: null }
|
|
156
|
-
: {
|
|
157
|
-
status: "not_ready",
|
|
158
|
-
reason: "needs_reauth",
|
|
159
|
-
basis: "connection",
|
|
160
|
-
checkedAt: null,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
if (source.kind === "workspace_connection") {
|
|
164
|
-
return input.workspaceGatewayConnectionActive
|
|
165
|
-
? { status: "ready", reason: null, basis: "connection", checkedAt: null }
|
|
166
|
-
: {
|
|
167
|
-
status: "not_ready",
|
|
168
|
-
reason: "needs_reauth",
|
|
169
|
-
basis: "connection",
|
|
170
|
-
checkedAt: null,
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
if (source.kind === "deployment" && source.mechanism === "none") {
|
|
174
|
-
return { status: "ready", reason: null, basis: "configuration", checkedAt: null };
|
|
175
|
-
}
|
|
176
|
-
if (source.kind === "deployment" && source.mechanism === "api_key") {
|
|
177
|
-
return input.provider?.apiKey
|
|
178
|
-
? { status: "ready", reason: null, basis: "configuration", checkedAt: null }
|
|
179
|
-
: {
|
|
180
|
-
status: "not_ready",
|
|
181
|
-
reason: "missing_credential",
|
|
182
|
-
basis: "configuration",
|
|
183
|
-
checkedAt: null,
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
return observedCredentialReadiness({
|
|
187
|
-
observation: input.observation,
|
|
188
|
-
basis: "resolver",
|
|
189
|
-
nowMs: input.nowMs,
|
|
190
|
-
maxAgeMs: input.maxAgeMs,
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function isXaiGrokModel(model: ConfiguredModel): boolean {
|
|
195
|
-
return model.providerId === "xai" && model.id.startsWith("xai/grok-");
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function observationTimestamp(observation: ModelAvailabilityObservation | undefined): {
|
|
199
|
-
checkedAt: string | null;
|
|
200
|
-
checkedAtMs: number | null;
|
|
201
|
-
} {
|
|
202
|
-
if (!observation || typeof observation.checkedAt !== "string") {
|
|
203
|
-
return { checkedAt: null, checkedAtMs: null };
|
|
204
|
-
}
|
|
205
|
-
const checkedAtMs = Date.parse(observation.checkedAt);
|
|
206
|
-
if (!Number.isFinite(checkedAtMs)) {
|
|
207
|
-
return { checkedAt: null, checkedAtMs: null };
|
|
208
|
-
}
|
|
209
|
-
return { checkedAt: new Date(checkedAtMs).toISOString(), checkedAtMs };
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function xaiGrokAvailabilityFor(input: {
|
|
213
|
-
observation: ModelAvailabilityObservation | undefined;
|
|
214
|
-
nowMs: number;
|
|
215
|
-
maxAgeMs: number;
|
|
216
|
-
}): ModelAvailabilityV1 {
|
|
217
|
-
const { checkedAt, checkedAtMs } = observationTimestamp(input.observation);
|
|
218
|
-
const freshSuccessfulObservation =
|
|
219
|
-
input.observation?.status === "available" &&
|
|
220
|
-
input.observation.reason === null &&
|
|
221
|
-
checkedAtMs !== null &&
|
|
222
|
-
checkedAtMs <= input.nowMs &&
|
|
223
|
-
input.nowMs - checkedAtMs <= input.maxAgeMs;
|
|
224
|
-
|
|
225
|
-
if (freshSuccessfulObservation) {
|
|
226
|
-
return {
|
|
227
|
-
status: "available",
|
|
228
|
-
selectable: true,
|
|
229
|
-
reason: null,
|
|
230
|
-
checkedAt,
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return {
|
|
235
|
-
status: "unavailable",
|
|
236
|
-
selectable: false,
|
|
237
|
-
reason:
|
|
238
|
-
input.observation?.status === "unavailable"
|
|
239
|
-
? (input.observation.reason ?? "provider_unhealthy")
|
|
240
|
-
: "provider_unhealthy",
|
|
241
|
-
checkedAt,
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function availabilityFor(input: {
|
|
246
|
-
model: ConfiguredModel;
|
|
247
|
-
credentialReadiness: ModelCredentialReadinessV1;
|
|
248
|
-
policyAllowed: boolean;
|
|
249
|
-
observation?: ModelAvailabilityObservation | undefined;
|
|
250
|
-
nowMs: number;
|
|
251
|
-
maxAgeMs: number;
|
|
252
|
-
}): ModelAvailabilityV1 {
|
|
253
|
-
if (!modelDefinitionRunnable(input.model)) {
|
|
254
|
-
return {
|
|
255
|
-
status: "unavailable",
|
|
256
|
-
selectable: false,
|
|
257
|
-
reason: "unsupported",
|
|
258
|
-
checkedAt: null,
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
if (input.credentialReadiness.status !== "ready") {
|
|
262
|
-
return {
|
|
263
|
-
status: "unavailable",
|
|
264
|
-
selectable: false,
|
|
265
|
-
reason:
|
|
266
|
-
input.credentialReadiness.reason === "missing_credential"
|
|
267
|
-
? "missing_credential"
|
|
268
|
-
: input.credentialReadiness.reason === "needs_reauth"
|
|
269
|
-
? "needs_reauth"
|
|
270
|
-
: "credential_not_ready",
|
|
271
|
-
checkedAt: input.credentialReadiness.checkedAt,
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
if (!input.policyAllowed) {
|
|
275
|
-
return {
|
|
276
|
-
status: "unavailable",
|
|
277
|
-
selectable: false,
|
|
278
|
-
reason: "policy_blocked",
|
|
279
|
-
checkedAt: null,
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
if (isXaiGrokModel(input.model)) {
|
|
283
|
-
return xaiGrokAvailabilityFor({
|
|
284
|
-
observation: input.observation,
|
|
285
|
-
nowMs: input.nowMs,
|
|
286
|
-
maxAgeMs: input.maxAgeMs,
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
if (!input.observation) {
|
|
290
|
-
// Credential readiness and policy are known-good, but no current
|
|
291
|
-
// provider-health observation is available. Unknown is intentionally
|
|
292
|
-
// selectable; the execution boundary rechecks all authoritative gates.
|
|
293
|
-
return { status: "unknown", selectable: true, reason: null, checkedAt: null };
|
|
294
|
-
}
|
|
295
|
-
if (input.observation.status === "unavailable") {
|
|
296
|
-
return {
|
|
297
|
-
status: "unavailable",
|
|
298
|
-
selectable: false,
|
|
299
|
-
reason: input.observation.reason ?? "provider_unhealthy",
|
|
300
|
-
checkedAt: input.observation.checkedAt,
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
return {
|
|
304
|
-
status: input.observation.status,
|
|
305
|
-
selectable: true,
|
|
306
|
-
reason: null,
|
|
307
|
-
checkedAt: input.observation.checkedAt,
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
|
|
311
74
|
/**
|
|
312
75
|
* Authenticated workspace catalog. Codex definitions are included only when
|
|
313
76
|
* the deployment enables that connection type; concrete readiness is supplied
|
|
@@ -316,69 +79,14 @@ function availabilityFor(input: {
|
|
|
316
79
|
* successful typed resolver observation; credential health and provider health
|
|
317
80
|
* are separate inputs and neither is fabricated.
|
|
318
81
|
*/
|
|
319
|
-
export function buildWorkspaceModelCatalog(
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
observations?: Readonly<Record<string, ModelAvailabilityObservation>> | undefined;
|
|
329
|
-
now?: Date | undefined;
|
|
330
|
-
credentialReadinessMaxAgeMs?: number | undefined;
|
|
331
|
-
}): WorkspaceModelCatalogResponseType {
|
|
332
|
-
const codexSettings = input.settings.codexSubscriptionEnabled
|
|
333
|
-
? withCodexCatalogProvider(input.settings)
|
|
334
|
-
: input.settings;
|
|
335
|
-
const xaiSettings = input.settings.supergrokSubscriptionEnabled
|
|
336
|
-
? withXaiSubscriptionCatalogProvider(codexSettings)
|
|
337
|
-
: codexSettings;
|
|
338
|
-
const catalogSettings = withWorkspaceGatewayCatalogProvider(xaiSettings);
|
|
339
|
-
const providers = new Map(
|
|
340
|
-
configuredProviders(catalogSettings).map((provider) => [provider.id, provider]),
|
|
341
|
-
);
|
|
342
|
-
const requestedNowMs = input.now?.getTime();
|
|
343
|
-
const nowMs =
|
|
344
|
-
typeof requestedNowMs === "number" && Number.isFinite(requestedNowMs)
|
|
345
|
-
? requestedNowMs
|
|
346
|
-
: Date.now();
|
|
347
|
-
const maxAgeMs =
|
|
348
|
-
typeof input.credentialReadinessMaxAgeMs === "number" &&
|
|
349
|
-
Number.isFinite(input.credentialReadinessMaxAgeMs) &&
|
|
350
|
-
input.credentialReadinessMaxAgeMs >= 0
|
|
351
|
-
? input.credentialReadinessMaxAgeMs
|
|
352
|
-
: MODEL_CREDENTIAL_READINESS_OBSERVATION_MAX_AGE_MS;
|
|
353
|
-
const models = configuredModels(catalogSettings).map((model) => {
|
|
354
|
-
const provider = providers.get(model.providerId);
|
|
355
|
-
const policyAllowed = evaluateWorkspaceModelPolicy(input.policy, {
|
|
356
|
-
providerId: model.providerId,
|
|
357
|
-
modelId: model.id,
|
|
358
|
-
}).allowed;
|
|
359
|
-
const credentialReadiness = credentialReadinessFor({
|
|
360
|
-
model,
|
|
361
|
-
provider,
|
|
362
|
-
codexSubscriptionActive: input.codexSubscriptionActive,
|
|
363
|
-
xaiSubscriptionActive: input.xaiSubscriptionActive === true,
|
|
364
|
-
workspaceGatewayConnectionActive: input.workspaceGatewayConnectionActive === true,
|
|
365
|
-
observation: input.credentialReadinessObservations?.[model.definitionVersion],
|
|
366
|
-
nowMs,
|
|
367
|
-
maxAgeMs,
|
|
368
|
-
});
|
|
369
|
-
return {
|
|
370
|
-
...projectClientModel(model),
|
|
371
|
-
credentialReadiness,
|
|
372
|
-
policyAllowed,
|
|
373
|
-
availability: availabilityFor({
|
|
374
|
-
model,
|
|
375
|
-
credentialReadiness,
|
|
376
|
-
policyAllowed,
|
|
377
|
-
observation: input.observations?.[model.definitionVersion],
|
|
378
|
-
nowMs,
|
|
379
|
-
maxAgeMs,
|
|
380
|
-
}),
|
|
381
|
-
};
|
|
382
|
-
});
|
|
82
|
+
export function buildWorkspaceModelCatalog(
|
|
83
|
+
input: WorkspaceModelSelectionInput,
|
|
84
|
+
): WorkspaceModelCatalogResponseType {
|
|
85
|
+
const models = resolveWorkspaceModelSelection(input).map((selection) => ({
|
|
86
|
+
...projectClientModel(selection.model),
|
|
87
|
+
credentialReadiness: selection.credentialReadiness,
|
|
88
|
+
policyAllowed: selection.policyAllowed,
|
|
89
|
+
availability: selection.availability,
|
|
90
|
+
}));
|
|
383
91
|
return WorkspaceModelCatalogResponse.parse({ models });
|
|
384
92
|
}
|
|
@@ -29,14 +29,21 @@ import {
|
|
|
29
29
|
resolveAutomationWebhookEndpoint,
|
|
30
30
|
updateAutomationSource,
|
|
31
31
|
updateAutomationTrigger,
|
|
32
|
+
withWorkspaceGatewayCustomModelReadLock,
|
|
33
|
+
withWorkspaceOpenRouterCustomModelReadLock,
|
|
32
34
|
type AutomationSourceSecret,
|
|
33
35
|
} from "@opengeni/db";
|
|
34
36
|
import {
|
|
35
37
|
automationRequestDigest,
|
|
38
|
+
assertWorkspaceModelPolicyAllows,
|
|
36
39
|
buildAutomationAcceptedExecution,
|
|
40
|
+
canonicalConfiguredModel,
|
|
41
|
+
workspaceCustomModelReference,
|
|
42
|
+
lockActiveCustomModelForAdmission,
|
|
37
43
|
requireAccessGrant,
|
|
38
44
|
requireAutomationAdapter,
|
|
39
45
|
requirePermission,
|
|
46
|
+
resolveWorkspaceCatalogSettings,
|
|
40
47
|
type ApiRouteDeps,
|
|
41
48
|
} from "@opengeni/core";
|
|
42
49
|
import type { Hono } from "hono";
|
|
@@ -157,13 +164,36 @@ export function registerAutomationRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
157
164
|
for (const permission of request.sessionTemplate.firstPartyMcpPermissions) {
|
|
158
165
|
requirePermission(grant, permission);
|
|
159
166
|
}
|
|
167
|
+
const catalogSettings = (
|
|
168
|
+
await resolveWorkspaceCatalogSettings(deps.db, deps.settings, {
|
|
169
|
+
accountId: grant.accountId,
|
|
170
|
+
workspaceId,
|
|
171
|
+
})
|
|
172
|
+
).settings;
|
|
173
|
+
const model = canonicalConfiguredModel(catalogSettings, request.sessionTemplate.model) ?? null;
|
|
174
|
+
if (model) {
|
|
175
|
+
await assertWorkspaceModelPolicyAllows(deps.db, catalogSettings, workspaceId, model);
|
|
176
|
+
}
|
|
177
|
+
const normalizedRequest = {
|
|
178
|
+
...request,
|
|
179
|
+
sessionTemplate: { ...request.sessionTemplate, model },
|
|
180
|
+
};
|
|
181
|
+
const beforeCreateCommit = model
|
|
182
|
+
? workspaceCustomModelCommitGuard({
|
|
183
|
+
settings: catalogSettings,
|
|
184
|
+
accountId: grant.accountId,
|
|
185
|
+
workspaceId,
|
|
186
|
+
modelId: model,
|
|
187
|
+
})
|
|
188
|
+
: undefined;
|
|
160
189
|
return c.json(
|
|
161
190
|
await createAutomationTrigger(deps.db, {
|
|
162
191
|
accountId: grant.accountId,
|
|
163
192
|
workspaceId,
|
|
164
193
|
createdBySubjectId: grant.subjectId,
|
|
165
|
-
request,
|
|
194
|
+
request: normalizedRequest,
|
|
166
195
|
adapterId: adapter.id,
|
|
196
|
+
...(beforeCreateCommit ? { beforeCreateCommit } : {}),
|
|
167
197
|
}),
|
|
168
198
|
201,
|
|
169
199
|
);
|
|
@@ -194,12 +224,54 @@ export function registerAutomationRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
194
224
|
requirePermission(grant, permission);
|
|
195
225
|
}
|
|
196
226
|
}
|
|
227
|
+
const catalogSettings = (
|
|
228
|
+
await resolveWorkspaceCatalogSettings(deps.db, deps.settings, {
|
|
229
|
+
accountId: grant.accountId,
|
|
230
|
+
workspaceId,
|
|
231
|
+
})
|
|
232
|
+
).settings;
|
|
233
|
+
const normalizedRequest = request.sessionTemplate
|
|
234
|
+
? {
|
|
235
|
+
...request,
|
|
236
|
+
sessionTemplate: {
|
|
237
|
+
...request.sessionTemplate,
|
|
238
|
+
model: canonicalConfiguredModel(catalogSettings, request.sessionTemplate.model) ?? null,
|
|
239
|
+
},
|
|
240
|
+
}
|
|
241
|
+
: request;
|
|
242
|
+
if (normalizedRequest.sessionTemplate?.model) {
|
|
243
|
+
await assertWorkspaceModelPolicyAllows(
|
|
244
|
+
deps.db,
|
|
245
|
+
catalogSettings,
|
|
246
|
+
workspaceId,
|
|
247
|
+
normalizedRequest.sessionTemplate.model,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
const effectiveModel = normalizedRequest.sessionTemplate
|
|
251
|
+
? normalizedRequest.sessionTemplate.model
|
|
252
|
+
: existing.sessionTemplate.model;
|
|
253
|
+
const materialExecutionChange =
|
|
254
|
+
request.eventTypes !== undefined ||
|
|
255
|
+
request.configuration !== undefined ||
|
|
256
|
+
request.parameters !== undefined ||
|
|
257
|
+
request.sessionTemplate !== undefined ||
|
|
258
|
+
(request.status === "active" && existing.status !== "active");
|
|
259
|
+
const beforeUpdateCommit =
|
|
260
|
+
materialExecutionChange && effectiveModel
|
|
261
|
+
? workspaceCustomModelCommitGuard({
|
|
262
|
+
settings: catalogSettings,
|
|
263
|
+
accountId: grant.accountId,
|
|
264
|
+
workspaceId,
|
|
265
|
+
modelId: effectiveModel,
|
|
266
|
+
})
|
|
267
|
+
: undefined;
|
|
197
268
|
try {
|
|
198
269
|
const trigger = await updateAutomationTrigger(deps.db, {
|
|
199
270
|
workspaceId,
|
|
200
271
|
triggerId: c.req.param("triggerId"),
|
|
201
272
|
subjectId: grant.subjectId,
|
|
202
|
-
request,
|
|
273
|
+
request: normalizedRequest,
|
|
274
|
+
...(beforeUpdateCommit ? { beforeUpdateCommit } : {}),
|
|
203
275
|
});
|
|
204
276
|
if (!trigger)
|
|
205
277
|
throw new HTTPException(404, {
|
|
@@ -377,29 +449,86 @@ export async function acceptAutomationEvent(
|
|
|
377
449
|
workspaceId: source.workspaceId,
|
|
378
450
|
sourceId: source.id,
|
|
379
451
|
});
|
|
380
|
-
const
|
|
452
|
+
const matchingByEvent = triggers.filter((trigger) =>
|
|
381
453
|
adapter.matches({ event: input.normalizedEvent, trigger }),
|
|
382
454
|
);
|
|
383
|
-
if (
|
|
455
|
+
if (matchingByEvent.length > AUTOMATION_MAX_MATCHED_TRIGGERS) {
|
|
384
456
|
throw new HTTPException(422, {
|
|
385
457
|
message: `automation event matches more than ${AUTOMATION_MAX_MATCHED_TRIGGERS} triggers`,
|
|
386
458
|
});
|
|
387
459
|
}
|
|
388
|
-
const stored =
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
460
|
+
const stored =
|
|
461
|
+
matchingByEvent.length === 0
|
|
462
|
+
? await recordAutomationEvent(deps.db, {
|
|
463
|
+
accountId: source.accountId,
|
|
464
|
+
workspaceId: source.workspaceId,
|
|
465
|
+
sourceId: source.id,
|
|
466
|
+
sourceVersion: source.version,
|
|
467
|
+
sourceConfiguration: source.configuration,
|
|
468
|
+
matchedTriggerRevisions: [],
|
|
469
|
+
deliveryKey: input.deliveryKey,
|
|
470
|
+
requestDigest: input.requestDigest,
|
|
471
|
+
normalizedEvent: input.normalizedEvent,
|
|
472
|
+
ignoredReason: "no_matching_triggers",
|
|
473
|
+
})
|
|
474
|
+
: await withWorkspaceGatewayCustomModelReadLock(
|
|
475
|
+
deps.db,
|
|
476
|
+
{ accountId: source.accountId, workspaceId: source.workspaceId },
|
|
477
|
+
async (gatewayLockedDb) =>
|
|
478
|
+
await withWorkspaceOpenRouterCustomModelReadLock(
|
|
479
|
+
gatewayLockedDb,
|
|
480
|
+
{ accountId: source.accountId, workspaceId: source.workspaceId },
|
|
481
|
+
async (lockedDb) => {
|
|
482
|
+
const catalogSourceSettings = deps.catalogSourceSettings ?? deps.settings;
|
|
483
|
+
const catalogSettings = (
|
|
484
|
+
await resolveWorkspaceCatalogSettings(lockedDb, catalogSourceSettings, {
|
|
485
|
+
accountId: source.accountId,
|
|
486
|
+
workspaceId: source.workspaceId,
|
|
487
|
+
})
|
|
488
|
+
).settings;
|
|
489
|
+
const matchingAtAcceptance = [];
|
|
490
|
+
for (const trigger of matchingByEvent) {
|
|
491
|
+
try {
|
|
492
|
+
const render = adapter.render({
|
|
493
|
+
event: input.normalizedEvent,
|
|
494
|
+
trigger,
|
|
495
|
+
source,
|
|
496
|
+
});
|
|
497
|
+
const requestedModel =
|
|
498
|
+
render.sessionTemplate.model ?? catalogSettings.openaiModel;
|
|
499
|
+
const model = canonicalConfiguredModel(catalogSettings, requestedModel);
|
|
500
|
+
if (!model) continue;
|
|
501
|
+
await assertWorkspaceModelPolicyAllows(
|
|
502
|
+
lockedDb,
|
|
503
|
+
catalogSettings,
|
|
504
|
+
source.workspaceId,
|
|
505
|
+
model,
|
|
506
|
+
);
|
|
507
|
+
matchingAtAcceptance.push(trigger);
|
|
508
|
+
} catch (error) {
|
|
509
|
+
if (isUnprocessableEntity(error)) continue;
|
|
510
|
+
throw error;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return await recordAutomationEvent(lockedDb, {
|
|
514
|
+
accountId: source.accountId,
|
|
515
|
+
workspaceId: source.workspaceId,
|
|
516
|
+
sourceId: source.id,
|
|
517
|
+
sourceVersion: source.version,
|
|
518
|
+
sourceConfiguration: source.configuration,
|
|
519
|
+
matchedTriggerRevisions: matchingAtAcceptance.map((trigger) => ({
|
|
520
|
+
triggerId: trigger.id,
|
|
521
|
+
revision: trigger.revision,
|
|
522
|
+
})),
|
|
523
|
+
deliveryKey: input.deliveryKey,
|
|
524
|
+
requestDigest: input.requestDigest,
|
|
525
|
+
normalizedEvent: input.normalizedEvent,
|
|
526
|
+
ignoredReason:
|
|
527
|
+
matchingAtAcceptance.length === 0 ? "no_executable_triggers" : null,
|
|
528
|
+
});
|
|
529
|
+
},
|
|
530
|
+
),
|
|
531
|
+
);
|
|
403
532
|
const matching = await getAutomationTriggerRevisions(deps.db, {
|
|
404
533
|
workspaceId: source.workspaceId,
|
|
405
534
|
refs: stored.event.matchedTriggerRevisions,
|
|
@@ -457,6 +586,36 @@ export async function acceptAutomationEvent(
|
|
|
457
586
|
});
|
|
458
587
|
}
|
|
459
588
|
|
|
589
|
+
function isUnprocessableEntity(error: unknown): boolean {
|
|
590
|
+
return (
|
|
591
|
+
error instanceof Error &&
|
|
592
|
+
"status" in error &&
|
|
593
|
+
(error as Error & { status?: unknown }).status === 422
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function workspaceCustomModelCommitGuard(input: {
|
|
598
|
+
settings: ApiRouteDeps["settings"];
|
|
599
|
+
accountId: string;
|
|
600
|
+
workspaceId: string;
|
|
601
|
+
modelId: string;
|
|
602
|
+
}): ((tx: ApiRouteDeps["db"]) => Promise<void>) | undefined {
|
|
603
|
+
const reference = workspaceCustomModelReference(input.settings, input.modelId);
|
|
604
|
+
if (!reference) return undefined;
|
|
605
|
+
return async (tx): Promise<void> => {
|
|
606
|
+
const active = await lockActiveCustomModelForAdmission(tx, {
|
|
607
|
+
accountId: input.accountId,
|
|
608
|
+
workspaceId: input.workspaceId,
|
|
609
|
+
reference,
|
|
610
|
+
});
|
|
611
|
+
if (!active) {
|
|
612
|
+
throw new HTTPException(422, {
|
|
613
|
+
message: `model is not available: ${input.modelId}`,
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
460
619
|
async function requireSource(
|
|
461
620
|
deps: ApiRouteDeps,
|
|
462
621
|
accountId: string,
|