@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.
@@ -4,6 +4,13 @@ import {
4
4
  CreateFileUploadResponse,
5
5
  FileAsset,
6
6
  FileDownloadUrlResponse,
7
+ RETAINED_OUTPUT_DEFAULT_PAGE_BYTES,
8
+ RETAINED_OUTPUT_MAX_PAGE_BYTES,
9
+ RetainedArtifactMetadataSchema,
10
+ retainedArtifactReferenceFromFile,
11
+ resolveRetainedOutputRange,
12
+ type RetainedArtifactMetadata,
13
+ type RetainedOutputUnavailableReason,
7
14
  } from "@opengeni/contracts";
8
15
  import {
9
16
  claimFileUploadCleanup,
@@ -11,7 +18,9 @@ import {
11
18
  completeFileUpload,
12
19
  createFileUpload,
13
20
  getFileUpload,
21
+ getRetainedFileArtifact,
14
22
  requireFile,
23
+ type RetainedFileArtifact,
15
24
  } from "@opengeni/db";
16
25
  import type { Hono } from "hono";
17
26
  import { HTTPException } from "hono/http-exception";
@@ -235,6 +244,90 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
235
244
  return c.json(FileAsset.parse(file));
236
245
  });
237
246
 
247
+ app.get("/v1/workspaces/:workspaceId/artifacts/:artifactId", async (c) => {
248
+ const workspaceId = c.req.param("workspaceId");
249
+ await requireAccessGrant(c, deps, workspaceId, "files:read");
250
+ const artifactId = retainedArtifactId(c.req.param("artifactId"));
251
+ const artifact = await getRetainedFileArtifact(db, workspaceId, artifactId);
252
+ if (!artifact) {
253
+ return c.json(retainedArtifactUnavailable(artifactId, "deleted"), 404);
254
+ }
255
+ return c.json(retainedArtifactMetadata(artifact));
256
+ });
257
+
258
+ app.get("/v1/workspaces/:workspaceId/artifacts/:artifactId/content", async (c) => {
259
+ const workspaceId = c.req.param("workspaceId");
260
+ await requireAccessGrant(c, deps, workspaceId, "files:read");
261
+ const artifactId = retainedArtifactId(c.req.param("artifactId"));
262
+ const artifact = await getRetainedFileArtifact(db, workspaceId, artifactId);
263
+ if (!artifact) {
264
+ return c.json(retainedArtifactUnavailable(artifactId, "deleted"), 404);
265
+ }
266
+
267
+ const metadata = retainedArtifactMetadata(artifact);
268
+ if (!metadata.available) {
269
+ return c.json(metadata, retainedArtifactUnavailableStatus(metadata.reason));
270
+ }
271
+ if (!objectStorage) {
272
+ return c.json(retainedArtifactUnavailable(artifactId, "missing_storage"), 503);
273
+ }
274
+
275
+ const rangeHeader = c.req.header("range");
276
+ const range = resolveRetainedOutputRange(
277
+ rangeHeader,
278
+ metadata.originalBytes,
279
+ rangeHeader ? RETAINED_OUTPUT_MAX_PAGE_BYTES : RETAINED_OUTPUT_DEFAULT_PAGE_BYTES,
280
+ );
281
+ if (range.kind === "invalid") {
282
+ return c.json(
283
+ {
284
+ message: "invalid retained artifact byte range",
285
+ reason: range.reason,
286
+ maxRangeBytes: RETAINED_OUTPUT_MAX_PAGE_BYTES,
287
+ },
288
+ 400,
289
+ );
290
+ }
291
+ if (range.kind === "unsatisfiable") {
292
+ return c.json(
293
+ { message: "retained artifact byte range is not satisfiable", reason: range.reason },
294
+ 416,
295
+ {
296
+ "Accept-Ranges": "bytes",
297
+ "Content-Range": range.contentRange,
298
+ "Cache-Control": "private, no-store",
299
+ },
300
+ );
301
+ }
302
+
303
+ const headers = {
304
+ "Accept-Ranges": range.acceptRanges,
305
+ "Cache-Control": "private, no-store",
306
+ "Content-Length": String(range.length),
307
+ "Content-Type": metadata.contentType,
308
+ "X-Content-Type-Options": "nosniff",
309
+ ...(range.contentRange ? { "Content-Range": range.contentRange } : {}),
310
+ };
311
+ if (range.kind === "empty") {
312
+ if (!(await objectStorage.fileExists(artifact.file))) {
313
+ return c.json(retainedArtifactUnavailable(artifactId, "missing_storage"), 410);
314
+ }
315
+ return c.body(null, 200, headers);
316
+ }
317
+
318
+ const bytes = await objectStorage.getFileRange(artifact.file, {
319
+ start: range.start,
320
+ end: range.end,
321
+ });
322
+ if (!bytes) {
323
+ return c.json(retainedArtifactUnavailable(artifactId, "missing_storage"), 410);
324
+ }
325
+ if (bytes.byteLength !== range.length) {
326
+ throw new HTTPException(502, { message: "object storage returned an invalid byte range" });
327
+ }
328
+ return c.body(new Uint8Array(bytes), range.status, headers);
329
+ });
330
+
238
331
  app.post("/v1/workspaces/:workspaceId/files/:fileId/download-url", async (c) => {
239
332
  const workspaceId = c.req.param("workspaceId");
240
333
  await requireAccessGrant(c, deps, workspaceId, "files:read");
@@ -271,3 +364,63 @@ function sanitizeFilename(filename: string): string {
271
364
  function publicFileUploadStatus(status: string): string {
272
365
  return status === "cleanup_pending" ? "failed" : status;
273
366
  }
367
+
368
+ function retainedArtifactId(value: string): string {
369
+ const parsed = FileAsset.shape.id.safeParse(value);
370
+ if (!parsed.success) {
371
+ throw new HTTPException(404, { message: "artifact not found" });
372
+ }
373
+ return parsed.data;
374
+ }
375
+
376
+ function retainedArtifactUnavailable(
377
+ artifactId: string,
378
+ reason: RetainedOutputUnavailableReason,
379
+ ): RetainedArtifactMetadata {
380
+ return RetainedArtifactMetadataSchema.parse({ available: false, artifactId, reason });
381
+ }
382
+
383
+ function retainedArtifactMetadata(artifact: RetainedFileArtifact): RetainedArtifactMetadata {
384
+ const reference = retainedArtifactReferenceFromFile(artifact.file);
385
+ if (reference) return reference;
386
+
387
+ const { file, uploadStatus, uploadExpiresAt } = artifact;
388
+ if (file.status === "deleted") {
389
+ return retainedArtifactUnavailable(file.id, "deleted");
390
+ }
391
+ if (
392
+ file.status === "expired" ||
393
+ uploadStatus === "expired" ||
394
+ (uploadStatus === "pending" &&
395
+ uploadExpiresAt !== null &&
396
+ uploadExpiresAt.getTime() < Date.now())
397
+ ) {
398
+ return retainedArtifactUnavailable(file.id, "expired");
399
+ }
400
+ if (file.status === "failed" || uploadStatus === "failed" || uploadStatus === "cleanup_pending") {
401
+ return retainedArtifactUnavailable(file.id, "failed");
402
+ }
403
+ if (file.status === "pending_upload" || uploadStatus === "pending") {
404
+ return retainedArtifactUnavailable(file.id, "pending");
405
+ }
406
+ return retainedArtifactUnavailable(file.id, "unsupported");
407
+ }
408
+
409
+ function retainedArtifactUnavailableStatus(
410
+ reason: RetainedOutputUnavailableReason,
411
+ ): 404 | 409 | 410 | 422 {
412
+ switch (reason) {
413
+ case "deleted":
414
+ return 404;
415
+ case "expired":
416
+ case "missing_storage":
417
+ return 410;
418
+ case "unsupported":
419
+ case "not_retained":
420
+ case "storage_write_failed":
421
+ return 422;
422
+ case "pending":
423
+ case "failed":
424
+ return 409;
425
+ }
426
+ }
@@ -27,19 +27,26 @@ import {
27
27
  SessionEventPayloadMode,
28
28
  SessionEventReadDirection,
29
29
  SessionEventReadMode,
30
+ SessionEventLatestClass,
31
+ SessionEventResultMode,
30
32
  SessionEventSemanticClass,
31
33
  SessionEventType,
34
+ SessionMcpServerId,
35
+ compactSessionEventResult,
36
+ sessionEventLatestClassToSemanticClass,
32
37
  SaveComposerDraftRequest,
33
38
  SteerSessionQueueItemRequest,
34
39
  SteerSessionMessageRequest,
35
40
  TerminalExecRequest,
36
41
  UpdateSessionPinRequest,
37
42
  UpdateSessionGoalRequest,
43
+ UpdateSessionMcpApprovalPolicyRequest,
38
44
  UpdateSessionRequest,
39
45
  ViewerHeartbeatRequest,
40
46
  WORKSPACE_CONTROL_ACTOR_MAX_BYTES,
41
47
  workspaceControlUtf8Bytes,
42
48
  type SandboxBackend,
49
+ type LineageNode,
43
50
  type Session,
44
51
  type SessionAuthorizationOperation,
45
52
  type SessionQueueSnapshot,
@@ -136,8 +143,12 @@ import {
136
143
  readSessionLineage,
137
144
  saveHumanComposerDraft,
138
145
  steerHumanQueuePrompt,
146
+ updateSessionMcpApprovalPolicy,
139
147
  updateSessionTitle,
140
148
  workflowIdForSession,
149
+ sessionWithEffectiveToolPolicy,
150
+ workspaceSessionToolPolicyDefaultServerIds,
151
+ workspaceSessionToolPolicyServerIds,
141
152
  } from "@opengeni/core";
142
153
  import { assertSessionExists, boundedLimit } from "../http/common";
143
154
  import { sseSessionStream } from "../http/sse";
@@ -204,7 +215,7 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
204
215
  const workspaceId = c.req.param("workspaceId");
205
216
  const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:create");
206
217
  const session = await createSessionForRequest(deps, grant, workspaceId, await c.req.json());
207
- return c.json(session, 202);
218
+ return c.json(await withEffectivePolicy(deps, workspaceId, session), 202);
208
219
  });
209
220
 
210
221
  app.get("/v1/workspaces/:workspaceId/sessions", async (c) => {
@@ -241,15 +252,26 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
241
252
  // body for older clients while still making its older-pin omission visible
242
253
  // to raw HTTP consumers without changing that response shape.
243
254
  c.header("x-opengeni-pinned-truncated", page.pinnedTruncated === true ? "true" : "false");
255
+ const policy = await loadEffectivePolicyContext(deps, workspaceId);
256
+ const decorate = (session: Session): Session =>
257
+ sessionWithEffectiveToolPolicy(
258
+ session,
259
+ policy.workspaceServerIds,
260
+ policy.workspaceDefaultServerIds,
261
+ );
244
262
  if (pageView) {
245
- return c.json(page);
263
+ return c.json({
264
+ ...page,
265
+ pinned: page.pinned.map(decorate),
266
+ sessions: page.sessions.map(decorate),
267
+ });
246
268
  }
247
269
  // Same-major compatibility: listSessions() has historically returned an
248
270
  // array. Preserve that wire shape while adding personal pin metadata/order;
249
271
  // cursor consumers opt into the additive page view. A query flag rather
250
272
  // than a /sessions/page path is deliberate: an older API safely ignores it
251
273
  // and returns its historical array instead of treating "page" as a UUID.
252
- return c.json([...page.pinned, ...page.sessions]);
274
+ return c.json([...page.pinned, ...page.sessions].map(decorate));
253
275
  });
254
276
 
255
277
  app.get("/v1/workspaces/:workspaceId/sessions/:sessionId", async (c) => {
@@ -269,7 +291,7 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
269
291
  if (!session) {
270
292
  throw new HTTPException(404, { message: "session not found" });
271
293
  }
272
- return c.json(session);
294
+ return c.json(await withEffectivePolicy(deps, workspaceId, session));
273
295
  });
274
296
 
275
297
  // Personal pin only: this is organization state for the authenticated member,
@@ -296,7 +318,13 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
296
318
  if (!session) {
297
319
  throw new HTTPException(404, { message: "session not found" });
298
320
  }
299
- return c.json(projectSessionForRelatedAccess(session, relatedSessionAccessFor(c)));
321
+ return c.json(
322
+ await withEffectivePolicy(
323
+ deps,
324
+ workspaceId,
325
+ projectSessionForRelatedAccess(session, relatedSessionAccessFor(c)),
326
+ ),
327
+ );
300
328
  } catch (error) {
301
329
  if (error instanceof SessionPinAccessError) {
302
330
  throw new HTTPException(403, { message: error.message });
@@ -317,7 +345,19 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
317
345
  app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/lineage", async (c) => {
318
346
  const workspaceId = c.req.param("workspaceId");
319
347
  const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:read");
320
- return c.json(await readSessionLineage(deps, grant, c.req.param("sessionId")));
348
+ const lineage = await readSessionLineage(deps, grant, c.req.param("sessionId"));
349
+ const policy = await loadEffectivePolicyContext(deps, workspaceId);
350
+ return c.json({
351
+ ...lineage,
352
+ ancestors: lineage.ancestors.map((session) =>
353
+ sessionWithEffectiveToolPolicy(
354
+ session,
355
+ policy.workspaceServerIds,
356
+ policy.workspaceDefaultServerIds,
357
+ ),
358
+ ),
359
+ children: mapLineageNodes(lineage.children, policy),
360
+ });
321
361
  });
322
362
 
323
363
  // Pin (or unpin) the session's Codex account. body { target: "auto" | "<id>" }:
@@ -397,9 +437,35 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
397
437
  if (!session) {
398
438
  throw new HTTPException(404, { message: "session not found" });
399
439
  }
400
- return c.json(session);
440
+ return c.json(await withEffectivePolicy(deps, workspaceId, session));
401
441
  });
