@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.
@@ -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 hasPermission4,
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 { measureSessionEventJson } from "@opengeni/contracts";
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(boundSessionDetailMcp(projected));
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 event in exactly one semantic class; it cannot be combined with type or class filters. 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.",
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(SessionEventSemanticClass.options).optional()
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 = latest ? "before" : requestedDirection ?? (before !== void 0 ? "before" : after !== void 0 ? "after" : "before");
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: latest ? 1 : boundedSessionEventMcpLimit(limit),
2118
+ limit: latestClass ? 1 : boundedSessionEventMcpLimit(limit),
2039
2119
  payloadMode,
2040
2120
  includeTypes: includeTypes ?? [],
2041
2121
  excludeTypes: excludeTypes ?? [],
2042
- includeClasses: latest ? [latest] : 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
- return json(await createSessionForRequest(deps, grant, grant.workspaceId, args));
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
- reserveToolspaceCallForTurn
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 toolListCache = /* @__PURE__ */ new Map();
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") && typeof grant.metadata?.sessionId === "string";
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 sessionId = grant.metadata.sessionId;
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
- let registryPromise = null;
2977
- const getRegistry = () => registryPromise ??= buildToolspaceRegistry(deps, grant.workspaceId, sessionId);
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
- activeTurnId: session.activeTurnId ?? null,
2985
- getRegistry
3189
+ activeTurn,
3190
+ getRegistry: () => getRegistry(attemptAuthority.attemptId)
2986
3191
  });
