@opengeni/api-router 0.5.7 → 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.d.ts +21 -3
- package/dist/app.js +3 -1
- package/dist/{chunk-HBEJMWD3.js → chunk-QOYQBYHM.js} +4302 -926
- package/dist/chunk-QOYQBYHM.js.map +1 -0
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/package.json +12 -11
- package/src/app.ts +83 -18
- package/src/codex-redemption-security.ts +96 -0
- package/src/github-access.ts +46 -0
- package/src/github-browser-flow.ts +83 -0
- package/src/http/auth.ts +12 -4
- package/src/http/sse.ts +526 -92
- package/src/index.ts +2 -1
- package/src/integrations/oauth-client.ts +147 -61
- package/src/mcp/server.ts +828 -146
- package/src/mcp/session-view.ts +721 -201
- package/src/mcp/toolspace.ts +482 -132
- package/src/model-catalog.ts +337 -0
- package/src/routes/codex.ts +883 -28
- package/src/routes/enrollments.ts +2 -2
- package/src/routes/files.ts +153 -0
- package/src/routes/github.ts +63 -202
- package/src/routes/install.ts +1 -1
- package/src/routes/machines.ts +2 -2
- package/src/routes/sessions.ts +789 -81
- package/src/routes/workspace-capture.ts +56 -38
- package/src/routes/workspaces.ts +66 -13
- package/src/sandbox/access.ts +1 -1
- package/src/sandbox/auth-callout.ts +1 -1
- package/src/sandbox/channel-a.ts +14 -1
- package/src/sandbox/enrollment.ts +4 -4
- package/src/sandbox/machines.ts +1 -1
- package/src/sandbox/metrics-ingestion.ts +1 -1
- package/src/sandbox/viewer.ts +8 -2
- package/dist/chunk-HBEJMWD3.js.map +0 -1
package/src/routes/sessions.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
FsMoveRequest,
|
|
13
13
|
FsReadRequest,
|
|
14
14
|
FsWriteRequest,
|
|
15
|
+
HumanInputRequestStatus,
|
|
15
16
|
GitDiffRequest,
|
|
16
17
|
GitLogRequest,
|
|
17
18
|
GitShowRequest,
|
|
@@ -22,16 +23,33 @@ import {
|
|
|
22
23
|
PtyResizeRequest,
|
|
23
24
|
PtyWriteRequest,
|
|
24
25
|
SessionControlRequest,
|
|
26
|
+
SESSION_EVENT_RAW_DELTA_TYPES,
|
|
27
|
+
SessionEventPayloadMode,
|
|
28
|
+
SessionEventReadDirection,
|
|
29
|
+
SessionEventReadMode,
|
|
30
|
+
SessionEventLatestClass,
|
|
31
|
+
SessionEventResultMode,
|
|
32
|
+
SessionEventSemanticClass,
|
|
33
|
+
SessionEventType,
|
|
34
|
+
SessionMcpServerId,
|
|
35
|
+
compactSessionEventResult,
|
|
36
|
+
sessionEventLatestClassToSemanticClass,
|
|
25
37
|
SaveComposerDraftRequest,
|
|
26
38
|
SteerSessionQueueItemRequest,
|
|
27
39
|
SteerSessionMessageRequest,
|
|
28
40
|
TerminalExecRequest,
|
|
29
41
|
UpdateSessionPinRequest,
|
|
30
42
|
UpdateSessionGoalRequest,
|
|
43
|
+
UpdateSessionMcpApprovalPolicyRequest,
|
|
31
44
|
UpdateSessionRequest,
|
|
32
45
|
ViewerHeartbeatRequest,
|
|
46
|
+
WORKSPACE_CONTROL_ACTOR_MAX_BYTES,
|
|
47
|
+
workspaceControlUtf8Bytes,
|
|
33
48
|
type SandboxBackend,
|
|
49
|
+
type LineageNode,
|
|
34
50
|
type Session,
|
|
51
|
+
type SessionAuthorizationOperation,
|
|
52
|
+
type SessionQueueSnapshot,
|
|
35
53
|
type TerminalPtyExitedPayload,
|
|
36
54
|
type TerminalPtyOutputDeltaPayload,
|
|
37
55
|
type TerminalPtyStartedPayload,
|
|
@@ -39,6 +57,7 @@ import {
|
|
|
39
57
|
import { streamTokenDegraded } from "@opengeni/config";
|
|
40
58
|
import {
|
|
41
59
|
acceptSessionApprovalDecision,
|
|
60
|
+
acceptSessionHumanInputResponse,
|
|
42
61
|
clearSessionGoal,
|
|
43
62
|
clearSessionContext,
|
|
44
63
|
closePtySession,
|
|
@@ -47,13 +66,17 @@ import {
|
|
|
47
66
|
getSession,
|
|
48
67
|
getSessionForSubject,
|
|
49
68
|
getSessionGoal,
|
|
69
|
+
getSessionHumanInputRequest,
|
|
50
70
|
getSessionQueueSnapshot,
|
|
51
71
|
getStreamAcknowledgment,
|
|
52
72
|
insertPtySession,
|
|
53
|
-
|
|
73
|
+
listSessionEventPage,
|
|
74
|
+
listSessionHumanInputRequests,
|
|
54
75
|
listSessionIdsInGroup,
|
|
55
76
|
listSessionsForSubject,
|
|
56
77
|
listSessionTurns,
|
|
78
|
+
projectEffectiveControlForRelatedAccess,
|
|
79
|
+
projectSessionForRelatedAccess,
|
|
57
80
|
recordStreamAcknowledgment,
|
|
58
81
|
requestSessionCompaction,
|
|
59
82
|
setSessionCodexPin,
|
|
@@ -71,21 +94,31 @@ import {
|
|
|
71
94
|
SessionCommandIdempotencyError,
|
|
72
95
|
SessionControlConflictError,
|
|
73
96
|
SessionContextBusyError,
|
|
97
|
+
HumanInputResponseValidationError,
|
|
74
98
|
latestWorkspaceCapture,
|
|
75
99
|
workspaceCaptureAtRevision,
|
|
76
100
|
type AppendEventInput,
|
|
77
101
|
} from "@opengeni/db";
|
|
78
102
|
import {
|
|
79
103
|
appendAndPublishEvents,
|
|
104
|
+
boundSessionEventHttpPage,
|
|
80
105
|
coalesceSessionEventDeltas,
|
|
81
106
|
publishDurableSessionEvents,
|
|
82
107
|
} from "@opengeni/events";
|
|
83
108
|
import { z } from "zod";
|
|
84
109
|
import { withChannelA } from "../sandbox/channel-a";
|
|
85
110
|
import { negotiateCapabilities } from "@opengeni/runtime/sandbox";
|
|
86
|
-
import type { Context, Hono } from "hono";
|
|
111
|
+
import type { Context, Hono, MiddlewareHandler } from "hono";
|
|
87
112
|
import { HTTPException } from "hono/http-exception";
|
|
88
|
-
import {
|
|
113
|
+
import {
|
|
114
|
+
requireAccessGrant,
|
|
115
|
+
requireSessionAuthorization,
|
|
116
|
+
requireSessionAuthorizationListScope,
|
|
117
|
+
SESSION_AUTHORIZATION_DEFAULT_REAUTHORIZE_MS,
|
|
118
|
+
SessionAuthorizationDeniedError,
|
|
119
|
+
SessionAuthorizationUnavailableError,
|
|
120
|
+
type ResolvedSessionAuthorization,
|
|
121
|
+
} from "@opengeni/core";
|
|
89
122
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
90
123
|
import {
|
|
91
124
|
attachViewer,
|
|
@@ -110,8 +143,12 @@ import {
|
|
|
110
143
|
readSessionLineage,
|
|
111
144
|
saveHumanComposerDraft,
|
|
112
145
|
steerHumanQueuePrompt,
|
|
146
|
+
updateSessionMcpApprovalPolicy,
|
|
113
147
|
updateSessionTitle,
|
|
114
148
|
workflowIdForSession,
|
|
149
|
+
sessionWithEffectiveToolPolicy,
|
|
150
|
+
workspaceSessionToolPolicyDefaultServerIds,
|
|
151
|
+
workspaceSessionToolPolicyServerIds,
|
|
115
152
|
} from "@opengeni/core";
|
|
116
153
|
import { assertSessionExists, boundedLimit } from "../http/common";
|
|
117
154
|
import { sseSessionStream } from "../http/sse";
|
|
@@ -119,17 +156,77 @@ import { serveWorkspaceCapture, serveWorkspaceCaptureFile } from "./workspace-ca
|
|
|
119
156
|
|
|
120
157
|
export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
121
158
|
const { settings, db, bus, workflowClient, objectStorage } = deps;
|
|
159
|
+
const requestSessionAuthorization = new WeakMap<Request, ResolvedSessionAuthorization>();
|
|
160
|
+
const relatedSessionAccessFor = (c: Context): "target" | "root" =>
|
|
161
|
+
requestSessionAuthorization.get(c.req.raw)?.relatedSessionAccess ?? "root";
|
|
162
|
+
const projectQueueSnapshot = (
|
|
163
|
+
snapshot: SessionQueueSnapshot,
|
|
164
|
+
sessionId: string,
|
|
165
|
+
access: "target" | "root",
|
|
166
|
+
): SessionQueueSnapshot => ({
|
|
167
|
+
...snapshot,
|
|
168
|
+
effectiveControl: projectEffectiveControlForRelatedAccess(
|
|
169
|
+
snapshot.effectiveControl,
|
|
170
|
+
sessionId,
|
|
171
|
+
access,
|
|
172
|
+
),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// A host-bound deployment has one fail-closed authorization seam for every
|
|
176
|
+
// HTTP session surface. Register it before the routes so a newly added path
|
|
177
|
+
// cannot accidentally inherit workspace access without an explicit operation
|
|
178
|
+
// classification. The long-lived event stream performs its own initial check
|
|
179
|
+
// and bounded reauthorization below.
|
|
180
|
+
const authorizeSessionHttp: MiddlewareHandler = async (c, next) => {
|
|
181
|
+
if (!deps.sessionAuthorization) {
|
|
182
|
+
await next();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const workspaceId = c.req.param("workspaceId") ?? "";
|
|
186
|
+
const sessionId = c.req.param("sessionId") ?? "";
|
|
187
|
+
const operation = sessionAuthorizationOperationForHttp(
|
|
188
|
+
c.req.method,
|
|
189
|
+
new URL(c.req.url).pathname,
|
|
190
|
+
sessionId,
|
|
191
|
+
);
|
|
192
|
+
if (operation === "session.stream.read") {
|
|
193
|
+
await next();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (!operation) {
|
|
197
|
+
throw sessionAuthorizationHttpError(new SessionAuthorizationUnavailableError());
|
|
198
|
+
}
|
|
199
|
+
const grant = await requireAccessGrant(c, deps, workspaceId);
|
|
200
|
+
try {
|
|
201
|
+
const authorization = await requireSessionAuthorization(deps, grant, {
|
|
202
|
+
sessionId,
|
|
203
|
+
operation,
|
|
204
|
+
surface: "http",
|
|
205
|
+
});
|
|
206
|
+
if (authorization) requestSessionAuthorization.set(c.req.raw, authorization);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
throw sessionAuthorizationHttpError(error);
|
|
209
|
+
}
|
|
210
|
+
await next();
|
|
211
|
+
};
|
|
212
|
+
app.use("/v1/workspaces/:workspaceId/sessions/:sessionId/*", authorizeSessionHttp);
|
|
122
213
|
|
|
123
214
|
app.post("/v1/workspaces/:workspaceId/sessions", async (c) => {
|
|
124
215
|
const workspaceId = c.req.param("workspaceId");
|
|
125
216
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:create");
|
|
126
217
|
const session = await createSessionForRequest(deps, grant, workspaceId, await c.req.json());
|
|
127
|
-
return c.json(session, 202);
|
|
218
|
+
return c.json(await withEffectivePolicy(deps, workspaceId, session), 202);
|
|
128
219
|
});
|
|
129
220
|
|
|
130
221
|
app.get("/v1/workspaces/:workspaceId/sessions", async (c) => {
|
|
131
222
|
const workspaceId = c.req.param("workspaceId");
|
|
132
223
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
224
|
+
let authorizationScope;
|
|
225
|
+
try {
|
|
226
|
+
authorizationScope = await requireSessionAuthorizationListScope(deps, grant, "http");
|
|
227
|
+
} catch (error) {
|
|
228
|
+
throw sessionAuthorizationHttpError(error);
|
|
229
|
+
}
|
|
133
230
|
const pageView = c.req.query("view") === "page";
|
|
134
231
|
const query = sessionListQuery(c.req.query(), pageView);
|
|
135
232
|
let page: Awaited<ReturnType<typeof listSessionsForSubject>>;
|
|
@@ -140,6 +237,7 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
140
237
|
...(query.cursor ? { cursor: query.cursor } : {}),
|
|
141
238
|
...(query.search ? { search: query.search } : {}),
|
|
142
239
|
...(query.parentSessionId !== undefined ? { parentSessionId: query.parentSessionId } : {}),
|
|
240
|
+
...(authorizationScope ? { authorizationScope } : {}),
|
|
143
241
|
});
|
|
144
242
|
} catch (error) {
|
|
145
243
|
if (error instanceof SessionListAccessError) {
|
|
@@ -150,15 +248,30 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
150
248
|
}
|
|
151
249
|
throw error;
|
|
152
250
|
}
|
|
251
|
+
// The page body carries this fact directly. Preserve the historical array
|
|
252
|
+
// body for older clients while still making its older-pin omission visible
|
|
253
|
+
// to raw HTTP consumers without changing that response shape.
|
|
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
|
+
);
|
|
153
262
|
if (pageView) {
|
|
154
|
-
return c.json(
|
|
263
|
+
return c.json({
|
|
264
|
+
...page,
|
|
265
|
+
pinned: page.pinned.map(decorate),
|
|
266
|
+
sessions: page.sessions.map(decorate),
|
|
267
|
+
});
|
|
155
268
|
}
|
|
156
269
|
// Same-major compatibility: listSessions() has historically returned an
|
|
157
270
|
// array. Preserve that wire shape while adding personal pin metadata/order;
|
|
158
271
|
// cursor consumers opt into the additive page view. A query flag rather
|
|
159
272
|
// than a /sessions/page path is deliberate: an older API safely ignores it
|
|
160
273
|
// and returns its historical array instead of treating "page" as a UUID.
|
|
161
|
-
return c.json([...page.pinned, ...page.sessions]);
|
|
274
|
+
return c.json([...page.pinned, ...page.sessions].map(decorate));
|
|
162
275
|
});
|
|
163
276
|
|
|
164
277
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId", async (c) => {
|
|
@@ -168,11 +281,17 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
168
281
|
if (!z.string().uuid().safeParse(sessionId).success) {
|
|
169
282
|
throw new HTTPException(404, { message: "session not found" });
|
|
170
283
|
}
|
|
171
|
-
const session = await getSessionForSubject(
|
|
284
|
+
const session = await getSessionForSubject(
|
|
285
|
+
db,
|
|
286
|
+
workspaceId,
|
|
287
|
+
sessionId,
|
|
288
|
+
grant.subjectId,
|
|
289
|
+
relatedSessionAccessFor(c),
|
|
290
|
+
);
|
|
172
291
|
if (!session) {
|
|
173
292
|
throw new HTTPException(404, { message: "session not found" });
|
|
174
293
|
}
|
|
175
|
-
return c.json(session);
|
|
294
|
+
return c.json(await withEffectivePolicy(deps, workspaceId, session));
|
|
176
295
|
});
|
|
177
296
|
|
|
178
297
|
// Personal pin only: this is organization state for the authenticated member,
|
|
@@ -199,14 +318,23 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
199
318
|
if (!session) {
|
|
200
319
|
throw new HTTPException(404, { message: "session not found" });
|
|
201
320
|
}
|
|
202
|
-
return c.json(
|
|
321
|
+
return c.json(
|
|
322
|
+
await withEffectivePolicy(
|
|
323
|
+
deps,
|
|
324
|
+
workspaceId,
|
|
325
|
+
projectSessionForRelatedAccess(session, relatedSessionAccessFor(c)),
|
|
326
|
+
),
|
|
327
|
+
);
|
|
203
328
|
} catch (error) {
|
|
204
329
|
if (error instanceof SessionPinAccessError) {
|
|
205
330
|
throw new HTTPException(403, { message: error.message });
|
|
206
331
|
}
|
|
207
332
|
if (error instanceof SessionPinVersionConflictError) {
|
|
208
333
|
return c.json(
|
|
209
|
-
{
|
|
334
|
+
{
|
|
335
|
+
message: "session pin changed in another client",
|
|
336
|
+
current: error.current,
|
|
337
|
+
},
|
|
210
338
|
409,
|
|
211
339
|
);
|
|
212
340
|
}
|
|
@@ -216,8 +344,20 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
216
344
|
|
|
217
345
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/lineage", async (c) => {
|
|
218
346
|
const workspaceId = c.req.param("workspaceId");
|
|
219
|
-
await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
220
|
-
|
|
347
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
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
|
+
});
|
|
221
361
|
});
|
|
222
362
|
|
|
223
363
|
// Pin (or unpin) the session's Codex account. body { target: "auto" | "<id>" }:
|
|
@@ -232,7 +372,9 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
232
372
|
const body = (await c.req.json()) as { target?: string };
|
|
233
373
|
const target = typeof body.target === "string" ? body.target : "";
|
|
234
374
|
if (!target) {
|
|
235
|
-
throw new HTTPException(400, {
|
|
375
|
+
throw new HTTPException(400, {
|
|
376
|
+
message: 'target is required ("auto" or an account id)',
|
|
377
|
+
});
|
|
236
378
|
}
|
|
237
379
|
const pinned = target === "auto" ? null : target;
|
|
238
380
|
const mutation = await withCodexCapacityMutation(
|
|
@@ -245,7 +387,9 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
245
387
|
);
|
|
246
388
|
const ok = mutation.result;
|
|
247
389
|
if (!ok) {
|
|
248
|
-
throw new HTTPException(404, {
|
|
390
|
+
throw new HTTPException(404, {
|
|
391
|
+
message: "session or codex account not found",
|
|
392
|
+
});
|
|
249
393
|
}
|
|
250
394
|
await Promise.allSettled(
|
|
251
395
|
mutation.wakeTargets.map((wake) =>
|
|
@@ -279,17 +423,49 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
279
423
|
const sessionId = c.req.param("sessionId");
|
|
280
424
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
281
425
|
const payload = UpdateSessionRequest.parse(await c.req.json());
|
|
282
|
-
await updateSessionTitle(
|
|
426
|
+
const titleUpdate = await updateSessionTitle(deps, grant, sessionId, payload.title, "user");
|
|
283
427
|
// A session-returning member route must preserve the caller's private pin
|
|
284
428
|
// projection. Returning the generic mapSession() default here would reset a
|
|
285
429
|
// pinned React consumer to false/version 0 after a harmless rename.
|
|
286
|
-
const session = await getSessionForSubject(
|
|
430
|
+
const session = await getSessionForSubject(
|
|
431
|
+
db,
|
|
432
|
+
workspaceId,
|
|
433
|
+
sessionId,
|
|
434
|
+
grant.subjectId,
|
|
435
|
+
titleUpdate.relatedSessionAccess,
|
|
436
|
+
);
|
|
287
437
|
if (!session) {
|
|
288
438
|
throw new HTTPException(404, { message: "session not found" });
|
|
289
439
|
}
|
|
290
|
-
return c.json(session);
|
|
440
|
+
return c.json(await withEffectivePolicy(deps, workspaceId, session));
|
|
291
441
|
});
|
|
292
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
|
+
|
|
293
469
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/goal", async (c) => {
|
|
294
470
|
const workspaceId = c.req.param("workspaceId");
|
|
295
471
|
await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
@@ -464,7 +640,10 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
464
640
|
workflowId: requested.temporalWorkflowId,
|
|
465
641
|
wakeRevision: requested.wakeRevision,
|
|
466
642
|
});
|
|
467
|
-
return c.json({
|
|
643
|
+
return c.json({
|
|
644
|
+
status: "pending",
|
|
645
|
+
message: "Compaction will run at the next safe boundary.",
|
|
646
|
+
});
|
|
468
647
|
});
|
|
469
648
|
|
|
470
649
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/events", async (c) => {
|
|
@@ -472,22 +651,171 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
472
651
|
await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
473
652
|
const sessionId = c.req.param("sessionId");
|
|
474
653
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
475
|
-
const
|
|
476
|
-
const
|
|
654
|
+
const rawAfter = c.req.query("after");
|
|
655
|
+
const rawBefore = c.req.query("before");
|
|
656
|
+
const after = eventSequence(rawAfter, 0);
|
|
657
|
+
const before = optionalEventSequence(rawBefore);
|
|
477
658
|
const compact = compactEvents(c.req.query("compact"));
|
|
478
|
-
const
|
|
479
|
-
const
|
|
659
|
+
const explicitReplay = rawAfter !== undefined || rawBefore !== undefined || compact;
|
|
660
|
+
const mode = eventEnumValue(
|
|
661
|
+
c.req.query("mode"),
|
|
662
|
+
SessionEventReadMode,
|
|
663
|
+
"mode",
|
|
664
|
+
explicitReplay ? "forensic" : "monitoring",
|
|
665
|
+
);
|
|
666
|
+
const latestRequested = eventEnumValue(
|
|
667
|
+
c.req.query("latest"),
|
|
668
|
+
SessionEventLatestClass,
|
|
669
|
+
"latest",
|
|
670
|
+
undefined,
|
|
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
|
+
}
|
|
687
|
+
if (
|
|
688
|
+
latestClass &&
|
|
689
|
+
["includeTypes", "excludeTypes", "includeClasses", "excludeClasses"].some(
|
|
690
|
+
(name) => c.req.query(name) !== undefined,
|
|
691
|
+
)
|
|
692
|
+
) {
|
|
693
|
+
throw new HTTPException(400, {
|
|
694
|
+
message: "latest cannot be combined with event filters",
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
const direction = latestClass
|
|
698
|
+
? "before"
|
|
699
|
+
: eventEnumValue(
|
|
700
|
+
c.req.query("direction"),
|
|
701
|
+
SessionEventReadDirection,
|
|
702
|
+
"direction",
|
|
703
|
+
before !== undefined
|
|
704
|
+
? "before"
|
|
705
|
+
: rawAfter !== undefined
|
|
706
|
+
? "after"
|
|
707
|
+
: mode === "monitoring"
|
|
708
|
+
? "before"
|
|
709
|
+
: "after",
|
|
710
|
+
);
|
|
711
|
+
const payloadMode = eventEnumValue(
|
|
712
|
+
c.req.query("payloadMode"),
|
|
713
|
+
SessionEventPayloadMode,
|
|
714
|
+
"payloadMode",
|
|
715
|
+
mode === "monitoring" ? "summary" : "full",
|
|
716
|
+
);
|
|
717
|
+
const includeTypes = eventEnumList(
|
|
718
|
+
c.req.query("includeTypes"),
|
|
719
|
+
SessionEventType,
|
|
720
|
+
"includeTypes",
|
|
721
|
+
);
|
|
722
|
+
const excludeTypes = eventEnumList(
|
|
723
|
+
c.req.query("excludeTypes"),
|
|
724
|
+
SessionEventType,
|
|
725
|
+
"excludeTypes",
|
|
726
|
+
);
|
|
727
|
+
const includeClasses = eventEnumList(
|
|
728
|
+
c.req.query("includeClasses"),
|
|
729
|
+
SessionEventSemanticClass,
|
|
730
|
+
"includeClasses",
|
|
731
|
+
);
|
|
732
|
+
const excludeClasses = eventEnumList(
|
|
733
|
+
c.req.query("excludeClasses"),
|
|
734
|
+
SessionEventSemanticClass,
|
|
735
|
+
"excludeClasses",
|
|
736
|
+
);
|
|
737
|
+
const limit = latestClass
|
|
738
|
+
? 1
|
|
739
|
+
: eventListLimit(
|
|
740
|
+
c.req.query("limit"),
|
|
741
|
+
compact ? 5000 : mode === "monitoring" ? 250 : 2000,
|
|
742
|
+
mode === "monitoring" ? 40 : 500,
|
|
743
|
+
);
|
|
744
|
+
const dbPayloadMode = resultMode === "compact" ? ("full" as const) : payloadMode;
|
|
745
|
+
const dbPage = await listSessionEventPage(db, workspaceId, sessionId, {
|
|
480
746
|
after,
|
|
481
747
|
...(before !== undefined ? { before } : {}),
|
|
482
748
|
limit,
|
|
749
|
+
direction,
|
|
750
|
+
payloadMode: dbPayloadMode,
|
|
751
|
+
includeTypes,
|
|
752
|
+
excludeTypes,
|
|
753
|
+
includeClasses: latestClass ? [latestClass] : includeClasses,
|
|
754
|
+
excludeClasses,
|
|
755
|
+
...(mode === "monitoring" ? { defaultExcludeTypes: SESSION_EVENT_RAW_DELTA_TYPES } : {}),
|
|
756
|
+
...(latestClass ? { authoritativeLatest: true } : {}),
|
|
483
757
|
});
|
|
484
|
-
|
|
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
|
+
}
|
|
777
|
+
const projected = compact ? coalesceSessionEventDeltas(events) : events;
|
|
778
|
+
const page = boundSessionEventHttpPage(projected, {
|
|
779
|
+
direction,
|
|
780
|
+
});
|
|
781
|
+
const hasMore = dbPage.hasMore || page.truncated;
|
|
782
|
+
c.header("X-OpenGeni-Page-Bytes", String(page.bytes));
|
|
783
|
+
c.header("X-OpenGeni-Page-Max-Bytes", String(1024 * 1024));
|
|
784
|
+
c.header("X-OpenGeni-Page-Truncated", String(hasMore));
|
|
785
|
+
c.header("X-OpenGeni-Has-More", String(hasMore));
|
|
786
|
+
c.header("X-OpenGeni-Event-Mode", mode);
|
|
787
|
+
c.header("X-OpenGeni-Event-Direction", direction);
|
|
788
|
+
c.header("X-OpenGeni-Payload-Mode", payloadMode);
|
|
789
|
+
c.header("X-OpenGeni-Forensic-Exact", String(mode === "forensic" && payloadMode === "full"));
|
|
790
|
+
const coveredFirst = page.events[0]?.sequence;
|
|
791
|
+
const coveredLast = page.events.at(-1)?.sequence;
|
|
792
|
+
if (coveredFirst !== undefined) c.header("X-OpenGeni-Covered-First", String(coveredFirst));
|
|
793
|
+
if (coveredLast !== undefined) c.header("X-OpenGeni-Covered-Last", String(coveredLast));
|
|
794
|
+
const truncatedBy = page.truncated ? "http_bytes" : dbPage.truncatedBy;
|
|
795
|
+
if (truncatedBy) c.header("X-OpenGeni-Truncated-By", truncatedBy);
|
|
796
|
+
if (page.nextSequence !== null) {
|
|
797
|
+
c.header(
|
|
798
|
+
direction === "before" ? "X-OpenGeni-Next-Before" : "X-OpenGeni-Next-After",
|
|
799
|
+
String(page.nextSequence),
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
return c.json(page.events);
|
|
485
803
|
});
|
|
486
804
|
|
|
487
805
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/events/stream", async (c) => {
|
|
488
806
|
const workspaceId = c.req.param("workspaceId");
|
|
489
|
-
await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
807
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
490
808
|
const sessionId = c.req.param("sessionId");
|
|
809
|
+
let authorization;
|
|
810
|
+
try {
|
|
811
|
+
authorization = await requireSessionAuthorization(deps, grant, {
|
|
812
|
+
sessionId,
|
|
813
|
+
operation: "session.stream.read",
|
|
814
|
+
surface: "stream",
|
|
815
|
+
});
|
|
816
|
+
} catch (error) {
|
|
817
|
+
throw sessionAuthorizationHttpError(error);
|
|
818
|
+
}
|
|
491
819
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
492
820
|
const after = Number(c.req.query("after") ?? c.req.header("Last-Event-ID") ?? 0);
|
|
493
821
|
return sseSessionStream(
|
|
@@ -497,6 +825,22 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
497
825
|
sessionId,
|
|
498
826
|
Number.isFinite(after) ? after : 0,
|
|
499
827
|
c.req.raw.signal,
|
|
828
|
+
{
|
|
829
|
+
observability: deps.observability,
|
|
830
|
+
...(authorization
|
|
831
|
+
? {
|
|
832
|
+
reauthorizeAfterMs:
|
|
833
|
+
authorization.reauthorizeAfterMs ?? SESSION_AUTHORIZATION_DEFAULT_REAUTHORIZE_MS,
|
|
834
|
+
reauthorize: async () => {
|
|
835
|
+
await requireSessionAuthorization(deps, grant, {
|
|
836
|
+
sessionId,
|
|
837
|
+
operation: "session.stream.read",
|
|
838
|
+
surface: "stream",
|
|
839
|
+
});
|
|
840
|
+
},
|
|
841
|
+
}
|
|
842
|
+
: {}),
|
|
843
|
+
},
|
|
500
844
|
);
|
|
501
845
|
});
|
|
502
846
|
|
|
@@ -516,7 +860,7 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
516
860
|
const sessionId = c.req.param("sessionId");
|
|
517
861
|
const snapshot = await getSessionQueueSnapshot(db, workspaceId, sessionId);
|
|
518
862
|
if (!snapshot) throw new HTTPException(404, { message: "session not found" });
|
|
519
|
-
return c.json(snapshot);
|
|
863
|
+
return c.json(projectQueueSnapshot(snapshot, sessionId, relatedSessionAccessFor(c)));
|
|
520
864
|
});
|
|
521
865
|
|
|
522
866
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/queue/:turnId/move", async (c) => {
|
|
@@ -526,14 +870,21 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
526
870
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
527
871
|
const payload = MoveSessionQueueItemRequest.parse(await c.req.json());
|
|
528
872
|
try {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
873
|
+
const response = await moveHumanQueuePrompt(
|
|
874
|
+
deps,
|
|
875
|
+
{
|
|
876
|
+
accountId: grant.accountId,
|
|
877
|
+
workspaceId,
|
|
878
|
+
sessionId,
|
|
879
|
+
subjectId: grant.subjectId,
|
|
880
|
+
},
|
|
881
|
+
c.req.param("turnId"),
|
|
882
|
+
payload,
|
|
536
883
|
);
|
|
884
|
+
return c.json({
|
|
885
|
+
...response,
|
|
886
|
+
snapshot: projectQueueSnapshot(response.snapshot, sessionId, relatedSessionAccessFor(c)),
|
|
887
|
+
});
|
|
537
888
|
} catch (error) {
|
|
538
889
|
return commandConflictResponse(c, error);
|
|
539
890
|
}
|
|
@@ -546,14 +897,21 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
546
897
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
547
898
|
const payload = EditSessionQueueItemRequest.parse(await c.req.json());
|
|
548
899
|
try {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
900
|
+
const response = await editHumanQueuePrompt(
|
|
901
|
+
deps,
|
|
902
|
+
{
|
|
903
|
+
accountId: grant.accountId,
|
|
904
|
+
workspaceId,
|
|
905
|
+
sessionId,
|
|
906
|
+
subjectId: grant.subjectId,
|
|
907
|
+
},
|
|
908
|
+
c.req.param("turnId"),
|
|
909
|
+
payload,
|
|
556
910
|
);
|
|
911
|
+
return c.json({
|
|
912
|
+
...response,
|
|
913
|
+
snapshot: projectQueueSnapshot(response.snapshot, sessionId, relatedSessionAccessFor(c)),
|
|
914
|
+
});
|
|
557
915
|
} catch (error) {
|
|
558
916
|
return commandConflictResponse(c, error);
|
|
559
917
|
}
|
|
@@ -566,14 +924,21 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
566
924
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
567
925
|
const payload = SteerSessionQueueItemRequest.parse(await c.req.json());
|
|
568
926
|
try {
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
927
|
+
const response = await steerHumanQueuePrompt(
|
|
928
|
+
deps,
|
|
929
|
+
{
|
|
930
|
+
accountId: grant.accountId,
|
|
931
|
+
workspaceId,
|
|
932
|
+
sessionId,
|
|
933
|
+
subjectId: grant.subjectId,
|
|
934
|
+
},
|
|
935
|
+
c.req.param("turnId"),
|
|
936
|
+
payload,
|
|
576
937
|
);
|
|
938
|
+
return c.json({
|
|
939
|
+
...response,
|
|
940
|
+
snapshot: projectQueueSnapshot(response.snapshot, sessionId, relatedSessionAccessFor(c)),
|
|
941
|
+
});
|
|
577
942
|
} catch (error) {
|
|
578
943
|
return commandConflictResponse(c, error);
|
|
579
944
|
}
|
|
@@ -586,14 +951,21 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
586
951
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
587
952
|
const payload = DeleteSessionQueueItemRequest.parse(await c.req.json());
|
|
588
953
|
try {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
954
|
+
const response = await deleteHumanQueuePrompt(
|
|
955
|
+
deps,
|
|
956
|
+
{
|
|
957
|
+
accountId: grant.accountId,
|
|
958
|
+
workspaceId,
|
|
959
|
+
sessionId,
|
|
960
|
+
subjectId: grant.subjectId,
|
|
961
|
+
},
|
|
962
|
+
c.req.param("turnId"),
|
|
963
|
+
payload,
|
|
596
964
|
);
|
|
965
|
+
return c.json({
|
|
966
|
+
...response,
|
|
967
|
+
snapshot: projectQueueSnapshot(response.snapshot, sessionId, relatedSessionAccessFor(c)),
|
|
968
|
+
});
|
|
597
969
|
} catch (error) {
|
|
598
970
|
return commandConflictResponse(c, error);
|
|
599
971
|
}
|
|
@@ -604,7 +976,7 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
604
976
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
605
977
|
const sessionId = c.req.param("sessionId");
|
|
606
978
|
return c.json(
|
|
607
|
-
await getHumanComposerDraft(
|
|
979
|
+
await getHumanComposerDraft(deps, {
|
|
608
980
|
accountId: grant.accountId,
|
|
609
981
|
workspaceId,
|
|
610
982
|
sessionId,
|
|
@@ -621,8 +993,13 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
621
993
|
try {
|
|
622
994
|
return c.json(
|
|
623
995
|
await saveHumanComposerDraft(
|
|
624
|
-
|
|
625
|
-
{
|
|
996
|
+
deps,
|
|
997
|
+
{
|
|
998
|
+
accountId: grant.accountId,
|
|
999
|
+
workspaceId,
|
|
1000
|
+
sessionId,
|
|
1001
|
+
subjectId: grant.subjectId,
|
|
1002
|
+
},
|
|
626
1003
|
payload,
|
|
627
1004
|
),
|
|
628
1005
|
);
|
|
@@ -634,16 +1011,33 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
634
1011
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/control", async (c) => {
|
|
635
1012
|
const workspaceId = c.req.param("workspaceId");
|
|
636
1013
|
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
|
|
1014
|
+
if (workspaceControlUtf8Bytes(grant.subjectId) > WORKSPACE_CONTROL_ACTOR_MAX_BYTES) {
|
|
1015
|
+
throw new HTTPException(400, { message: "workspace-control actor is too large" });
|
|
1016
|
+
}
|
|
637
1017
|
const sessionId = c.req.param("sessionId");
|
|
638
|
-
const
|
|
1018
|
+
const parsed = SessionControlRequest.safeParse(await c.req.json().catch(() => null));
|
|
1019
|
+
if (!parsed.success) {
|
|
1020
|
+
throw new HTTPException(400, { message: "invalid session control request" });
|
|
1021
|
+
}
|
|
639
1022
|
try {
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
1023
|
+
const response = await controlHumanSessionWorkstream(
|
|
1024
|
+
deps,
|
|
1025
|
+
{
|
|
1026
|
+
accountId: grant.accountId,
|
|
1027
|
+
workspaceId,
|
|
1028
|
+
sessionId,
|
|
1029
|
+
subjectId: grant.subjectId,
|
|
1030
|
+
},
|
|
1031
|
+
parsed.data,
|
|
646
1032
|
);
|
|
1033
|
+
return c.json({
|
|
1034
|
+
...response,
|
|
1035
|
+
effectiveControl: projectEffectiveControlForRelatedAccess(
|
|
1036
|
+
response.effectiveControl,
|
|
1037
|
+
sessionId,
|
|
1038
|
+
relatedSessionAccessFor(c),
|
|
1039
|
+
),
|
|
1040
|
+
});
|
|
647
1041
|
} catch (error) {
|
|
648
1042
|
return commandConflictResponse(c, error);
|
|
649
1043
|
}
|
|
@@ -658,6 +1052,7 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
658
1052
|
const payload = SteerSessionMessageRequest.parse(raw);
|
|
659
1053
|
const result = await acceptSessionUserMessage(deps, grant, workspaceId, sessionId, {
|
|
660
1054
|
text: payload.text,
|
|
1055
|
+
turnInstructions: payload.turnInstructions ?? null,
|
|
661
1056
|
resources: payload.resources,
|
|
662
1057
|
tools: payload.tools,
|
|
663
1058
|
toolsProvided: userMessagePayloadHasOwnProperty({ payload: raw }, "tools"),
|
|
@@ -681,9 +1076,27 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
681
1076
|
const sessionId = c.req.param("sessionId");
|
|
682
1077
|
const rawEvent = await c.req.json();
|
|
683
1078
|
const event = ClientSessionEvent.parse(rawEvent);
|
|
1079
|
+
const refinedOperation =
|
|
1080
|
+
event.type === "user.approvalDecision"
|
|
1081
|
+
? "session.approval.write"
|
|
1082
|
+
: event.type === "user.humanInputResponse"
|
|
1083
|
+
? "session.human_input.write"
|
|
1084
|
+
: null;
|
|
1085
|
+
if (refinedOperation) {
|
|
1086
|
+
try {
|
|
1087
|
+
await requireSessionAuthorization(deps, grant, {
|
|
1088
|
+
sessionId,
|
|
1089
|
+
operation: refinedOperation,
|
|
1090
|
+
surface: "http",
|
|
1091
|
+
});
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
throw sessionAuthorizationHttpError(error);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
684
1096
|
if (event.type === "user.message") {
|
|
685
1097
|
const { accepted } = await acceptSessionUserMessage(deps, grant, workspaceId, sessionId, {
|
|
686
1098
|
text: event.payload.text,
|
|
1099
|
+
turnInstructions: event.payload.turnInstructions ?? null,
|
|
687
1100
|
resources: event.payload.resources ?? [],
|
|
688
1101
|
tools: event.payload.tools ?? [],
|
|
689
1102
|
toolsProvided: userMessagePayloadHasOwnProperty(rawEvent, "tools"),
|
|
@@ -726,8 +1139,83 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
726
1139
|
});
|
|
727
1140
|
return c.json(accepted.event, 202);
|
|
728
1141
|
}
|
|
1142
|
+
|
|
1143
|
+
if (event.type === "user.humanInputResponse") {
|
|
1144
|
+
let accepted;
|
|
1145
|
+
try {
|
|
1146
|
+
accepted = await acceptSessionHumanInputResponse(db, {
|
|
1147
|
+
accountId: grant.accountId,
|
|
1148
|
+
workspaceId,
|
|
1149
|
+
sessionId,
|
|
1150
|
+
requestId: event.payload.requestId,
|
|
1151
|
+
response: event.payload.response,
|
|
1152
|
+
respondedBy: grant.subjectId,
|
|
1153
|
+
clientEventId: event.clientEventId ?? null,
|
|
1154
|
+
});
|
|
1155
|
+
} catch (error) {
|
|
1156
|
+
if (error instanceof HumanInputResponseValidationError) {
|
|
1157
|
+
throw new HTTPException(error.code === "SKIP_NOT_ALLOWED" ? 409 : 422, {
|
|
1158
|
+
message: error.message,
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
throw error;
|
|
1162
|
+
}
|
|
1163
|
+
if (accepted.action === "not_found") {
|
|
1164
|
+
throw new HTTPException(404, { message: "human-input request not found" });
|
|
1165
|
+
}
|
|
1166
|
+
await publishDurableSessionEvents(bus, workspaceId, sessionId, accepted.events);
|
|
1167
|
+
if (accepted.workflowWakeRevision !== null) {
|
|
1168
|
+
await workflowClient.signalApprovalDecision({
|
|
1169
|
+
accountId: grant.accountId,
|
|
1170
|
+
workspaceId,
|
|
1171
|
+
sessionId,
|
|
1172
|
+
eventId: accepted.events[0]?.id ?? event.payload.requestId,
|
|
1173
|
+
workflowId: workflowIdForSession(sessionId),
|
|
1174
|
+
workflowWakeRevision: accepted.workflowWakeRevision,
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
if (accepted.action === "conflict") {
|
|
1178
|
+
throw new HTTPException(409, {
|
|
1179
|
+
message: `human-input request is ${accepted.request.status}`,
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
return c.json(accepted.event, 202);
|
|
1183
|
+
}
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/human-input-requests", async (c) => {
|
|
1187
|
+
const workspaceId = c.req.param("workspaceId");
|
|
1188
|
+
await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
1189
|
+
const sessionId = c.req.param("sessionId");
|
|
1190
|
+
await assertSessionExists(db, workspaceId, sessionId);
|
|
1191
|
+
const rawStatus = c.req.query("status");
|
|
1192
|
+
const status = rawStatus ? HumanInputRequestStatus.safeParse(rawStatus) : null;
|
|
1193
|
+
if (status && !status.success) {
|
|
1194
|
+
throw new HTTPException(400, { message: "invalid human-input request status" });
|
|
1195
|
+
}
|
|
1196
|
+
const requests = await listSessionHumanInputRequests(db, workspaceId, sessionId, {
|
|
1197
|
+
...(status?.success ? { status: status.data } : {}),
|
|
1198
|
+
});
|
|
1199
|
+
return c.json({ requests });
|
|
729
1200
|
});
|
|
730
1201
|
|
|
1202
|
+
app.get(
|
|
1203
|
+
"/v1/workspaces/:workspaceId/sessions/:sessionId/human-input-requests/:requestId",
|
|
1204
|
+
async (c) => {
|
|
1205
|
+
const workspaceId = c.req.param("workspaceId");
|
|
1206
|
+
await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
1207
|
+
const sessionId = c.req.param("sessionId");
|
|
1208
|
+
const request = await getSessionHumanInputRequest(
|
|
1209
|
+
db,
|
|
1210
|
+
workspaceId,
|
|
1211
|
+
sessionId,
|
|
1212
|
+
c.req.param("requestId"),
|
|
1213
|
+
);
|
|
1214
|
+
if (!request) throw new HTTPException(404, { message: "human-input request not found" });
|
|
1215
|
+
return c.json(request);
|
|
1216
|
+
},
|
|
1217
|
+
);
|
|
1218
|
+
|
|
731
1219
|
// ── API-direct stream capabilities + viewer attach (P1.4) ─────────────────
|
|
732
1220
|
//
|
|
733
1221
|
// All IN-PROCESS: capability negotiation reads the descriptor + the group
|
|
@@ -781,6 +1269,7 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
781
1269
|
{ workspaceId, sandboxGroupId: session.sandboxGroupId },
|
|
782
1270
|
);
|
|
783
1271
|
const { shared, sharedSessionIds } = await resolveSharedExposure(workspaceId, session);
|
|
1272
|
+
const visibleSharedSessionIds = relatedSessionAccessFor(c) === "root" ? sharedSessionIds : [];
|
|
784
1273
|
// Per-principal acknowledgment: A acknowledging does not consent for B. The
|
|
785
1274
|
// un-redacted desktop stream ALWAYS requires the un-redacted ack; a shared box
|
|
786
1275
|
// ADDITIONALLY requires the shared-exposure ack. Both must match the POST
|
|
@@ -872,13 +1361,13 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
872
1361
|
// tracks the desktop tier + a desktop-capable backend.
|
|
873
1362
|
computerUseEnabled: settings.computerUseEnabled,
|
|
874
1363
|
computerUseReadOnly: settings.computerUseReadOnly,
|
|
875
|
-
// Graceful degrade (
|
|
1364
|
+
// Graceful degrade (stream-token availability contract): if desktop is enabled but no stream-token
|
|
876
1365
|
// secret is resolvable, the desktop cell reports transport:null rather
|
|
877
1366
|
// than advertising a plane we can never authorize.
|
|
878
1367
|
streamTokenSecretAvailable: !streamTokenDegraded(settings),
|
|
879
1368
|
desktopAcknowledged: acknowledged,
|
|
880
1369
|
shared,
|
|
881
|
-
sharedSessionIds,
|
|
1370
|
+
sharedSessionIds: visibleSharedSessionIds,
|
|
882
1371
|
// The minted live address (null when not unlocked/degraded). The resolver
|
|
883
1372
|
// only folds it in when the desktop gates pass + the ack is present.
|
|
884
1373
|
...(desktopStream
|
|
@@ -960,7 +1449,9 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
960
1449
|
}
|
|
961
1450
|
const parsed = AcknowledgeStreamRequest.safeParse(await c.req.json().catch(() => ({})));
|
|
962
1451
|
if (!parsed.success) {
|
|
963
|
-
throw new HTTPException(400, {
|
|
1452
|
+
throw new HTTPException(400, {
|
|
1453
|
+
message: "invalid stream acknowledgment request",
|
|
1454
|
+
});
|
|
964
1455
|
}
|
|
965
1456
|
const recorded = await recordStreamAcknowledgment(db, {
|
|
966
1457
|
accountId: grant.accountId,
|
|
@@ -996,7 +1487,9 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
996
1487
|
}
|
|
997
1488
|
const parsed = AttachViewerRequest.safeParse(await c.req.json().catch(() => ({})));
|
|
998
1489
|
if (!parsed.success) {
|
|
999
|
-
throw new HTTPException(400, {
|
|
1490
|
+
throw new HTTPException(400, {
|
|
1491
|
+
message: "invalid viewer attach request",
|
|
1492
|
+
});
|
|
1000
1493
|
}
|
|
1001
1494
|
// Consent gate (P3.2 / addendum E.1): ONLY the un-redacted DESKTOP pixel plane
|
|
1002
1495
|
// requires the calling principal's acknowledgment (recorded per group+subject;
|
|
@@ -1017,10 +1510,14 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1017
1510
|
subjectId: grant.subjectId,
|
|
1018
1511
|
});
|
|
1019
1512
|
if (!ack?.acknowledgedUnredacted) {
|
|
1020
|
-
throw new HTTPException(409, {
|
|
1513
|
+
throw new HTTPException(409, {
|
|
1514
|
+
message: "stream_acknowledgment_required",
|
|
1515
|
+
});
|
|
1021
1516
|
}
|
|
1022
1517
|
if (shared && !ack.acknowledgedShared) {
|
|
1023
|
-
throw new HTTPException(409, {
|
|
1518
|
+
throw new HTTPException(409, {
|
|
1519
|
+
message: "shared_acknowledgment_required",
|
|
1520
|
+
});
|
|
1024
1521
|
}
|
|
1025
1522
|
}
|
|
1026
1523
|
// SELFHOSTED ACTIVE: when the session's active sandbox is selfhosted, skip
|
|
@@ -1177,7 +1674,9 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1177
1674
|
}
|
|
1178
1675
|
const parsed = ViewerHeartbeatRequest.safeParse(await c.req.json().catch(() => ({})));
|
|
1179
1676
|
if (!parsed.success) {
|
|
1180
|
-
throw new HTTPException(400, {
|
|
1677
|
+
throw new HTTPException(400, {
|
|
1678
|
+
message: "viewer heartbeat requires { leaseEpoch }",
|
|
1679
|
+
});
|
|
1181
1680
|
}
|
|
1182
1681
|
const alive = await heartbeatViewer(
|
|
1183
1682
|
{ db, settings },
|
|
@@ -1240,7 +1739,10 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1240
1739
|
idleGraceMs: settings.sandboxIdleGraceMs,
|
|
1241
1740
|
});
|
|
1242
1741
|
// null ⇒ the lease was already cold-and-reaped (revoke is an idempotent no-op).
|
|
1243
|
-
return c.json({
|
|
1742
|
+
return c.json({
|
|
1743
|
+
liveness: result?.liveness ?? null,
|
|
1744
|
+
refcount: result?.refcount ?? null,
|
|
1745
|
+
});
|
|
1244
1746
|
},
|
|
1245
1747
|
);
|
|
1246
1748
|
|
|
@@ -1281,12 +1783,19 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1281
1783
|
if (!session) {
|
|
1282
1784
|
throw new HTTPException(404, { message: "session not found" });
|
|
1283
1785
|
}
|
|
1284
|
-
return {
|
|
1786
|
+
return {
|
|
1787
|
+
accountId: grant.accountId,
|
|
1788
|
+
workspaceId,
|
|
1789
|
+
session,
|
|
1790
|
+
subjectId: grant.subjectId,
|
|
1791
|
+
};
|
|
1285
1792
|
}
|
|
1286
1793
|
|
|
1287
1794
|
async function parseChannelABody<T>(
|
|
1288
1795
|
c: Context,
|
|
1289
|
-
schema: {
|
|
1796
|
+
schema: {
|
|
1797
|
+
safeParse: (v: unknown) => { success: true; data: T } | { success: false };
|
|
1798
|
+
},
|
|
1290
1799
|
): Promise<T> {
|
|
1291
1800
|
const raw = await c.req.json().catch(() => undefined);
|
|
1292
1801
|
const result = schema.safeParse(raw ?? {});
|
|
@@ -1416,7 +1925,9 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1416
1925
|
const sessionId = c.req.param("sessionId") ?? "";
|
|
1417
1926
|
const path = c.req.query("path");
|
|
1418
1927
|
if (!path) {
|
|
1419
|
-
throw new HTTPException(400, {
|
|
1928
|
+
throw new HTTPException(400, {
|
|
1929
|
+
message: "path query parameter is required",
|
|
1930
|
+
});
|
|
1420
1931
|
}
|
|
1421
1932
|
const session = await getSession(db, workspaceId, sessionId);
|
|
1422
1933
|
if (!session) {
|
|
@@ -1431,7 +1942,9 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1431
1942
|
if (revisionParam !== undefined && revisionParam !== "") {
|
|
1432
1943
|
const revision = Number(revisionParam);
|
|
1433
1944
|
if (!Number.isInteger(revision) || revision < 0) {
|
|
1434
|
-
throw new HTTPException(400, {
|
|
1945
|
+
throw new HTTPException(400, {
|
|
1946
|
+
message: "revision must be a non-negative integer",
|
|
1947
|
+
});
|
|
1435
1948
|
}
|
|
1436
1949
|
row = await workspaceCaptureAtRevision(db, workspaceId, sessionId, revision);
|
|
1437
1950
|
} else {
|
|
@@ -1503,7 +2016,9 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1503
2016
|
throw new HTTPException(404, { message: "pty not found or closed" });
|
|
1504
2017
|
}
|
|
1505
2018
|
if (pty.execSessionId === null) {
|
|
1506
|
-
throw new HTTPException(409, {
|
|
2019
|
+
throw new HTTPException(409, {
|
|
2020
|
+
message: "interactive terminal unsupported on this backend",
|
|
2021
|
+
});
|
|
1507
2022
|
}
|
|
1508
2023
|
let seq = 1;
|
|
1509
2024
|
await withChannelA({ db, settings, bus }, ctx, async ({ service }) => {
|
|
@@ -1565,7 +2080,11 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1565
2080
|
workspaceId: ctx.workspaceId,
|
|
1566
2081
|
ptyId: req.ptyId,
|
|
1567
2082
|
});
|
|
1568
|
-
const exited: TerminalPtyExitedPayload = {
|
|
2083
|
+
const exited: TerminalPtyExitedPayload = {
|
|
2084
|
+
ptyId: req.ptyId,
|
|
2085
|
+
exitCode: 0,
|
|
2086
|
+
reason: "exit",
|
|
2087
|
+
};
|
|
1569
2088
|
await appendAndPublishEvents(db, bus, ctx.workspaceId, ctx.session.id, [
|
|
1570
2089
|
{ type: "terminal.pty.exited", payload: exited },
|
|
1571
2090
|
]);
|
|
@@ -1574,14 +2093,160 @@ export function registerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1574
2093
|
});
|
|
1575
2094
|
}
|
|
1576
2095
|
|
|
1577
|
-
function eventListLimit(raw: string | undefined, max = 2000): number {
|
|
1578
|
-
const limit = Number(raw ??
|
|
2096
|
+
function eventListLimit(raw: string | undefined, max = 2000, fallback = 500): number {
|
|
2097
|
+
const limit = Number(raw ?? fallback);
|
|
1579
2098
|
if (!Number.isFinite(limit)) {
|
|
1580
|
-
return
|
|
2099
|
+
return fallback;
|
|
1581
2100
|
}
|
|
1582
2101
|
return Math.min(max, Math.max(1, Math.floor(limit)));
|
|
1583
2102
|
}
|
|
1584
2103
|
|
|
2104
|
+
/**
|
|
2105
|
+
* Map every mounted session-addressed HTTP path to the host-neutral operation
|
|
2106
|
+
* the embedding port authorizes. Returning null is deliberately fail-closed in
|
|
2107
|
+
* host-managed mode; standalone deployments never consult this classifier.
|
|
2108
|
+
*/
|
|
2109
|
+
export function sessionAuthorizationOperationForHttp(
|
|
2110
|
+
method: string,
|
|
2111
|
+
pathname: string,
|
|
2112
|
+
sessionId: string,
|
|
2113
|
+
): SessionAuthorizationOperation | null {
|
|
2114
|
+
const marker = `/sessions/${sessionId}`;
|
|
2115
|
+
const markerAt = pathname.indexOf(marker);
|
|
2116
|
+
if (markerAt < 0) return null;
|
|
2117
|
+
const suffix = pathname.slice(markerAt + marker.length);
|
|
2118
|
+
const verb = method.toUpperCase();
|
|
2119
|
+
|
|
2120
|
+
if (suffix === "") {
|
|
2121
|
+
if (verb === "GET") return "session.read";
|
|
2122
|
+
if (verb === "PATCH") return "session.title.write";
|
|
2123
|
+
return null;
|
|
2124
|
+
}
|
|
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
|
+
}
|
|
2129
|
+
if (suffix === "/lineage" && verb === "GET") return "session.lineage.read";
|
|
2130
|
+
if (suffix === "/codex-account" && verb === "POST") {
|
|
2131
|
+
return "session.codex_account.write";
|
|
2132
|
+
}
|
|
2133
|
+
if (suffix === "/goal") {
|
|
2134
|
+
return verb === "GET"
|
|
2135
|
+
? "session.goal.read"
|
|
2136
|
+
: ["PATCH", "DELETE"].includes(verb)
|
|
2137
|
+
? "session.goal.write"
|
|
2138
|
+
: null;
|
|
2139
|
+
}
|
|
2140
|
+
if (suffix === "/context/clear" || suffix === "/context/compact") {
|
|
2141
|
+
return verb === "POST" ? "session.context.write" : null;
|
|
2142
|
+
}
|
|
2143
|
+
if (suffix === "/events/stream" && verb === "GET") return "session.stream.read";
|
|
2144
|
+
if (suffix === "/events") {
|
|
2145
|
+
if (verb === "GET") return "session.events.read";
|
|
2146
|
+
if (verb === "POST") return "session.append";
|
|
2147
|
+
return null;
|
|
2148
|
+
}
|
|
2149
|
+
if (suffix === "/turns" && verb === "GET") return "session.turns.read";
|
|
2150
|
+
if (suffix === "/queue" && verb === "GET") return "session.queue.read";
|
|
2151
|
+
if (suffix.startsWith("/queue/") && verb === "POST") return "session.queue.control";
|
|
2152
|
+
if (suffix === "/composer-draft") {
|
|
2153
|
+
if (verb === "GET") return "session.composer.read";
|
|
2154
|
+
if (verb === "PUT") return "session.composer.write";
|
|
2155
|
+
return null;
|
|
2156
|
+
}
|
|
2157
|
+
if (suffix === "/control" && verb === "POST") return "session.control";
|
|
2158
|
+
if (suffix === "/steer" && verb === "POST") return "session.steer";
|
|
2159
|
+
if (suffix === "/human-input-requests" && verb === "GET") {
|
|
2160
|
+
return "session.human_input.read";
|
|
2161
|
+
}
|
|
2162
|
+
if (suffix.startsWith("/human-input-requests/") && verb === "GET") {
|
|
2163
|
+
return "session.human_input.read";
|
|
2164
|
+
}
|
|
2165
|
+
if (suffix === "/stream-capabilities" && verb === "GET") return "session.viewer.read";
|
|
2166
|
+
if (suffix === "/stream-capabilities/acknowledge" && verb === "POST") {
|
|
2167
|
+
return "session.stream.acknowledge";
|
|
2168
|
+
}
|
|
2169
|
+
if (suffix === "/viewers" && verb === "POST") return "session.viewer.control";
|
|
2170
|
+
if (suffix.startsWith("/viewers/") && ["POST", "DELETE"].includes(verb)) {
|
|
2171
|
+
return "session.viewer.control";
|
|
2172
|
+
}
|
|
2173
|
+
if (suffix === "/fs/list" || suffix === "/fs/read") {
|
|
2174
|
+
return verb === "POST" ? "session.files.read" : null;
|
|
2175
|
+
}
|
|
2176
|
+
if (["/fs/write", "/fs/delete", "/fs/move", "/fs/mkdir"].includes(suffix)) {
|
|
2177
|
+
return verb === "POST" ? "session.files.write" : null;
|
|
2178
|
+
}
|
|
2179
|
+
if (suffix.startsWith("/git/") && verb === "POST") return "session.git.read";
|
|
2180
|
+
if ((suffix === "/workspace/capture" || suffix === "/workspace/capture/file") && verb === "GET") {
|
|
2181
|
+
return "session.capture.read";
|
|
2182
|
+
}
|
|
2183
|
+
if (suffix === "/terminal/exec" && verb === "POST") return "session.terminal.control";
|
|
2184
|
+
if (suffix === "/terminal/pty" && verb === "POST") return "session.terminal.control";
|
|
2185
|
+
if (suffix.startsWith("/terminal/pty/") && verb === "POST") {
|
|
2186
|
+
return "session.terminal.control";
|
|
2187
|
+
}
|
|
2188
|
+
return null;
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
function sessionAuthorizationHttpError(error: unknown): HTTPException {
|
|
2192
|
+
if (error instanceof SessionAuthorizationDeniedError) {
|
|
2193
|
+
return new HTTPException(404, { message: "session not found" });
|
|
2194
|
+
}
|
|
2195
|
+
if (error instanceof SessionAuthorizationUnavailableError) {
|
|
2196
|
+
return new HTTPException(503, { message: "session authorization is unavailable" });
|
|
2197
|
+
}
|
|
2198
|
+
if (error instanceof HTTPException) return error;
|
|
2199
|
+
throw error;
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
function eventEnumValue<T extends string>(
|
|
2203
|
+
raw: string | undefined,
|
|
2204
|
+
schema: { safeParse(value: unknown): { success: boolean; data?: T } },
|
|
2205
|
+
name: string,
|
|
2206
|
+
fallback: T,
|
|
2207
|
+
): T;
|
|
2208
|
+
function eventEnumValue<T extends string>(
|
|
2209
|
+
raw: string | undefined,
|
|
2210
|
+
schema: { safeParse(value: unknown): { success: boolean; data?: T } },
|
|
2211
|
+
name: string,
|
|
2212
|
+
fallback: undefined,
|
|
2213
|
+
): T | undefined;
|
|
2214
|
+
function eventEnumValue<T extends string>(
|
|
2215
|
+
raw: string | undefined,
|
|
2216
|
+
schema: { safeParse(value: unknown): { success: boolean; data?: T } },
|
|
2217
|
+
name: string,
|
|
2218
|
+
fallback: T | undefined,
|
|
2219
|
+
): T | undefined {
|
|
2220
|
+
if (raw === undefined) return fallback;
|
|
2221
|
+
const parsed = schema.safeParse(raw);
|
|
2222
|
+
if (!parsed.success) {
|
|
2223
|
+
throw new HTTPException(400, { message: `${name} is invalid` });
|
|
2224
|
+
}
|
|
2225
|
+
return parsed.data as T;
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
function eventEnumList<T extends string>(
|
|
2229
|
+
raw: string | undefined,
|
|
2230
|
+
schema: { safeParse(value: unknown): { success: boolean; data?: T } },
|
|
2231
|
+
name: string,
|
|
2232
|
+
): T[] {
|
|
2233
|
+
if (raw === undefined || raw.trim() === "") return [];
|
|
2234
|
+
const values = raw
|
|
2235
|
+
.split(",")
|
|
2236
|
+
.map((value) => value.trim())
|
|
2237
|
+
.filter(Boolean);
|
|
2238
|
+
if (values.length > 100) {
|
|
2239
|
+
throw new HTTPException(400, { message: `${name} accepts at most 100 values` });
|
|
2240
|
+
}
|
|
2241
|
+
return values.map((value) => {
|
|
2242
|
+
const parsed = schema.safeParse(value);
|
|
2243
|
+
if (!parsed.success) {
|
|
2244
|
+
throw new HTTPException(400, { message: `${name} contains an invalid value` });
|
|
2245
|
+
}
|
|
2246
|
+
return parsed.data as T;
|
|
2247
|
+
});
|
|
2248
|
+
}
|
|
2249
|
+
|
|
1585
2250
|
function sessionListQuery(
|
|
1586
2251
|
query: Record<string, string>,
|
|
1587
2252
|
allowCursor = true,
|
|
@@ -1611,7 +2276,9 @@ function sessionListQuery(
|
|
|
1611
2276
|
}
|
|
1612
2277
|
const search = query.search?.trim();
|
|
1613
2278
|
if (search && search.length > 200) {
|
|
1614
|
-
throw new HTTPException(400, {
|
|
2279
|
+
throw new HTTPException(400, {
|
|
2280
|
+
message: "search must be at most 200 characters",
|
|
2281
|
+
});
|
|
1615
2282
|
}
|
|
1616
2283
|
return {
|
|
1617
2284
|
limit: query.limit,
|
|
@@ -1675,3 +2342,44 @@ function commandConflictResponse(c: Context, error: unknown): Response {
|
|
|
1675
2342
|
}
|
|
1676
2343
|
throw error;
|
|
1677
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
|
+
}
|