402
442
 
443
+ app.patch(
444
+ "/v1/workspaces/:workspaceId/sessions/:sessionId/mcp-servers/:serverId/approval-policy",
445
+ async (c) => {
446
+ const workspaceId = c.req.param("workspaceId");
447
+ const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
448
+ const sessionId = c.req.param("sessionId");
449
+ const parsedServerId = SessionMcpServerId.safeParse(c.req.param("serverId"));
450
+ const payload = UpdateSessionMcpApprovalPolicyRequest.safeParse(
451
+ await c.req.json().catch(() => null),
452
+ );
453
+ if (!parsedServerId.success || !payload.success) {
454
+ throw new HTTPException(400, { message: "invalid MCP approval-policy request" });
455
+ }
456
+ await assertSessionExists(db, workspaceId, sessionId);
457
+ return c.json(
458
+ await updateSessionMcpApprovalPolicy(
459
+ deps,
460
+ grant,
461
+ sessionId,
462
+ parsedServerId.data,
463
+ payload.data.requireApproval,
464
+ ),
465
+ );
466
+ },
467
+ );
468
+
403
469
  app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/goal", async (c) => {
404
470
  const workspaceId = c.req.param("workspaceId");
405
471
  await requireAccessGrant(c, deps, workspaceId, "sessions:read");