2987
3192
  const tools = listing.map(
2988
- (entry) => toolspaceToolFor({ deps, grant, sessionId, rootSessionId, entry, getRegistry })
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, activeTurnId, getRegistry } = input;
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
- for (const serverId of proxyableIds) {
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
- continue;
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
- continue;
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
- for (const tool of listed.tools) {
3059
- if (!tool?.name || !allowedByConfig(config, tool.name)) {
3060
- continue;
3061
- }
3062
- entries.push({ serverId, tool, requireApproval: config.requireApproval });
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
- const hit = toolListCache.get(key);
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
- if (toolListCache.size >= TOOLSPACE_TOOL_LIST_CACHE_MAX_ENTRIES) {
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 baseFetch = input.config.connectionRef ? connectionBrokerFetch(globalThis.fetch, input) : globalThis.fetch;
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
- await client.connect(transport, toolspaceRequestOptions(input.config));
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, sessionId, rootSessionId, entry, getRegistry } = input;
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
- if (approvalRequired) {
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
- await appendAndPublishEvents2(deps.db, deps.bus, grant.workspaceId, sessionId, [
3215
- {
3216
- type: "agent.toolCall.created",
3217
- turnId,
3218
- producerId: grant.subjectId,
3219
- payload: {
3220
- id: callId,
3221
- name,
3222
- arguments: args,
3223
- origin: "toolspace",
3224
- subjectId: grant.subjectId,
3225
- raw: {
3226
- type: "toolspace_call",
3227
- serverId,
3228
- toolName: tool.name
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
- await appendAndPublishEvents2(deps.db, deps.bus, grant.workspaceId, sessionId, [
3235
- {
3236
- type: "agent.toolCall.output",
3237
- turnId,
3238
- producerId: grant.subjectId,
3239
- payload: {
3240
- id: callId,
3241
- output,
3242
- origin: "toolspace",
3243
- subjectId: grant.subjectId
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
- return await server.client.callTool(
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
- async function reserveActiveTurnCall(deps, workspaceId, sessionId) {
3277
- const session = await requireSession2(deps.db, workspaceId, sessionId);
3278
- if (!session.activeTurnId) {
3279
- return { status: "no_active_turn" };
3550
+ function toolspaceAuditSummary(value) {
3551
+ let serialized;
3552
+ try {
3553
+ serialized = JSON.stringify(value) ?? "null";
3554
+ } catch {
3555
+ serialized = "[unserializable]";
3280
3556
  }
3281
- const reservation = await reserveToolspaceCallForTurn(
3282
- deps.db,
3283
- workspaceId,
3284
- sessionId,
3285
- session.activeTurnId,
3286
- deps.settings.toolspaceMaxCallsPerTurn
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
- const turn = await getSessionTurn2(deps.db, workspaceId, session.activeTurnId);
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
- return await baseFetch(
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(_input, init) {
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(_input, init, authHeaders) {
3472
- const headers = new Headers(init?.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 object3 = await objectStorage.getObjectBytes(key);
3818
- if (!object3) {
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(object3.bytes), 200, { ...headers, "Content-Type": contentType });
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 { requireAccessGrant as requireAccessGrant2 } from "@opengeni/core";
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, { message: "codex connect state is invalid or expired" });
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: true };
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, { message: "codex rotation settings not found" });
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 removed2 = await disconnectAllCodexAccounts(tx, workspaceId);
4254
- return { result: removed2, changed: removed2 > 0 };
4929
+ const result2 = await disconnectAllCodexAccounts(tx, workspaceId);
4930
+ return { result: result2, changed: result2.removed > 0 };
4255
4931
  }
4256
4932
  );
4257
- const removed = mutation.result;
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, { message: "codex subscription is not connected" });
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 { Buffer as Buffer2 } from "buffer";
4362
- import { createHash, randomBytes } from "crypto";
4363
- import { lookup } from "dns/promises";
4364
- import { isIP } from "net";
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
- if (response.status !== 401) {
4618
- return {};
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 candidates = uniqueStrings([
5708
+ const safeAuthorizationServer = oauthEndpointUrl(
4652
5709
  authorizationServer,
4653
- ...wellKnownCandidates(authorizationServer, "oauth-authorization-server"),
4654
- ...wellKnownCandidates(authorizationServer, "openid-configuration")
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
- return {
4672
- issuer: stringValue(payload.issuer) ?? authorizationServer.replace(/\/+$/, ""),
4673
- authorizationServer: authorizationServer.replace(/\/+$/, ""),
5733
+ const safeAuthorizationEndpoint = oauthEndpointUrl(
4674
5734
  authorizationEndpoint,
4675
- tokenEndpoint,
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
- ...stringValue(payload.registration_endpoint) ? { registrationEndpoint: stringValue(payload.registration_endpoint) } : {}
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 response.json();
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 url = new URL(input.endpoint);
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: requiredString(payload.tokenEndpoint, "state.tokenEndpoint"),
4919
- authorizationServer: requiredString(payload.authorizationServer, "state.authorizationServer"),
4920
- issuer: requiredString(payload.issuer, "state.issuer"),
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 ${Buffer2.from(`${input.client.clientId}:${input.client.clientSecret}`).toString("base64")}`;
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 response.json();
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 response.clone().json().catch(() => null);
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 response.json();
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
- await assertOAuthFetchAllowed(rawUrl, settings);
5241
- const response = await fetch(rawUrl, { ...init, redirect: "manual" });
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
- async function assertOAuthFetchAllowed(rawUrl, settings) {
5261
- const url = new URL(rawUrl);
5262
- if (!["https:", "http:"].includes(url.protocol)) {
5263
- throw new HTTPException6(422, { message: "OAuth discovery only supports http and https URLs" });
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 createHash("sha256").update(verifier).digest("base64url");
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 z from "zod/v4";
6716
+ import * as z2 from "zod/v4";
5593
6717
  var SearchInputSchema = {
5594
- query: z.string().min(1),
5595
- baseIds: z.array(z.string().uuid()).optional(),
5596
- limit: z.number().int().positive().max(50).optional(),
5597
- mode: z.enum(["hybrid", "vector", "keyword"]).optional(),
5598
- sourceKinds: z.array(
5599
- z.enum([
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: z.array(z.string().min(1)).optional()
6734
+ aclTags: z2.array(z2.string().min(1)).optional()
5611
6735
  };
5612
- var MemoryKindSchema2 = z.enum(["semantic", "episodic", "procedural", "decision", "preference"]);
5613
- var SourceRefSchema = z.object({
5614
- kind: z.enum(["document_chunk", "document", "session_event", "memory", "external"]),
5615
- id: z.string().min(1),
5616
- uri: z.string().min(1).optional(),
5617
- title: z.string().min(1).optional(),
5618
- metadata: z.record(z.string(), z.unknown()).optional()
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: z.string().uuid()
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: z.string().uuid()
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: z.string().min(1).optional(),
6816
+ query: z2.string().min(1).optional(),
5693
6817
  kind: MemoryKindSchema2.optional(),
5694
- scope: z.string().min(1).optional(),
5695
- limit: z.number().int().positive().max(100).optional()
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: z.string().min(1),
6844
+ text: z2.string().min(1),
5721
6845
  kind: MemoryKindSchema2.optional(),
5722
- scope: z.string().min(1).optional(),
5723
- sourceRefs: z.array(SourceRefSchema).optional(),
5724
- confidence: z.number().min(0).max(1).optional(),
5725
- metadata: z.record(z.string(), z.unknown()).optional()
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 z2 } from "zod";
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 createHash2 } from "crypto";
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 = createHash2("sha256").update(`opengeni:stream-viewer:${rawViewerId}`).digest("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 encoder = new TextEncoder();
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 = encoder.encode(frame);
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(page);
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 (!z2.string().uuid().safeParse(sessionId).success) {
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 (!z2.string().uuid().safeParse(sessionId).success) {
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(projectSessionForRelatedAccess2(session, relatedSessionAccessFor(c)));
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
- return c.json(await readSessionLineage(deps, grant, c.req.param("sessionId")));
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 latestClass = eventEnumValue(
11998
+ const latestRequested = eventEnumValue(
10678
11999
  c.req.query("latest"),
10679
- SessionEventSemanticClass2,
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" && !z2.string().uuid().safeParse(parentSessionId).success) {
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 z3 } from "zod";
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 = z3.array(z3.string().uuid()).safeParse(values);
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 hasPermission3, requireAccessContext as requireAccessContext2, requireAccessGrant as requireAccessGrant16 } from "@opengeni/core";
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) => hasPermission3(grant.permissions, "workspace:read")).map((grant) => grant.workspaceId)
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 ?? null
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: configuredModels(deps.settings).map((model) => ({
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
- const toolspace = toolspaceGrant ? await prepareToolspaceMcpSurface({ deps: routeDeps, grant }) : null;
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(c.req.raw);
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 (hasPermission4(grant.permissions, "workspace:read")) {
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-EYYTFA7N.js.map
14861
+ //# sourceMappingURL=chunk-QOYQBYHM.js.map