@opengeni/api-router 0.7.3 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +1 -1
- package/dist/{chunk-EYYTFA7N.js → chunk-QOYQBYHM.js} +1920 -264
- package/dist/chunk-QOYQBYHM.js.map +1 -0
- package/dist/index.js +1 -1
- package/package.json +12 -11
- package/src/app.ts +37 -15
- package/src/codex-redemption-security.ts +96 -0
- package/src/integrations/oauth-client.ts +147 -61
- package/src/mcp/server.ts +62 -8
- package/src/mcp/session-view.ts +102 -1
- package/src/mcp/toolspace.ts +389 -124
- package/src/model-catalog.ts +337 -0
- package/src/routes/codex.ts +866 -14
- package/src/routes/files.ts +153 -0
- package/src/routes/sessions.ts +155 -10
- package/src/routes/workspaces.ts +36 -2
- package/src/sandbox/viewer.ts +7 -1
- package/dist/chunk-EYYTFA7N.js.map +0 -1
package/src/routes/files.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/routes/sessions.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
666
|
+
const latestRequested = eventEnumValue(
|
|
601
667
|
c.req.query("latest"),
|
|
602
|
-
|
|
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
|
+
}
|
package/src/routes/workspaces.ts
CHANGED
|
@@ -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
|
|
203
|
+
allowedModels: canonicalWorkspacePolicyModelIds(deps.settings, payload.allowedModels),
|
|
170
204
|
});
|
|
171
205
|
return c.json(policy);
|
|
172
206
|
});
|
package/src/sandbox/viewer.ts
CHANGED
|
@@ -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;
|