@@ -597,12 +663,27 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
597
663
  "mode",
598
664
  explicitReplay ? "forensic" : "monitoring",
599
665
  );
600
- const latestClass = eventEnumValue(
666
+ const latestRequested = eventEnumValue(
601
667
  c.req.query("latest"),
602
- SessionEventSemanticClass,
668
+ SessionEventLatestClass,
603
669
  "latest",
604
670
  undefined,
605
671
  );
672
+ const latestClass =
673
+ latestRequested === undefined
674
+ ? undefined
675
+ : sessionEventLatestClassToSemanticClass(latestRequested);
676
+ const resultMode = eventEnumValue(
677
+ c.req.query("resultMode") ?? c.req.query("result"),
678
+ SessionEventResultMode,
679
+ "resultMode",
680
+ "events",
681
+ );
682
+ if (resultMode === "compact" && latestClass === undefined) {
683
+ throw new HTTPException(400, {
684
+ message: "resultMode=compact requires latest",
685
+ });
686
+ }
606
687
  if (
607
688
  latestClass &&
608
689
  ["includeTypes", "excludeTypes", "includeClasses", "excludeClasses"].some(
@@ -660,19 +741,39 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
660
741
  compact ? 5000 : mode === "monitoring" ? 250 : 2000,
661
742
  mode === "monitoring" ? 40 : 500,
662
743
  );
744
+ const dbPayloadMode = resultMode === "compact" ? ("full" as const) : payloadMode;
663
745
  const dbPage = await listSessionEventPage(db, workspaceId, sessionId, {
664
746
  after,
665
747
  ...(before !== undefined ? { before } : {}),
666
748
  limit,
667
749
  direction,
668
- payloadMode,
750
+ payloadMode: dbPayloadMode,
669
751
  includeTypes,
670
752
  excludeTypes,
671
753
  includeClasses: latestClass ? [latestClass] : includeClasses,
672
754
  excludeClasses,
673
755
  ...(mode === "monitoring" ? { defaultExcludeTypes: SESSION_EVENT_RAW_DELTA_TYPES } : {}),
756
+ ...(latestClass ? { authoritativeLatest: true } : {}),
674
757
  });
675
758
  const events = dbPage.events;
759
+ if (resultMode === "compact") {
760
+ const event = events[0];
761
+ c.header("X-OpenGeni-Event-Result-Mode", "compact");
762
+ c.header("X-OpenGeni-Event-Result", event ? "found" : "not_found");
763
+ c.header("X-OpenGeni-Event-Mode", mode);
764
+ c.header("X-OpenGeni-Event-Direction", direction);
765
+ c.header("X-OpenGeni-Payload-Mode", "full");
766
+ c.header("X-OpenGeni-Forensic-Exact", "false");
767
+ if (!event) return c.json(null, 200);
768
+ const result = compactSessionEventResult(
769
+ event,
770
+ latestClass!,
771
+ dbPage.coveredSequence ?? { first: event.sequence, last: event.sequence },
772
+ );
773
+ c.header("X-OpenGeni-Covered-First", String(result.coveredSequence.first));
774
+ c.header("X-OpenGeni-Covered-Last", String(result.coveredSequence.last));
775
+ return c.json(result);
776
+ }
676
777
  const projected = compact ? coalesceSessionEventDeltas(events) : events;
677
778
  const page = boundSessionEventHttpPage(projected, {
678
779
  direction,
@@ -2022,6 +2123,9 @@ export function sessionAuthorizationOperationForHttp(
2022
2123
  return null;
2023
2124
  }
2024
2125
  if (suffix === "/pin" && verb === "PUT") return "session.pin.write";
2126
+ if (/^\/mcp-servers\/[^/]+\/approval-policy$/.test(suffix) && verb === "PATCH") {
2127
+ return "session.mcp.approval_policy.write";
2128
+ }
2025
2129
  if (suffix === "/lineage" && verb === "GET") return "session.lineage.read";
2026
2130
  if (suffix === "/codex-account" && verb === "POST") {
2027
2131
  return "session.codex_account.write";
@@ -2238,3 +2342,44 @@ function commandConflictResponse(c: Context, error: unknown): Response {
2238
2342
  }
2239
2343
  throw error;
2240
2344
  }
2345
+
2346
+ type EffectivePolicyContext = {
2347
+ workspaceServerIds: string[];
2348
+ workspaceDefaultServerIds: string[];
2349
+ };
2350
+
2351
+ async function loadEffectivePolicyContext(
2352
+ deps: ApiRouteDeps,
2353
+ workspaceId: string,
2354
+ ): Promise<EffectivePolicyContext> {
2355
+ const [workspaceServerIds, workspaceDefaultServerIds] = await Promise.all([
2356
+ workspaceSessionToolPolicyServerIds(deps.db, workspaceId, deps.settings),
2357
+ workspaceSessionToolPolicyDefaultServerIds(deps.db, workspaceId, deps.settings),
2358
+ ]);
2359
+ return { workspaceServerIds, workspaceDefaultServerIds };
2360
+ }
2361
+
2362
+ async function withEffectivePolicy(
2363
+ deps: ApiRouteDeps,
2364
+ workspaceId: string,
2365
+ session: Session,
2366
+ ): Promise<Session> {
2367
+ const policy = await loadEffectivePolicyContext(deps, workspaceId);
2368
+ return sessionWithEffectiveToolPolicy(
2369
+ session,
2370
+ policy.workspaceServerIds,
2371
+ policy.workspaceDefaultServerIds,
2372
+ );
2373
+ }
2374
+
2375
+ function mapLineageNodes(nodes: LineageNode[], policy: EffectivePolicyContext): LineageNode[] {
2376
+ return nodes.map((node) => ({
2377
+ ...node,
2378
+ session: sessionWithEffectiveToolPolicy(
2379
+ node.session as Session,
2380
+ policy.workspaceServerIds,
2381
+ policy.workspaceDefaultServerIds,
2382
+ ),
2383
+ children: mapLineageNodes(node.children, policy),
2384
+ }));
2385
+ }
@@ -8,6 +8,7 @@ import {
8
8
  UpdateWorkspaceRequest,
9
9
  UpdateWorkspaceSettingsRequest,
10
10
  WORKSPACE_CONTROL_ACTOR_MAX_BYTES,
11
+ WorkspaceModelCatalogResponse,
11
12
  WorkspaceInferenceControlRequest,
12
13
  Workspace,
13
14
  WorkspaceMember,
@@ -35,6 +36,7 @@ import {
35
36
  updateWorkspace,
36
37
  updateWorkspaceSettings,
37
38
  upsertWorkspaceModelPolicy,
39
+ workspaceCodexSubscriptionActive,
38
40
  } from "@opengeni/db";
39
41
  import { boundWorkspaceControlHttpPage } from "@opengeni/events";
40
42
  import type { Hono } from "hono";
@@ -50,6 +52,18 @@ import {
50
52
  } from "@opengeni/core";
51
53
  import { boundedLimit } from "../http/common";
52
54
  import { sseWorkspaceControlStream } from "../http/sse";
55
+ import { buildWorkspaceModelCatalog } from "../model-catalog";
56
+ import { canonicalizeConfiguredModelId, type Settings } from "@opengeni/config";
57
+
58
+ export function canonicalWorkspacePolicyModelIds(
59
+ settings: Settings,
60
+ modelIds: string[] | null | undefined,
61
+ ): string[] | null {
62
+ if (modelIds === null || modelIds === undefined) {
63
+ return null;
64
+ }
65
+ return [...new Set(modelIds.map((modelId) => canonicalizeConfiguredModelId(settings, modelId)))];
66
+ }
53
67
 
54
68
  export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
55
69
  app.get("/v1/access/me", async (c) => {
@@ -143,7 +157,27 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
143
157
 
144
158
  // Per-workspace model/provider availability policy (the HARD blocker over
145
159
  // which providers/models may serve a turn at all). Absent row reads as
146
- // unrestricted {null, null}.
160
+ // unrestricted {null, null}. No Azure AD credential resolver is wired here,
161
+ // so bearer/federated definitions intentionally fail closed as not ready.
162
+ app.get("/v1/workspaces/:workspaceId/model-catalog", async (c) => {
163
+ const workspaceId = c.req.param("workspaceId");
164
+ await requireAccessGrant(c, deps, workspaceId, "workspace:read");
165
+ const [policy, codexSubscriptionActive] = await Promise.all([
166
+ getWorkspaceModelPolicy(deps.db, workspaceId),
167
+ workspaceCodexSubscriptionActive(deps.db, deps.settings, workspaceId),
168
+ ]);
169
+ c.header("cache-control", "private, no-store");
170
+ return c.json(
171
+ WorkspaceModelCatalogResponse.parse(
172
+ buildWorkspaceModelCatalog({
173
+ settings: deps.settings,
174
+ policy,
175
+ codexSubscriptionActive,
176
+ }),
177
+ ),
178
+ );
179
+ });
180
+
147
181
  app.get("/v1/workspaces/:workspaceId/model-policy", async (c) => {
148
182
  const workspaceId = c.req.param("workspaceId");
149
183
  await requireAccessGrant(c, deps, workspaceId, "workspace:read");
@@ -166,7 +200,7 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
166
200
  accountId: grant.accountId,
167
201
  workspaceId,
168
202
  allowedProviders: payload.allowedProviders ?? null,
169
- allowedModels: payload.allowedModels ?? null,
203
+ allowedModels: canonicalWorkspacePolicyModelIds(deps.settings, payload.allowedModels),
170
204
  });
171
205
  return c.json(policy);
172
206
  });
@@ -648,7 +648,13 @@ export async function mintDesktopStream(
648
648
  // Idempotent display stack (flock-guarded; a no-op when already up). A box
649
649
  // that genuinely can't run the stack degrades to transport:null, not a throw.
650
650
  try {
651
- await ensureDisplayStack(established.session);
651
+ await ensureDisplayStack(established.session, {
652
+ telemetryContext: {
653
+ callerKind: "viewer",
654
+ ...(lease.instanceId ? { sandboxId: lease.instanceId } : {}),
655
+ leaseEpoch: lease.leaseEpoch,
656
+ },
657
+ });
652
658
  } catch (error) {
653
659
  if (error instanceof DisplayStackUnsupportedError) {
654
660
  return null;