@opengeni/api-router 0.7.3 → 0.9.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/app.js +1 -1
- package/dist/{chunk-EYYTFA7N.js → chunk-QOYQBYHM.js} +1920 -264
- package/dist/chunk-QOYQBYHM.js.map +1 -0
- package/dist/index.js +1 -1
- package/package.json +12 -11
- package/src/app.ts +37 -15
- package/src/codex-redemption-security.ts +96 -0
- package/src/integrations/oauth-client.ts +147 -61
- package/src/mcp/server.ts +62 -8
- package/src/mcp/session-view.ts +102 -1
- package/src/mcp/toolspace.ts +389 -124
- package/src/model-catalog.ts +337 -0
- package/src/routes/codex.ts +866 -14
- package/src/routes/files.ts +153 -0
- package/src/routes/sessions.ts +155 -10
- package/src/routes/workspaces.ts +36 -2
- package/src/sandbox/viewer.ts +7 -1
- package/dist/chunk-EYYTFA7N.js.map +0 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// src/app.ts
|
|
2
2
|
import {
|
|
3
|
+
canonicalizeConfiguredModelId as canonicalizeConfiguredModelId2,
|
|
3
4
|
configuredAllowedModels,
|
|
4
5
|
configuredAllowedReasoningEfforts,
|
|
5
|
-
configuredModels
|
|
6
|
+
configuredModels as configuredModels2
|
|
6
7
|
} from "@opengeni/config";
|
|
7
8
|
import {
|
|
8
9
|
ClientConfig,
|
|
@@ -23,7 +24,7 @@ import { bodyLimit } from "hono/body-limit";
|
|
|
23
24
|
import { cors } from "hono/cors";
|
|
24
25
|
import { HTTPException as HTTPException24 } from "hono/http-exception";
|
|
25
26
|
import {
|
|
26
|
-
hasPermission as
|
|
27
|
+
hasPermission as hasPermission5,
|
|
27
28
|
requireAccessGrant as requireAccessGrant17,
|
|
28
29
|
requirePermission,
|
|
29
30
|
requireSessionAuthorization as requireSessionAuthorization3,
|
|
@@ -289,11 +290,15 @@ import {
|
|
|
289
290
|
CreateScheduledTaskRequest,
|
|
290
291
|
defaultRepositoryMountPath,
|
|
291
292
|
SESSION_EVENT_RAW_DELTA_TYPES,
|
|
293
|
+
SessionEventLatestClass,
|
|
292
294
|
SessionEventPayloadMode,
|
|
293
295
|
SessionEventReadDirection,
|
|
294
296
|
SessionEventReadMode,
|
|
297
|
+
SessionEventResultMode,
|
|
295
298
|
SessionEventSemanticClass,
|
|
296
299
|
SessionEventType,
|
|
300
|
+
compactSessionEventResult,
|
|
301
|
+
sessionEventLatestClassToSemanticClass,
|
|
297
302
|
SessionMcpCredentialUpdateInput,
|
|
298
303
|
VariableSetVariableName,
|
|
299
304
|
UpdateScheduledTaskRequest
|
|
@@ -419,7 +424,10 @@ import {
|
|
|
419
424
|
createSessionForRequest,
|
|
420
425
|
sendAgentSessionMessage,
|
|
421
426
|
steerAgentSession,
|
|
422
|
-
updateSessionTitle
|
|
427
|
+
updateSessionTitle,
|
|
428
|
+
sessionWithEffectiveToolPolicy,
|
|
429
|
+
workspaceSessionToolPolicyDefaultServerIds,
|
|
430
|
+
workspaceSessionToolPolicyServerIds
|
|
423
431
|
} from "@opengeni/core";
|
|
424
432
|
import {
|
|
425
433
|
buildFleetContextForSession,
|
|
@@ -430,11 +438,70 @@ import {
|
|
|
430
438
|
} from "@opengeni/core";
|
|
431
439
|
|
|
432
440
|
// src/mcp/session-view.ts
|
|
433
|
-
import {
|
|
441
|
+
import {
|
|
442
|
+
boundSessionEventPayload,
|
|
443
|
+
measureSessionEventJson,
|
|
444
|
+
sessionEventJsonBytes
|
|
445
|
+
} from "@opengeni/contracts";
|
|
434
446
|
var SESSION_EVENT_MCP_MAX_BYTES = 64 * 1024;
|
|
435
447
|
var SESSION_EVENT_MCP_FIELD_MAX_CHARS = 4e3;
|
|
436
448
|
var SESSION_DETAIL_MCP_MAX_BYTES = 64 * 1024;
|
|
437
449
|
var RIG_DETAIL_MCP_MAX_BYTES = 64 * 1024;
|
|
450
|
+
function boundSessionEventCompactResult(result, maxBytes = SESSION_EVENT_MCP_MAX_BYTES) {
|
|
451
|
+
const envelopeMaxBytes = Math.max(8 * 1024, maxBytes);
|
|
452
|
+
const project = (budget) => {
|
|
453
|
+
const noValues = budget <= 0;
|
|
454
|
+
const text = noValues || result.text === null ? null : clampString(result.text, Math.max(128, budget));
|
|
455
|
+
const boundValue = (value) => noValues || value === null ? null : boundSessionEventPayload(value, {
|
|
456
|
+
surface: "http_projection",
|
|
457
|
+
maxBytes: Math.max(1024, budget)
|
|
458
|
+
});
|
|
459
|
+
const output = boundValue(result.output);
|
|
460
|
+
const resultValue = boundValue(result.result);
|
|
461
|
+
const checkpoint = boundValue(result.checkpoint);
|
|
462
|
+
const receipt = boundValue(result.receipt);
|
|
463
|
+
const failure = noValues || result.failure === null ? null : {
|
|
464
|
+
error: clampString(result.failure.error ?? "", Math.max(128, Math.floor(budget / 3))) || null,
|
|
465
|
+
code: clampString(result.failure.code ?? "", Math.max(128, Math.floor(budget / 6))) || null,
|
|
466
|
+
retryable: result.failure.retryable,
|
|
467
|
+
recovery: clampString(result.failure.recovery ?? "", Math.max(128, Math.floor(budget / 3))) || null
|
|
468
|
+
};
|
|
469
|
+
const changed = text !== result.text || output !== result.output || resultValue !== result.result || checkpoint !== result.checkpoint || receipt !== result.receipt || JSON.stringify(failure) !== JSON.stringify(result.failure);
|
|
470
|
+
const inheritedSourceBoundary = result.truncation.fields.includes("payload");
|
|
471
|
+
const mcpBoundaryRecorded = changed || inheritedSourceBoundary;
|
|
472
|
+
const deliveredBytes = sessionEventJsonBytes({
|
|
473
|
+
text,
|
|
474
|
+
output,
|
|
475
|
+
result: resultValue,
|
|
476
|
+
failure,
|
|
477
|
+
checkpoint,
|
|
478
|
+
receipt
|
|
479
|
+
});
|
|
480
|
+
return {
|
|
481
|
+
...result,
|
|
482
|
+
text,
|
|
483
|
+
output,
|
|
484
|
+
result: resultValue,
|
|
485
|
+
failure,
|
|
486
|
+
checkpoint,
|
|
487
|
+
receipt,
|
|
488
|
+
truncation: {
|
|
489
|
+
...result.truncation,
|
|
490
|
+
truncated: result.truncation.truncated || changed,
|
|
491
|
+
fields: mcpBoundaryRecorded ? [.../* @__PURE__ */ new Set([...result.truncation.fields, "mcp_envelope"])] : result.truncation.fields,
|
|
492
|
+
originalBytes: changed ? result.truncation.originalBytes ?? result.truncation.deliveredBytes : result.truncation.originalBytes,
|
|
493
|
+
deliveredBytes
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
};
|
|
497
|
+
for (const budget of [12e3, 8e3, 4e3, 2e3, 1e3, 0]) {
|
|
498
|
+
const candidate = project(budget);
|
|
499
|
+
if (prettyJsonBytes(candidate) <= envelopeMaxBytes) return candidate;
|
|
500
|
+
}
|
|
501
|
+
throw new RangeError(
|
|
502
|
+
`Session-event compact result exceeds its ${envelopeMaxBytes}-byte envelope`
|
|
503
|
+
);
|
|
504
|
+
}
|
|
438
505
|
function safeStringify(value) {
|
|
439
506
|
if (typeof value === "string") return value;
|
|
440
507
|
try {
|
|
@@ -1985,13 +2052,20 @@ function registerWorkspaceOrchestrationTools(server, deps, grant, can, callerSes
|
|
|
1985
2052
|
},
|
|
1986
2053
|
authorization?.relatedSessionAccess ?? "root"
|
|
1987
2054
|
);
|
|
1988
|
-
return json(
|
|
2055
|
+
return json(
|
|
2056
|
+
boundSessionDetailMcp(
|
|
2057
|
+
await withMcpEffectivePolicy(deps, grant.workspaceId, {
|
|
2058
|
+
...projected,
|
|
2059
|
+
effectiveControl: queue?.effectiveControl ?? projected.effectiveControl
|
|
2060
|
+
})
|
|
2061
|
+
)
|
|
2062
|
+
);
|
|
1989
2063
|
}
|
|
1990
2064
|
);
|
|
1991
2065
|
server.registerTool(
|
|
1992
2066
|
"session_events",
|
|
1993
2067
|
{
|
|
1994
|
-
description: "Read a compact semantic tail only when session_get status is insufficient. With no cursor, this returns the newest matching events and excludes raw message/reasoning/command/PTY deltas. Use `latest` as an exclusive lookup for the newest
|
|
2068
|
+
description: "Read a compact semantic tail only when session_get status is insufficient. With no cursor, this returns the newest matching events and excludes raw message/reasoning/command/PTY deltas. Use `latest` as an exclusive lookup for the authoritative newest durable sequence in exactly one semantic class; `receipt` is the concise alias for `tool_receipt`, and latest cannot be combined with type or class filters. Add `resultMode=compact` to latest for one bounded result-bearing completion/checkpoint/receipt without another inference. Use nextBefore to page older or explicit after/nextAfter to page forward. Type/class filters run in the RLS-scoped database query. payloadMode none|summary|full controls retained audit payload projection, but every model result is independently byte-capped with explicit truncation and exact covered sequence bounds. Exact retained forensic payloads require the access-controlled REST/SDK events API with mode=forensic&payloadMode=full; generic source bytes never retained by the audit boundary remain unavailable.",
|
|
1995
2069
|
inputSchema: {
|
|
1996
2070
|
sessionId: z4.string().uuid(),
|
|
1997
2071
|
after: z4.number().int().nonnegative().optional(),
|
|
@@ -2000,11 +2074,12 @@ function registerWorkspaceOrchestrationTools(server, deps, grant, can, callerSes
|
|
|
2000
2074
|
direction: z4.enum(SessionEventReadDirection.options).optional(),
|
|
2001
2075
|
mode: z4.enum(SessionEventReadMode.options).optional(),
|
|
2002
2076
|
payloadMode: z4.enum(SessionEventPayloadMode.options).optional(),
|
|
2077
|
+
resultMode: z4.enum(SessionEventResultMode.options).optional(),
|
|
2003
2078
|
includeTypes: z4.array(z4.enum(SessionEventType.options)).max(100).optional(),
|
|
2004
2079
|
excludeTypes: z4.array(z4.enum(SessionEventType.options)).max(100).optional(),
|
|
2005
2080
|
includeClasses: z4.array(z4.enum(SessionEventSemanticClass.options)).max(SessionEventSemanticClass.options.length).optional(),
|
|
2006
2081
|
excludeClasses: z4.array(z4.enum(SessionEventSemanticClass.options)).max(SessionEventSemanticClass.options.length).optional(),
|
|
2007
|
-
latest: z4.enum(
|
|
2082
|
+
latest: z4.enum(SessionEventLatestClass.options).optional()
|
|
2008
2083
|
}
|
|
2009
2084
|
},
|
|
2010
2085
|
async ({
|
|
@@ -2015,6 +2090,7 @@ function registerWorkspaceOrchestrationTools(server, deps, grant, can, callerSes
|
|
|
2015
2090
|
direction: requestedDirection,
|
|
2016
2091
|
mode: requestedMode,
|
|
2017
2092
|
payloadMode: requestedPayloadMode,
|
|
2093
|
+
resultMode: requestedResultMode,
|
|
2018
2094
|
includeTypes,
|
|
2019
2095
|
excludeTypes,
|
|
2020
2096
|
includeClasses,
|
|
@@ -2022,6 +2098,10 @@ function registerWorkspaceOrchestrationTools(server, deps, grant, can, callerSes
|
|
|
2022
2098
|
latest
|
|
2023
2099
|
}) => {
|
|
2024
2100
|
await authorizeFirstPartySession(deps, grant, sessionId, "session.events.read");
|
|
2101
|
+
const latestClass = latest === void 0 ? void 0 : sessionEventLatestClassToSemanticClass(latest);
|
|
2102
|
+
if (requestedResultMode === "compact" && latestClass === void 0) {
|
|
2103
|
+
throw new Error("resultMode=compact requires latest");
|
|
2104
|
+
}
|
|
2025
2105
|
await requireSession(deps.db, grant.workspaceId, sessionId);
|
|
2026
2106
|
if (latest && [includeTypes, excludeTypes, includeClasses, excludeClasses].some(
|
|
2027
2107
|
(filter) => filter !== void 0
|
|
@@ -2029,21 +2109,34 @@ function registerWorkspaceOrchestrationTools(server, deps, grant, can, callerSes
|
|
|
2029
2109
|
throw new Error("latest cannot be combined with event filters");
|
|
2030
2110
|
}
|
|
2031
2111
|
const mode = requestedMode ?? (after !== void 0 ? "forensic" : "monitoring");
|
|
2032
|
-
const direction =
|
|
2033
|
-
const payloadMode = requestedPayloadMode ?? (mode === "monitoring" ? "summary" : "full");
|
|
2112
|
+
const direction = latestClass ? "before" : requestedDirection ?? (before !== void 0 ? "before" : after !== void 0 ? "after" : "before");
|
|
2113
|
+
const payloadMode = requestedResultMode === "compact" ? "full" : requestedPayloadMode ?? (mode === "monitoring" ? "summary" : "full");
|
|
2034
2114
|
const dbPage = await listSessionEventPage(deps.db, grant.workspaceId, sessionId, {
|
|
2035
2115
|
after: after ?? 0,
|
|
2036
2116
|
...before !== void 0 ? { before } : {},
|
|
2037
2117
|
direction,
|
|
2038
|
-
limit:
|
|
2118
|
+
limit: latestClass ? 1 : boundedSessionEventMcpLimit(limit),
|
|
2039
2119
|
payloadMode,
|
|
2040
2120
|
includeTypes: includeTypes ?? [],
|
|
2041
2121
|
excludeTypes: excludeTypes ?? [],
|
|
2042
|
-
includeClasses:
|
|
2122
|
+
includeClasses: latestClass ? [latestClass] : includeClasses ?? [],
|
|
2043
2123
|
excludeClasses: excludeClasses ?? [],
|
|
2044
2124
|
...mode === "monitoring" ? { defaultExcludeTypes: SESSION_EVENT_RAW_DELTA_TYPES } : {},
|
|
2125
|
+
...latestClass ? { authoritativeLatest: true } : {},
|
|
2045
2126
|
maxBytes: SESSION_EVENT_MCP_MAX_BYTES * 4
|
|
2046
2127
|
});
|
|
2128
|
+
if (requestedResultMode === "compact") {
|
|
2129
|
+
const event = dbPage.events[0];
|
|
2130
|
+
return json(
|
|
2131
|
+
event ? boundSessionEventCompactResult(
|
|
2132
|
+
compactSessionEventResult(
|
|
2133
|
+
event,
|
|
2134
|
+
latestClass,
|
|
2135
|
+
dbPage.coveredSequence ?? { first: event.sequence, last: event.sequence }
|
|
2136
|
+
)
|
|
2137
|
+
) : null
|
|
2138
|
+
);
|
|
2139
|
+
}
|
|
2047
2140
|
return json(
|
|
2048
2141
|
boundSessionEventMcpPage({
|
|
2049
2142
|
events: dbPage.events,
|
|
@@ -2143,7 +2236,8 @@ function registerWorkspaceOrchestrationTools(server, deps, grant, can, callerSes
|
|
|
2143
2236
|
if (callerSessionId !== null) {
|
|
2144
2237
|
await authorizeFirstPartySession(deps, grant, callerSessionId, "session.child.create");
|
|
2145
2238
|
}
|
|
2146
|
-
|
|
2239
|
+
const created = await createSessionForRequest(deps, grant, grant.workspaceId, args);
|
|
2240
|
+
return json(await withMcpEffectivePolicy(deps, grant.workspaceId, created));
|
|
2147
2241
|
}
|
|
2148
2242
|
);
|
|
2149
2243
|
}
|
|
@@ -2916,6 +3010,13 @@ function parseMcpDate(raw, label) {
|
|
|
2916
3010
|
}
|
|
2917
3011
|
return date;
|
|
2918
3012
|
}
|
|
3013
|
+
async function withMcpEffectivePolicy(deps, workspaceId, session) {
|
|
3014
|
+
const [workspaceServerIds, workspaceDefaultServerIds] = await Promise.all([
|
|
3015
|
+
workspaceSessionToolPolicyServerIds(deps.db, workspaceId, deps.settings),
|
|
3016
|
+
workspaceSessionToolPolicyDefaultServerIds(deps.db, workspaceId, deps.settings)
|
|
3017
|
+
]);
|
|
3018
|
+
return sessionWithEffectiveToolPolicy(session, workspaceServerIds, workspaceDefaultServerIds);
|
|
3019
|
+
}
|
|
2919
3020
|
|
|
2920
3021
|
// src/mcp/toolspace.ts
|
|
2921
3022
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
@@ -2931,14 +3032,34 @@ import {
|
|
|
2931
3032
|
import {
|
|
2932
3033
|
buildConnectionTokenResolver,
|
|
2933
3034
|
buildHostConnectionTokenResolver,
|
|
3035
|
+
clearPendingSessionToolspaceCall,
|
|
3036
|
+
getActiveSessionTurnForExecution,
|
|
2934
3037
|
getSessionRootId,
|
|
2935
|
-
getSessionTurn as getSessionTurn2,
|
|
2936
3038
|
listSessionMcpServerMetadata,
|
|
2937
3039
|
listSessionMcpServersForRun,
|
|
3040
|
+
registerPendingSessionToolCall,
|
|
2938
3041
|
requireSession as requireSession2,
|
|
2939
|
-
|
|
3042
|
+
reserveToolspaceCallForAttempt
|
|
2940
3043
|
} from "@opengeni/db";
|
|
2941
|
-
import { appendAndPublishEvents as appendAndPublishEvents2 } from "@opengeni/events";
|
|
3044
|
+
import { appendAndPublishEvents as appendAndPublishEvents2, appendAndPublishTurnEventsFenced } from "@opengeni/events";
|
|
3045
|
+
import { undiciFetch } from "@opengeni/network";
|
|
3046
|
+
import {
|
|
3047
|
+
MCP_MAX_AGGREGATE_TOOL_LIST_BYTES,
|
|
3048
|
+
MCP_MAX_AGGREGATE_TOOL_LIST_ENTRIES,
|
|
3049
|
+
MCP_MAX_CONCURRENT_SERVER_OPERATIONS,
|
|
3050
|
+
MCP_MAX_TOOL_RESULT_BYTES,
|
|
3051
|
+
McpAggregateToolListBudget,
|
|
3052
|
+
McpPayloadTooLargeError,
|
|
3053
|
+
assertMcpPayloadWithinBytes,
|
|
3054
|
+
assertMcpServerSelectionWithinBounds,
|
|
3055
|
+
assertMcpToolListWithinBounds,
|
|
3056
|
+
boundedParallelMap,
|
|
3057
|
+
cancelMcpResponseBody,
|
|
3058
|
+
guardedMcpFetch,
|
|
3059
|
+
mcpSerializedSizeBytes
|
|
3060
|
+
} from "@opengeni/runtime/mcp-network";
|
|
3061
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
3062
|
+
import { createHash } from "crypto";
|
|
2942
3063
|
var APPROVAL_REQUIRED_MESSAGE = "requires approval - invoke via the agent";
|
|
2943
3064
|
var TOOLSPACE_AUTH_NEEDED_ERROR_CODE = -32001;
|
|
2944
3065
|
var TOOLSPACE_AUTH_NEEDED_MESSAGE = "Authentication required - a connection link was posted to the session.";
|
|
@@ -2946,16 +3067,91 @@ var TOOLSPACE_NO_ACTIVE_TURN_MESSAGE = "no active turn - toolspace calls require
|
|
|
2946
3067
|
var FIRST_PARTY_PROXY_IDS = /* @__PURE__ */ new Set(["files", "docs"]);
|
|
2947
3068
|
var TOOLSPACE_TOOL_LIST_TTL_MS = 3e4;
|
|
2948
3069
|
var TOOLSPACE_TOOL_LIST_CACHE_MAX_ENTRIES = 2e3;
|
|
2949
|
-
var
|
|
3070
|
+
var TOOLSPACE_TOOL_LIST_CACHE_MAX_BYTES = 64 * 1024 * 1024;
|
|
3071
|
+
var ToolspaceToolListCache = class {
|
|
3072
|
+
constructor(maxEntries = TOOLSPACE_TOOL_LIST_CACHE_MAX_ENTRIES, maxBytes = TOOLSPACE_TOOL_LIST_CACHE_MAX_BYTES, ttlMs = TOOLSPACE_TOOL_LIST_TTL_MS) {
|
|
3073
|
+
this.maxEntries = maxEntries;
|
|
3074
|
+
this.maxBytes = maxBytes;
|
|
3075
|
+
this.ttlMs = ttlMs;
|
|
3076
|
+
if (maxEntries < 1 || maxBytes < 1 || ttlMs < 1) {
|
|
3077
|
+
throw new Error("toolspace cache limits must be positive");
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
values = /* @__PURE__ */ new Map();
|
|
3081
|
+
retainedBytes = 0;
|
|
3082
|
+
read(key, now = Date.now()) {
|
|
3083
|
+
const hit = this.values.get(key);
|
|
3084
|
+
if (!hit) return null;
|
|
3085
|
+
if (hit.expiresAt <= now) {
|
|
3086
|
+
this.delete(key);
|
|
3087
|
+
return null;
|
|
3088
|
+
}
|
|
3089
|
+
this.values.delete(key);
|
|
3090
|
+
this.values.set(key, hit);
|
|
3091
|
+
return hit.entries;
|
|
3092
|
+
}
|
|
3093
|
+
write(key, entries, now = Date.now()) {
|
|
3094
|
+
const sizeBytes = Buffer2.byteLength(key) + mcpSerializedSizeBytes(entries);
|
|
3095
|
+
if (sizeBytes > this.maxBytes) return false;
|
|
3096
|
+
this.delete(key);
|
|
3097
|
+
for (const [existingKey, value] of this.values) {
|
|
3098
|
+
if (value.expiresAt <= now) this.delete(existingKey);
|
|
3099
|
+
}
|
|
3100
|
+
while (this.values.size >= this.maxEntries || this.retainedBytes + sizeBytes > this.maxBytes) {
|
|
3101
|
+
const oldestKey = this.values.keys().next().value;
|
|
3102
|
+
if (oldestKey === void 0) break;
|
|
3103
|
+
this.delete(oldestKey);
|
|
3104
|
+
}
|
|
3105
|
+
this.values.set(key, { expiresAt: now + this.ttlMs, entries, sizeBytes });
|
|
3106
|
+
this.retainedBytes += sizeBytes;
|
|
3107
|
+
return true;
|
|
3108
|
+
}
|
|
3109
|
+
clear() {
|
|
3110
|
+
this.values.clear();
|
|
3111
|
+
this.retainedBytes = 0;
|
|
3112
|
+
}
|
|
3113
|
+
snapshot() {
|
|
3114
|
+
return {
|
|
3115
|
+
entries: this.values.size,
|
|
3116
|
+
bytes: this.retainedBytes,
|
|
3117
|
+
keys: [...this.values.keys()]
|
|
3118
|
+
};
|
|
3119
|
+
}
|
|
3120
|
+
delete(key) {
|
|
3121
|
+
const existing = this.values.get(key);
|
|
3122
|
+
if (!existing) return;
|
|
3123
|
+
this.values.delete(key);
|
|
3124
|
+
this.retainedBytes -= existing.sizeBytes;
|
|
3125
|
+
}
|
|
3126
|
+
};
|
|
3127
|
+
var toolListCache = new ToolspaceToolListCache();
|
|
3128
|
+
function toolspaceAuthorityForGrant(grant) {
|
|
3129
|
+
const sessionId = grant.metadata?.sessionId;
|
|
3130
|
+
return typeof sessionId === "string" ? { sessionId } : null;
|
|
3131
|
+
}
|
|
2950
3132
|
function isToolspaceGrant(settings, grant) {
|
|
2951
|
-
return settings.toolspaceEnabled && hasPermission2(grant.permissions, "toolspace:call") &&
|
|
3133
|
+
return settings.toolspaceEnabled && hasPermission2(grant.permissions, "toolspace:call") && toolspaceAuthorityForGrant(grant) !== null;
|
|
2952
3134
|
}
|
|
2953
3135
|
async function prepareToolspaceMcpSurface(input) {
|
|
2954
3136
|
const { deps, grant } = input;
|
|
2955
3137
|
if (!isToolspaceGrant(deps.settings, grant)) {
|
|
2956
3138
|
return null;
|
|
2957
3139
|
}
|
|
2958
|
-
const
|
|
3140
|
+
const authority = toolspaceAuthorityForGrant(grant);
|
|
3141
|
+
if (!authority) {
|
|
3142
|
+
return null;
|
|
3143
|
+
}
|
|
3144
|
+
const { sessionId } = authority;
|
|
3145
|
+
const activeTurn = await getActiveSessionTurnForExecution(deps.db, grant.workspaceId, sessionId);
|
|
3146
|
+
if (!activeTurn?.activeAttemptId || activeTurn.status !== "running") {
|
|
3147
|
+
return emptyToolspaceSurface(sessionId, grant.subjectId);
|
|
3148
|
+
}
|
|
3149
|
+
const attemptAuthority = {
|
|
3150
|
+
sessionId,
|
|
3151
|
+
turnId: activeTurn.id,
|
|
3152
|
+
attemptId: activeTurn.activeAttemptId,
|
|
3153
|
+
executionGeneration: activeTurn.executionGeneration
|
|
3154
|
+
};
|
|
2959
3155
|
const session = await requireSession2(deps.db, grant.workspaceId, sessionId);
|
|
2960
3156
|
let rootSessionId = sessionId;
|
|
2961
3157
|
if (deps.connectionCredentials?.mcpCredentials) {
|
|
@@ -2970,22 +3166,38 @@ async function prepareToolspaceMcpSurface(input) {
|
|
|
2970
3166
|
session.mcpServers.map((server) => server.id)
|
|
2971
3167
|
);
|
|
2972
3168
|
const proxyableIds = [...selectedIds].filter((id) => toolspaceCanProxyServerId(id));
|
|
3169
|
+
assertMcpServerSelectionWithinBounds(proxyableIds);
|
|
2973
3170
|
if (proxyableIds.length === 0) {
|
|
2974
3171
|
return emptyToolspaceSurface(sessionId, grant.subjectId);
|
|
2975
3172
|
}
|
|
2976
|
-
|
|
2977
|
-
const getRegistry = () =>
|
|
3173
|
+
const registryPromises = /* @__PURE__ */ new Map();
|
|
3174
|
+
const getRegistry = (attemptId) => {
|
|
3175
|
+
const existing = registryPromises.get(attemptId);
|
|
3176
|
+
if (existing) {
|
|
3177
|
+
return existing;
|
|
3178
|
+
}
|
|
3179
|
+
const created = buildToolspaceRegistry(deps, grant.workspaceId, sessionId, attemptId);
|
|
3180
|
+
registryPromises.set(attemptId, created);
|
|
3181
|
+
return created;
|
|
3182
|
+
};
|
|
2978
3183
|
const listing = await resolveToolListing({
|
|
2979
3184
|
deps,
|
|
2980
3185
|
grant,
|
|
2981
3186
|
sessionId,
|
|
2982
3187
|
rootSessionId,
|
|
2983
3188
|
proxyableIds,
|
|
2984
|
-
|
|
2985
|
-
getRegistry
|
|
3189
|
+
activeTurn,
|
|
3190
|
+
getRegistry: () => getRegistry(attemptAuthority.attemptId)
|
|
2986
3191
|
});
|
|
2987
3192
|
const tools = listing.map(
|
|
2988
|
-
(entry) => toolspaceToolFor({
|
|
3193
|
+
(entry) => toolspaceToolFor({
|
|
3194
|
+
deps,
|
|
3195
|
+
grant,
|
|
3196
|
+
authority: attemptAuthority,
|
|
3197
|
+
rootSessionId,
|
|
3198
|
+
entry,
|
|
3199
|
+
getRegistry
|
|
3200
|
+
})
|
|
2989
3201
|
);
|
|
2990
3202
|
return {
|
|
2991
3203
|
sessionId,
|
|
@@ -3001,7 +3213,7 @@ function emptyToolspaceSurface(sessionId, subjectId) {
|
|
|
3001
3213
|
return { sessionId, subjectId, tools: [], close: async () => {
|
|
3002
3214
|
} };
|
|
3003
3215
|
}
|
|
3004
|
-
async function buildToolspaceRegistry(deps, workspaceId, sessionId) {
|
|
3216
|
+
async function buildToolspaceRegistry(deps, workspaceId, sessionId, attemptId) {
|
|
3005
3217
|
const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
3006
3218
|
deps.db,
|
|
3007
3219
|
workspaceId,
|
|
@@ -3011,19 +3223,13 @@ async function buildToolspaceRegistry(deps, workspaceId, sessionId) {
|
|
|
3011
3223
|
deps,
|
|
3012
3224
|
workspaceId,
|
|
3013
3225
|
sessionId,
|
|
3226
|
+
attemptId,
|
|
3014
3227
|
runtimeSettings
|
|
3015
3228
|
);
|
|
3016
3229
|
return new Map(withSessionServers.mcpServers.map((server) => [server.id, server]));
|
|
3017
3230
|
}
|
|
3018
3231
|
async function resolveToolListing(input) {
|
|
3019
|
-
const { deps, grant, sessionId, rootSessionId, proxyableIds,
|
|
3020
|
-
if (!activeTurnId) {
|
|
3021
|
-
return [];
|
|
3022
|
-
}
|
|
3023
|
-
const activeTurn = await getSessionTurn2(deps.db, grant.workspaceId, activeTurnId);
|
|
3024
|
-
if (!activeTurn || activeTurn.sessionId !== sessionId) {
|
|
3025
|
-
return [];
|
|
3026
|
-
}
|
|
3232
|
+
const { deps, grant, sessionId, rootSessionId, proxyableIds, activeTurn, getRegistry } = input;
|
|
3027
3233
|
const cacheKey = await toolListCacheKey(
|
|
3028
3234
|
deps,
|
|
3029
3235
|
grant.workspaceId,
|
|
@@ -3036,11 +3242,17 @@ async function resolveToolListing(input) {
|
|
|
3036
3242
|
return cached;
|
|
3037
3243
|
}
|
|
3038
3244
|
const registry = await getRegistry();
|
|
3245
|
+
const aggregateBudget = new McpAggregateToolListBudget(
|
|
3246
|
+
"aggregate Toolspace tool list",
|
|
3247
|
+
MCP_MAX_AGGREGATE_TOOL_LIST_ENTRIES,
|
|
3248
|
+
MCP_MAX_AGGREGATE_TOOL_LIST_BYTES
|
|
3249
|
+
);
|
|
3039
3250
|
const entries = [];
|
|
3040
|
-
|
|
3251
|
+
await boundedParallelMap(proxyableIds, MCP_MAX_CONCURRENT_SERVER_OPERATIONS, async (serverId) => {
|
|
3041
3252
|
const config = registry.get(serverId);
|
|
3042
3253
|
if (!config || !toolspaceCanProxyServer(config)) {
|
|
3043
|
-
|
|
3254
|
+
aggregateBudget.replace(serverId, []);
|
|
3255
|
+
return;
|
|
3044
3256
|
}
|
|
3045
3257
|
const connection = await connectToolspaceServer({
|
|
3046
3258
|
deps,
|
|
@@ -3051,20 +3263,33 @@ async function resolveToolListing(input) {
|
|
|
3051
3263
|
turn: activeTurn
|
|
3052
3264
|
}).catch(() => null);
|
|
3053
3265
|
if (!connection) {
|
|
3054
|
-
|
|
3266
|
+
aggregateBudget.replace(serverId, []);
|
|
3267
|
+
return;
|
|
3055
3268
|
}
|
|
3056
3269
|
try {
|
|
3057
3270
|
const listed = await connection.client.listTools(void 0, toolspaceRequestOptions(config)).catch(() => ({ tools: [] }));
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3271
|
+
let boundedTools;
|
|
3272
|
+
try {
|
|
3273
|
+
boundedTools = assertMcpToolListWithinBounds(listed.tools);
|
|
3274
|
+
} catch (error) {
|
|
3275
|
+
deps.observability?.warn("toolspace upstream tool list exceeded safety limit", {
|
|
3276
|
+
serverId,
|
|
3277
|
+
errorClass: error instanceof Error ? error.name : typeof error
|
|
3278
|
+
});
|
|
3279
|
+
aggregateBudget.replace(serverId, []);
|
|
3280
|
+
return;
|
|
3281
|
+
}
|
|
3282
|
+
const sourceEntries = boundedTools.filter((tool) => Boolean(tool?.name) && allowedByConfig(config, tool.name)).map((tool) => ({
|
|
3283
|
+
serverId,
|
|
3284
|
+
tool,
|
|
3285
|
+
requireApproval: config.requireApproval
|
|
3286
|
+
}));
|
|
3287
|
+
aggregateBudget.replace(serverId, sourceEntries);
|
|
3288
|
+
entries.push(...sourceEntries);
|
|
3064
3289
|
} finally {
|
|
3065
3290
|
await connection.close();
|
|
3066
3291
|
}
|
|
3067
|
-
}
|
|
3292
|
+
});
|
|
3068
3293
|
writeToolListCache(cacheKey, entries);
|
|
3069
3294
|
return entries;
|
|
3070
3295
|
}
|
|
@@ -3075,36 +3300,18 @@ async function toolListCacheKey(deps, workspaceId, sessionId, proxyableIds, turn
|
|
|
3075
3300
|
const authority = JSON.stringify({
|
|
3076
3301
|
turnId: turn.id,
|
|
3077
3302
|
executionGeneration: turn.executionGeneration,
|
|
3303
|
+
attemptId: turn.activeAttemptId,
|
|
3078
3304
|
initiator: turn.initiator
|
|
3079
3305
|
});
|
|
3080
3306
|
return `${workspaceId}:${sessionId}:${signature}:${authority}`;
|
|
3081
3307
|
}
|
|
3082
3308
|
function readToolListCache(key) {
|
|
3083
|
-
|
|
3084
|
-
if (!hit) {
|
|
3085
|
-
return null;
|
|
3086
|
-
}
|
|
3087
|
-
if (hit.expiresAt <= Date.now()) {
|
|
3088
|
-
toolListCache.delete(key);
|
|
3089
|
-
return null;
|
|
3090
|
-
}
|
|
3091
|
-
return hit.entries;
|
|
3309
|
+
return toolListCache.read(key);
|
|
3092
3310
|
}
|
|
3093
3311
|
function writeToolListCache(key, entries) {
|
|
3094
|
-
|
|
3095
|
-
const now = Date.now();
|
|
3096
|
-
for (const [existingKey, value] of toolListCache) {
|
|
3097
|
-
if (value.expiresAt <= now) {
|
|
3098
|
-
toolListCache.delete(existingKey);
|
|
3099
|
-
}
|
|
3100
|
-
}
|
|
3101
|
-
if (toolListCache.size >= TOOLSPACE_TOOL_LIST_CACHE_MAX_ENTRIES) {
|
|
3102
|
-
toolListCache.clear();
|
|
3103
|
-
}
|
|
3104
|
-
}
|
|
3105
|
-
toolListCache.set(key, { expiresAt: Date.now() + TOOLSPACE_TOOL_LIST_TTL_MS, entries });
|
|
3312
|
+
toolListCache.write(key, entries);
|
|
3106
3313
|
}
|
|
3107
|
-
async function settingsWithSessionMcpServersForToolspace(deps, workspaceId, sessionId, settings) {
|
|
3314
|
+
async function settingsWithSessionMcpServersForToolspace(deps, workspaceId, sessionId, attemptId, settings) {
|
|
3108
3315
|
const encryptionKey = environmentsEncryptionKeyBytes(settings);
|
|
3109
3316
|
if (!encryptionKey) {
|
|
3110
3317
|
const metadata = await listSessionMcpServerMetadata(deps.db, workspaceId, sessionId);
|
|
@@ -3121,6 +3328,7 @@ async function settingsWithSessionMcpServersForToolspace(deps, workspaceId, sess
|
|
|
3121
3328
|
deps.db,
|
|
3122
3329
|
workspaceId,
|
|
3123
3330
|
sessionId,
|
|
3331
|
+
attemptId,
|
|
3124
3332
|
encryptionKey ?? null
|
|
3125
3333
|
);
|
|
3126
3334
|
if (servers.length === 0) {
|
|
@@ -3146,7 +3354,8 @@ async function settingsWithSessionMcpServersForToolspace(deps, workspaceId, sess
|
|
|
3146
3354
|
};
|
|
3147
3355
|
}
|
|
3148
3356
|
async function connectToolspaceServer(input) {
|
|
3149
|
-
const
|
|
3357
|
+
const guardedFetch = guardedMcpFetch(input.deps.settings, undiciFetch);
|
|
3358
|
+
const baseFetch = input.config.connectionRef ? connectionBrokerFetch(guardedFetch, input) : guardedFetch;
|
|
3150
3359
|
const client = new Client(
|
|
3151
3360
|
{ name: `opengeni-toolspace-${input.config.id}`, version: "1.0.0" },
|
|
3152
3361
|
{ capabilities: {} }
|
|
@@ -3157,7 +3366,12 @@ async function connectToolspaceServer(input) {
|
|
|
3157
3366
|
headers: toolspaceServerHeaders(input.config)
|
|
3158
3367
|
}
|
|
3159
3368
|
});
|
|
3160
|
-
|
|
3369
|
+
try {
|
|
3370
|
+
await client.connect(transport, toolspaceRequestOptions(input.config));
|
|
3371
|
+
} catch (error) {
|
|
3372
|
+
await client.close().catch(() => void 0);
|
|
3373
|
+
throw error;
|
|
3374
|
+
}
|
|
3161
3375
|
return {
|
|
3162
3376
|
config: input.config,
|
|
3163
3377
|
client,
|
|
@@ -3167,7 +3381,8 @@ async function connectToolspaceServer(input) {
|
|
|
3167
3381
|
};
|
|
3168
3382
|
}
|
|
3169
3383
|
function toolspaceToolFor(input) {
|
|
3170
|
-
const { deps, grant,
|
|
3384
|
+
const { deps, grant, authority, rootSessionId, entry, getRegistry } = input;
|
|
3385
|
+
const { sessionId } = authority;
|
|
3171
3386
|
const { serverId, tool } = entry;
|
|
3172
3387
|
const name = prefixedMcpToolName(serverId, tool.name);
|
|
3173
3388
|
const approvalRequired = mcpToolRequiresApproval(entry.requireApproval, tool.name);
|
|
@@ -3177,10 +3392,7 @@ function toolspaceToolFor(input) {
|
|
|
3177
3392
|
...description ? { description } : {},
|
|
3178
3393
|
...tool.inputSchema ? { inputSchema: tool.inputSchema } : {},
|
|
3179
3394
|
call: async (args) => {
|
|
3180
|
-
|
|
3181
|
-
return mcpError(APPROVAL_REQUIRED_MESSAGE);
|
|
3182
|
-
}
|
|
3183
|
-
const reservation = await reserveActiveTurnCall(deps, grant.workspaceId, sessionId);
|
|
3395
|
+
const reservation = await reserveExactAttemptCall(deps, grant, authority);
|
|
3184
3396
|
if (reservation.status === "no_active_turn") {
|
|
3185
3397
|
return mcpError(TOOLSPACE_NO_ACTIVE_TURN_MESSAGE);
|
|
3186
3398
|
}
|
|
@@ -3190,7 +3402,7 @@ function toolspaceToolFor(input) {
|
|
|
3190
3402
|
);
|
|
3191
3403
|
}
|
|
3192
3404
|
const turnId = reservation.turn.id;
|
|
3193
|
-
const registry = await getRegistry();
|
|
3405
|
+
const registry = await getRegistry(authority.attemptId);
|
|
3194
3406
|
const config = registry.get(serverId);
|
|
3195
3407
|
if (!config || !toolspaceCanProxyServer(config) || !allowedByConfig(config, tool.name)) {
|
|
3196
3408
|
return mcpError(`upstream tool failed: ${name}`);
|
|
@@ -3211,39 +3423,91 @@ function toolspaceToolFor(input) {
|
|
|
3211
3423
|
}
|
|
3212
3424
|
try {
|
|
3213
3425
|
const callId = crypto.randomUUID();
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3426
|
+
const receipt = {
|
|
3427
|
+
accountId: grant.accountId,
|
|
3428
|
+
workspaceId: grant.workspaceId,
|
|
3429
|
+
sessionId,
|
|
3430
|
+
turnId,
|
|
3431
|
+
executionGeneration: authority.executionGeneration,
|
|
3432
|
+
attemptId: authority.attemptId,
|
|
3433
|
+
callId
|
|
3434
|
+
};
|
|
3435
|
+
const registered = await registerPendingSessionToolCall(deps.db, {
|
|
3436
|
+
...receipt,
|
|
3437
|
+
callType: "toolspace_call",
|
|
3438
|
+
callItem: {
|
|
3439
|
+
type: "toolspace_call",
|
|
3440
|
+
id: callId,
|
|
3441
|
+
name,
|
|
3442
|
+
arguments: toolspaceAuditSummary(args),
|
|
3443
|
+
serverId,
|
|
3444
|
+
toolName: tool.name
|
|
3445
|
+
}
|
|
3446
|
+
});
|
|
3447
|
+
if (!registered.accepted || !registered.registered) {
|
|
3448
|
+
return mcpError(TOOLSPACE_NO_ACTIVE_TURN_MESSAGE);
|
|
3449
|
+
}
|
|
3450
|
+
const created = await appendAndPublishTurnEventsFenced(
|
|
3451
|
+
deps.db,
|
|
3452
|
+
deps.bus,
|
|
3453
|
+
grant.workspaceId,
|
|
3454
|
+
sessionId,
|
|
3455
|
+
turnId,
|
|
3456
|
+
authority.executionGeneration,
|
|
3457
|
+
authority.attemptId,
|
|
3458
|
+
[
|
|
3459
|
+
{
|
|
3460
|
+
type: "agent.toolCall.created",
|
|
3461
|
+
turnId,
|
|
3462
|
+
turnGeneration: authority.executionGeneration,
|
|
3463
|
+
turnAttemptId: authority.attemptId,
|
|
3464
|
+
producerId: grant.subjectId,
|
|
3465
|
+
payload: {
|
|
3466
|
+
id: callId,
|
|
3467
|
+
name,
|
|
3468
|
+
arguments: args,
|
|
3469
|
+
origin: "toolspace",
|
|
3470
|
+
subjectId: grant.subjectId,
|
|
3471
|
+
raw: {
|
|
3472
|
+
type: "toolspace_call",
|
|
3473
|
+
serverId,
|
|
3474
|
+
toolName: tool.name
|
|
3475
|
+
}
|
|
3229
3476
|
}
|
|
3230
3477
|
}
|
|
3231
|
-
|
|
3232
|
-
|
|
3478
|
+
]
|
|
3479
|
+
);
|
|
3480
|
+
if (!created.accepted) {
|
|
3481
|
+
return mcpError(TOOLSPACE_NO_ACTIVE_TURN_MESSAGE);
|
|
3482
|
+
}
|
|
3233
3483
|
const output = await callRemoteTool(deps, connection, tool.name, args);
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3484
|
+
const completed = await appendAndPublishTurnEventsFenced(
|
|
3485
|
+
deps.db,
|
|
3486
|
+
deps.bus,
|
|
3487
|
+
grant.workspaceId,
|
|
3488
|
+
sessionId,
|
|
3489
|
+
turnId,
|
|
3490
|
+
authority.executionGeneration,
|
|
3491
|
+
authority.attemptId,
|
|
3492
|
+
[
|
|
3493
|
+
{
|
|
3494
|
+
type: "agent.toolCall.output",
|
|
3495
|
+
turnId,
|
|
3496
|
+
turnGeneration: authority.executionGeneration,
|
|
3497
|
+
turnAttemptId: authority.attemptId,
|
|
3498
|
+
producerId: grant.subjectId,
|
|
3499
|
+
payload: {
|
|
3500
|
+
id: callId,
|
|
3501
|
+
output: toolspaceAuditSummary(output),
|
|
3502
|
+
origin: "toolspace",
|
|
3503
|
+
subjectId: grant.subjectId
|
|
3504
|
+
}
|
|
3244
3505
|
}
|
|
3245
|
-
|
|
3246
|
-
|
|
3506
|
+
]
|
|
3507
|
+
);
|
|
3508
|
+
if (completed.accepted) {
|
|
3509
|
+
await clearPendingSessionToolspaceCall(deps.db, receipt);
|
|
3510
|
+
}
|
|
3247
3511
|
return output;
|
|
3248
3512
|
} finally {
|
|
3249
3513
|
await connection.close();
|
|
@@ -3253,7 +3517,7 @@ function toolspaceToolFor(input) {
|
|
|
3253
3517
|
}
|
|
3254
3518
|
async function callRemoteTool(deps, server, toolName, args) {
|
|
3255
3519
|
try {
|
|
3256
|
-
|
|
3520
|
+
const output = await server.client.callTool(
|
|
3257
3521
|
{
|
|
3258
3522
|
name: toolName,
|
|
3259
3523
|
arguments: args
|
|
@@ -3261,7 +3525,17 @@ async function callRemoteTool(deps, server, toolName, args) {
|
|
|
3261
3525
|
void 0,
|
|
3262
3526
|
toolspaceRequestOptions(server.config)
|
|
3263
3527
|
);
|
|
3528
|
+
assertMcpPayloadWithinBytes(output, MCP_MAX_TOOL_RESULT_BYTES, "MCP tool result");
|
|
3529
|
+
return output;
|
|
3264
3530
|
} catch (error) {
|
|
3531
|
+
if (error instanceof McpPayloadTooLargeError) {
|
|
3532
|
+
deps.observability?.warn("toolspace upstream tool result exceeded safety limit", {
|
|
3533
|
+
serverId: server.config.id,
|
|
3534
|
+
toolName,
|
|
3535
|
+
errorClass: error.name
|
|
3536
|
+
});
|
|
3537
|
+
return mcpError("upstream tool result exceeded the safety limit");
|
|
3538
|
+
}
|
|
3265
3539
|
if (isToolspaceAuthNeededError(error)) {
|
|
3266
3540
|
return mcpError(TOOLSPACE_AUTH_NEEDED_MESSAGE);
|
|
3267
3541
|
}
|
|
@@ -3273,23 +3547,33 @@ async function callRemoteTool(deps, server, toolName, args) {
|
|
|
3273
3547
|
return mcpError(`upstream tool failed: ${prefixedMcpToolName(server.config.id, toolName)}`);
|
|
3274
3548
|
}
|
|
3275
3549
|
}
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3550
|
+
function toolspaceAuditSummary(value) {
|
|
3551
|
+
let serialized;
|
|
3552
|
+
try {
|
|
3553
|
+
serialized = JSON.stringify(value) ?? "null";
|
|
3554
|
+
} catch {
|
|
3555
|
+
serialized = "[unserializable]";
|
|
3280
3556
|
}
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3557
|
+
return {
|
|
3558
|
+
redacted: true,
|
|
3559
|
+
sizeBytes: Buffer2.byteLength(serialized),
|
|
3560
|
+
sha256: createHash("sha256").update(serialized).digest("hex")
|
|
3561
|
+
};
|
|
3562
|
+
}
|
|
3563
|
+
async function reserveExactAttemptCall(deps, grant, authority) {
|
|
3564
|
+
const reservation = await reserveToolspaceCallForAttempt(deps.db, {
|
|
3565
|
+
accountId: grant.accountId,
|
|
3566
|
+
workspaceId: grant.workspaceId,
|
|
3567
|
+
sessionId: authority.sessionId,
|
|
3568
|
+
turnId: authority.turnId,
|
|
3569
|
+
executionGeneration: authority.executionGeneration,
|
|
3570
|
+
attemptId: authority.attemptId,
|
|
3571
|
+
limit: deps.settings.toolspaceMaxCallsPerTurn
|
|
3572
|
+
});
|
|
3288
3573
|
if (!reservation.reserved) {
|
|
3289
|
-
return { status: "budget_exhausted" };
|
|
3574
|
+
return reservation.reason === "budget_exhausted" ? { status: "budget_exhausted" } : { status: "no_active_turn" };
|
|
3290
3575
|
}
|
|
3291
|
-
|
|
3292
|
-
return turn && turn.sessionId === sessionId ? { status: "ok", turn } : { status: "no_active_turn" };
|
|
3576
|
+
return { status: "ok", turn: reservation.turn };
|
|
3293
3577
|
}
|
|
3294
3578
|
function selectedMcpServerIds(tools, sessionServerIds) {
|
|
3295
3579
|
const out = new Set(sessionServerIds);
|
|
@@ -3331,6 +3615,9 @@ function mcpError(message) {
|
|
|
3331
3615
|
function toolspaceRequestOptions(config) {
|
|
3332
3616
|
return config.timeoutMs ? { timeout: config.timeoutMs, maxTotalTimeout: config.timeoutMs } : {};
|
|
3333
3617
|
}
|
|
3618
|
+
function mcpRequestDestinationUrl(input) {
|
|
3619
|
+
return new URL(input instanceof Request ? input.url : input.toString()).toString();
|
|
3620
|
+
}
|
|
3334
3621
|
function connectionBrokerFetch(baseFetch, input) {
|
|
3335
3622
|
const connectionRef = input.config.connectionRef;
|
|
3336
3623
|
if (!connectionRef) {
|
|
@@ -3350,10 +3637,12 @@ function connectionBrokerFetch(baseFetch, input) {
|
|
|
3350
3637
|
}) : buildConnectionTokenResolver(input.deps.db, input.deps.settings);
|
|
3351
3638
|
return async (requestInput, init) => {
|
|
3352
3639
|
const request = await mcpRequestInfo(requestInput, init);
|
|
3640
|
+
const destinationUrl = mcpRequestDestinationUrl(requestInput);
|
|
3353
3641
|
const first = await resolveCredential({
|
|
3354
3642
|
workspaceId: input.grant.workspaceId,
|
|
3355
3643
|
serverId: input.config.id,
|
|
3356
3644
|
connectionRef,
|
|
3645
|
+
destinationUrl,
|
|
3357
3646
|
forceRefresh: false,
|
|
3358
3647
|
...request.toolName ? { toolName: request.toolName } : {},
|
|
3359
3648
|
subjectId: input.grant.subjectId
|
|
@@ -3366,10 +3655,12 @@ function connectionBrokerFetch(baseFetch, input) {
|
|
|
3366
3655
|
withConnectionHeaders(requestInput, init, first.headers)
|
|
3367
3656
|
);
|
|
3368
3657
|
if (response.status === 401) {
|
|
3658
|
+
await cancelMcpResponseBody(response);
|
|
3369
3659
|
const refreshed = await resolveCredential({
|
|
3370
3660
|
workspaceId: input.grant.workspaceId,
|
|
3371
3661
|
serverId: input.config.id,
|
|
3372
3662
|
connectionRef,
|
|
3663
|
+
destinationUrl,
|
|
3373
3664
|
forceRefresh: true,
|
|
3374
3665
|
...request.toolName ? { toolName: request.toolName } : {},
|
|
3375
3666
|
subjectId: input.grant.subjectId
|
|
@@ -3377,12 +3668,30 @@ function connectionBrokerFetch(baseFetch, input) {
|
|
|
3377
3668
|
if (refreshed.status === "auth_needed") {
|
|
3378
3669
|
return await authNeededFetchResponse(input, request, refreshed);
|
|
3379
3670
|
}
|
|
3380
|
-
|
|
3671
|
+
const retry = await baseFetch(
|
|
3381
3672
|
fetchInputForAttempt(requestInput),
|
|
3382
3673
|
withConnectionHeaders(requestInput, init, refreshed.headers)
|
|
3383
3674
|
);
|
|
3675
|
+
if (retry.status === 401) {
|
|
3676
|
+
await cancelMcpResponseBody(retry);
|
|
3677
|
+
return await authNeededFetchResponse(
|
|
3678
|
+
input,
|
|
3679
|
+
request,
|
|
3680
|
+
authNeededFromStatus(input.config, refreshed, "expired")
|
|
3681
|
+
);
|
|
3682
|
+
}
|
|
3683
|
+
if (retry.status === 403) {
|
|
3684
|
+
await cancelMcpResponseBody(retry);
|
|
3685
|
+
return await authNeededFetchResponse(
|
|
3686
|
+
input,
|
|
3687
|
+
request,
|
|
3688
|
+
authNeededFromStatus(input.config, refreshed, "insufficient_scope")
|
|
3689
|
+
);
|
|
3690
|
+
}
|
|
3691
|
+
return retry;
|
|
3384
3692
|
}
|
|
3385
3693
|
if (response.status === 403) {
|
|
3694
|
+
await cancelMcpResponseBody(response);
|
|
3386
3695
|
return await authNeededFetchResponse(
|
|
3387
3696
|
input,
|
|
3388
3697
|
request,
|
|
@@ -3449,8 +3758,8 @@ async function authNeededFetchResponse(input, request, auth) {
|
|
|
3449
3758
|
}
|
|
3450
3759
|
return new Response("Authentication required for MCP server connection", { status: 401 });
|
|
3451
3760
|
}
|
|
3452
|
-
async function mcpRequestInfo(
|
|
3453
|
-
const body = typeof init?.body === "string" ? init.body : "";
|
|
3761
|
+
async function mcpRequestInfo(input, init) {
|
|
3762
|
+
const body = typeof init?.body === "string" ? init.body : input instanceof Request && (init?.method ?? input.method).toUpperCase() === "POST" ? await input.clone().text().catch(() => "") : "";
|
|
3454
3763
|
if (!body) {
|
|
3455
3764
|
return {};
|
|
3456
3765
|
}
|
|
@@ -3468,20 +3777,25 @@ async function mcpRequestInfo(_input, init) {
|
|
|
3468
3777
|
return {};
|
|
3469
3778
|
}
|
|
3470
3779
|
}
|
|
3471
|
-
function withConnectionHeaders(
|
|
3472
|
-
const headers = new Headers(
|
|
3780
|
+
function withConnectionHeaders(input, init, authHeaders) {
|
|
3781
|
+
const headers = new Headers(
|
|
3782
|
+
init?.headers ?? (input instanceof Request ? input.headers : void 0)
|
|
3783
|
+
);
|
|
3473
3784
|
for (const [name, value] of Object.entries(authHeaders)) {
|
|
3474
3785
|
headers.set(name, value);
|
|
3475
3786
|
}
|
|
3476
3787
|
return { ...init, headers };
|
|
3477
3788
|
}
|
|
3478
3789
|
function fetchInputForAttempt(input) {
|
|
3479
|
-
return input;
|
|
3790
|
+
return input instanceof Request ? input.clone() : input;
|
|
3480
3791
|
}
|
|
3481
3792
|
function isToolspaceAuthNeededError(error) {
|
|
3482
3793
|
return error instanceof Error && (error.code === TOOLSPACE_AUTH_NEEDED_ERROR_CODE || error.message.includes(TOOLSPACE_AUTH_NEEDED_MESSAGE));
|
|
3483
3794
|
}
|
|
3484
3795
|
|
|
3796
|
+
// src/app.ts
|
|
3797
|
+
import { boundedMcpRequest, McpPayloadTooLargeError as McpPayloadTooLargeError2 } from "@opengeni/runtime/mcp-network";
|
|
3798
|
+
|
|
3485
3799
|
// src/routes/install.ts
|
|
3486
3800
|
import { readFile, stat } from "fs/promises";
|
|
3487
3801
|
import { HTTPException } from "hono/http-exception";
|
|
@@ -3814,8 +4128,8 @@ function registerCatalogAssetRoutes(app, deps) {
|
|
|
3814
4128
|
if (!contentType) {
|
|
3815
4129
|
throw new HTTPException3(404, { message: "asset not found" });
|
|
3816
4130
|
}
|
|
3817
|
-
const
|
|
3818
|
-
if (!
|
|
4131
|
+
const object4 = await objectStorage.getObjectBytes(key);
|
|
4132
|
+
if (!object4) {
|
|
3819
4133
|
throw new HTTPException3(404, { message: "asset not found" });
|
|
3820
4134
|
}
|
|
3821
4135
|
const etag = etagForKey(key);
|
|
@@ -3828,7 +4142,7 @@ function registerCatalogAssetRoutes(app, deps) {
|
|
|
3828
4142
|
if (ifNoneMatchSatisfied(c.req.header("if-none-match"), etag)) {
|
|
3829
4143
|
return c.body(null, 304, headers);
|
|
3830
4144
|
}
|
|
3831
|
-
return c.body(new Uint8Array(
|
|
4145
|
+
return c.body(new Uint8Array(object4.bytes), 200, { ...headers, "Content-Type": contentType });
|
|
3832
4146
|
});
|
|
3833
4147
|
}
|
|
3834
4148
|
function catalogAssetKeyFromPath(pathname) {
|
|
@@ -3880,6 +4194,7 @@ import {
|
|
|
3880
4194
|
CODEX_PROVIDER_ID,
|
|
3881
4195
|
CODEX_WEEKLY_WINDOW_SECONDS,
|
|
3882
4196
|
CodexDeviceError,
|
|
4197
|
+
consumeCodexRateLimitResetCredit,
|
|
3883
4198
|
exchangeDeviceCode,
|
|
3884
4199
|
fetchCodexModels,
|
|
3885
4200
|
parseIdToken,
|
|
@@ -3887,15 +4202,26 @@ import {
|
|
|
3887
4202
|
startDeviceCode
|
|
3888
4203
|
} from "@opengeni/codex";
|
|
3889
4204
|
import {
|
|
4205
|
+
abandonCodexResetRedemptionBeforeProvider,
|
|
4206
|
+
adoptCodexResetRedemptionAttempt,
|
|
4207
|
+
buildCodexTokenResolver,
|
|
4208
|
+
claimCodexResetRedemption,
|
|
4209
|
+
completeCodexResetRedemption,
|
|
3890
4210
|
disconnectAllCodexAccounts,
|
|
3891
4211
|
disconnectCodexAccount,
|
|
3892
4212
|
encryptEnvironmentValue,
|
|
3893
4213
|
ensureCodexRotationSettings,
|
|
3894
4214
|
fetchCodexUsageForAccount,
|
|
4215
|
+
fetchCodexRateLimitResetCreditsForAccount,
|
|
4216
|
+
fenceCodexResetRedemptionSend,
|
|
4217
|
+
getCodexResetRedemptionAttempt,
|
|
3895
4218
|
getCodexCredentialStatus,
|
|
3896
4219
|
getCodexRotationSettings,
|
|
3897
4220
|
listPendingCodexCapacityWakeTargets,
|
|
3898
4221
|
listCodexAccountStatuses,
|
|
4222
|
+
listCodexResetRedemptionRecoveries,
|
|
4223
|
+
releaseCodexResetRedemptionClaim,
|
|
4224
|
+
updateCodexAllocatorEligibility,
|
|
3899
4225
|
loadCodexCredentialForRun,
|
|
3900
4226
|
renameCodexAccount,
|
|
3901
4227
|
setActiveCodexCredential,
|
|
@@ -3905,8 +4231,70 @@ import {
|
|
|
3905
4231
|
withCodexCapacityMutation
|
|
3906
4232
|
} from "@opengeni/db";
|
|
3907
4233
|
import { createSignedState, readSignedState } from "@opengeni/github";
|
|
4234
|
+
import { hasPermission as hasPermission3, requireAccessGrant as requireAccessGrant2 } from "@opengeni/core";
|
|
3908
4235
|
import { HTTPException as HTTPException4 } from "hono/http-exception";
|
|
3909
|
-
import
|
|
4236
|
+
import * as z from "zod/v4";
|
|
4237
|
+
|
|
4238
|
+
// src/codex-redemption-security.ts
|
|
4239
|
+
var encoder = new TextEncoder();
|
|
4240
|
+
function base64UrlEncode(bytes) {
|
|
4241
|
+
return Buffer.from(bytes).toString("base64url");
|
|
4242
|
+
}
|
|
4243
|
+
function base64UrlDecode(value) {
|
|
4244
|
+
try {
|
|
4245
|
+
return new Uint8Array(Buffer.from(value, "base64url"));
|
|
4246
|
+
} catch {
|
|
4247
|
+
return null;
|
|
4248
|
+
}
|
|
4249
|
+
}
|
|
4250
|
+
async function hmac(secret, payload) {
|
|
4251
|
+
const key = await crypto.subtle.importKey(
|
|
4252
|
+
"raw",
|
|
4253
|
+
encoder.encode(secret),
|
|
4254
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
4255
|
+
false,
|
|
4256
|
+
["sign"]
|
|
4257
|
+
);
|
|
4258
|
+
return new Uint8Array(await crypto.subtle.sign("HMAC", key, encoder.encode(payload)));
|
|
4259
|
+
}
|
|
4260
|
+
function constantTimeEqual2(left, right) {
|
|
4261
|
+
if (left.length !== right.length) return false;
|
|
4262
|
+
let diff = 0;
|
|
4263
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
4264
|
+
diff |= left[index] ^ right[index];
|
|
4265
|
+
}
|
|
4266
|
+
return diff === 0;
|
|
4267
|
+
}
|
|
4268
|
+
async function hashCodexBrowserSession(sessionId) {
|
|
4269
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(sessionId));
|
|
4270
|
+
return base64UrlEncode(new Uint8Array(digest));
|
|
4271
|
+
}
|
|
4272
|
+
async function signCodexRedemptionConfirmation(secret, claims) {
|
|
4273
|
+
const payload = base64UrlEncode(encoder.encode(JSON.stringify(claims)));
|
|
4274
|
+
return `${payload}.${base64UrlEncode(await hmac(secret, payload))}`;
|
|
4275
|
+
}
|
|
4276
|
+
async function verifyCodexRedemptionConfirmation(secret, token, now = Date.now()) {
|
|
4277
|
+
const [payload, signature, extra] = token.split(".");
|
|
4278
|
+
if (!payload || !signature || extra !== void 0) return null;
|
|
4279
|
+
const supplied = base64UrlDecode(signature);
|
|
4280
|
+
const encodedClaims = base64UrlDecode(payload);
|
|
4281
|
+
if (!supplied || !encodedClaims) return null;
|
|
4282
|
+
if (!constantTimeEqual2(supplied, await hmac(secret, payload))) return null;
|
|
4283
|
+
let claims;
|
|
4284
|
+
try {
|
|
4285
|
+
claims = JSON.parse(new TextDecoder().decode(encodedClaims));
|
|
4286
|
+
} catch {
|
|
4287
|
+
return null;
|
|
4288
|
+
}
|
|
4289
|
+
if (!claims || typeof claims !== "object") return null;
|
|
4290
|
+
const value = claims;
|
|
4291
|
+
if (value.version !== 1 || typeof value.attemptId !== "string" || typeof value.workspaceId !== "string" || typeof value.credentialId !== "string" || typeof value.creditId !== "string" || typeof value.subjectId !== "string" || typeof value.browserSessionHash !== "string" || typeof value.expiresAt !== "number" || !Number.isFinite(value.expiresAt) || value.expiresAt * 1e3 <= now) {
|
|
4292
|
+
return null;
|
|
4293
|
+
}
|
|
4294
|
+
return value;
|
|
4295
|
+
}
|
|
4296
|
+
|
|
4297
|
+
// src/routes/codex.ts
|
|
3910
4298
|
var CODEX_PROVIDER_LABEL = "Codex subscription \xB7 no credits";
|
|
3911
4299
|
function codexAccountJson(row) {
|
|
3912
4300
|
return {
|
|
@@ -3931,6 +4319,11 @@ function codexAccountJson(row) {
|
|
|
3931
4319
|
CODEX_WEEKLY_WINDOW_SECONDS
|
|
3932
4320
|
),
|
|
3933
4321
|
usageCheckedAt: row.usageCheckedAt,
|
|
4322
|
+
allocatorEnabled: row.allocatorEnabled,
|
|
4323
|
+
allocatorVersion: row.allocatorVersion,
|
|
4324
|
+
allocatorUpdatedAt: row.allocatorUpdatedAt,
|
|
4325
|
+
resetCreditAvailableCount: row.resetCreditAvailableCount,
|
|
4326
|
+
resetCreditsCheckedAt: row.resetCreditsCheckedAt,
|
|
3934
4327
|
// P3 rotation cooldown: when set and in the future, this account is cooling-down.
|
|
3935
4328
|
exhaustedUntil: row.exhaustedUntil
|
|
3936
4329
|
};
|
|
@@ -3952,6 +4345,240 @@ function codexModelsForPicker(liveSlugs) {
|
|
|
3952
4345
|
api: "responses"
|
|
3953
4346
|
}));
|
|
3954
4347
|
}
|
|
4348
|
+
var CODEX_OVERVIEW_STALE_MS = 15 * 6e4;
|
|
4349
|
+
var CODEX_REDEMPTION_CONFIRMATION_SECONDS = 5 * 60;
|
|
4350
|
+
var CODEX_REDEMPTION_CONFIRMATION = "REDEEM_USAGE_LIMIT_RESET";
|
|
4351
|
+
var redemptionPrepareBody = z.object({
|
|
4352
|
+
attemptId: z.string().uuid(),
|
|
4353
|
+
creditId: z.string().min(1).max(1024)
|
|
4354
|
+
});
|
|
4355
|
+
var redemptionBody = redemptionPrepareBody.extend({
|
|
4356
|
+
confirmationToken: z.string().min(1).max(8192),
|
|
4357
|
+
confirmation: z.literal(CODEX_REDEMPTION_CONFIRMATION)
|
|
4358
|
+
});
|
|
4359
|
+
async function managedCookieHuman(c, deps) {
|
|
4360
|
+
if (deps.settings.productAccessMode !== "managed" || !deps.managedAuth || !c.req.header("cookie") || c.req.header("authorization")) {
|
|
4361
|
+
return null;
|
|
4362
|
+
}
|
|
4363
|
+
const session = await deps.managedAuth.api.getSession({
|
|
4364
|
+
headers: c.req.raw.headers
|
|
4365
|
+
});
|
|
4366
|
+
if (!session?.user?.id || !session.session?.id) return null;
|
|
4367
|
+
return {
|
|
4368
|
+
subjectId: `user:${session.user.id}`,
|
|
4369
|
+
browserSessionHash: await hashCodexBrowserSession(session.session.id)
|
|
4370
|
+
};
|
|
4371
|
+
}
|
|
4372
|
+
function requireSameOriginBrowserMutation(c, deps) {
|
|
4373
|
+
const contentType = c.req.header("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
4374
|
+
if (contentType !== "application/json") {
|
|
4375
|
+
throw new HTTPException4(403, {
|
|
4376
|
+
message: "JSON browser request required"
|
|
4377
|
+
});
|
|
4378
|
+
}
|
|
4379
|
+
if (!deps.settings.publicBaseUrl) {
|
|
4380
|
+
throw new HTTPException4(503, {
|
|
4381
|
+
message: "managed browser origin is not configured"
|
|
4382
|
+
});
|
|
4383
|
+
}
|
|
4384
|
+
const expectedOrigin = new URL(deps.settings.publicBaseUrl).origin;
|
|
4385
|
+
if (c.req.header("origin") !== expectedOrigin) {
|
|
4386
|
+
throw new HTTPException4(403, {
|
|
4387
|
+
message: "same-origin browser request required"
|
|
4388
|
+
});
|
|
4389
|
+
}
|
|
4390
|
+
if (c.req.header("sec-fetch-site")?.toLowerCase() !== "same-origin") {
|
|
4391
|
+
throw new HTTPException4(403, {
|
|
4392
|
+
message: "same-origin fetch metadata required"
|
|
4393
|
+
});
|
|
4394
|
+
}
|
|
4395
|
+
}
|
|
4396
|
+
async function requireRedemptionHuman(c, deps, workspaceId) {
|
|
4397
|
+
if (deps.settings.productAccessMode !== "managed") {
|
|
4398
|
+
throw new HTTPException4(403, {
|
|
4399
|
+
message: "reset redemption requires managed product mode"
|
|
4400
|
+
});
|
|
4401
|
+
}
|
|
4402
|
+
if (c.req.header("authorization")) {
|
|
4403
|
+
throw new HTTPException4(403, {
|
|
4404
|
+
message: "authorization bearer is not allowed for redemption"
|
|
4405
|
+
});
|
|
4406
|
+
}
|
|
4407
|
+
requireSameOriginBrowserMutation(c, deps);
|
|
4408
|
+
const human = await managedCookieHuman(c, deps);
|
|
4409
|
+
if (!human) {
|
|
4410
|
+
throw new HTTPException4(401, {
|
|
4411
|
+
message: "managed browser session required"
|
|
4412
|
+
});
|
|
4413
|
+
}
|
|
4414
|
+
const grant = await requireAccessGrant2(c, deps, workspaceId, "workspace:admin");
|
|
4415
|
+
if (grant.subjectId !== human.subjectId) {
|
|
4416
|
+
throw new HTTPException4(403, {
|
|
4417
|
+
message: "managed browser identity mismatch"
|
|
4418
|
+
});
|
|
4419
|
+
}
|
|
4420
|
+
return { human, accountId: grant.accountId };
|
|
4421
|
+
}
|
|
4422
|
+
function cachedUsage(row) {
|
|
4423
|
+
const fiveHour = buildCodexUsageWindowFromCache(
|
|
4424
|
+
row.primaryUsedPercent,
|
|
4425
|
+
row.primaryResetAt,
|
|
4426
|
+
CODEX_FIVE_HOUR_WINDOW_SECONDS
|
|
4427
|
+
);
|
|
4428
|
+
const weekly = buildCodexUsageWindowFromCache(
|
|
4429
|
+
row.secondaryUsedPercent,
|
|
4430
|
+
row.secondaryResetAt,
|
|
4431
|
+
CODEX_WEEKLY_WINDOW_SECONDS
|
|
4432
|
+
);
|
|
4433
|
+
if (!fiveHour && !weekly && row.resetCreditAvailableCount == null) return null;
|
|
4434
|
+
const limitReached = (fiveHour?.percent ?? 0) >= 100 || (weekly?.percent ?? 0) >= 100;
|
|
4435
|
+
return {
|
|
4436
|
+
status: limitReached ? "limit_reached" : fiveHour || weekly ? "ok" : "no-data",
|
|
4437
|
+
planType: row.planType,
|
|
4438
|
+
fiveHour,
|
|
4439
|
+
weekly,
|
|
4440
|
+
limitReached,
|
|
4441
|
+
fetchedAt: (row.usageCheckedAt ?? row.resetCreditsCheckedAt ?? /* @__PURE__ */ new Date(0)).toISOString(),
|
|
4442
|
+
rateLimitResetCredits: row.resetCreditAvailableCount == null ? null : { availableCount: row.resetCreditAvailableCount, credits: null }
|
|
4443
|
+
};
|
|
4444
|
+
}
|
|
4445
|
+
function staleAt(value) {
|
|
4446
|
+
return !value || Date.now() - value.getTime() > CODEX_OVERVIEW_STALE_MS;
|
|
4447
|
+
}
|
|
4448
|
+
function sortedCredits(credits) {
|
|
4449
|
+
return [...credits].sort((left, right) => {
|
|
4450
|
+
if (left.expiresAt == null && right.expiresAt == null) return left.id.localeCompare(right.id);
|
|
4451
|
+
if (left.expiresAt == null) return 1;
|
|
4452
|
+
if (right.expiresAt == null) return -1;
|
|
4453
|
+
return left.expiresAt - right.expiresAt || left.id.localeCompare(right.id);
|
|
4454
|
+
});
|
|
4455
|
+
}
|
|
4456
|
+
function actionableCredit(credit, nowSeconds = Date.now() / 1e3) {
|
|
4457
|
+
return credit.resetType === "codexRateLimits" && credit.status === "available" && (credit.expiresAt == null || credit.expiresAt > nowSeconds);
|
|
4458
|
+
}
|
|
4459
|
+
function freshActionableCredit(details, creditId) {
|
|
4460
|
+
const availableDetailCount = details.credits.filter(
|
|
4461
|
+
(credit2) => credit2.status === "available"
|
|
4462
|
+
).length;
|
|
4463
|
+
if (details.availableCount !== availableDetailCount || details.credits.some((credit2) => credit2.resetType === "unknown" || credit2.status === "unknown")) {
|
|
4464
|
+
return null;
|
|
4465
|
+
}
|
|
4466
|
+
const credit = details.credits.find((candidate) => candidate.id === creditId);
|
|
4467
|
+
return credit && actionableCredit(credit) ? credit : null;
|
|
4468
|
+
}
|
|
4469
|
+
var CODEX_OVERVIEW_ROUTE_TIMEOUT_MS = 12e3;
|
|
4470
|
+
function createProviderCallLimiter(limit) {
|
|
4471
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
4472
|
+
throw new Error("Codex provider concurrency limit must be a positive integer");
|
|
4473
|
+
}
|
|
4474
|
+
let permits = limit;
|
|
4475
|
+
const waiters = [];
|
|
4476
|
+
const acquire = async () => {
|
|
4477
|
+
if (permits > 0) {
|
|
4478
|
+
permits -= 1;
|
|
4479
|
+
return;
|
|
4480
|
+
}
|
|
4481
|
+
await new Promise((resolve) => waiters.push(resolve));
|
|
4482
|
+
};
|
|
4483
|
+
const release = () => {
|
|
4484
|
+
const next = waiters.shift();
|
|
4485
|
+
if (next) next();
|
|
4486
|
+
else permits += 1;
|
|
4487
|
+
};
|
|
4488
|
+
return async (operation) => {
|
|
4489
|
+
await acquire();
|
|
4490
|
+
try {
|
|
4491
|
+
return await operation();
|
|
4492
|
+
} finally {
|
|
4493
|
+
release();
|
|
4494
|
+
}
|
|
4495
|
+
};
|
|
4496
|
+
}
|
|
4497
|
+
async function fetchCodexAccountOverview(deps, workspaceId, row, canRedeem, canResumeRedemption, redemptions = [], providerCall = async (operation) => await operation()) {
|
|
4498
|
+
const fetchImpl = deps.codexFetch ?? fetch;
|
|
4499
|
+
const [usageSettled, detailsSettled] = await Promise.allSettled([
|
|
4500
|
+
providerCall(
|
|
4501
|
+
async () => await fetchCodexUsageForAccount(deps.db, deps.settings, workspaceId, row.id, fetchImpl)
|
|
4502
|
+
),
|
|
4503
|
+
providerCall(
|
|
4504
|
+
async () => await fetchCodexRateLimitResetCreditsForAccount(
|
|
4505
|
+
deps.db,
|
|
4506
|
+
deps.settings,
|
|
4507
|
+
workspaceId,
|
|
4508
|
+
row.id,
|
|
4509
|
+
fetchImpl
|
|
4510
|
+
)
|
|
4511
|
+
)
|
|
4512
|
+
]);
|
|
4513
|
+
const liveUsage = usageSettled.status === "fulfilled" ? usageSettled.value : null;
|
|
4514
|
+
const cached = cachedUsage(row);
|
|
4515
|
+
const usageFromProvider = liveUsage != null && liveUsage.status !== "error";
|
|
4516
|
+
const usageValue = usageFromProvider ? liveUsage : cached;
|
|
4517
|
+
const usageSource = usageFromProvider ? "provider" : cached ? "cache" : "none";
|
|
4518
|
+
const liveSummary = liveUsage?.rateLimitResetCredits ?? null;
|
|
4519
|
+
const detailsResult = detailsSettled.status === "fulfilled" ? detailsSettled.value : null;
|
|
4520
|
+
const details = detailsResult?.ok ? detailsResult.details : null;
|
|
4521
|
+
const availableCount = details?.availableCount ?? liveSummary?.availableCount ?? row.resetCreditAvailableCount;
|
|
4522
|
+
const availableDetailCount = details?.credits.filter((credit) => credit.status === "available").length ?? 0;
|
|
4523
|
+
const availableDetailsComplete = !!details && details.availableCount === availableDetailCount;
|
|
4524
|
+
const availableDetailsCapped = !!details && availableDetailCount < details.availableCount;
|
|
4525
|
+
const availableDetailsImpossible = !!details && availableDetailCount > details.availableCount;
|
|
4526
|
+
const summaryAgrees = !details || liveSummary == null || liveSummary.availableCount === details.availableCount;
|
|
4527
|
+
const hasUnknown = details?.credits.some(
|
|
4528
|
+
(credit) => credit.resetType === "unknown" || credit.status === "unknown"
|
|
4529
|
+
) ?? false;
|
|
4530
|
+
const detailsComplete = availableDetailsComplete && summaryAgrees && !hasUnknown;
|
|
4531
|
+
let detailState;
|
|
4532
|
+
if (details) {
|
|
4533
|
+
detailState = !summaryAgrees || hasUnknown || availableDetailsImpossible ? "unknown" : availableDetailsCapped ? "capped" : "detailed";
|
|
4534
|
+
} else if (availableCount != null) {
|
|
4535
|
+
detailState = "count_only";
|
|
4536
|
+
} else if (detailsResult && !detailsResult.ok && detailsResult.reason === "invalid_response") {
|
|
4537
|
+
detailState = "unknown";
|
|
4538
|
+
} else if (detailsResult && !detailsResult.ok && detailsResult.reason === "http_error" && detailsResult.status === 404) {
|
|
4539
|
+
detailState = "unsupported";
|
|
4540
|
+
} else {
|
|
4541
|
+
detailState = "error";
|
|
4542
|
+
}
|
|
4543
|
+
const resetSource = details || liveSummary ? "provider" : availableCount != null ? "cache" : "none";
|
|
4544
|
+
const sorted = sortedCredits(details?.credits ?? []);
|
|
4545
|
+
const actionAuthority = canRedeem && detailsComplete && detailState === "detailed";
|
|
4546
|
+
return {
|
|
4547
|
+
accountId: row.id,
|
|
4548
|
+
usage: {
|
|
4549
|
+
source: usageSource,
|
|
4550
|
+
fetchedAt: usageValue?.fetchedAt ?? null,
|
|
4551
|
+
stale: usageSource === "provider" ? false : staleAt(row.usageCheckedAt),
|
|
4552
|
+
error: liveUsage?.status === "error" ? liveUsage.reason ?? "unavailable" : usageSettled.status === "rejected" ? "unavailable" : null,
|
|
4553
|
+
value: usageValue
|
|
4554
|
+
},
|
|
4555
|
+
resetCredits: {
|
|
4556
|
+
source: resetSource,
|
|
4557
|
+
fetchedAt: resetSource === "provider" ? liveUsage?.fetchedAt ?? (/* @__PURE__ */ new Date()).toISOString() : row.resetCreditsCheckedAt?.toISOString() ?? null,
|
|
4558
|
+
stale: resetSource === "provider" ? false : staleAt(row.resetCreditsCheckedAt),
|
|
4559
|
+
error: detailsResult && !detailsResult.ok ? detailsResult.reason : detailsSettled.status === "rejected" ? "unavailable" : null,
|
|
4560
|
+
detailState,
|
|
4561
|
+
detailsComplete,
|
|
4562
|
+
availableCount: availableCount ?? null,
|
|
4563
|
+
credits: sorted.map((credit) => ({
|
|
4564
|
+
...credit,
|
|
4565
|
+
actionable: actionAuthority && actionableCredit(credit)
|
|
4566
|
+
}))
|
|
4567
|
+
},
|
|
4568
|
+
canRedeem,
|
|
4569
|
+
canResumeRedemption,
|
|
4570
|
+
redemptions: redemptions.map((redemption) => ({
|
|
4571
|
+
attemptId: redemption.attemptId,
|
|
4572
|
+
creditId: redemption.creditId,
|
|
4573
|
+
status: redemption.status,
|
|
4574
|
+
outcome: redemption.outcome,
|
|
4575
|
+
providerStartedAt: redemption.providerStartedAt?.toISOString() ?? null,
|
|
4576
|
+
completedAt: redemption.completedAt?.toISOString() ?? null,
|
|
4577
|
+
createdAt: redemption.createdAt.toISOString(),
|
|
4578
|
+
updatedAt: redemption.updatedAt.toISOString()
|
|
4579
|
+
}))
|
|
4580
|
+
};
|
|
4581
|
+
}
|
|
3955
4582
|
async function signalCodexCapacityTargets(deps, targets) {
|
|
3956
4583
|
await Promise.allSettled(
|
|
3957
4584
|
targets.map(
|
|
@@ -4008,7 +4635,9 @@ function registerCodexRoutes(app, deps) {
|
|
|
4008
4635
|
const { state } = await c.req.json();
|
|
4009
4636
|
const payload = state ? readSignedState(state, githubStateSecret) : null;
|
|
4010
4637
|
if (!payload || payload.workspaceId !== workspaceId || !payload.deviceAuthId || !payload.userCode) {
|
|
4011
|
-
throw new HTTPException4(400, {
|
|
4638
|
+
throw new HTTPException4(400, {
|
|
4639
|
+
message: "codex connect state is invalid or expired"
|
|
4640
|
+
});
|
|
4012
4641
|
}
|
|
4013
4642
|
if (typeof payload.iat === "number" && Date.now() / 1e3 - payload.iat > CODEX_DEVICE_EXPIRY_SECONDS) {
|
|
4014
4643
|
return c.json({ status: "expired" });
|
|
@@ -4042,6 +4671,7 @@ function registerCodexRoutes(app, deps) {
|
|
|
4042
4671
|
});
|
|
4043
4672
|
}
|
|
4044
4673
|
const id = parseIdToken(tokens.idToken);
|
|
4674
|
+
const connectingHuman = await managedCookieHuman(c, deps);
|
|
4045
4675
|
const key = environmentsEncryptionKeyBytes2(settings);
|
|
4046
4676
|
if (!key) {
|
|
4047
4677
|
throw new HTTPException4(500, {
|
|
@@ -4072,12 +4702,18 @@ function registerCodexRoutes(app, deps) {
|
|
|
4072
4702
|
expiresAt: accessTokenExpiry(tokens.accessToken),
|
|
4073
4703
|
lastRefreshAt: /* @__PURE__ */ new Date(),
|
|
4074
4704
|
accountEmail: id.email ?? null,
|
|
4075
|
-
label: id.email ?? id.chatgptAccountId ?? null
|
|
4705
|
+
label: id.email ?? id.chatgptAccountId ?? null,
|
|
4706
|
+
connectedBySubjectId: connectingHuman?.subjectId === grant.subjectId ? connectingHuman.subjectId : null
|
|
4076
4707
|
});
|
|
4077
|
-
return { result: upserted2, changed:
|
|
4708
|
+
return { result: upserted2, changed: upserted2.kind === "upserted" };
|
|
4078
4709
|
}
|
|
4079
4710
|
);
|
|
4080
4711
|
const upserted = mutation.result;
|
|
4712
|
+
if (upserted.kind === "unresolved_redemption") {
|
|
4713
|
+
throw new HTTPException4(409, {
|
|
4714
|
+
message: "this subscription has an unresolved reset redemption; recover it before changing ownership"
|
|
4715
|
+
});
|
|
4716
|
+
}
|
|
4081
4717
|
const rotation = await getCodexRotationSettings(db, workspaceId);
|
|
4082
4718
|
let isActive = rotation?.activeCredentialId === upserted.id;
|
|
4083
4719
|
if (!isActive && rotation?.activeCredentialId == null) {
|
|
@@ -4200,7 +4836,9 @@ function registerCodexRoutes(app, deps) {
|
|
|
4200
4836
|
);
|
|
4201
4837
|
const updated = mutation.result;
|
|
4202
4838
|
if (!updated) {
|
|
4203
|
-
throw new HTTPException4(404, {
|
|
4839
|
+
throw new HTTPException4(404, {
|
|
4840
|
+
message: "codex rotation settings not found"
|
|
4841
|
+
});
|
|
4204
4842
|
}
|
|
4205
4843
|
await signalCodexCapacityTargets(deps, mutation.wakeTargets);
|
|
4206
4844
|
return c.json({
|
|
@@ -4227,6 +4865,39 @@ function registerCodexRoutes(app, deps) {
|
|
|
4227
4865
|
}
|
|
4228
4866
|
return c.json(codexAccountJson(row));
|
|
4229
4867
|
});
|
|
4868
|
+
app.patch("/v1/workspaces/:workspaceId/codex/accounts/:accountId/allocator", async (c) => {
|
|
4869
|
+
const workspaceId = c.req.param("workspaceId");
|
|
4870
|
+
const grant = await requireAccessGrant2(c, deps, workspaceId, "workspace:admin");
|
|
4871
|
+
const parsed = z.object({
|
|
4872
|
+
enabled: z.boolean(),
|
|
4873
|
+
expectedVersion: z.number().int().positive()
|
|
4874
|
+
}).safeParse(await c.req.json().catch(() => null));
|
|
4875
|
+
if (!parsed.success) {
|
|
4876
|
+
throw new HTTPException4(400, {
|
|
4877
|
+
message: "enabled and expectedVersion are required"
|
|
4878
|
+
});
|
|
4879
|
+
}
|
|
4880
|
+
const mutation = await updateCodexAllocatorEligibility(db, {
|
|
4881
|
+
accountId: grant.accountId,
|
|
4882
|
+
workspaceId,
|
|
4883
|
+
credentialId: c.req.param("accountId"),
|
|
4884
|
+
subjectId: grant.subjectId,
|
|
4885
|
+
enabled: parsed.data.enabled,
|
|
4886
|
+
expectedVersion: parsed.data.expectedVersion
|
|
4887
|
+
});
|
|
4888
|
+
const result = mutation.result;
|
|
4889
|
+
if (result.kind === "not_found") {
|
|
4890
|
+
throw new HTTPException4(404, { message: "codex account not found" });
|
|
4891
|
+
}
|
|
4892
|
+
const response = {
|
|
4893
|
+
allocatorEnabled: result.allocatorEnabled,
|
|
4894
|
+
allocatorVersion: result.allocatorVersion,
|
|
4895
|
+
allocatorUpdatedAt: result.allocatorUpdatedAt,
|
|
4896
|
+
changed: result.kind === "updated"
|
|
4897
|
+
};
|
|
4898
|
+
await signalCodexCapacityTargets(deps, mutation.wakeTargets);
|
|
4899
|
+
return result.kind === "conflict" ? c.json(response, 409) : c.json(response);
|
|
4900
|
+
});
|
|
4230
4901
|
app.delete("/v1/workspaces/:workspaceId/codex/accounts/:accountId", async (c) => {
|
|
4231
4902
|
const workspaceId = c.req.param("workspaceId");
|
|
4232
4903
|
await requireAccessGrant2(c, deps, workspaceId, "workspace:admin");
|
|
@@ -4240,6 +4911,11 @@ function registerCodexRoutes(app, deps) {
|
|
|
4240
4911
|
}
|
|
4241
4912
|
);
|
|
4242
4913
|
const result = mutation.result;
|
|
4914
|
+
if (result.blockedByUnresolvedRedemption) {
|
|
4915
|
+
throw new HTTPException4(409, {
|
|
4916
|
+
message: "this subscription has an unresolved reset redemption; recover it before disconnecting"
|
|
4917
|
+
});
|
|
4918
|
+
}
|
|
4243
4919
|
await signalCodexCapacityTargets(deps, mutation.wakeTargets);
|
|
4244
4920
|
return c.json({ disconnected: result.removed, newActiveId: result.newActiveCredentialId });
|
|
4245
4921
|
});
|
|
@@ -4250,20 +4926,27 @@ function registerCodexRoutes(app, deps) {
|
|
|
4250
4926
|
db,
|
|
4251
4927
|
{ workspaceId, reason: "codex_credentials_disconnected" },
|
|
4252
4928
|
async (tx) => {
|
|
4253
|
-
const
|
|
4254
|
-
return { result:
|
|
4929
|
+
const result2 = await disconnectAllCodexAccounts(tx, workspaceId);
|
|
4930
|
+
return { result: result2, changed: result2.removed > 0 };
|
|
4255
4931
|
}
|
|
4256
4932
|
);
|
|
4257
|
-
const
|
|
4933
|
+
const result = mutation.result;
|
|
4934
|
+
if (result.blockedCredentialIds.length > 0) {
|
|
4935
|
+
throw new HTTPException4(409, {
|
|
4936
|
+
message: "one or more subscriptions have unresolved reset redemptions; recover them before disconnecting"
|
|
4937
|
+
});
|
|
4938
|
+
}
|
|
4258
4939
|
await signalCodexCapacityTargets(deps, mutation.wakeTargets);
|
|
4259
|
-
return c.json({ disconnected: removed > 0 });
|
|
4940
|
+
return c.json({ disconnected: result.removed > 0 });
|
|
4260
4941
|
});
|
|
4261
4942
|
app.get("/v1/workspaces/:workspaceId/codex/usage", async (c) => {
|
|
4262
4943
|
const workspaceId = c.req.param("workspaceId");
|
|
4263
4944
|
await requireAccessGrant2(c, deps, workspaceId, "workspace:read");
|
|
4264
4945
|
const status = await getCodexCredentialStatus(db, workspaceId);
|
|
4265
4946
|
if (!status?.credentialId) {
|
|
4266
|
-
throw new HTTPException4(404, {
|
|
4947
|
+
throw new HTTPException4(404, {
|
|
4948
|
+
message: "codex subscription is not connected"
|
|
4949
|
+
});
|
|
4267
4950
|
}
|
|
4268
4951
|
const payload = await fetchCodexUsageForAccount(db, settings, workspaceId, status.credentialId);
|
|
4269
4952
|
await signalPendingCodexCapacityTargets(deps, workspaceId);
|
|
@@ -4304,17 +4987,380 @@ function registerCodexRoutes(app, deps) {
|
|
|
4304
4987
|
fiveHour: null,
|
|
4305
4988
|
weekly: null,
|
|
4306
4989
|
limitReached: false,
|
|
4307
|
-
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4990
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4991
|
+
rateLimitResetCredits: null
|
|
4308
4992
|
}
|
|
4309
4993
|
};
|
|
4310
4994
|
}
|
|
4311
|
-
};
|
|
4312
|
-
await Promise.all(
|
|
4313
|
-
Array.from({ length: Math.min(CONCURRENCY, Math.max(1, accounts.length)) }, () => worker())
|
|
4314
|
-
);
|
|
4315
|
-
await signalPendingCodexCapacityTargets(deps, workspaceId);
|
|
4316
|
-
return c.json({ usage });
|
|
4317
|
-
});
|
|
4995
|
+
};
|
|
4996
|
+
await Promise.all(
|
|
4997
|
+
Array.from({ length: Math.min(CONCURRENCY, Math.max(1, accounts.length)) }, () => worker())
|
|
4998
|
+
);
|
|
4999
|
+
await signalPendingCodexCapacityTargets(deps, workspaceId);
|
|
5000
|
+
return c.json({ usage });
|
|
5001
|
+
});
|
|
5002
|
+
app.get("/v1/workspaces/:workspaceId/codex/overview", async (c) => {
|
|
5003
|
+
const workspaceId = c.req.param("workspaceId");
|
|
5004
|
+
const grant = await requireAccessGrant2(c, deps, workspaceId, "workspace:read");
|
|
5005
|
+
const human = await managedCookieHuman(c, deps);
|
|
5006
|
+
const accounts = await listCodexAccountStatuses(db, workspaceId);
|
|
5007
|
+
const ownerRecoveries = human && human.subjectId === grant.subjectId && hasPermission3(grant.permissions, "workspace:admin") ? await listCodexResetRedemptionRecoveries(db, {
|
|
5008
|
+
accountId: grant.accountId,
|
|
5009
|
+
workspaceId,
|
|
5010
|
+
subjectId: human.subjectId
|
|
5011
|
+
}) : [];
|
|
5012
|
+
const overview = {};
|
|
5013
|
+
const queue = [...accounts];
|
|
5014
|
+
const providerCall = createProviderCallLimiter(4);
|
|
5015
|
+
let routeTimedOut = false;
|
|
5016
|
+
const worker = async () => {
|
|
5017
|
+
for (; ; ) {
|
|
5018
|
+
if (routeTimedOut) return;
|
|
5019
|
+
const account = queue.shift();
|
|
5020
|
+
if (!account) return;
|
|
5021
|
+
const canResumeRedemption = Boolean(
|
|
5022
|
+
human && human.subjectId === grant.subjectId && human.subjectId === account.connectedBySubjectId && hasPermission3(grant.permissions, "workspace:admin")
|
|
5023
|
+
);
|
|
5024
|
+
const canRedeem = canResumeRedemption && account.status === "active";
|
|
5025
|
+
overview[account.id] = await fetchCodexAccountOverview(
|
|
5026
|
+
deps,
|
|
5027
|
+
workspaceId,
|
|
5028
|
+
account,
|
|
5029
|
+
canRedeem,
|
|
5030
|
+
canResumeRedemption,
|
|
5031
|
+
canResumeRedemption ? ownerRecoveries.filter((recovery) => recovery.credentialId === account.id) : [],
|
|
5032
|
+
providerCall
|
|
5033
|
+
);
|
|
5034
|
+
}
|
|
5035
|
+
};
|
|
5036
|
+
const workers = Promise.all(
|
|
5037
|
+
Array.from({ length: Math.min(4, Math.max(1, accounts.length)) }, () => worker())
|
|
5038
|
+
);
|
|
5039
|
+
let deadline;
|
|
5040
|
+
await Promise.race([
|
|
5041
|
+
workers,
|
|
5042
|
+
new Promise((resolve) => {
|
|
5043
|
+
deadline = setTimeout(() => {
|
|
5044
|
+
routeTimedOut = true;
|
|
5045
|
+
queue.length = 0;
|
|
5046
|
+
resolve();
|
|
5047
|
+
}, CODEX_OVERVIEW_ROUTE_TIMEOUT_MS);
|
|
5048
|
+
})
|
|
5049
|
+
]);
|
|
5050
|
+
if (deadline) clearTimeout(deadline);
|
|
5051
|
+
if (routeTimedOut) {
|
|
5052
|
+
const unavailableProviderCall = async () => {
|
|
5053
|
+
throw new Error("Codex overview route deadline reached");
|
|
5054
|
+
};
|
|
5055
|
+
await Promise.all(
|
|
5056
|
+
accounts.filter((account) => overview[account.id] == null).map(async (account) => {
|
|
5057
|
+
const canResumeRedemption = Boolean(
|
|
5058
|
+
human && human.subjectId === grant.subjectId && human.subjectId === account.connectedBySubjectId && hasPermission3(grant.permissions, "workspace:admin")
|
|
5059
|
+
);
|
|
5060
|
+
const fallback = await fetchCodexAccountOverview(
|
|
5061
|
+
deps,
|
|
5062
|
+
workspaceId,
|
|
5063
|
+
account,
|
|
5064
|
+
false,
|
|
5065
|
+
canResumeRedemption,
|
|
5066
|
+
canResumeRedemption ? ownerRecoveries.filter((recovery) => recovery.credentialId === account.id) : [],
|
|
5067
|
+
unavailableProviderCall
|
|
5068
|
+
);
|
|
5069
|
+
overview[account.id] ??= fallback;
|
|
5070
|
+
})
|
|
5071
|
+
);
|
|
5072
|
+
void workers.catch(() => void 0);
|
|
5073
|
+
}
|
|
5074
|
+
void signalPendingCodexCapacityTargets(deps, workspaceId).catch(() => void 0);
|
|
5075
|
+
return c.json({ accounts: overview });
|
|
5076
|
+
});
|
|
5077
|
+
app.post(
|
|
5078
|
+
"/v1/workspaces/:workspaceId/codex/accounts/:accountId/reset-credits/prepare",
|
|
5079
|
+
async (c) => {
|
|
5080
|
+
const workspaceId = c.req.param("workspaceId");
|
|
5081
|
+
const credentialId = c.req.param("accountId");
|
|
5082
|
+
const { human, accountId } = await requireRedemptionHuman(c, deps, workspaceId);
|
|
5083
|
+
c.header("cache-control", "no-store");
|
|
5084
|
+
const parsed = redemptionPrepareBody.safeParse(await c.req.json().catch(() => null));
|
|
5085
|
+
if (!parsed.success) {
|
|
5086
|
+
throw new HTTPException4(400, {
|
|
5087
|
+
message: "attemptId and creditId are required"
|
|
5088
|
+
});
|
|
5089
|
+
}
|
|
5090
|
+
const accounts = await listCodexAccountStatuses(db, workspaceId);
|
|
5091
|
+
const account = accounts.find((candidate) => candidate.id === credentialId);
|
|
5092
|
+
if (!account) throw new HTTPException4(404, { message: "codex account not found" });
|
|
5093
|
+
let existing = await getCodexResetRedemptionAttempt(db, workspaceId, parsed.data.attemptId);
|
|
5094
|
+
if (account.connectedBySubjectId !== human.subjectId) {
|
|
5095
|
+
throw new HTTPException4(403, {
|
|
5096
|
+
message: "only the human who connected this subscription may redeem its reset credits"
|
|
5097
|
+
});
|
|
5098
|
+
}
|
|
5099
|
+
if (existing && (existing.credentialId !== credentialId || existing.creditId !== parsed.data.creditId || existing.subjectId !== human.subjectId)) {
|
|
5100
|
+
throw new HTTPException4(409, {
|
|
5101
|
+
message: "logical redemption attempt identity mismatch"
|
|
5102
|
+
});
|
|
5103
|
+
}
|
|
5104
|
+
if (existing) {
|
|
5105
|
+
const adoption = await adoptCodexResetRedemptionAttempt(db, {
|
|
5106
|
+
accountId,
|
|
5107
|
+
workspaceId,
|
|
5108
|
+
attemptId: existing.id,
|
|
5109
|
+
credentialId,
|
|
5110
|
+
creditId: existing.creditId,
|
|
5111
|
+
subjectId: human.subjectId,
|
|
5112
|
+
browserSessionHash: human.browserSessionHash
|
|
5113
|
+
});
|
|
5114
|
+
if (adoption.kind === "in_progress") {
|
|
5115
|
+
throw new HTTPException4(409, {
|
|
5116
|
+
message: "this redemption is still in progress in another browser request"
|
|
5117
|
+
});
|
|
5118
|
+
}
|
|
5119
|
+
if (adoption.kind === "not_found") {
|
|
5120
|
+
throw new HTTPException4(409, { message: "redemption recovery state changed" });
|
|
5121
|
+
}
|
|
5122
|
+
if (adoption.kind === "forbidden") {
|
|
5123
|
+
throw new HTTPException4(403, { message: "redemption owner is unavailable" });
|
|
5124
|
+
}
|
|
5125
|
+
if (adoption.kind === "conflict") {
|
|
5126
|
+
throw new HTTPException4(409, {
|
|
5127
|
+
message: "logical redemption attempt identity mismatch"
|
|
5128
|
+
});
|
|
5129
|
+
}
|
|
5130
|
+
existing = adoption.attempt;
|
|
5131
|
+
}
|
|
5132
|
+
if (account.status !== "active" && existing?.status !== "completed") {
|
|
5133
|
+
throw new HTTPException4(403, { message: "redemption credential is unavailable" });
|
|
5134
|
+
}
|
|
5135
|
+
const secret = settings.betterAuthSecret;
|
|
5136
|
+
if (!secret) {
|
|
5137
|
+
throw new HTTPException4(503, {
|
|
5138
|
+
message: "managed browser confirmation is unavailable"
|
|
5139
|
+
});
|
|
5140
|
+
}
|
|
5141
|
+
const expiresAt = Math.floor(Date.now() / 1e3) + CODEX_REDEMPTION_CONFIRMATION_SECONDS;
|
|
5142
|
+
const confirmationToken = await signCodexRedemptionConfirmation(secret, {
|
|
5143
|
+
version: 1,
|
|
5144
|
+
attemptId: parsed.data.attemptId,
|
|
5145
|
+
workspaceId,
|
|
5146
|
+
credentialId,
|
|
5147
|
+
creditId: parsed.data.creditId,
|
|
5148
|
+
subjectId: human.subjectId,
|
|
5149
|
+
browserSessionHash: human.browserSessionHash,
|
|
5150
|
+
expiresAt
|
|
5151
|
+
});
|
|
5152
|
+
return c.json({
|
|
5153
|
+
attemptId: parsed.data.attemptId,
|
|
5154
|
+
confirmationToken,
|
|
5155
|
+
expiresAt: new Date(expiresAt * 1e3).toISOString(),
|
|
5156
|
+
// A completed attempt may have lost its HTTP response after its outcome
|
|
5157
|
+
// committed. Keep that exact logical id replayable without another
|
|
5158
|
+
// provider consume call, just like an ambiguous provider_started attempt.
|
|
5159
|
+
resumable: existing?.status === "provider_started" || existing?.status === "completed",
|
|
5160
|
+
recoveryStatus: existing?.status === "provider_started" || existing?.status === "completed" ? existing.status : null
|
|
5161
|
+
});
|
|
5162
|
+
}
|
|
5163
|
+
);
|
|
5164
|
+
app.post(
|
|
5165
|
+
"/v1/workspaces/:workspaceId/codex/accounts/:accountId/reset-credits/redeem",
|
|
5166
|
+
async (c) => {
|
|
5167
|
+
const workspaceId = c.req.param("workspaceId");
|
|
5168
|
+
const credentialId = c.req.param("accountId");
|
|
5169
|
+
const { human, accountId } = await requireRedemptionHuman(c, deps, workspaceId);
|
|
5170
|
+
c.header("cache-control", "no-store");
|
|
5171
|
+
const parsed = redemptionBody.safeParse(await c.req.json().catch(() => null));
|
|
5172
|
+
if (!parsed.success) {
|
|
5173
|
+
throw new HTTPException4(400, {
|
|
5174
|
+
message: "explicit redemption confirmation is required"
|
|
5175
|
+
});
|
|
5176
|
+
}
|
|
5177
|
+
const secret = settings.betterAuthSecret;
|
|
5178
|
+
if (!secret) {
|
|
5179
|
+
throw new HTTPException4(503, {
|
|
5180
|
+
message: "managed browser confirmation is unavailable"
|
|
5181
|
+
});
|
|
5182
|
+
}
|
|
5183
|
+
const claims = await verifyCodexRedemptionConfirmation(secret, parsed.data.confirmationToken);
|
|
5184
|
+
if (!claims || claims.attemptId !== parsed.data.attemptId || claims.workspaceId !== workspaceId || claims.credentialId !== credentialId || claims.creditId !== parsed.data.creditId || claims.subjectId !== human.subjectId || claims.browserSessionHash !== human.browserSessionHash) {
|
|
5185
|
+
throw new HTTPException4(403, {
|
|
5186
|
+
message: "redemption confirmation is invalid or expired"
|
|
5187
|
+
});
|
|
5188
|
+
}
|
|
5189
|
+
const claimHolderId = crypto.randomUUID();
|
|
5190
|
+
const claimed = await claimCodexResetRedemption(db, {
|
|
5191
|
+
id: parsed.data.attemptId,
|
|
5192
|
+
accountId,
|
|
5193
|
+
workspaceId,
|
|
5194
|
+
credentialId,
|
|
5195
|
+
subjectId: human.subjectId,
|
|
5196
|
+
browserSessionHash: human.browserSessionHash,
|
|
5197
|
+
creditId: parsed.data.creditId,
|
|
5198
|
+
confirmationExpiresAt: new Date(claims.expiresAt * 1e3),
|
|
5199
|
+
claimHolderId
|
|
5200
|
+
});
|
|
5201
|
+
if (claimed.kind === "not_found") {
|
|
5202
|
+
throw new HTTPException4(404, { message: "codex account not found" });
|
|
5203
|
+
}
|
|
5204
|
+
if (claimed.kind === "forbidden") {
|
|
5205
|
+
throw new HTTPException4(403, {
|
|
5206
|
+
message: "redemption owner or credential is unavailable"
|
|
5207
|
+
});
|
|
5208
|
+
}
|
|
5209
|
+
if (claimed.kind === "conflict") {
|
|
5210
|
+
throw new HTTPException4(409, {
|
|
5211
|
+
message: "logical redemption attempt identity mismatch"
|
|
5212
|
+
});
|
|
5213
|
+
}
|
|
5214
|
+
if (claimed.kind === "in_progress") {
|
|
5215
|
+
return c.json({ status: "in_progress", attemptId: parsed.data.attemptId }, 409);
|
|
5216
|
+
}
|
|
5217
|
+
const finishResponse = (outcome) => c.json({
|
|
5218
|
+
status: "completed",
|
|
5219
|
+
attemptId: parsed.data.attemptId,
|
|
5220
|
+
outcome,
|
|
5221
|
+
// Durable provider truth must never wait for best-effort provider
|
|
5222
|
+
// readback. The browser refreshes overview independently after this
|
|
5223
|
+
// response; a hung account cannot suppress a completed outcome.
|
|
5224
|
+
overview: null
|
|
5225
|
+
});
|
|
5226
|
+
if (claimed.kind === "completed") {
|
|
5227
|
+
return finishResponse(claimed.attempt.outcome);
|
|
5228
|
+
}
|
|
5229
|
+
const attempt = claimed.attempt;
|
|
5230
|
+
const fetchImpl = deps.codexFetch ?? fetch;
|
|
5231
|
+
if (attempt.status === "processing") {
|
|
5232
|
+
const details = await fetchCodexRateLimitResetCreditsForAccount(
|
|
5233
|
+
db,
|
|
5234
|
+
settings,
|
|
5235
|
+
workspaceId,
|
|
5236
|
+
credentialId,
|
|
5237
|
+
fetchImpl
|
|
5238
|
+
);
|
|
5239
|
+
if (!details.ok) {
|
|
5240
|
+
await abandonCodexResetRedemptionBeforeProvider(db, {
|
|
5241
|
+
accountId,
|
|
5242
|
+
workspaceId,
|
|
5243
|
+
attemptId: attempt.id,
|
|
5244
|
+
claimHolderId
|
|
5245
|
+
});
|
|
5246
|
+
return c.json(
|
|
5247
|
+
{
|
|
5248
|
+
status: "preflight_unavailable",
|
|
5249
|
+
attemptId: attempt.id,
|
|
5250
|
+
retryable: true
|
|
5251
|
+
},
|
|
5252
|
+
503
|
|
5253
|
+
);
|
|
5254
|
+
}
|
|
5255
|
+
if (!freshActionableCredit(details.details, attempt.creditId)) {
|
|
5256
|
+
await abandonCodexResetRedemptionBeforeProvider(db, {
|
|
5257
|
+
accountId,
|
|
5258
|
+
workspaceId,
|
|
5259
|
+
attemptId: attempt.id,
|
|
5260
|
+
claimHolderId
|
|
5261
|
+
});
|
|
5262
|
+
return c.json(
|
|
5263
|
+
{
|
|
5264
|
+
status: "not_actionable",
|
|
5265
|
+
attemptId: attempt.id,
|
|
5266
|
+
retryable: false
|
|
5267
|
+
},
|
|
5268
|
+
409
|
|
5269
|
+
);
|
|
5270
|
+
}
|
|
5271
|
+
}
|
|
5272
|
+
let token;
|
|
5273
|
+
try {
|
|
5274
|
+
token = await buildCodexTokenResolver(db, settings, workspaceId, credentialId).getToken();
|
|
5275
|
+
} catch {
|
|
5276
|
+
if (attempt.status === "processing") {
|
|
5277
|
+
await abandonCodexResetRedemptionBeforeProvider(db, {
|
|
5278
|
+
accountId,
|
|
5279
|
+
workspaceId,
|
|
5280
|
+
attemptId: attempt.id,
|
|
5281
|
+
claimHolderId
|
|
5282
|
+
});
|
|
5283
|
+
} else {
|
|
5284
|
+
await releaseCodexResetRedemptionClaim(db, {
|
|
5285
|
+
accountId,
|
|
5286
|
+
workspaceId,
|
|
5287
|
+
attemptId: attempt.id,
|
|
5288
|
+
claimHolderId,
|
|
5289
|
+
failureKind: "provider_auth_unavailable"
|
|
5290
|
+
});
|
|
5291
|
+
}
|
|
5292
|
+
return c.json(
|
|
5293
|
+
{
|
|
5294
|
+
status: "provider_unavailable",
|
|
5295
|
+
attemptId: attempt.id,
|
|
5296
|
+
retryable: true
|
|
5297
|
+
},
|
|
5298
|
+
503
|
|
5299
|
+
);
|
|
5300
|
+
}
|
|
5301
|
+
const fenced = await fenceCodexResetRedemptionSend(db, {
|
|
5302
|
+
accountId,
|
|
5303
|
+
workspaceId,
|
|
5304
|
+
attemptId: attempt.id,
|
|
5305
|
+
claimHolderId,
|
|
5306
|
+
credentialId,
|
|
5307
|
+
subjectId: human.subjectId,
|
|
5308
|
+
browserSessionHash: human.browserSessionHash
|
|
5309
|
+
});
|
|
5310
|
+
if (fenced.kind !== "ready") {
|
|
5311
|
+
if (fenced.reason === "confirmation_expired") {
|
|
5312
|
+
return c.json(
|
|
5313
|
+
{ status: "confirmation_expired", attemptId: attempt.id, retryable: true },
|
|
5314
|
+
403
|
|
5315
|
+
);
|
|
5316
|
+
}
|
|
5317
|
+
if (fenced.reason === "credential_unavailable") {
|
|
5318
|
+
return c.json(
|
|
5319
|
+
{ status: "provider_unavailable", attemptId: attempt.id, retryable: true },
|
|
5320
|
+
503
|
|
5321
|
+
);
|
|
5322
|
+
}
|
|
5323
|
+
return c.json({ status: "in_progress", attemptId: attempt.id }, 409);
|
|
5324
|
+
}
|
|
5325
|
+
const sendAttempt = fenced.attempt;
|
|
5326
|
+
const consumed = await consumeCodexRateLimitResetCredit(
|
|
5327
|
+
{
|
|
5328
|
+
accessToken: token.accessToken,
|
|
5329
|
+
chatgptAccountId: token.chatgptAccountId,
|
|
5330
|
+
isFedramp: token.isFedramp,
|
|
5331
|
+
clientVersion: CODEX_CLIENT_VERSION
|
|
5332
|
+
},
|
|
5333
|
+
{
|
|
5334
|
+
idempotencyKey: sendAttempt.upstreamIdempotencyKey,
|
|
5335
|
+
creditId: sendAttempt.creditId
|
|
5336
|
+
},
|
|
5337
|
+
fetchImpl
|
|
5338
|
+
);
|
|
5339
|
+
if (!consumed.ok) {
|
|
5340
|
+
await releaseCodexResetRedemptionClaim(db, {
|
|
5341
|
+
accountId,
|
|
5342
|
+
workspaceId,
|
|
5343
|
+
attemptId: attempt.id,
|
|
5344
|
+
claimHolderId,
|
|
5345
|
+
failureKind: `provider_${consumed.reason}`
|
|
5346
|
+
});
|
|
5347
|
+
return c.json({ status: "ambiguous", attemptId: attempt.id, retryable: true }, 503);
|
|
5348
|
+
}
|
|
5349
|
+
const completion = await completeCodexResetRedemption(db, {
|
|
5350
|
+
accountId,
|
|
5351
|
+
workspaceId,
|
|
5352
|
+
attemptId: attempt.id,
|
|
5353
|
+
claimHolderId,
|
|
5354
|
+
outcome: consumed.result.outcome
|
|
5355
|
+
});
|
|
5356
|
+
const completed = completion.result;
|
|
5357
|
+
if (!completed) {
|
|
5358
|
+
return c.json({ status: "in_progress", attemptId: attempt.id }, 409);
|
|
5359
|
+
}
|
|
5360
|
+
void signalCodexCapacityTargets(deps, completion.wakeTargets).catch(() => void 0);
|
|
5361
|
+
return finishResponse(completed.outcome);
|
|
5362
|
+
}
|
|
5363
|
+
);
|
|
4318
5364
|
}
|
|
4319
5365
|
|
|
4320
5366
|
// src/routes/connections.ts
|
|
@@ -4350,7 +5396,6 @@ import {
|
|
|
4350
5396
|
decryptEnvironmentValue,
|
|
4351
5397
|
encryptEnvironmentValue as encryptEnvironmentValue2,
|
|
4352
5398
|
getConnectionMetadata,
|
|
4353
|
-
isPrivateAddress,
|
|
4354
5399
|
listConnectionsMetadata,
|
|
4355
5400
|
loadIntegrationOAuthClient,
|
|
4356
5401
|
normalizeBearerScheme,
|
|
@@ -4358,10 +5403,16 @@ import {
|
|
|
4358
5403
|
updateConnection
|
|
4359
5404
|
} from "@opengeni/db";
|
|
4360
5405
|
import { createSignedState as createSignedState2, readSignedState as readSignedState2 } from "@opengeni/github";
|
|
4361
|
-
import {
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
5406
|
+
import {
|
|
5407
|
+
DestinationPolicyError,
|
|
5408
|
+
OAUTH_MAX_RESPONSE_BYTES,
|
|
5409
|
+
isLocalTestEnvironment,
|
|
5410
|
+
pinnedFetch,
|
|
5411
|
+
readResponseJsonBounded,
|
|
5412
|
+
validateHttpUrl
|
|
5413
|
+
} from "@opengeni/network";
|
|
5414
|
+
import { Buffer as Buffer3 } from "buffer";
|
|
5415
|
+
import { createHash as createHash2, randomBytes } from "crypto";
|
|
4365
5416
|
import { HTTPException as HTTPException6 } from "hono/http-exception";
|
|
4366
5417
|
|
|
4367
5418
|
// src/integrations/provider-domain.ts
|
|
@@ -4375,6 +5426,7 @@ function canonicalProviderDomain(value) {
|
|
|
4375
5426
|
}
|
|
4376
5427
|
|
|
4377
5428
|
// src/integrations/oauth-client.ts
|
|
5429
|
+
import { OAUTH_MAX_RESPONSE_BYTES as OAUTH_MAX_RESPONSE_BYTES2 } from "@opengeni/network";
|
|
4378
5430
|
var oauthStateTtlMs = 10 * 60 * 1e3;
|
|
4379
5431
|
var OAuthCallbackStageError = class extends Error {
|
|
4380
5432
|
constructor(stage, reason, cause) {
|
|
@@ -4444,6 +5496,7 @@ async function startMcpOAuth(deps, context) {
|
|
|
4444
5496
|
});
|
|
4445
5497
|
const authorizationUrl = buildAuthorizationUrl({
|
|
4446
5498
|
endpoint: discovery.as.authorizationEndpoint,
|
|
5499
|
+
settings,
|
|
4447
5500
|
clientId: client.clientId,
|
|
4448
5501
|
redirectUri,
|
|
4449
5502
|
state,
|
|
@@ -4614,10 +5667,14 @@ async function probeMcpChallenge(resource, settings) {
|
|
|
4614
5667
|
method: "GET",
|
|
4615
5668
|
headers: { accept: "application/json" }
|
|
4616
5669
|
});
|
|
4617
|
-
|
|
4618
|
-
|
|
5670
|
+
try {
|
|
5671
|
+
if (response.status !== 401) {
|
|
5672
|
+
return {};
|
|
5673
|
+
}
|
|
5674
|
+
return parseWwwAuthenticate(response.headers.get("www-authenticate"));
|
|
5675
|
+
} finally {
|
|
5676
|
+
await cancelResponseBody(response);
|
|
4619
5677
|
}
|
|
4620
|
-
return parseWwwAuthenticate(response.headers.get("www-authenticate"));
|
|
4621
5678
|
}
|
|
4622
5679
|
async function discoverProtectedResourceMetadata(resource, settings, advertisedUrl) {
|
|
4623
5680
|
const candidates = uniqueStrings([
|
|
@@ -4648,10 +5705,15 @@ async function discoverProtectedResourceMetadata(resource, settings, advertisedU
|
|
|
4648
5705
|
throw new HTTPException6(422, { message: "could not discover MCP protected resource metadata" });
|
|
4649
5706
|
}
|
|
4650
5707
|
async function discoverAuthorizationServerMetadata(authorizationServer, settings) {
|
|
4651
|
-
const
|
|
5708
|
+
const safeAuthorizationServer = oauthEndpointUrl(
|
|
4652
5709
|
authorizationServer,
|
|
4653
|
-
|
|
4654
|
-
|
|
5710
|
+
settings,
|
|
5711
|
+
"OAuth authorization server"
|
|
5712
|
+
).replace(/\/+$/, "");
|
|
5713
|
+
const candidates = uniqueStrings([
|
|
5714
|
+
safeAuthorizationServer,
|
|
5715
|
+
...wellKnownCandidates(safeAuthorizationServer, "oauth-authorization-server"),
|
|
5716
|
+
...wellKnownCandidates(safeAuthorizationServer, "openid-configuration")
|
|
4655
5717
|
]);
|
|
4656
5718
|
for (const candidate of candidates) {
|
|
4657
5719
|
const payload = await fetchJsonObject(candidate, settings).catch((error) => {
|
|
@@ -4668,16 +5730,29 @@ async function discoverAuthorizationServerMetadata(authorizationServer, settings
|
|
|
4668
5730
|
if (!authorizationEndpoint || !tokenEndpoint) {
|
|
4669
5731
|
continue;
|
|
4670
5732
|
}
|
|
4671
|
-
|
|
4672
|
-
issuer: stringValue(payload.issuer) ?? authorizationServer.replace(/\/+$/, ""),
|
|
4673
|
-
authorizationServer: authorizationServer.replace(/\/+$/, ""),
|
|
5733
|
+
const safeAuthorizationEndpoint = oauthEndpointUrl(
|
|
4674
5734
|
authorizationEndpoint,
|
|
4675
|
-
|
|
5735
|
+
settings,
|
|
5736
|
+
"OAuth authorization endpoint"
|
|
5737
|
+
);
|
|
5738
|
+
const safeTokenEndpoint = oauthEndpointUrl(tokenEndpoint, settings, "OAuth token endpoint");
|
|
5739
|
+
const registrationEndpoint = stringValue(payload.registration_endpoint);
|
|
5740
|
+
const issuer = oauthEndpointUrl(
|
|
5741
|
+
stringValue(payload.issuer) ?? safeAuthorizationServer,
|
|
5742
|
+
settings,
|
|
5743
|
+
"OAuth issuer"
|
|
5744
|
+
);
|
|
5745
|
+
const safeRegistrationEndpoint = registrationEndpoint ? oauthEndpointUrl(registrationEndpoint, settings, "OAuth registration endpoint") : void 0;
|
|
5746
|
+
return {
|
|
5747
|
+
issuer,
|
|
5748
|
+
authorizationServer: safeAuthorizationServer,
|
|
5749
|
+
authorizationEndpoint: safeAuthorizationEndpoint,
|
|
5750
|
+
tokenEndpoint: safeTokenEndpoint,
|
|
4676
5751
|
clientIdMetadataDocumentSupported: payload.client_id_metadata_document_supported === true,
|
|
4677
5752
|
tokenEndpointAuthMethodsSupported: stringArray(payload.token_endpoint_auth_methods_supported),
|
|
4678
5753
|
codeChallengeMethodsSupported: stringArray(payload.code_challenge_methods_supported),
|
|
4679
5754
|
raw: payload,
|
|
4680
|
-
...
|
|
5755
|
+
...safeRegistrationEndpoint ? { registrationEndpoint: safeRegistrationEndpoint } : {}
|
|
4681
5756
|
};
|
|
4682
5757
|
}
|
|
4683
5758
|
throw new HTTPException6(422, {
|
|
@@ -4829,7 +5904,6 @@ async function dynamicClientRegistration(settings, as, redirectUri, scopes) {
|
|
|
4829
5904
|
message: "authorization server does not support dynamic client registration"
|
|
4830
5905
|
});
|
|
4831
5906
|
}
|
|
4832
|
-
await assertOAuthFetchAllowed(as.registrationEndpoint, settings);
|
|
4833
5907
|
const response = await fetchOAuth(as.registrationEndpoint, settings, {
|
|
4834
5908
|
method: "POST",
|
|
4835
5909
|
headers: { "content-type": "application/json", accept: "application/json" },
|
|
@@ -4843,11 +5917,16 @@ async function dynamicClientRegistration(settings, as, redirectUri, scopes) {
|
|
|
4843
5917
|
})
|
|
4844
5918
|
});
|
|
4845
5919
|
if (!response.ok) {
|
|
5920
|
+
await cancelResponseBody(response);
|
|
4846
5921
|
throw new HTTPException6(422, {
|
|
4847
5922
|
message: `dynamic client registration failed with HTTP ${response.status}`
|
|
4848
5923
|
});
|
|
4849
5924
|
}
|
|
4850
|
-
const payload = await
|
|
5925
|
+
const payload = await readResponseJsonBounded(
|
|
5926
|
+
response,
|
|
5927
|
+
OAUTH_MAX_RESPONSE_BYTES,
|
|
5928
|
+
"OAuth dynamic registration response"
|
|
5929
|
+
);
|
|
4851
5930
|
const clientId = stringValue(payload.client_id);
|
|
4852
5931
|
if (!clientId) {
|
|
4853
5932
|
throw new HTTPException6(422, {
|
|
@@ -4877,7 +5956,8 @@ async function existingOAuthConnectionForStart(db, input) {
|
|
|
4877
5956
|
) ?? null;
|
|
4878
5957
|
}
|
|
4879
5958
|
function buildAuthorizationUrl(input) {
|
|
4880
|
-
const
|
|
5959
|
+
const endpoint = oauthEndpointUrl(input.endpoint, input.settings, "OAuth authorization endpoint");
|
|
5960
|
+
const url = new URL(endpoint);
|
|
4881
5961
|
url.searchParams.set("response_type", "code");
|
|
4882
5962
|
url.searchParams.set("client_id", input.clientId);
|
|
4883
5963
|
url.searchParams.set("redirect_uri", input.redirectUri);
|
|
@@ -4915,9 +5995,21 @@ function readOAuthState(state, settings) {
|
|
|
4915
5995
|
"state.encryptedPkceVerifier"
|
|
4916
5996
|
),
|
|
4917
5997
|
clientId: requiredString(payload.clientId, "state.clientId"),
|
|
4918
|
-
tokenEndpoint:
|
|
4919
|
-
|
|
4920
|
-
|
|
5998
|
+
tokenEndpoint: oauthEndpointUrl(
|
|
5999
|
+
requiredString(payload.tokenEndpoint, "state.tokenEndpoint"),
|
|
6000
|
+
settings,
|
|
6001
|
+
"OAuth token endpoint"
|
|
6002
|
+
),
|
|
6003
|
+
authorizationServer: oauthEndpointUrl(
|
|
6004
|
+
requiredString(payload.authorizationServer, "state.authorizationServer"),
|
|
6005
|
+
settings,
|
|
6006
|
+
"OAuth authorization server"
|
|
6007
|
+
).replace(/\/+$/, ""),
|
|
6008
|
+
issuer: oauthEndpointUrl(
|
|
6009
|
+
requiredString(payload.issuer, "state.issuer"),
|
|
6010
|
+
settings,
|
|
6011
|
+
"OAuth issuer"
|
|
6012
|
+
),
|
|
4921
6013
|
clientRegistrationMethod: registrationMethod(payload.clientRegistrationMethod),
|
|
4922
6014
|
tokenEndpointAuthMethod: tokenAuthMethod(stringValue(payload.tokenEndpointAuthMethod), false),
|
|
4923
6015
|
...stringValue(payload.encryptedClientSecret) ? { encryptedClientSecret: stringValue(payload.encryptedClientSecret) } : {},
|
|
@@ -4956,7 +6048,7 @@ async function clientForState(db, settings, state) {
|
|
|
4956
6048
|
}
|
|
4957
6049
|
if (state.clientRegistrationMethod === "dcr") {
|
|
4958
6050
|
const stored = await loadIntegrationOAuthClient(db, settings, state.issuer);
|
|
4959
|
-
if (!stored || stored.clientId !== state.clientId) {
|
|
6051
|
+
if (!stored || stored.clientId !== state.clientId || stored.issuer !== state.issuer || stored.authorizationServer !== state.authorizationServer) {
|
|
4960
6052
|
throw new HTTPException6(400, { message: "OAuth client registration is no longer available" });
|
|
4961
6053
|
}
|
|
4962
6054
|
return {
|
|
@@ -4990,7 +6082,6 @@ async function clientForState(db, settings, state) {
|
|
|
4990
6082
|
};
|
|
4991
6083
|
}
|
|
4992
6084
|
async function exchangeAuthorizationCode(settings, input) {
|
|
4993
|
-
await assertOAuthFetchAllowed(input.tokenEndpoint, settings);
|
|
4994
6085
|
const body = new URLSearchParams();
|
|
4995
6086
|
body.set("grant_type", "authorization_code");
|
|
4996
6087
|
body.set("code", input.code);
|
|
@@ -5005,7 +6096,7 @@ async function exchangeAuthorizationCode(settings, input) {
|
|
|
5005
6096
|
body.set("client_id", input.client.clientId);
|
|
5006
6097
|
body.set("client_secret", input.client.clientSecret);
|
|
5007
6098
|
} else if (input.client.clientSecret && input.client.tokenEndpointAuthMethod === "client_secret_basic") {
|
|
5008
|
-
headers.authorization = `Basic ${
|
|
6099
|
+
headers.authorization = `Basic ${Buffer3.from(`${input.client.clientId}:${input.client.clientSecret}`).toString("base64")}`;
|
|
5009
6100
|
} else {
|
|
5010
6101
|
body.set("client_id", input.client.clientId);
|
|
5011
6102
|
}
|
|
@@ -5022,7 +6113,11 @@ async function exchangeAuthorizationCode(settings, input) {
|
|
|
5022
6113
|
new Error(`OAuth token endpoint returned HTTP ${response.status}`)
|
|
5023
6114
|
);
|
|
5024
6115
|
}
|
|
5025
|
-
const payload = await
|
|
6116
|
+
const payload = await readResponseJsonBounded(
|
|
6117
|
+
response,
|
|
6118
|
+
OAUTH_MAX_RESPONSE_BYTES,
|
|
6119
|
+
"OAuth token response"
|
|
6120
|
+
);
|
|
5026
6121
|
const accessToken = stringValue(payload.access_token);
|
|
5027
6122
|
if (!accessToken) {
|
|
5028
6123
|
throw new Error("OAuth token response did not include access_token");
|
|
@@ -5093,9 +6188,14 @@ function safeHost(rawUrl) {
|
|
|
5093
6188
|
async function oauthErrorFromResponse(response) {
|
|
5094
6189
|
const contentType = response.headers.get("content-type") ?? "";
|
|
5095
6190
|
if (!contentType.toLowerCase().includes("application/json")) {
|
|
6191
|
+
await cancelResponseBody(response);
|
|
5096
6192
|
return null;
|
|
5097
6193
|
}
|
|
5098
|
-
const payload = await
|
|
6194
|
+
const payload = await readResponseJsonBounded(
|
|
6195
|
+
response,
|
|
6196
|
+
OAUTH_MAX_RESPONSE_BYTES,
|
|
6197
|
+
"OAuth token error response"
|
|
6198
|
+
).catch(() => null);
|
|
5099
6199
|
const error = stringValue(payload?.error);
|
|
5100
6200
|
if (!error || !/^[a-zA-Z0-9_.-]{1,80}$/.test(error)) {
|
|
5101
6201
|
return null;
|
|
@@ -5103,7 +6203,6 @@ async function oauthErrorFromResponse(response) {
|
|
|
5103
6203
|
return error;
|
|
5104
6204
|
}
|
|
5105
6205
|
async function verifyMcpToolsList(settings, resource, token) {
|
|
5106
|
-
await assertOAuthFetchAllowed(resource, settings);
|
|
5107
6206
|
const client = new Client2(
|
|
5108
6207
|
{ name: "opengeni-integration-verify", version: "0.1.0" },
|
|
5109
6208
|
{ capabilities: {} }
|
|
@@ -5215,6 +6314,19 @@ function canonicalOAuthResource(value) {
|
|
|
5215
6314
|
});
|
|
5216
6315
|
}
|
|
5217
6316
|
}
|
|
6317
|
+
function oauthEndpointUrl(rawUrl, settings, label) {
|
|
6318
|
+
try {
|
|
6319
|
+
return validateHttpUrl(rawUrl, {
|
|
6320
|
+
label,
|
|
6321
|
+
allowLoopbackHttp: isLocalTestEnvironment(settings.environment)
|
|
6322
|
+
});
|
|
6323
|
+
} catch (error) {
|
|
6324
|
+
if (error instanceof DestinationPolicyError) {
|
|
6325
|
+
throw new HTTPException6(422, { message: error.message });
|
|
6326
|
+
}
|
|
6327
|
+
throw error;
|
|
6328
|
+
}
|
|
6329
|
+
}
|
|
5218
6330
|
function safeReturnPath(value) {
|
|
5219
6331
|
if (!value.startsWith("/") || value.startsWith("//")) {
|
|
5220
6332
|
throw new HTTPException6(400, { message: "OAuth returnPath must be a relative path" });
|
|
@@ -5228,59 +6340,71 @@ function safeReturnPath(value) {
|
|
|
5228
6340
|
async function fetchJsonObject(url, settings) {
|
|
5229
6341
|
const response = await fetchOAuth(url, settings, { headers: { accept: "application/json" } });
|
|
5230
6342
|
if (!response.ok) {
|
|
6343
|
+
await cancelResponseBody(response);
|
|
5231
6344
|
throw new Error(`HTTP ${response.status}`);
|
|
5232
6345
|
}
|
|
5233
|
-
const payload = await
|
|
6346
|
+
const payload = await readResponseJsonBounded(
|
|
6347
|
+
response,
|
|
6348
|
+
OAUTH_MAX_RESPONSE_BYTES,
|
|
6349
|
+
"OAuth metadata response"
|
|
6350
|
+
);
|
|
5234
6351
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
5235
6352
|
throw new Error("metadata response was not a JSON object");
|
|
5236
6353
|
}
|
|
5237
6354
|
return payload;
|
|
5238
6355
|
}
|
|
5239
6356
|
async function fetchOAuth(rawUrl, settings, init = {}, hop = 0) {
|
|
5240
|
-
|
|
5241
|
-
|
|
6357
|
+
let response;
|
|
6358
|
+
try {
|
|
6359
|
+
const endpoint = oauthEndpointUrl(rawUrl, settings, "OAuth endpoint");
|
|
6360
|
+
response = await pinnedFetch(endpoint, init, settings, {
|
|
6361
|
+
label: "OAuth discovery",
|
|
6362
|
+
requireHttpsOutsideLocalTest: true
|
|
6363
|
+
});
|
|
6364
|
+
} catch (error) {
|
|
6365
|
+
if (error instanceof DestinationPolicyError) {
|
|
6366
|
+
throw new HTTPException6(422, { message: error.message });
|
|
6367
|
+
}
|
|
6368
|
+
throw error;
|
|
6369
|
+
}
|
|
5242
6370
|
if (response.status < 300 || response.status >= 400) {
|
|
5243
6371
|
return response;
|
|
5244
6372
|
}
|
|
6373
|
+
if (!oauthRequestMayFollowRedirect(init)) {
|
|
6374
|
+
await cancelResponseBody(response);
|
|
6375
|
+
throw new HTTPException6(422, {
|
|
6376
|
+
message: "OAuth credential-bearing requests may not follow redirects"
|
|
6377
|
+
});
|
|
6378
|
+
}
|
|
5245
6379
|
if (hop >= 3) {
|
|
6380
|
+
await cancelResponseBody(response);
|
|
5246
6381
|
throw new HTTPException6(422, { message: "OAuth fetch exceeded maximum redirect hops" });
|
|
5247
6382
|
}
|
|
5248
6383
|
const location = response.headers.get("location");
|
|
5249
6384
|
if (!location) {
|
|
6385
|
+
await cancelResponseBody(response);
|
|
5250
6386
|
throw new HTTPException6(422, { message: "OAuth fetch redirect was missing Location" });
|
|
5251
6387
|
}
|
|
5252
6388
|
let nextUrl;
|
|
5253
6389
|
try {
|
|
5254
6390
|
nextUrl = new URL(location, rawUrl).toString();
|
|
5255
6391
|
} catch {
|
|
6392
|
+
await cancelResponseBody(response);
|
|
5256
6393
|
throw new HTTPException6(422, { message: "OAuth fetch redirect Location was invalid" });
|
|
5257
6394
|
}
|
|
6395
|
+
await cancelResponseBody(response);
|
|
5258
6396
|
return await fetchOAuth(nextUrl, settings, init, hop + 1);
|
|
5259
6397
|
}
|
|
5260
|
-
|
|
5261
|
-
const
|
|
5262
|
-
if (
|
|
5263
|
-
|
|
5264
|
-
}
|
|
5265
|
-
if (settings.integrationsAllowPrivateNetworkTargets || ["local", "test"].includes(settings.environment)) {
|
|
5266
|
-
return;
|
|
5267
|
-
}
|
|
5268
|
-
if (url.protocol !== "https:") {
|
|
5269
|
-
throw new HTTPException6(422, {
|
|
5270
|
-
message: "OAuth discovery targets must use https outside local/test"
|
|
5271
|
-
});
|
|
5272
|
-
}
|
|
5273
|
-
const hostname = url.hostname.toLowerCase();
|
|
5274
|
-
if (hostname === "localhost" || hostname.endsWith(".localhost")) {
|
|
5275
|
-
throw new HTTPException6(422, { message: "OAuth discovery may not target localhost" });
|
|
5276
|
-
}
|
|
5277
|
-
const literal2 = isIP(hostname);
|
|
5278
|
-
const addresses = literal2 ? [hostname] : (await lookup(hostname, { all: true })).map((entry) => entry.address);
|
|
5279
|
-
if (addresses.some(isPrivateAddress)) {
|
|
5280
|
-
throw new HTTPException6(422, {
|
|
5281
|
-
message: "OAuth discovery may not target private network addresses"
|
|
5282
|
-
});
|
|
6398
|
+
function oauthRequestMayFollowRedirect(init) {
|
|
6399
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
6400
|
+
if (method !== "GET" && method !== "HEAD" || init.body != null) {
|
|
6401
|
+
return false;
|
|
5283
6402
|
}
|
|
6403
|
+
const headers = new Headers(init.headers);
|
|
6404
|
+
return [...headers.keys()].every((name) => name === "accept");
|
|
6405
|
+
}
|
|
6406
|
+
async function cancelResponseBody(response) {
|
|
6407
|
+
await response.body?.cancel().catch(() => void 0);
|
|
5284
6408
|
}
|
|
5285
6409
|
function parseWwwAuthenticate(header) {
|
|
5286
6410
|
if (!header) {
|
|
@@ -5353,7 +6477,7 @@ function expiresAtFromTokenResponse(payload) {
|
|
|
5353
6477
|
return null;
|
|
5354
6478
|
}
|
|
5355
6479
|
function pkceChallenge(verifier) {
|
|
5356
|
-
return
|
|
6480
|
+
return createHash2("sha256").update(verifier).digest("base64url");
|
|
5357
6481
|
}
|
|
5358
6482
|
function randomPkceVerifier() {
|
|
5359
6483
|
return randomBytes(32).toString("base64url");
|
|
@@ -5589,14 +6713,14 @@ import {
|
|
|
5589
6713
|
} from "@opengeni/documents";
|
|
5590
6714
|
import { createKnowledgeMemory, listKnowledgeMemories } from "@opengeni/db";
|
|
5591
6715
|
import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5592
|
-
import * as
|
|
6716
|
+
import * as z2 from "zod/v4";
|
|
5593
6717
|
var SearchInputSchema = {
|
|
5594
|
-
query:
|
|
5595
|
-
baseIds:
|
|
5596
|
-
limit:
|
|
5597
|
-
mode:
|
|
5598
|
-
sourceKinds:
|
|
5599
|
-
|
|
6718
|
+
query: z2.string().min(1),
|
|
6719
|
+
baseIds: z2.array(z2.string().uuid()).optional(),
|
|
6720
|
+
limit: z2.number().int().positive().max(50).optional(),
|
|
6721
|
+
mode: z2.enum(["hybrid", "vector", "keyword"]).optional(),
|
|
6722
|
+
sourceKinds: z2.array(
|
|
6723
|
+
z2.enum([
|
|
5600
6724
|
"manual_upload",
|
|
5601
6725
|
"meeting_transcript",
|
|
5602
6726
|
"repository",
|
|
@@ -5607,15 +6731,15 @@ var SearchInputSchema = {
|
|
|
5607
6731
|
"other"
|
|
5608
6732
|
])
|
|
5609
6733
|
).optional(),
|
|
5610
|
-
aclTags:
|
|
6734
|
+
aclTags: z2.array(z2.string().min(1)).optional()
|
|
5611
6735
|
};
|
|
5612
|
-
var MemoryKindSchema2 =
|
|
5613
|
-
var SourceRefSchema =
|
|
5614
|
-
kind:
|
|
5615
|
-
id:
|
|
5616
|
-
uri:
|
|
5617
|
-
title:
|
|
5618
|
-
metadata:
|
|
6736
|
+
var MemoryKindSchema2 = z2.enum(["semantic", "episodic", "procedural", "decision", "preference"]);
|
|
6737
|
+
var SourceRefSchema = z2.object({
|
|
6738
|
+
kind: z2.enum(["document_chunk", "document", "session_event", "memory", "external"]),
|
|
6739
|
+
id: z2.string().min(1),
|
|
6740
|
+
uri: z2.string().min(1).optional(),
|
|
6741
|
+
title: z2.string().min(1).optional(),
|
|
6742
|
+
metadata: z2.record(z2.string(), z2.unknown()).optional()
|
|
5619
6743
|
});
|
|
5620
6744
|
function buildDocumentsMcpServer(db, accountId, workspaceId, documentServices, options = {}) {
|
|
5621
6745
|
const server = new McpServer2({
|
|
@@ -5653,7 +6777,7 @@ function buildDocumentsMcpServer(db, accountId, workspaceId, documentServices, o
|
|
|
5653
6777
|
{
|
|
5654
6778
|
description: "Fetch one indexed document chunk by id.",
|
|
5655
6779
|
inputSchema: {
|
|
5656
|
-
chunkId:
|
|
6780
|
+
chunkId: z2.string().uuid()
|
|
5657
6781
|
}
|
|
5658
6782
|
},
|
|
5659
6783
|
async ({ chunkId }) => {
|
|
@@ -5671,7 +6795,7 @@ function buildDocumentsMcpServer(db, accountId, workspaceId, documentServices, o
|
|
|
5671
6795
|
{
|
|
5672
6796
|
description: "Fetch one knowledge source chunk by id.",
|
|
5673
6797
|
inputSchema: {
|
|
5674
|
-
chunkId:
|
|
6798
|
+
chunkId: z2.string().uuid()
|
|
5675
6799
|
}
|
|
5676
6800
|
},
|
|
5677
6801
|
async ({ chunkId }) => {
|
|
@@ -5689,10 +6813,10 @@ function buildDocumentsMcpServer(db, accountId, workspaceId, documentServices, o
|
|
|
5689
6813
|
{
|
|
5690
6814
|
description: "Search approved company memory records.",
|
|
5691
6815
|
inputSchema: {
|
|
5692
|
-
query:
|
|
6816
|
+
query: z2.string().min(1).optional(),
|
|
5693
6817
|
kind: MemoryKindSchema2.optional(),
|
|
5694
|
-
scope:
|
|
5695
|
-
limit:
|
|
6818
|
+
scope: z2.string().min(1).optional(),
|
|
6819
|
+
limit: z2.number().int().positive().max(100).optional()
|
|
5696
6820
|
}
|
|
5697
6821
|
},
|
|
5698
6822
|
async ({ query, kind, scope, limit }) => ({
|
|
@@ -5717,12 +6841,12 @@ function buildDocumentsMcpServer(db, accountId, workspaceId, documentServices, o
|
|
|
5717
6841
|
{
|
|
5718
6842
|
description: "Propose a company memory record for human review.",
|
|
5719
6843
|
inputSchema: {
|
|
5720
|
-
text:
|
|
6844
|
+
text: z2.string().min(1),
|
|
5721
6845
|
kind: MemoryKindSchema2.optional(),
|
|
5722
|
-
scope:
|
|
5723
|
-
sourceRefs:
|
|
5724
|
-
confidence:
|
|
5725
|
-
metadata:
|
|
6846
|
+
scope: z2.string().min(1).optional(),
|
|
6847
|
+
sourceRefs: z2.array(SourceRefSchema).optional(),
|
|
6848
|
+
confidence: z2.number().min(0).max(1).optional(),
|
|
6849
|
+
metadata: z2.record(z2.string(), z2.unknown()).optional()
|
|
5726
6850
|
}
|
|
5727
6851
|
},
|
|
5728
6852
|
async ({ text, kind, scope, sourceRefs, confidence, metadata }) => ({
|
|
@@ -7158,7 +8282,12 @@ import {
|
|
|
7158
8282
|
CreateFileUploadRequest,
|
|
7159
8283
|
CreateFileUploadResponse,
|
|
7160
8284
|
FileAsset,
|
|
7161
|
-
FileDownloadUrlResponse
|
|
8285
|
+
FileDownloadUrlResponse,
|
|
8286
|
+
RETAINED_OUTPUT_DEFAULT_PAGE_BYTES,
|
|
8287
|
+
RETAINED_OUTPUT_MAX_PAGE_BYTES,
|
|
8288
|
+
RetainedArtifactMetadataSchema,
|
|
8289
|
+
retainedArtifactReferenceFromFile,
|
|
8290
|
+
resolveRetainedOutputRange
|
|
7162
8291
|
} from "@opengeni/contracts";
|
|
7163
8292
|
import {
|
|
7164
8293
|
claimFileUploadCleanup,
|
|
@@ -7166,6 +8295,7 @@ import {
|
|
|
7166
8295
|
completeFileUpload,
|
|
7167
8296
|
createFileUpload,
|
|
7168
8297
|
getFileUpload,
|
|
8298
|
+
getRetainedFileArtifact,
|
|
7169
8299
|
requireFile as requireFile2
|
|
7170
8300
|
} from "@opengeni/db";
|
|
7171
8301
|
import { HTTPException as HTTPException12 } from "hono/http-exception";
|
|
@@ -7361,6 +8491,84 @@ function registerFileRoutes(app, deps) {
|
|
|
7361
8491
|
}
|
|
7362
8492
|
return c.json(FileAsset.parse(file));
|
|
7363
8493
|
});
|
|
8494
|
+
app.get("/v1/workspaces/:workspaceId/artifacts/:artifactId", async (c) => {
|
|
8495
|
+
const workspaceId = c.req.param("workspaceId");
|
|
8496
|
+
await requireAccessGrant8(c, deps, workspaceId, "files:read");
|
|
8497
|
+
const artifactId = retainedArtifactId(c.req.param("artifactId"));
|
|
8498
|
+
const artifact = await getRetainedFileArtifact(db, workspaceId, artifactId);
|
|
8499
|
+
if (!artifact) {
|
|
8500
|
+
return c.json(retainedArtifactUnavailable(artifactId, "deleted"), 404);
|
|
8501
|
+
}
|
|
8502
|
+
return c.json(retainedArtifactMetadata(artifact));
|
|
8503
|
+
});
|
|
8504
|
+
app.get("/v1/workspaces/:workspaceId/artifacts/:artifactId/content", async (c) => {
|
|
8505
|
+
const workspaceId = c.req.param("workspaceId");
|
|
8506
|
+
await requireAccessGrant8(c, deps, workspaceId, "files:read");
|
|
8507
|
+
const artifactId = retainedArtifactId(c.req.param("artifactId"));
|
|
8508
|
+
const artifact = await getRetainedFileArtifact(db, workspaceId, artifactId);
|
|
8509
|
+
if (!artifact) {
|
|
8510
|
+
return c.json(retainedArtifactUnavailable(artifactId, "deleted"), 404);
|
|
8511
|
+
}
|
|
8512
|
+
const metadata = retainedArtifactMetadata(artifact);
|
|
8513
|
+
if (!metadata.available) {
|
|
8514
|
+
return c.json(metadata, retainedArtifactUnavailableStatus(metadata.reason));
|
|
8515
|
+
}
|
|
8516
|
+
if (!objectStorage) {
|
|
8517
|
+
return c.json(retainedArtifactUnavailable(artifactId, "missing_storage"), 503);
|
|
8518
|
+
}
|
|
8519
|
+
const rangeHeader = c.req.header("range");
|
|
8520
|
+
const range = resolveRetainedOutputRange(
|
|
8521
|
+
rangeHeader,
|
|
8522
|
+
metadata.originalBytes,
|
|
8523
|
+
rangeHeader ? RETAINED_OUTPUT_MAX_PAGE_BYTES : RETAINED_OUTPUT_DEFAULT_PAGE_BYTES
|
|
8524
|
+
);
|
|
8525
|
+
if (range.kind === "invalid") {
|
|
8526
|
+
return c.json(
|
|
8527
|
+
{
|
|
8528
|
+
message: "invalid retained artifact byte range",
|
|
8529
|
+
reason: range.reason,
|
|
8530
|
+
maxRangeBytes: RETAINED_OUTPUT_MAX_PAGE_BYTES
|
|
8531
|
+
},
|
|
8532
|
+
400
|
|
8533
|
+
);
|
|
8534
|
+
}
|
|
8535
|
+
if (range.kind === "unsatisfiable") {
|
|
8536
|
+
return c.json(
|
|
8537
|
+
{ message: "retained artifact byte range is not satisfiable", reason: range.reason },
|
|
8538
|
+
416,
|
|
8539
|
+
{
|
|
8540
|
+
"Accept-Ranges": "bytes",
|
|
8541
|
+
"Content-Range": range.contentRange,
|
|
8542
|
+
"Cache-Control": "private, no-store"
|
|
8543
|
+
}
|
|
8544
|
+
);
|
|
8545
|
+
}
|
|
8546
|
+
const headers = {
|
|
8547
|
+
"Accept-Ranges": range.acceptRanges,
|
|
8548
|
+
"Cache-Control": "private, no-store",
|
|
8549
|
+
"Content-Length": String(range.length),
|
|
8550
|
+
"Content-Type": metadata.contentType,
|
|
8551
|
+
"X-Content-Type-Options": "nosniff",
|
|
8552
|
+
...range.contentRange ? { "Content-Range": range.contentRange } : {}
|
|
8553
|
+
};
|
|
8554
|
+
if (range.kind === "empty") {
|
|
8555
|
+
if (!await objectStorage.fileExists(artifact.file)) {
|
|
8556
|
+
return c.json(retainedArtifactUnavailable(artifactId, "missing_storage"), 410);
|
|
8557
|
+
}
|
|
8558
|
+
return c.body(null, 200, headers);
|
|
8559
|
+
}
|
|
8560
|
+
const bytes = await objectStorage.getFileRange(artifact.file, {
|
|
8561
|
+
start: range.start,
|
|
8562
|
+
end: range.end
|
|
8563
|
+
});
|
|
8564
|
+
if (!bytes) {
|
|
8565
|
+
return c.json(retainedArtifactUnavailable(artifactId, "missing_storage"), 410);
|
|
8566
|
+
}
|
|
8567
|
+
if (bytes.byteLength !== range.length) {
|
|
8568
|
+
throw new HTTPException12(502, { message: "object storage returned an invalid byte range" });
|
|
8569
|
+
}
|
|
8570
|
+
return c.body(new Uint8Array(bytes), range.status, headers);
|
|
8571
|
+
});
|
|
7364
8572
|
app.post("/v1/workspaces/:workspaceId/files/:fileId/download-url", async (c) => {
|
|
7365
8573
|
const workspaceId = c.req.param("workspaceId");
|
|
7366
8574
|
await requireAccessGrant8(c, deps, workspaceId, "files:read");
|
|
@@ -7391,6 +8599,50 @@ function sanitizeFilename(filename) {
|
|
|
7391
8599
|
function publicFileUploadStatus(status) {
|
|
7392
8600
|
return status === "cleanup_pending" ? "failed" : status;
|
|
7393
8601
|
}
|
|
8602
|
+
function retainedArtifactId(value) {
|
|
8603
|
+
const parsed = FileAsset.shape.id.safeParse(value);
|
|
8604
|
+
if (!parsed.success) {
|
|
8605
|
+
throw new HTTPException12(404, { message: "artifact not found" });
|
|
8606
|
+
}
|
|
8607
|
+
return parsed.data;
|
|
8608
|
+
}
|
|
8609
|
+
function retainedArtifactUnavailable(artifactId, reason) {
|
|
8610
|
+
return RetainedArtifactMetadataSchema.parse({ available: false, artifactId, reason });
|
|
8611
|
+
}
|
|
8612
|
+
function retainedArtifactMetadata(artifact) {
|
|
8613
|
+
const reference = retainedArtifactReferenceFromFile(artifact.file);
|
|
8614
|
+
if (reference) return reference;
|
|
8615
|
+
const { file, uploadStatus, uploadExpiresAt } = artifact;
|
|
8616
|
+
if (file.status === "deleted") {
|
|
8617
|
+
return retainedArtifactUnavailable(file.id, "deleted");
|
|
8618
|
+
}
|
|
8619
|
+
if (file.status === "expired" || uploadStatus === "expired" || uploadStatus === "pending" && uploadExpiresAt !== null && uploadExpiresAt.getTime() < Date.now()) {
|
|
8620
|
+
return retainedArtifactUnavailable(file.id, "expired");
|
|
8621
|
+
}
|
|
8622
|
+
if (file.status === "failed" || uploadStatus === "failed" || uploadStatus === "cleanup_pending") {
|
|
8623
|
+
return retainedArtifactUnavailable(file.id, "failed");
|
|
8624
|
+
}
|
|
8625
|
+
if (file.status === "pending_upload" || uploadStatus === "pending") {
|
|
8626
|
+
return retainedArtifactUnavailable(file.id, "pending");
|
|
8627
|
+
}
|
|
8628
|
+
return retainedArtifactUnavailable(file.id, "unsupported");
|
|
8629
|
+
}
|
|
8630
|
+
function retainedArtifactUnavailableStatus(reason) {
|
|
8631
|
+
switch (reason) {
|
|
8632
|
+
case "deleted":
|
|
8633
|
+
return 404;
|
|
8634
|
+
case "expired":
|
|
8635
|
+
case "missing_storage":
|
|
8636
|
+
return 410;
|
|
8637
|
+
case "unsupported":
|
|
8638
|
+
case "not_retained":
|
|
8639
|
+
case "storage_write_failed":
|
|
8640
|
+
return 422;
|
|
8641
|
+
case "pending":
|
|
8642
|
+
case "failed":
|
|
8643
|
+
return 409;
|
|
8644
|
+
}
|
|
8645
|
+
}
|
|
7394
8646
|
|
|
7395
8647
|
// src/routes/api-keys.ts
|
|
7396
8648
|
import { CreateApiKeyRequest, CreateApiKeyResponse } from "@opengeni/contracts";
|
|
@@ -8783,14 +10035,20 @@ import {
|
|
|
8783
10035
|
SessionEventPayloadMode as SessionEventPayloadMode2,
|
|
8784
10036
|
SessionEventReadDirection as SessionEventReadDirection2,
|
|
8785
10037
|
SessionEventReadMode as SessionEventReadMode2,
|
|
10038
|
+
SessionEventLatestClass as SessionEventLatestClass2,
|
|
10039
|
+
SessionEventResultMode as SessionEventResultMode2,
|
|
8786
10040
|
SessionEventSemanticClass as SessionEventSemanticClass2,
|
|
8787
10041
|
SessionEventType as SessionEventType2,
|
|
10042
|
+
SessionMcpServerId,
|
|
10043
|
+
compactSessionEventResult as compactSessionEventResult2,
|
|
10044
|
+
sessionEventLatestClassToSemanticClass as sessionEventLatestClassToSemanticClass2,
|
|
8788
10045
|
SaveComposerDraftRequest,
|
|
8789
10046
|
SteerSessionQueueItemRequest,
|
|
8790
10047
|
SteerSessionMessageRequest,
|
|
8791
10048
|
TerminalExecRequest,
|
|
8792
10049
|
UpdateSessionPinRequest,
|
|
8793
10050
|
UpdateSessionGoalRequest,
|
|
10051
|
+
UpdateSessionMcpApprovalPolicyRequest,
|
|
8794
10052
|
UpdateSessionRequest,
|
|
8795
10053
|
ViewerHeartbeatRequest,
|
|
8796
10054
|
WORKSPACE_CONTROL_ACTOR_MAX_BYTES,
|
|
@@ -8846,7 +10104,7 @@ import {
|
|
|
8846
10104
|
coalesceSessionEventDeltas,
|
|
8847
10105
|
publishDurableSessionEvents
|
|
8848
10106
|
} from "@opengeni/events";
|
|
8849
|
-
import { z as
|
|
10107
|
+
import { z as z3 } from "zod";
|
|
8850
10108
|
|
|
8851
10109
|
// src/sandbox/channel-a.ts
|
|
8852
10110
|
import {
|
|
@@ -9081,7 +10339,7 @@ import {
|
|
|
9081
10339
|
} from "@opengeni/core";
|
|
9082
10340
|
|
|
9083
10341
|
// src/sandbox/viewer.ts
|
|
9084
|
-
import { createHash as
|
|
10342
|
+
import { createHash as createHash3 } from "crypto";
|
|
9085
10343
|
import {
|
|
9086
10344
|
applyGitAuthPointerEnvironment as applyGitAuthPointerEnvironment2,
|
|
9087
10345
|
hasGitCredentialRepositorySelection as hasGitCredentialRepositorySelection2,
|
|
@@ -9376,7 +10634,13 @@ async function mintDesktopStream(services, input) {
|
|
|
9376
10634
|
return null;
|
|
9377
10635
|
}
|
|
9378
10636
|
try {
|
|
9379
|
-
await ensureDisplayStack(established.session
|
|
10637
|
+
await ensureDisplayStack(established.session, {
|
|
10638
|
+
telemetryContext: {
|
|
10639
|
+
callerKind: "viewer",
|
|
10640
|
+
...lease.instanceId ? { sandboxId: lease.instanceId } : {},
|
|
10641
|
+
leaseEpoch: lease.leaseEpoch
|
|
10642
|
+
}
|
|
10643
|
+
});
|
|
9380
10644
|
} catch (error) {
|
|
9381
10645
|
if (error instanceof DisplayStackUnsupportedError) {
|
|
9382
10646
|
return null;
|
|
@@ -9634,7 +10898,7 @@ function viewerIdAsUuid(rawViewerId) {
|
|
|
9634
10898
|
if (UUID_RE.test(rawViewerId)) {
|
|
9635
10899
|
return rawViewerId;
|
|
9636
10900
|
}
|
|
9637
|
-
const hex =
|
|
10901
|
+
const hex = createHash3("sha256").update(`opengeni:stream-viewer:${rawViewerId}`).digest("hex");
|
|
9638
10902
|
const b = hex.slice(0, 32).split("");
|
|
9639
10903
|
b[12] = "5";
|
|
9640
10904
|
const variantNibble = parseInt(b[16], 16) & 3 | 8;
|
|
@@ -9655,8 +10919,12 @@ import {
|
|
|
9655
10919
|
readSessionLineage,
|
|
9656
10920
|
saveHumanComposerDraft,
|
|
9657
10921
|
steerHumanQueuePrompt,
|
|
10922
|
+
updateSessionMcpApprovalPolicy,
|
|
9658
10923
|
updateSessionTitle as updateSessionTitle2,
|
|
9659
|
-
workflowIdForSession
|
|
10924
|
+
workflowIdForSession,
|
|
10925
|
+
sessionWithEffectiveToolPolicy as sessionWithEffectiveToolPolicy2,
|
|
10926
|
+
workspaceSessionToolPolicyDefaultServerIds as workspaceSessionToolPolicyDefaultServerIds2,
|
|
10927
|
+
workspaceSessionToolPolicyServerIds as workspaceSessionToolPolicyServerIds2
|
|
9660
10928
|
} from "@opengeni/core";
|
|
9661
10929
|
|
|
9662
10930
|
// src/http/sse.ts
|
|
@@ -9679,7 +10947,7 @@ function createByteBoundedSseStream(options = {}) {
|
|
|
9679
10947
|
if (!Number.isSafeInteger(stallTimeoutMs) || stallTimeoutMs <= 0) {
|
|
9680
10948
|
throw new RangeError("SSE write stall timeout must be a positive safe integer");
|
|
9681
10949
|
}
|
|
9682
|
-
const
|
|
10950
|
+
const encoder2 = new TextEncoder();
|
|
9683
10951
|
let controller;
|
|
9684
10952
|
let stopped = false;
|
|
9685
10953
|
let capacityWake = null;
|
|
@@ -9725,7 +10993,7 @@ function createByteBoundedSseStream(options = {}) {
|
|
|
9725
10993
|
return {
|
|
9726
10994
|
stream,
|
|
9727
10995
|
write: async (frame) => {
|
|
9728
|
-
const chunk =
|
|
10996
|
+
const chunk = encoder2.encode(frame);
|
|
9729
10997
|
if (chunk.byteLength > maxQueuedBytes) {
|
|
9730
10998
|
const error = new RangeError(
|
|
9731
10999
|
`SSE frame cannot fit in the configured queue (${chunk.byteLength} > ${maxQueuedBytes} bytes)`
|
|
@@ -10335,7 +11603,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
10335
11603
|
const workspaceId = c.req.param("workspaceId");
|
|
10336
11604
|
const grant = await requireAccessGrant14(c, deps, workspaceId, "sessions:create");
|
|
10337
11605
|
const session = await createSessionForRequest2(deps, grant, workspaceId, await c.req.json());
|
|
10338
|
-
return c.json(session, 202);
|
|
11606
|
+
return c.json(await withEffectivePolicy(deps, workspaceId, session), 202);
|
|
10339
11607
|
});
|
|
10340
11608
|
app.get("/v1/workspaces/:workspaceId/sessions", async (c) => {
|
|
10341
11609
|
const workspaceId = c.req.param("workspaceId");
|
|
@@ -10368,16 +11636,26 @@ function registerSessionRoutes(app, deps) {
|
|
|
10368
11636
|
throw error;
|
|
10369
11637
|
}
|
|
10370
11638
|
c.header("x-opengeni-pinned-truncated", page.pinnedTruncated === true ? "true" : "false");
|
|
11639
|
+
const policy = await loadEffectivePolicyContext(deps, workspaceId);
|
|
11640
|
+
const decorate = (session) => sessionWithEffectiveToolPolicy2(
|
|
11641
|
+
session,
|
|
11642
|
+
policy.workspaceServerIds,
|
|
11643
|
+
policy.workspaceDefaultServerIds
|
|
11644
|
+
);
|
|
10371
11645
|
if (pageView) {
|
|
10372
|
-
return c.json(
|
|
11646
|
+
return c.json({
|
|
11647
|
+
...page,
|
|
11648
|
+
pinned: page.pinned.map(decorate),
|
|
11649
|
+
sessions: page.sessions.map(decorate)
|
|
11650
|
+
});
|
|
10373
11651
|
}
|
|
10374
|
-
return c.json([...page.pinned, ...page.sessions]);
|
|
11652
|
+
return c.json([...page.pinned, ...page.sessions].map(decorate));
|
|
10375
11653
|
});
|
|
10376
11654
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId", async (c) => {
|
|
10377
11655
|
const workspaceId = c.req.param("workspaceId");
|
|
10378
11656
|
const grant = await requireAccessGrant14(c, deps, workspaceId, "sessions:read");
|
|
10379
11657
|
const sessionId = c.req.param("sessionId");
|
|
10380
|
-
if (!
|
|
11658
|
+
if (!z3.string().uuid().safeParse(sessionId).success) {
|
|
10381
11659
|
throw new HTTPException21(404, { message: "session not found" });
|
|
10382
11660
|
}
|
|
10383
11661
|
const session = await getSessionForSubject(
|
|
@@ -10390,13 +11668,13 @@ function registerSessionRoutes(app, deps) {
|
|
|
10390
11668
|
if (!session) {
|
|
10391
11669
|
throw new HTTPException21(404, { message: "session not found" });
|
|
10392
11670
|
}
|
|
10393
|
-
return c.json(session);
|
|
11671
|
+
return c.json(await withEffectivePolicy(deps, workspaceId, session));
|
|
10394
11672
|
});
|
|
10395
11673
|
app.put("/v1/workspaces/:workspaceId/sessions/:sessionId/pin", async (c) => {
|
|
10396
11674
|
const workspaceId = c.req.param("workspaceId");
|
|
10397
11675
|
const grant = await requireAccessGrant14(c, deps, workspaceId, "sessions:read");
|
|
10398
11676
|
const sessionId = c.req.param("sessionId");
|
|
10399
|
-
if (!
|
|
11677
|
+
if (!z3.string().uuid().safeParse(sessionId).success) {
|
|
10400
11678
|
throw new HTTPException21(404, { message: "session not found" });
|
|
10401
11679
|
}
|
|
10402
11680
|
const parsed = UpdateSessionPinRequest.safeParse(await c.req.json().catch(() => null));
|
|
@@ -10413,7 +11691,13 @@ function registerSessionRoutes(app, deps) {
|
|
|
10413
11691
|
if (!session) {
|
|
10414
11692
|
throw new HTTPException21(404, { message: "session not found" });
|
|
10415
11693
|
}
|
|
10416
|
-
return c.json(
|
|
11694
|
+
return c.json(
|
|
11695
|
+
await withEffectivePolicy(
|
|
11696
|
+
deps,
|
|
11697
|
+
workspaceId,
|
|
11698
|
+
projectSessionForRelatedAccess2(session, relatedSessionAccessFor(c))
|
|
11699
|
+
)
|
|
11700
|
+
);
|
|
10417
11701
|
} catch (error) {
|
|
10418
11702
|
if (error instanceof SessionPinAccessError) {
|
|
10419
11703
|
throw new HTTPException21(403, { message: error.message });
|
|
@@ -10433,7 +11717,19 @@ function registerSessionRoutes(app, deps) {
|
|
|
10433
11717
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/lineage", async (c) => {
|
|
10434
11718
|
const workspaceId = c.req.param("workspaceId");
|
|
10435
11719
|
const grant = await requireAccessGrant14(c, deps, workspaceId, "sessions:read");
|
|
10436
|
-
|
|
11720
|
+
const lineage = await readSessionLineage(deps, grant, c.req.param("sessionId"));
|
|
11721
|
+
const policy = await loadEffectivePolicyContext(deps, workspaceId);
|
|
11722
|
+
return c.json({
|
|
11723
|
+
...lineage,
|
|
11724
|
+
ancestors: lineage.ancestors.map(
|
|
11725
|
+
(session) => sessionWithEffectiveToolPolicy2(
|
|
11726
|
+
session,
|
|
11727
|
+
policy.workspaceServerIds,
|
|
11728
|
+
policy.workspaceDefaultServerIds
|
|
11729
|
+
)
|
|
11730
|
+
),
|
|
11731
|
+
children: mapLineageNodes(lineage.children, policy)
|
|
11732
|
+
});
|
|
10437
11733
|
});
|
|
10438
11734
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/codex-account", async (c) => {
|
|
10439
11735
|
const workspaceId = c.req.param("workspaceId");
|
|
@@ -10498,8 +11794,33 @@ function registerSessionRoutes(app, deps) {
|
|
|
10498
11794
|
if (!session) {
|
|
10499
11795
|
throw new HTTPException21(404, { message: "session not found" });
|
|
10500
11796
|
}
|
|
10501
|
-
return c.json(session);
|
|
11797
|
+
return c.json(await withEffectivePolicy(deps, workspaceId, session));
|
|
10502
11798
|
});
|
|
11799
|
+
app.patch(
|
|
11800
|
+
"/v1/workspaces/:workspaceId/sessions/:sessionId/mcp-servers/:serverId/approval-policy",
|
|
11801
|
+
async (c) => {
|
|
11802
|
+
const workspaceId = c.req.param("workspaceId");
|
|
11803
|
+
const grant = await requireAccessGrant14(c, deps, workspaceId, "sessions:control");
|
|
11804
|
+
const sessionId = c.req.param("sessionId");
|
|
11805
|
+
const parsedServerId = SessionMcpServerId.safeParse(c.req.param("serverId"));
|
|
11806
|
+
const payload = UpdateSessionMcpApprovalPolicyRequest.safeParse(
|
|
11807
|
+
await c.req.json().catch(() => null)
|
|
11808
|
+
);
|
|
11809
|
+
if (!parsedServerId.success || !payload.success) {
|
|
11810
|
+
throw new HTTPException21(400, { message: "invalid MCP approval-policy request" });
|
|
11811
|
+
}
|
|
11812
|
+
await assertSessionExists(db, workspaceId, sessionId);
|
|
11813
|
+
return c.json(
|
|
11814
|
+
await updateSessionMcpApprovalPolicy(
|
|
11815
|
+
deps,
|
|
11816
|
+
grant,
|
|
11817
|
+
sessionId,
|
|
11818
|
+
parsedServerId.data,
|
|
11819
|
+
payload.data.requireApproval
|
|
11820
|
+
)
|
|
11821
|
+
);
|
|
11822
|
+
}
|
|
11823
|
+
);
|
|
10503
11824
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/goal", async (c) => {
|
|
10504
11825
|
const workspaceId = c.req.param("workspaceId");
|
|
10505
11826
|
await requireAccessGrant14(c, deps, workspaceId, "sessions:read");
|
|
@@ -10674,12 +11995,24 @@ function registerSessionRoutes(app, deps) {
|
|
|
10674
11995
|
"mode",
|
|
10675
11996
|
explicitReplay ? "forensic" : "monitoring"
|
|
10676
11997
|
);
|
|
10677
|
-
const
|
|
11998
|
+
const latestRequested = eventEnumValue(
|
|
10678
11999
|
c.req.query("latest"),
|
|
10679
|
-
|
|
12000
|
+
SessionEventLatestClass2,
|
|
10680
12001
|
"latest",
|
|
10681
12002
|
void 0
|
|
10682
12003
|
);
|
|
12004
|
+
const latestClass = latestRequested === void 0 ? void 0 : sessionEventLatestClassToSemanticClass2(latestRequested);
|
|
12005
|
+
const resultMode = eventEnumValue(
|
|
12006
|
+
c.req.query("resultMode") ?? c.req.query("result"),
|
|
12007
|
+
SessionEventResultMode2,
|
|
12008
|
+
"resultMode",
|
|
12009
|
+
"events"
|
|
12010
|
+
);
|
|
12011
|
+
if (resultMode === "compact" && latestClass === void 0) {
|
|
12012
|
+
throw new HTTPException21(400, {
|
|
12013
|
+
message: "resultMode=compact requires latest"
|
|
12014
|
+
});
|
|
12015
|
+
}
|
|
10683
12016
|
if (latestClass && ["includeTypes", "excludeTypes", "includeClasses", "excludeClasses"].some(
|
|
10684
12017
|
(name) => c.req.query(name) !== void 0
|
|
10685
12018
|
)) {
|
|
@@ -10724,19 +12057,39 @@ function registerSessionRoutes(app, deps) {
|
|
|
10724
12057
|
compact ? 5e3 : mode === "monitoring" ? 250 : 2e3,
|
|
10725
12058
|
mode === "monitoring" ? 40 : 500
|
|
10726
12059
|
);
|
|
12060
|
+
const dbPayloadMode = resultMode === "compact" ? "full" : payloadMode;
|
|
10727
12061
|
const dbPage = await listSessionEventPage2(db, workspaceId, sessionId, {
|
|
10728
12062
|
after,
|
|
10729
12063
|
...before !== void 0 ? { before } : {},
|
|
10730
12064
|
limit,
|
|
10731
12065
|
direction,
|
|
10732
|
-
payloadMode,
|
|
12066
|
+
payloadMode: dbPayloadMode,
|
|
10733
12067
|
includeTypes,
|
|
10734
12068
|
excludeTypes,
|
|
10735
12069
|
includeClasses: latestClass ? [latestClass] : includeClasses,
|
|
10736
12070
|
excludeClasses,
|
|
10737
|
-
...mode === "monitoring" ? { defaultExcludeTypes: SESSION_EVENT_RAW_DELTA_TYPES2 } : {}
|
|
12071
|
+
...mode === "monitoring" ? { defaultExcludeTypes: SESSION_EVENT_RAW_DELTA_TYPES2 } : {},
|
|
12072
|
+
...latestClass ? { authoritativeLatest: true } : {}
|
|
10738
12073
|
});
|
|
10739
12074
|
const events = dbPage.events;
|
|
12075
|
+
if (resultMode === "compact") {
|
|
12076
|
+
const event = events[0];
|
|
12077
|
+
c.header("X-OpenGeni-Event-Result-Mode", "compact");
|
|
12078
|
+
c.header("X-OpenGeni-Event-Result", event ? "found" : "not_found");
|
|
12079
|
+
c.header("X-OpenGeni-Event-Mode", mode);
|
|
12080
|
+
c.header("X-OpenGeni-Event-Direction", direction);
|
|
12081
|
+
c.header("X-OpenGeni-Payload-Mode", "full");
|
|
12082
|
+
c.header("X-OpenGeni-Forensic-Exact", "false");
|
|
12083
|
+
if (!event) return c.json(null, 200);
|
|
12084
|
+
const result = compactSessionEventResult2(
|
|
12085
|
+
event,
|
|
12086
|
+
latestClass,
|
|
12087
|
+
dbPage.coveredSequence ?? { first: event.sequence, last: event.sequence }
|
|
12088
|
+
);
|
|
12089
|
+
c.header("X-OpenGeni-Covered-First", String(result.coveredSequence.first));
|
|
12090
|
+
c.header("X-OpenGeni-Covered-Last", String(result.coveredSequence.last));
|
|
12091
|
+
return c.json(result);
|
|
12092
|
+
}
|
|
10740
12093
|
const projected = compact ? coalesceSessionEventDeltas(events) : events;
|
|
10741
12094
|
const page = boundSessionEventHttpPage(projected, {
|
|
10742
12095
|
direction
|
|
@@ -11843,6 +13196,9 @@ function sessionAuthorizationOperationForHttp(method, pathname, sessionId) {
|
|
|
11843
13196
|
return null;
|
|
11844
13197
|
}
|
|
11845
13198
|
if (suffix === "/pin" && verb === "PUT") return "session.pin.write";
|
|
13199
|
+
if (/^\/mcp-servers\/[^/]+\/approval-policy$/.test(suffix) && verb === "PATCH") {
|
|
13200
|
+
return "session.mcp.approval_policy.write";
|
|
13201
|
+
}
|
|
11846
13202
|
if (suffix === "/lineage" && verb === "GET") return "session.lineage.read";
|
|
11847
13203
|
if (suffix === "/codex-account" && verb === "POST") {
|
|
11848
13204
|
return "session.codex_account.write";
|
|
@@ -11934,7 +13290,7 @@ function eventEnumList(raw, schema, name) {
|
|
|
11934
13290
|
}
|
|
11935
13291
|
function sessionListQuery(query, allowCursor = true) {
|
|
11936
13292
|
const parentSessionId = query.parentSessionId;
|
|
11937
|
-
if (parentSessionId !== void 0 && parentSessionId !== "null" && !
|
|
13293
|
+
if (parentSessionId !== void 0 && parentSessionId !== "null" && !z3.string().uuid().safeParse(parentSessionId).success) {
|
|
11938
13294
|
throw new HTTPException21(400, {
|
|
11939
13295
|
message: 'parentSessionId must be a session id or the literal "null"'
|
|
11940
13296
|
});
|
|
@@ -12001,6 +13357,32 @@ function commandConflictResponse(c, error) {
|
|
|
12001
13357
|
}
|
|
12002
13358
|
throw error;
|
|
12003
13359
|
}
|
|
13360
|
+
async function loadEffectivePolicyContext(deps, workspaceId) {
|
|
13361
|
+
const [workspaceServerIds, workspaceDefaultServerIds] = await Promise.all([
|
|
13362
|
+
workspaceSessionToolPolicyServerIds2(deps.db, workspaceId, deps.settings),
|
|
13363
|
+
workspaceSessionToolPolicyDefaultServerIds2(deps.db, workspaceId, deps.settings)
|
|
13364
|
+
]);
|
|
13365
|
+
return { workspaceServerIds, workspaceDefaultServerIds };
|
|
13366
|
+
}
|
|
13367
|
+
async function withEffectivePolicy(deps, workspaceId, session) {
|
|
13368
|
+
const policy = await loadEffectivePolicyContext(deps, workspaceId);
|
|
13369
|
+
return sessionWithEffectiveToolPolicy2(
|
|
13370
|
+
session,
|
|
13371
|
+
policy.workspaceServerIds,
|
|
13372
|
+
policy.workspaceDefaultServerIds
|
|
13373
|
+
);
|
|
13374
|
+
}
|
|
13375
|
+
function mapLineageNodes(nodes, policy) {
|
|
13376
|
+
return nodes.map((node) => ({
|
|
13377
|
+
...node,
|
|
13378
|
+
session: sessionWithEffectiveToolPolicy2(
|
|
13379
|
+
node.session,
|
|
13380
|
+
policy.workspaceServerIds,
|
|
13381
|
+
policy.workspaceDefaultServerIds
|
|
13382
|
+
),
|
|
13383
|
+
children: mapLineageNodes(node.children, policy)
|
|
13384
|
+
}));
|
|
13385
|
+
}
|
|
12004
13386
|
|
|
12005
13387
|
// src/routes/social.ts
|
|
12006
13388
|
import { CreateSocialConnectionRequest, CreateSocialPostRequest } from "@opengeni/contracts";
|
|
@@ -12011,7 +13393,7 @@ import {
|
|
|
12011
13393
|
listSocialPosts as listSocialPosts2
|
|
12012
13394
|
} from "@opengeni/db";
|
|
12013
13395
|
import { HTTPException as HTTPException22 } from "hono/http-exception";
|
|
12014
|
-
import { z as
|
|
13396
|
+
import { z as z5 } from "zod";
|
|
12015
13397
|
import { requireAccessGrant as requireAccessGrant15 } from "@opengeni/core";
|
|
12016
13398
|
function registerSocialRoutes(app, deps) {
|
|
12017
13399
|
const { db } = deps;
|
|
@@ -12101,7 +13483,7 @@ function parseConnectionIds(raw) {
|
|
|
12101
13483
|
return void 0;
|
|
12102
13484
|
}
|
|
12103
13485
|
const values = raw.split(",").map((value) => value.trim()).filter(Boolean);
|
|
12104
|
-
const parsed =
|
|
13486
|
+
const parsed = z5.array(z5.string().uuid()).safeParse(values);
|
|
12105
13487
|
if (!parsed.success) {
|
|
12106
13488
|
throw new HTTPException22(422, {
|
|
12107
13489
|
message: "connectionIds must be a comma-separated list of UUIDs"
|
|
@@ -12132,6 +13514,7 @@ import {
|
|
|
12132
13514
|
UpdateWorkspaceRequest,
|
|
12133
13515
|
UpdateWorkspaceSettingsRequest,
|
|
12134
13516
|
WORKSPACE_CONTROL_ACTOR_MAX_BYTES as WORKSPACE_CONTROL_ACTOR_MAX_BYTES2,
|
|
13517
|
+
WorkspaceModelCatalogResponse as WorkspaceModelCatalogResponse2,
|
|
12135
13518
|
WorkspaceInferenceControlRequest,
|
|
12136
13519
|
Workspace,
|
|
12137
13520
|
WorkspaceMember,
|
|
@@ -12156,11 +13539,12 @@ import {
|
|
|
12156
13539
|
setWorkspaceDefaultRig,
|
|
12157
13540
|
updateWorkspace,
|
|
12158
13541
|
updateWorkspaceSettings,
|
|
12159
|
-
upsertWorkspaceModelPolicy
|
|
13542
|
+
upsertWorkspaceModelPolicy,
|
|
13543
|
+
workspaceCodexSubscriptionActive
|
|
12160
13544
|
} from "@opengeni/db";
|
|
12161
13545
|
import { boundWorkspaceControlHttpPage } from "@opengeni/events";
|
|
12162
13546
|
import { HTTPException as HTTPException23 } from "hono/http-exception";
|
|
12163
|
-
import { hasPermission as
|
|
13547
|
+
import { hasPermission as hasPermission4, requireAccessContext as requireAccessContext2, requireAccessGrant as requireAccessGrant16 } from "@opengeni/core";
|
|
12164
13548
|
import { requireLimit as requireLimit7 } from "@opengeni/core";
|
|
12165
13549
|
import {
|
|
12166
13550
|
assertWorkspaceDeletable,
|
|
@@ -12168,6 +13552,237 @@ import {
|
|
|
12168
13552
|
controlHumanWorkspace,
|
|
12169
13553
|
resolveMemberSubjectId
|
|
12170
13554
|
} from "@opengeni/core";
|
|
13555
|
+
|
|
13556
|
+
// src/model-catalog.ts
|
|
13557
|
+
import {
|
|
13558
|
+
configuredModels,
|
|
13559
|
+
configuredProviders,
|
|
13560
|
+
withCodexCatalogProvider
|
|
13561
|
+
} from "@opengeni/config";
|
|
13562
|
+
import {
|
|
13563
|
+
ClientModel,
|
|
13564
|
+
WorkspaceModelCatalogResponse,
|
|
13565
|
+
evaluateWorkspaceModelPolicy
|
|
13566
|
+
} from "@opengeni/contracts";
|
|
13567
|
+
var MODEL_CREDENTIAL_READINESS_OBSERVATION_MAX_AGE_MS = 5 * 6e4;
|
|
13568
|
+
function projectClientModel(model) {
|
|
13569
|
+
return ClientModel.parse({
|
|
13570
|
+
id: model.id,
|
|
13571
|
+
label: model.label,
|
|
13572
|
+
provider: model.providerId,
|
|
13573
|
+
providerLabel: model.providerLabel,
|
|
13574
|
+
api: model.api,
|
|
13575
|
+
...model.contextWindowTokens === void 0 ? {} : { contextWindowTokens: model.contextWindowTokens },
|
|
13576
|
+
schemaVersion: model.schemaVersion,
|
|
13577
|
+
aliases: model.aliases,
|
|
13578
|
+
deployment: model.deployment,
|
|
13579
|
+
executionLimits: model.executionLimits,
|
|
13580
|
+
credentialSource: model.credentialSource,
|
|
13581
|
+
billing: model.billing,
|
|
13582
|
+
capabilities: model.capabilities,
|
|
13583
|
+
...model.pricing === void 0 ? {} : { pricing: model.pricing },
|
|
13584
|
+
definitionVersion: model.definitionVersion
|
|
13585
|
+
});
|
|
13586
|
+
}
|
|
13587
|
+
function modelDefinitionRunnable(model) {
|
|
13588
|
+
return model.capabilities.inputModalities.includes("text") && model.capabilities.outputModalities.includes("text") && model.capabilities.transports.sse.runnable;
|
|
13589
|
+
}
|
|
13590
|
+
function observedCredentialReadiness(input) {
|
|
13591
|
+
if (!input.observation) {
|
|
13592
|
+
return {
|
|
13593
|
+
status: "not_ready",
|
|
13594
|
+
reason: "prerequisites_missing",
|
|
13595
|
+
basis: input.basis,
|
|
13596
|
+
checkedAt: null
|
|
13597
|
+
};
|
|
13598
|
+
}
|
|
13599
|
+
const checkedAtMs = Date.parse(input.observation.checkedAt);
|
|
13600
|
+
if (!Number.isFinite(checkedAtMs)) {
|
|
13601
|
+
return {
|
|
13602
|
+
status: "error",
|
|
13603
|
+
reason: "resolver_error",
|
|
13604
|
+
basis: input.basis,
|
|
13605
|
+
checkedAt: null
|
|
13606
|
+
};
|
|
13607
|
+
}
|
|
13608
|
+
const checkedAt = new Date(checkedAtMs).toISOString();
|
|
13609
|
+
if (Math.abs(input.nowMs - checkedAtMs) > input.maxAgeMs) {
|
|
13610
|
+
return {
|
|
13611
|
+
status: "not_ready",
|
|
13612
|
+
reason: "observation_stale",
|
|
13613
|
+
basis: input.basis,
|
|
13614
|
+
checkedAt
|
|
13615
|
+
};
|
|
13616
|
+
}
|
|
13617
|
+
if (input.observation.status === "ready") {
|
|
13618
|
+
return { status: "ready", reason: null, basis: input.basis, checkedAt };
|
|
13619
|
+
}
|
|
13620
|
+
if (input.observation.status === "not_ready") {
|
|
13621
|
+
return {
|
|
13622
|
+
status: "not_ready",
|
|
13623
|
+
reason: input.observation.reason === "needs_reauth" ? "needs_reauth" : "prerequisites_missing",
|
|
13624
|
+
basis: input.basis,
|
|
13625
|
+
checkedAt
|
|
13626
|
+
};
|
|
13627
|
+
}
|
|
13628
|
+
return {
|
|
13629
|
+
status: "error",
|
|
13630
|
+
reason: "resolver_error",
|
|
13631
|
+
basis: input.basis,
|
|
13632
|
+
checkedAt
|
|
13633
|
+
};
|
|
13634
|
+
}
|
|
13635
|
+
function credentialReadinessFor(input) {
|
|
13636
|
+
const source = input.model.credentialSource;
|
|
13637
|
+
if (source.kind === "connected_subscription") {
|
|
13638
|
+
return input.codexSubscriptionActive ? { status: "ready", reason: null, basis: "connection", checkedAt: null } : {
|
|
13639
|
+
status: "not_ready",
|
|
13640
|
+
reason: "needs_reauth",
|
|
13641
|
+
basis: "connection",
|
|
13642
|
+
checkedAt: null
|
|
13643
|
+
};
|
|
13644
|
+
}
|
|
13645
|
+
if (source.kind === "deployment" && source.mechanism === "api_key") {
|
|
13646
|
+
return input.provider?.apiKey ? { status: "ready", reason: null, basis: "configuration", checkedAt: null } : {
|
|
13647
|
+
status: "not_ready",
|
|
13648
|
+
reason: "missing_credential",
|
|
13649
|
+
basis: "configuration",
|
|
13650
|
+
checkedAt: null
|
|
13651
|
+
};
|
|
13652
|
+
}
|
|
13653
|
+
return observedCredentialReadiness({
|
|
13654
|
+
observation: input.observation,
|
|
13655
|
+
basis: source.kind === "workspace_connection" ? "connection" : "resolver",
|
|
13656
|
+
nowMs: input.nowMs,
|
|
13657
|
+
maxAgeMs: input.maxAgeMs
|
|
13658
|
+
});
|
|
13659
|
+
}
|
|
13660
|
+
function isXaiGrokModel(model) {
|
|
13661
|
+
return model.providerId === "xai" && model.id.startsWith("xai/grok-");
|
|
13662
|
+
}
|
|
13663
|
+
function observationTimestamp(observation) {
|
|
13664
|
+
if (!observation || typeof observation.checkedAt !== "string") {
|
|
13665
|
+
return { checkedAt: null, checkedAtMs: null };
|
|
13666
|
+
}
|
|
13667
|
+
const checkedAtMs = Date.parse(observation.checkedAt);
|
|
13668
|
+
if (!Number.isFinite(checkedAtMs)) {
|
|
13669
|
+
return { checkedAt: null, checkedAtMs: null };
|
|
13670
|
+
}
|
|
13671
|
+
return { checkedAt: new Date(checkedAtMs).toISOString(), checkedAtMs };
|
|
13672
|
+
}
|
|
13673
|
+
function xaiGrokAvailabilityFor(input) {
|
|
13674
|
+
const { checkedAt, checkedAtMs } = observationTimestamp(input.observation);
|
|
13675
|
+
const freshSuccessfulObservation = input.observation?.status === "available" && input.observation.reason === null && checkedAtMs !== null && checkedAtMs <= input.nowMs && input.nowMs - checkedAtMs <= input.maxAgeMs;
|
|
13676
|
+
if (freshSuccessfulObservation) {
|
|
13677
|
+
return {
|
|
13678
|
+
status: "available",
|
|
13679
|
+
selectable: true,
|
|
13680
|
+
reason: null,
|
|
13681
|
+
checkedAt
|
|
13682
|
+
};
|
|
13683
|
+
}
|
|
13684
|
+
return {
|
|
13685
|
+
status: "unavailable",
|
|
13686
|
+
selectable: false,
|
|
13687
|
+
reason: input.observation?.status === "unavailable" ? input.observation.reason ?? "provider_unhealthy" : "provider_unhealthy",
|
|
13688
|
+
checkedAt
|
|
13689
|
+
};
|
|
13690
|
+
}
|
|
13691
|
+
function availabilityFor(input) {
|
|
13692
|
+
if (!modelDefinitionRunnable(input.model)) {
|
|
13693
|
+
return {
|
|
13694
|
+
status: "unavailable",
|
|
13695
|
+
selectable: false,
|
|
13696
|
+
reason: "unsupported",
|
|
13697
|
+
checkedAt: null
|
|
13698
|
+
};
|
|
13699
|
+
}
|
|
13700
|
+
if (input.credentialReadiness.status !== "ready") {
|
|
13701
|
+
return {
|
|
13702
|
+
status: "unavailable",
|
|
13703
|
+
selectable: false,
|
|
13704
|
+
reason: input.credentialReadiness.reason === "missing_credential" ? "missing_credential" : input.credentialReadiness.reason === "needs_reauth" ? "needs_reauth" : "credential_not_ready",
|
|
13705
|
+
checkedAt: input.credentialReadiness.checkedAt
|
|
13706
|
+
};
|
|
13707
|
+
}
|
|
13708
|
+
if (!evaluateWorkspaceModelPolicy(input.policy, {
|
|
13709
|
+
providerId: input.model.providerId,
|
|
13710
|
+
modelId: input.model.id
|
|
13711
|
+
}).allowed) {
|
|
13712
|
+
return {
|
|
13713
|
+
status: "unavailable",
|
|
13714
|
+
selectable: false,
|
|
13715
|
+
reason: "policy_blocked",
|
|
13716
|
+
checkedAt: null
|
|
13717
|
+
};
|
|
13718
|
+
}
|
|
13719
|
+
if (isXaiGrokModel(input.model)) {
|
|
13720
|
+
return xaiGrokAvailabilityFor({
|
|
13721
|
+
observation: input.observation,
|
|
13722
|
+
nowMs: input.nowMs,
|
|
13723
|
+
maxAgeMs: input.maxAgeMs
|
|
13724
|
+
});
|
|
13725
|
+
}
|
|
13726
|
+
if (!input.observation) {
|
|
13727
|
+
return { status: "unknown", selectable: true, reason: null, checkedAt: null };
|
|
13728
|
+
}
|
|
13729
|
+
if (input.observation.status === "unavailable") {
|
|
13730
|
+
return {
|
|
13731
|
+
status: "unavailable",
|
|
13732
|
+
selectable: false,
|
|
13733
|
+
reason: input.observation.reason ?? "provider_unhealthy",
|
|
13734
|
+
checkedAt: input.observation.checkedAt
|
|
13735
|
+
};
|
|
13736
|
+
}
|
|
13737
|
+
return {
|
|
13738
|
+
status: input.observation.status,
|
|
13739
|
+
selectable: true,
|
|
13740
|
+
reason: null,
|
|
13741
|
+
checkedAt: input.observation.checkedAt
|
|
13742
|
+
};
|
|
13743
|
+
}
|
|
13744
|
+
function buildWorkspaceModelCatalog(input) {
|
|
13745
|
+
const catalogSettings = input.settings.codexSubscriptionEnabled ? withCodexCatalogProvider(input.settings) : input.settings;
|
|
13746
|
+
const providers = new Map(
|
|
13747
|
+
configuredProviders(catalogSettings).map((provider) => [provider.id, provider])
|
|
13748
|
+
);
|
|
13749
|
+
const requestedNowMs = input.now?.getTime();
|
|
13750
|
+
const nowMs = typeof requestedNowMs === "number" && Number.isFinite(requestedNowMs) ? requestedNowMs : Date.now();
|
|
13751
|
+
const maxAgeMs = typeof input.credentialReadinessMaxAgeMs === "number" && Number.isFinite(input.credentialReadinessMaxAgeMs) && input.credentialReadinessMaxAgeMs >= 0 ? input.credentialReadinessMaxAgeMs : MODEL_CREDENTIAL_READINESS_OBSERVATION_MAX_AGE_MS;
|
|
13752
|
+
const models = configuredModels(catalogSettings).map((model) => {
|
|
13753
|
+
const provider = providers.get(model.providerId);
|
|
13754
|
+
const credentialReadiness = credentialReadinessFor({
|
|
13755
|
+
model,
|
|
13756
|
+
provider,
|
|
13757
|
+
codexSubscriptionActive: input.codexSubscriptionActive,
|
|
13758
|
+
observation: input.credentialReadinessObservations?.[model.definitionVersion],
|
|
13759
|
+
nowMs,
|
|
13760
|
+
maxAgeMs
|
|
13761
|
+
});
|
|
13762
|
+
return {
|
|
13763
|
+
...projectClientModel(model),
|
|
13764
|
+
credentialReadiness,
|
|
13765
|
+
availability: availabilityFor({
|
|
13766
|
+
model,
|
|
13767
|
+
credentialReadiness,
|
|
13768
|
+
policy: input.policy,
|
|
13769
|
+
observation: input.observations?.[model.definitionVersion],
|
|
13770
|
+
nowMs,
|
|
13771
|
+
maxAgeMs
|
|
13772
|
+
})
|
|
13773
|
+
};
|
|
13774
|
+
});
|
|
13775
|
+
return WorkspaceModelCatalogResponse.parse({ models });
|
|
13776
|
+
}
|
|
13777
|
+
|
|
13778
|
+
// src/routes/workspaces.ts
|
|
13779
|
+
import { canonicalizeConfiguredModelId } from "@opengeni/config";
|
|
13780
|
+
function canonicalWorkspacePolicyModelIds(settings, modelIds) {
|
|
13781
|
+
if (modelIds === null || modelIds === void 0) {
|
|
13782
|
+
return null;
|
|
13783
|
+
}
|
|
13784
|
+
return [...new Set(modelIds.map((modelId) => canonicalizeConfiguredModelId(settings, modelId)))];
|
|
13785
|
+
}
|
|
12171
13786
|
function registerWorkspaceRoutes(app, deps) {
|
|
12172
13787
|
app.get("/v1/access/me", async (c) => {
|
|
12173
13788
|
return c.json(await requireAccessContext2(c, deps));
|
|
@@ -12176,7 +13791,7 @@ function registerWorkspaceRoutes(app, deps) {
|
|
|
12176
13791
|
const context = await requireAccessContext2(c, deps);
|
|
12177
13792
|
const readableWorkspaceIds = [
|
|
12178
13793
|
...new Set(
|
|
12179
|
-
context.workspaceGrants.filter((grant) =>
|
|
13794
|
+
context.workspaceGrants.filter((grant) => hasPermission4(grant.permissions, "workspace:read")).map((grant) => grant.workspaceId)
|
|
12180
13795
|
)
|
|
12181
13796
|
];
|
|
12182
13797
|
if (readableWorkspaceIds.length > 0) {
|
|
@@ -12244,6 +13859,24 @@ function registerWorkspaceRoutes(app, deps) {
|
|
|
12244
13859
|
const workspace = await updateWorkspaceSettings(deps.db, workspaceId, parsed.data);
|
|
12245
13860
|
return c.json(Workspace.parse(workspace));
|
|
12246
13861
|
});
|
|
13862
|
+
app.get("/v1/workspaces/:workspaceId/model-catalog", async (c) => {
|
|
13863
|
+
const workspaceId = c.req.param("workspaceId");
|
|
13864
|
+
await requireAccessGrant16(c, deps, workspaceId, "workspace:read");
|
|
13865
|
+
const [policy, codexSubscriptionActive] = await Promise.all([
|
|
13866
|
+
getWorkspaceModelPolicy(deps.db, workspaceId),
|
|
13867
|
+
workspaceCodexSubscriptionActive(deps.db, deps.settings, workspaceId)
|
|
13868
|
+
]);
|
|
13869
|
+
c.header("cache-control", "private, no-store");
|
|
13870
|
+
return c.json(
|
|
13871
|
+
WorkspaceModelCatalogResponse2.parse(
|
|
13872
|
+
buildWorkspaceModelCatalog({
|
|
13873
|
+
settings: deps.settings,
|
|
13874
|
+
policy,
|
|
13875
|
+
codexSubscriptionActive
|
|
13876
|
+
})
|
|
13877
|
+
)
|
|
13878
|
+
);
|
|
13879
|
+
});
|
|
12247
13880
|
app.get("/v1/workspaces/:workspaceId/model-policy", async (c) => {
|
|
12248
13881
|
const workspaceId = c.req.param("workspaceId");
|
|
12249
13882
|
await requireAccessGrant16(c, deps, workspaceId, "workspace:read");
|
|
@@ -12261,7 +13894,7 @@ function registerWorkspaceRoutes(app, deps) {
|
|
|
12261
13894
|
accountId: grant.accountId,
|
|
12262
13895
|
workspaceId,
|
|
12263
13896
|
allowedProviders: payload.allowedProviders ?? null,
|
|
12264
|
-
allowedModels: payload.allowedModels
|
|
13897
|
+
allowedModels: canonicalWorkspacePolicyModelIds(deps.settings, payload.allowedModels)
|
|
12265
13898
|
});
|
|
12266
13899
|
return c.json(policy);
|
|
12267
13900
|
});
|
|
@@ -12619,20 +14252,13 @@ function createApp(deps) {
|
|
|
12619
14252
|
deploymentRevision: deps.settings.deploymentRevision,
|
|
12620
14253
|
apiContractRevision: OPENGENI_API_CONTRACT_REVISION,
|
|
12621
14254
|
...deps.settings.serverVersion ? { serverVersion: deps.settings.serverVersion } : {},
|
|
12622
|
-
defaultModel: deps.settings.openaiModel,
|
|
14255
|
+
defaultModel: canonicalizeConfiguredModelId2(deps.settings, deps.settings.openaiModel),
|
|
12623
14256
|
allowedModels: configuredAllowedModels(deps.settings),
|
|
12624
14257
|
// Provider-grouped model list for the picker. configuredModels() carries the
|
|
12625
14258
|
// union of the built-in allow-list and every registry provider's models, in
|
|
12626
14259
|
// selection order (default model first); project each to the client-safe
|
|
12627
14260
|
// ClientModel shape (ConfiguredModel.providerId → ClientModel.provider).
|
|
12628
|
-
models:
|
|
12629
|
-
id: model.id,
|
|
12630
|
-
label: model.label,
|
|
12631
|
-
provider: model.providerId,
|
|
12632
|
-
providerLabel: model.providerLabel,
|
|
12633
|
-
api: model.api,
|
|
12634
|
-
...model.contextWindowTokens === void 0 ? {} : { contextWindowTokens: model.contextWindowTokens }
|
|
12635
|
-
})),
|
|
14261
|
+
models: configuredModels2(deps.settings).map(projectClientModel),
|
|
12636
14262
|
defaultReasoningEffort: deps.settings.openaiReasoningEffort,
|
|
12637
14263
|
allowedReasoningEfforts: configuredAllowedReasoningEfforts(deps.settings),
|
|
12638
14264
|
mcpServers: deps.settings.mcpServers.map((server) => ({
|
|
@@ -12654,6 +14280,15 @@ function createApp(deps) {
|
|
|
12654
14280
|
});
|
|
12655
14281
|
app.all("/v1/workspaces/:workspaceId/mcp", async (c) => {
|
|
12656
14282
|
const workspaceId = c.req.param("workspaceId");
|
|
14283
|
+
let boundedRequest;
|
|
14284
|
+
try {
|
|
14285
|
+
boundedRequest = await boundedMcpRequest(c.req.raw);
|
|
14286
|
+
} catch (error) {
|
|
14287
|
+
if (error instanceof McpPayloadTooLargeError2) {
|
|
14288
|
+
throw new HTTPException24(413, { message: "MCP request body exceeds the safety limit" });
|
|
14289
|
+
}
|
|
14290
|
+
throw error;
|
|
14291
|
+
}
|
|
12657
14292
|
const grant = await requireMcpAccessGrant(c, routeDeps, workspaceId);
|
|
12658
14293
|
const toolspaceGrant = isToolspaceGrant(routeDeps.settings, grant);
|
|
12659
14294
|
const boundSessionId = grant.metadata?.sessionId;
|
|
@@ -12677,7 +14312,17 @@ function createApp(deps) {
|
|
|
12677
14312
|
throw error;
|
|
12678
14313
|
}
|
|
12679
14314
|
}
|
|
12680
|
-
|
|
14315
|
+
let toolspace = null;
|
|
14316
|
+
if (toolspaceGrant) {
|
|
14317
|
+
try {
|
|
14318
|
+
toolspace = await prepareToolspaceMcpSurface({ deps: routeDeps, grant });
|
|
14319
|
+
} catch (error) {
|
|
14320
|
+
if (error instanceof McpPayloadTooLargeError2) {
|
|
14321
|
+
throw new HTTPException24(413, { message: "MCP tool list exceeds the safety limit" });
|
|
14322
|
+
}
|
|
14323
|
+
throw error;
|
|
14324
|
+
}
|
|
14325
|
+
}
|
|
12681
14326
|
const workspace = await getWorkspace2(routeDeps.db, workspaceId);
|
|
12682
14327
|
const workspaceMemoryEnabled = resolveWorkspaceMemoryEnabled(workspace?.settings);
|
|
12683
14328
|
const transport = new WebStandardStreamableHTTPServerTransport2({
|
|
@@ -12690,7 +14335,7 @@ function createApp(deps) {
|
|
|
12690
14335
|
});
|
|
12691
14336
|
try {
|
|
12692
14337
|
await mcp.connect(transport);
|
|
12693
|
-
return await transport.handleRequest(
|
|
14338
|
+
return await transport.handleRequest(boundedRequest);
|
|
12694
14339
|
} finally {
|
|
12695
14340
|
await toolspace?.close().catch(() => void 0);
|
|
12696
14341
|
}
|
|
@@ -12718,7 +14363,7 @@ function createApp(deps) {
|
|
|
12718
14363
|
}
|
|
12719
14364
|
async function requireMcpAccessGrant(c, deps, workspaceId) {
|
|
12720
14365
|
const grant = await requireAccessGrant17(c, deps, workspaceId);
|
|
12721
|
-
if (
|
|
14366
|
+
if (hasPermission5(grant.permissions, "workspace:read")) {
|
|
12722
14367
|
return grant;
|
|
12723
14368
|
}
|
|
12724
14369
|
if (isToolspaceGrant(deps.settings, grant)) {
|
|
@@ -12757,6 +14402,9 @@ function httpStatusForError(error) {
|
|
|
12757
14402
|
if (error instanceof HTTPException24) {
|
|
12758
14403
|
return error.status;
|
|
12759
14404
|
}
|
|
14405
|
+
if (error instanceof McpPayloadTooLargeError2) {
|
|
14406
|
+
return 413;
|
|
14407
|
+
}
|
|
12760
14408
|
return 500;
|
|
12761
14409
|
}
|
|
12762
14410
|
function readinessChecks(deps) {
|
|
@@ -12946,6 +14594,14 @@ var routeLabelPatterns = [
|
|
|
12946
14594
|
pattern: /^\/v1\/workspaces\/[^/]+\/files\/[^/]+$/,
|
|
12947
14595
|
label: "/v1/workspaces/:workspaceId/files/:id"
|
|
12948
14596
|
},
|
|
14597
|
+
{
|
|
14598
|
+
pattern: /^\/v1\/workspaces\/[^/]+\/artifacts\/[^/]+\/content$/,
|
|
14599
|
+
label: "/v1/workspaces/:workspaceId/artifacts/:id/content"
|
|
14600
|
+
},
|
|
14601
|
+
{
|
|
14602
|
+
pattern: /^\/v1\/workspaces\/[^/]+\/artifacts\/[^/]+$/,
|
|
14603
|
+
label: "/v1/workspaces/:workspaceId/artifacts/:id"
|
|
14604
|
+
},
|
|
12949
14605
|
{
|
|
12950
14606
|
pattern: /^\/v1\/workspaces\/[^/]+\/api-keys$/,
|
|
12951
14607
|
label: "/v1/workspaces/:workspaceId/api-keys"
|
|
@@ -13202,4 +14858,4 @@ export {
|
|
|
13202
14858
|
withDefaultEnabledCapabilityMcpTools,
|
|
13203
14859
|
workflowIdForSession2 as workflowIdForSession
|
|
13204
14860
|
};
|
|
13205
|
-
//# sourceMappingURL=chunk-
|
|
14861
|
+
//# sourceMappingURL=chunk-QOYQBYHM.js.map
|