@opengeni/api-router 0.5.7 → 0.7.3
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-EYYTFA7N.js} +2396 -676
- package/dist/chunk-EYYTFA7N.js.map +1 -0
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/package.json +11 -11
- package/src/app.ts +47 -4
- 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/mcp/server.ts +774 -146
- package/src/mcp/session-view.ts +622 -203
- package/src/mcp/toolspace.ts +110 -25
- package/src/routes/codex.ts +17 -14
- package/src/routes/enrollments.ts +2 -2
- 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 +639 -76
- package/src/routes/workspace-capture.ts +56 -38
- package/src/routes/workspaces.ts +30 -11
- 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 +1 -1
- package/dist/chunk-HBEJMWD3.js.map +0 -1
|
@@ -22,7 +22,14 @@ import { Hono } from "hono";
|
|
|
22
22
|
import { bodyLimit } from "hono/body-limit";
|
|
23
23
|
import { cors } from "hono/cors";
|
|
24
24
|
import { HTTPException as HTTPException24 } from "hono/http-exception";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
hasPermission as hasPermission4,
|
|
27
|
+
requireAccessGrant as requireAccessGrant17,
|
|
28
|
+
requirePermission,
|
|
29
|
+
requireSessionAuthorization as requireSessionAuthorization3,
|
|
30
|
+
SessionAuthorizationDeniedError as SessionAuthorizationDeniedError2,
|
|
31
|
+
SessionAuthorizationUnavailableError as SessionAuthorizationUnavailableError2
|
|
32
|
+
} from "@opengeni/core";
|
|
26
33
|
|
|
27
34
|
// src/auth/managed-auth.ts
|
|
28
35
|
import { ensureManagedAccessForUser } from "@opengeni/db";
|
|
@@ -280,6 +287,13 @@ import { requireLimit as requireLimit8 } from "@opengeni/core";
|
|
|
280
287
|
// src/mcp/server.ts
|
|
281
288
|
import {
|
|
282
289
|
CreateScheduledTaskRequest,
|
|
290
|
+
defaultRepositoryMountPath,
|
|
291
|
+
SESSION_EVENT_RAW_DELTA_TYPES,
|
|
292
|
+
SessionEventPayloadMode,
|
|
293
|
+
SessionEventReadDirection,
|
|
294
|
+
SessionEventReadMode,
|
|
295
|
+
SessionEventSemanticClass,
|
|
296
|
+
SessionEventType,
|
|
283
297
|
SessionMcpCredentialUpdateInput,
|
|
284
298
|
VariableSetVariableName,
|
|
285
299
|
UpdateScheduledTaskRequest
|
|
@@ -297,12 +311,16 @@ import {
|
|
|
297
311
|
getSessionTurn,
|
|
298
312
|
getVariableSet,
|
|
299
313
|
getVariableSetByName,
|
|
300
|
-
|
|
314
|
+
areGitHubRepositoriesAllowedForWorkspace,
|
|
301
315
|
listScheduledTaskRuns,
|
|
302
316
|
listScheduledTasks,
|
|
303
|
-
|
|
304
|
-
|
|
317
|
+
listSessionEventPage,
|
|
318
|
+
listSessionDiscoverySummaries,
|
|
319
|
+
projectEffectiveControlForRelatedAccess,
|
|
320
|
+
projectSessionForRelatedAccess,
|
|
305
321
|
listRigs,
|
|
322
|
+
listRigChangeMonitoringSummaries,
|
|
323
|
+
listRigVersionMonitoringSummaries,
|
|
306
324
|
listSocialConnections,
|
|
307
325
|
listSocialPosts,
|
|
308
326
|
listVariableSets,
|
|
@@ -314,6 +332,7 @@ import {
|
|
|
314
332
|
requireSession,
|
|
315
333
|
saveWorkspaceMemory,
|
|
316
334
|
searchWorkspaceMemories,
|
|
335
|
+
serializeEffectiveSessionControl,
|
|
317
336
|
setSessionGoalStatus,
|
|
318
337
|
setVariableSetVariable,
|
|
319
338
|
updateScheduledTask,
|
|
@@ -325,19 +344,54 @@ import {
|
|
|
325
344
|
import { appendAndPublishEvents } from "@opengeni/events";
|
|
326
345
|
import {
|
|
327
346
|
createGitHubAppInstallationToken,
|
|
328
|
-
createSignedState,
|
|
329
347
|
GitHubAppConfigurationError,
|
|
330
|
-
githubAppMissingSettings
|
|
331
|
-
listGitHubAppRepositories,
|
|
332
|
-
stateMaxAgeSeconds
|
|
348
|
+
githubAppMissingSettings
|
|
333
349
|
} from "@opengeni/github";
|
|
334
350
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
335
351
|
import * as z4 from "zod/v4";
|
|
336
|
-
import {
|
|
352
|
+
import {
|
|
353
|
+
hasPermission,
|
|
354
|
+
requireSessionAuthorization,
|
|
355
|
+
requireSessionAuthorizationListScope
|
|
356
|
+
} from "@opengeni/core";
|
|
337
357
|
import { recordWorkspaceUsage, requireLimit } from "@opengeni/core";
|
|
358
|
+
|
|
359
|
+
// src/github-access.ts
|
|
360
|
+
import { listGitHubInstallationAccessForWorkspace } from "@opengeni/db";
|
|
361
|
+
import { listGitHubAppRepositories } from "@opengeni/github";
|
|
362
|
+
async function listWorkspaceGitHubInstallationBindings(deps, workspaceId) {
|
|
363
|
+
const installations = await listGitHubInstallationAccessForWorkspace(deps.db, workspaceId);
|
|
364
|
+
return installations.map((installation) => ({
|
|
365
|
+
installationId: installation.installationId,
|
|
366
|
+
accountLogin: installation.accountLogin,
|
|
367
|
+
accountType: installation.accountType,
|
|
368
|
+
repositoryScope: installation.repositoryScope,
|
|
369
|
+
repositoryCount: installation.repositoryIds.length,
|
|
370
|
+
createdAt: installation.createdAt,
|
|
371
|
+
updatedAt: installation.updatedAt
|
|
372
|
+
}));
|
|
373
|
+
}
|
|
374
|
+
async function listWorkspaceGitHubRepositories(deps, workspaceId) {
|
|
375
|
+
const access = await listGitHubInstallationAccessForWorkspace(deps.db, workspaceId);
|
|
376
|
+
if (access.length === 0) {
|
|
377
|
+
return [];
|
|
378
|
+
}
|
|
379
|
+
const installationIds = access.map((installation) => installation.installationId);
|
|
380
|
+
const repositories = deps.githubAppApi?.listRepositories ? await deps.githubAppApi.listRepositories({ installationIds }) : await listGitHubAppRepositories(deps.settings, { installationIds });
|
|
381
|
+
const accessByInstallation = new Map(
|
|
382
|
+
access.map((installation) => [installation.installationId, installation])
|
|
383
|
+
);
|
|
384
|
+
return repositories.filter((repository) => {
|
|
385
|
+
const installation = accessByInstallation.get(repository.installationId);
|
|
386
|
+
if (!installation) {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
return installation.repositoryScope === "all" || installation.repositoryIds.includes(repository.id);
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// src/mcp/server.ts
|
|
338
394
|
import {
|
|
339
|
-
listRigChangesForApi,
|
|
340
|
-
listRigVersionsForApi,
|
|
341
395
|
promoteVerifiedDefinitionEditChangeForApi,
|
|
342
396
|
proposeRigChangeForApi,
|
|
343
397
|
requireRigChangeForApi,
|
|
@@ -376,24 +430,13 @@ import {
|
|
|
376
430
|
} from "@opengeni/core";
|
|
377
431
|
|
|
378
432
|
// src/mcp/session-view.ts
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
return estimateTokensFromChars(safeStringify(value).length);
|
|
385
|
-
}
|
|
386
|
-
var DEFAULT_EVENT_CAP = {
|
|
387
|
-
perFieldChars: 2e3,
|
|
388
|
-
pageTokenBudget: 1e4,
|
|
389
|
-
headEvents: 8,
|
|
390
|
-
tailEvents: 8
|
|
391
|
-
};
|
|
392
|
-
var DEFAULT_SESSION_DETAIL_CHARS = 6e3;
|
|
433
|
+
import { measureSessionEventJson } from "@opengeni/contracts";
|
|
434
|
+
var SESSION_EVENT_MCP_MAX_BYTES = 64 * 1024;
|
|
435
|
+
var SESSION_EVENT_MCP_FIELD_MAX_CHARS = 4e3;
|
|
436
|
+
var SESSION_DETAIL_MCP_MAX_BYTES = 64 * 1024;
|
|
437
|
+
var RIG_DETAIL_MCP_MAX_BYTES = 64 * 1024;
|
|
393
438
|
function safeStringify(value) {
|
|
394
|
-
if (typeof value === "string")
|
|
395
|
-
return value;
|
|
396
|
-
}
|
|
439
|
+
if (typeof value === "string") return value;
|
|
397
440
|
try {
|
|
398
441
|
return JSON.stringify(value) ?? String(value);
|
|
399
442
|
} catch {
|
|
@@ -401,130 +444,510 @@ function safeStringify(value) {
|
|
|
401
444
|
}
|
|
402
445
|
}
|
|
403
446
|
function truncationMarker(droppedChars) {
|
|
404
|
-
return `\u2026[${droppedChars} chars
|
|
447
|
+
return `\u2026[${droppedChars} chars omitted from this model monitoring projection; request explicit forensic full mode for any retained audit preview; original source output may not have been retained]`;
|
|
405
448
|
}
|
|
406
449
|
function clampString(value, maxChars) {
|
|
407
|
-
if (value.length <= maxChars)
|
|
408
|
-
return value;
|
|
409
|
-
}
|
|
450
|
+
if (value.length <= maxChars) return value;
|
|
410
451
|
const dropped = value.length - maxChars;
|
|
411
452
|
const headChars = Math.max(0, Math.floor(maxChars * 0.7));
|
|
412
453
|
const tailChars = Math.max(0, maxChars - headChars);
|
|
413
|
-
const head = value.slice(0, headChars);
|
|
414
454
|
const tail = tailChars > 0 ? value.slice(value.length - tailChars) : "";
|
|
415
|
-
return `${
|
|
455
|
+
return `${value.slice(0, headChars)}${truncationMarker(dropped)}${tail}`;
|
|
416
456
|
}
|
|
417
457
|
function capPayloadValue(value, perFieldChars, depth = 0) {
|
|
418
|
-
if (typeof value === "string")
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
if (value === null || typeof value !== "object") {
|
|
422
|
-
return value;
|
|
423
|
-
}
|
|
424
|
-
if (depth >= 8) {
|
|
425
|
-
return clampString(safeStringify(value), perFieldChars);
|
|
426
|
-
}
|
|
458
|
+
if (typeof value === "string") return clampString(value, perFieldChars);
|
|
459
|
+
if (value === null || typeof value !== "object") return value;
|
|
460
|
+
if (depth >= 8) return clampString(safeStringify(value), perFieldChars);
|
|
427
461
|
const serializedLength = safeStringify(value).length;
|
|
428
|
-
if (serializedLength <= perFieldChars)
|
|
429
|
-
return value;
|
|
430
|
-
}
|
|
462
|
+
if (serializedLength <= perFieldChars) return value;
|
|
431
463
|
if (Array.isArray(value)) {
|
|
432
464
|
const mapped = value.map((entry) => capPayloadValue(entry, perFieldChars, depth + 1));
|
|
433
|
-
|
|
434
|
-
return mapped;
|
|
435
|
-
}
|
|
436
|
-
return clampString(safeStringify(value), perFieldChars);
|
|
465
|
+
return safeStringify(mapped).length <= perFieldChars * 2 ? mapped : clampString(safeStringify(value), perFieldChars);
|
|
437
466
|
}
|
|
438
467
|
const out = {};
|
|
439
468
|
for (const [key, entry] of Object.entries(value)) {
|
|
440
469
|
out[key] = capPayloadValue(entry, perFieldChars, depth + 1);
|
|
441
470
|
}
|
|
442
|
-
|
|
443
|
-
return out;
|
|
444
|
-
}
|
|
445
|
-
return clampString(safeStringify(value), perFieldChars);
|
|
471
|
+
return safeStringify(out).length <= perFieldChars * 4 ? out : clampString(safeStringify(value), perFieldChars);
|
|
446
472
|
}
|
|
447
473
|
function capEventPayload(event, perFieldChars) {
|
|
448
474
|
const cappedPayload = capPayloadValue(event.payload, perFieldChars);
|
|
449
|
-
|
|
450
|
-
|
|
475
|
+
return cappedPayload === event.payload ? event : { ...event, payload: cappedPayload };
|
|
476
|
+
}
|
|
477
|
+
function prettyJsonBytes(value) {
|
|
478
|
+
return Buffer.byteLength(JSON.stringify(value, null, 2), "utf8");
|
|
479
|
+
}
|
|
480
|
+
function setMeasuredBytes(page) {
|
|
481
|
+
let measured = page.bytes;
|
|
482
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
483
|
+
page.bytes = measured;
|
|
484
|
+
const next = prettyJsonBytes(page);
|
|
485
|
+
if (next === measured) return next;
|
|
486
|
+
measured = next;
|
|
487
|
+
}
|
|
488
|
+
page.bytes = measured;
|
|
489
|
+
return prettyJsonBytes(page);
|
|
490
|
+
}
|
|
491
|
+
function boundSessionEventMcpPage(input) {
|
|
492
|
+
const maxBytes = Math.max(8 * 1024, input.maxBytes ?? SESSION_EVENT_MCP_MAX_BYTES);
|
|
493
|
+
let payloadTrimmed = false;
|
|
494
|
+
const events = input.events.map((event) => {
|
|
495
|
+
const capped = capEventPayload(event, SESSION_EVENT_MCP_FIELD_MAX_CHARS);
|
|
496
|
+
if (capped !== event) payloadTrimmed = true;
|
|
497
|
+
return capped;
|
|
498
|
+
});
|
|
499
|
+
let modelRowsDropped = false;
|
|
500
|
+
const build = () => {
|
|
501
|
+
const first = events[0]?.sequence ?? null;
|
|
502
|
+
const last = events.at(-1)?.sequence ?? null;
|
|
503
|
+
const reasons = [];
|
|
504
|
+
if (input.sourceHasMore) {
|
|
505
|
+
reasons.push(input.sourceTruncatedBy === "bytes" ? "source_bytes" : "source_count");
|
|
506
|
+
}
|
|
507
|
+
if (payloadTrimmed) reasons.push("model_payload");
|
|
508
|
+
if (modelRowsDropped) reasons.push("model_bytes");
|
|
509
|
+
const nextAfter = input.direction === "after" ? last ?? input.after : null;
|
|
510
|
+
const nextBefore = input.direction === "before" ? first ?? input.before : null;
|
|
511
|
+
const page2 = {
|
|
512
|
+
mode: input.mode,
|
|
513
|
+
payloadMode: input.payloadMode,
|
|
514
|
+
direction: input.direction,
|
|
515
|
+
events: [...events],
|
|
516
|
+
coveredSequence: first === null || last === null ? null : { first, last },
|
|
517
|
+
nextAfter,
|
|
518
|
+
nextBefore,
|
|
519
|
+
hasMore: input.sourceHasMore || modelRowsDropped,
|
|
520
|
+
truncated: reasons.length > 0,
|
|
521
|
+
...reasons.length > 0 ? {
|
|
522
|
+
truncation: {
|
|
523
|
+
reasons,
|
|
524
|
+
omittedSide: input.direction,
|
|
525
|
+
resumeCursor: input.direction === "after" ? nextAfter : nextBefore
|
|
526
|
+
}
|
|
527
|
+
} : {},
|
|
528
|
+
bytes: 0,
|
|
529
|
+
maxBytes
|
|
530
|
+
};
|
|
531
|
+
setMeasuredBytes(page2);
|
|
532
|
+
return page2;
|
|
533
|
+
};
|
|
534
|
+
let page = build();
|
|
535
|
+
while (page.bytes > maxBytes && events.length > 0) {
|
|
536
|
+
if (input.direction === "before") events.shift();
|
|
537
|
+
else events.pop();
|
|
538
|
+
modelRowsDropped = true;
|
|
539
|
+
page = build();
|
|
540
|
+
}
|
|
541
|
+
if (page.bytes > maxBytes) {
|
|
542
|
+
throw new RangeError(`Session-event MCP metadata exceeds its ${maxBytes}-byte envelope`);
|
|
451
543
|
}
|
|
452
|
-
return
|
|
544
|
+
return page;
|
|
453
545
|
}
|
|
454
|
-
function
|
|
546
|
+
function modelStringProjection(value, maxBytes) {
|
|
547
|
+
const originalBytes = Buffer.byteLength(value, "utf8");
|
|
548
|
+
if (originalBytes <= maxBytes) {
|
|
549
|
+
return {
|
|
550
|
+
value,
|
|
551
|
+
fact: {
|
|
552
|
+
truncated: false,
|
|
553
|
+
originalBytes,
|
|
554
|
+
deliveredBytes: originalBytes,
|
|
555
|
+
originalChars: value.length
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
let omittedBytes = originalBytes - maxBytes;
|
|
560
|
+
let head = "";
|
|
561
|
+
let tail = "";
|
|
562
|
+
let marker = "";
|
|
563
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
564
|
+
marker = `\u2026[${omittedBytes} UTF-8 bytes omitted from model monitoring projection]\u2026`;
|
|
565
|
+
const contentBudget = Math.max(0, maxBytes - Buffer.byteLength(marker, "utf8"));
|
|
566
|
+
head = utf8Prefix(value, Math.floor(contentBudget * 0.7));
|
|
567
|
+
tail = utf8Suffix(value, contentBudget - Buffer.byteLength(head, "utf8"));
|
|
568
|
+
const exact = Math.max(
|
|
569
|
+
0,
|
|
570
|
+
originalBytes - Buffer.byteLength(head, "utf8") - Buffer.byteLength(tail, "utf8")
|
|
571
|
+
);
|
|
572
|
+
if (exact === omittedBytes) break;
|
|
573
|
+
omittedBytes = exact;
|
|
574
|
+
}
|
|
575
|
+
const projected = `${head}${marker}${tail}`;
|
|
455
576
|
return {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
note: `${droppedCount} event(s) (sequence ${firstDroppedSequence}\u2013${lastDroppedSequence}) omitted from this monitoring view to keep the response bounded. Page the gap with session_events after=${firstDroppedSequence - 1} limit=\u2026 if you need them verbatim, or read the worker's session notebook.`,
|
|
464
|
-
droppedCount,
|
|
465
|
-
omittedSequenceRange: [firstDroppedSequence, lastDroppedSequence]
|
|
466
|
-
},
|
|
467
|
-
occurredAt: template.occurredAt,
|
|
468
|
-
clientEventId: null,
|
|
469
|
-
turnId: null
|
|
577
|
+
value: projected,
|
|
578
|
+
fact: {
|
|
579
|
+
truncated: true,
|
|
580
|
+
originalBytes,
|
|
581
|
+
deliveredBytes: Buffer.byteLength(projected, "utf8"),
|
|
582
|
+
originalChars: value.length
|
|
583
|
+
}
|
|
470
584
|
};
|
|
471
585
|
}
|
|
472
|
-
function
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
586
|
+
function utf8Prefix(value, maxBytes) {
|
|
587
|
+
let index = 0;
|
|
588
|
+
let bytes = 0;
|
|
589
|
+
while (index < value.length) {
|
|
590
|
+
const codePoint = value.codePointAt(index);
|
|
591
|
+
const character = String.fromCodePoint(codePoint);
|
|
592
|
+
const nextBytes = Buffer.byteLength(character, "utf8");
|
|
593
|
+
if (bytes + nextBytes > maxBytes) break;
|
|
594
|
+
bytes += nextBytes;
|
|
595
|
+
index += character.length;
|
|
596
|
+
}
|
|
597
|
+
return value.slice(0, index);
|
|
598
|
+
}
|
|
599
|
+
function utf8Suffix(value, maxBytes) {
|
|
600
|
+
let index = value.length;
|
|
601
|
+
let bytes = 0;
|
|
602
|
+
while (index > 0) {
|
|
603
|
+
const last = value.charCodeAt(index - 1);
|
|
604
|
+
const width = last >= 56320 && last <= 57343 && index > 1 ? 2 : 1;
|
|
605
|
+
const character = value.slice(index - width, index);
|
|
606
|
+
const nextBytes = Buffer.byteLength(character, "utf8");
|
|
607
|
+
if (bytes + nextBytes > maxBytes) break;
|
|
608
|
+
bytes += nextBytes;
|
|
609
|
+
index -= width;
|
|
610
|
+
}
|
|
611
|
+
return value.slice(index);
|
|
612
|
+
}
|
|
613
|
+
function previewMonitoringValue(value, state, path = "$", depth = 0) {
|
|
614
|
+
if (state.remainingNodes <= 0 || depth >= 8) {
|
|
615
|
+
state.truncated = true;
|
|
616
|
+
if (state.details.length < 24) state.details.push(`${path}: traversal boundary`);
|
|
617
|
+
return "[nested value omitted from model monitoring projection]";
|
|
618
|
+
}
|
|
619
|
+
state.remainingNodes -= 1;
|
|
620
|
+
if (typeof value === "string") {
|
|
621
|
+
const projected = modelStringProjection(value, Math.min(1e3, state.remainingStringBytes));
|
|
622
|
+
state.remainingStringBytes = Math.max(
|
|
623
|
+
0,
|
|
624
|
+
state.remainingStringBytes - projected.fact.deliveredBytes
|
|
625
|
+
);
|
|
626
|
+
if (projected.fact.truncated) {
|
|
627
|
+
state.truncated = true;
|
|
628
|
+
if (state.details.length < 24) state.details.push(`${path}: string truncated`);
|
|
629
|
+
}
|
|
630
|
+
return projected.value;
|
|
631
|
+
}
|
|
632
|
+
if (value === null || typeof value === "boolean" || typeof value === "number") return value;
|
|
633
|
+
if (typeof value !== "object") {
|
|
634
|
+
state.truncated = true;
|
|
635
|
+
if (state.details.length < 24) state.details.push(`${path}: non-JSON value omitted`);
|
|
636
|
+
return `[${typeof value} value omitted from model monitoring projection]`;
|
|
637
|
+
}
|
|
638
|
+
if (Array.isArray(value)) {
|
|
639
|
+
const keep2 = Math.min(24, value.length);
|
|
640
|
+
const out2 = value.slice(0, keep2).map((entry, index) => previewMonitoringValue(entry, state, `${path}[${index}]`, depth + 1));
|
|
641
|
+
if (keep2 < value.length) {
|
|
642
|
+
state.truncated = true;
|
|
643
|
+
if (state.details.length < 24) {
|
|
644
|
+
state.details.push(`${path}: ${value.length - keep2} array entries omitted`);
|
|
645
|
+
}
|
|
646
|
+
out2.push({ omittedEntries: value.length - keep2 });
|
|
647
|
+
}
|
|
648
|
+
return out2;
|
|
649
|
+
}
|
|
650
|
+
const out = {};
|
|
651
|
+
const entries = Object.entries(value);
|
|
652
|
+
const keep = Math.min(24, entries.length);
|
|
653
|
+
for (let index = 0; index < keep; index += 1) {
|
|
654
|
+
const [rawKey, entry] = entries[index];
|
|
655
|
+
const keyProjection = modelStringProjection(rawKey, 128).value;
|
|
656
|
+
const key = Object.prototype.hasOwnProperty.call(out, keyProjection) ? `${keyProjection}#${index}` : keyProjection;
|
|
657
|
+
out[key] = previewMonitoringValue(entry, state, `${path}.${keyProjection}`, depth + 1);
|
|
658
|
+
}
|
|
659
|
+
if (keep < entries.length) {
|
|
660
|
+
state.truncated = true;
|
|
661
|
+
if (state.details.length < 24) {
|
|
662
|
+
state.details.push(`${path}: ${entries.length - keep} object fields omitted`);
|
|
663
|
+
}
|
|
664
|
+
out.omittedFields = entries.length - keep;
|
|
665
|
+
}
|
|
666
|
+
return out;
|
|
667
|
+
}
|
|
668
|
+
function projectMonitoringContainer(value, stringBytes) {
|
|
669
|
+
const measurement = measureSessionEventJson(value);
|
|
670
|
+
const state = {
|
|
671
|
+
remainingStringBytes: stringBytes,
|
|
672
|
+
remainingNodes: 128,
|
|
673
|
+
truncated: false,
|
|
674
|
+
details: []
|
|
675
|
+
};
|
|
676
|
+
const preview = previewMonitoringValue(value, state);
|
|
677
|
+
const deliveredBytes = Buffer.byteLength(JSON.stringify(preview), "utf8");
|
|
678
|
+
const originalCount = Array.isArray(value) ? value.length : value !== null && typeof value === "object" ? Object.keys(value).length : void 0;
|
|
679
|
+
const deliveredCount = originalCount === void 0 ? void 0 : Math.min(24, originalCount);
|
|
680
|
+
const originalBytes = measurement.bytes;
|
|
681
|
+
const truncated = state.truncated || originalBytes === null || (originalBytes ?? 0) !== deliveredBytes;
|
|
504
682
|
return {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
683
|
+
value: preview,
|
|
684
|
+
fact: {
|
|
685
|
+
truncated,
|
|
686
|
+
originalBytes,
|
|
687
|
+
deliveredBytes,
|
|
688
|
+
...originalCount === void 0 ? {} : { originalCount },
|
|
689
|
+
...deliveredCount === void 0 ? {} : { deliveredCount },
|
|
690
|
+
...measurement.bytes === null ? { measurementBounded: true } : {}
|
|
691
|
+
},
|
|
692
|
+
details: state.details
|
|
508
693
|
};
|
|
509
694
|
}
|
|
510
|
-
function
|
|
511
|
-
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
695
|
+
function boundSessionDetailMcp(session, effectiveControl = session.effectiveControl, maxBytes = SESSION_DETAIL_MCP_MAX_BYTES) {
|
|
696
|
+
const title = session.title === null ? null : modelStringProjection(session.title, 512);
|
|
697
|
+
const initialMessage = modelStringProjection(session.initialMessage, 4e3);
|
|
698
|
+
const instructions = session.instructions === null ? null : modelStringProjection(session.instructions, 4e3);
|
|
699
|
+
const metadata = projectMonitoringContainer(session.metadata, 3e3);
|
|
700
|
+
const resources = projectMonitoringContainer(session.resources, 3e3);
|
|
701
|
+
const tools = projectMonitoringContainer(session.tools, 4e3);
|
|
702
|
+
const mcpServers = projectMonitoringContainer(session.mcpServers, 3e3);
|
|
703
|
+
const permissions = projectMonitoringContainer(session.firstPartyMcpPermissions, 1500);
|
|
704
|
+
const control = projectMonitoringContainer(effectiveControl, 2e3);
|
|
705
|
+
const fieldFacts = {
|
|
706
|
+
title: title?.fact ?? {
|
|
707
|
+
truncated: false,
|
|
708
|
+
originalBytes: 0,
|
|
709
|
+
deliveredBytes: 0
|
|
710
|
+
},
|
|
711
|
+
initialMessage: initialMessage.fact,
|
|
712
|
+
instructions: instructions?.fact ?? {
|
|
713
|
+
truncated: false,
|
|
714
|
+
originalBytes: 0,
|
|
715
|
+
deliveredBytes: 0
|
|
716
|
+
},
|
|
717
|
+
metadata: metadata.fact,
|
|
718
|
+
resources: resources.fact,
|
|
719
|
+
tools: tools.fact,
|
|
720
|
+
mcpServers: mcpServers.fact,
|
|
721
|
+
firstPartyMcpPermissions: permissions.fact,
|
|
722
|
+
effectiveControl: control.fact
|
|
723
|
+
};
|
|
724
|
+
const details = [
|
|
725
|
+
...metadata.details,
|
|
726
|
+
...resources.details,
|
|
727
|
+
...tools.details,
|
|
728
|
+
...mcpServers.details,
|
|
729
|
+
...permissions.details,
|
|
730
|
+
...control.details
|
|
731
|
+
].slice(0, 32);
|
|
732
|
+
const result = {
|
|
733
|
+
id: session.id,
|
|
734
|
+
workspaceId: session.workspaceId,
|
|
735
|
+
accountId: session.accountId,
|
|
736
|
+
status: session.status,
|
|
737
|
+
title: title?.value ?? null,
|
|
738
|
+
titleSource: session.titleSource,
|
|
739
|
+
initialMessage: initialMessage.value,
|
|
740
|
+
instructions: instructions?.value ?? null,
|
|
741
|
+
resources: resources.value,
|
|
742
|
+
tools: tools.value,
|
|
743
|
+
metadata: metadata.value,
|
|
744
|
+
model: modelStringProjection(session.model, 512).value,
|
|
745
|
+
sandboxBackend: modelStringProjection(session.sandboxBackend, 128).value,
|
|
746
|
+
sandboxOs: session.sandboxOs,
|
|
747
|
+
sandboxGroupId: session.sandboxGroupId,
|
|
748
|
+
activeSandboxId: session.activeSandboxId,
|
|
749
|
+
activeEpoch: session.activeEpoch,
|
|
750
|
+
variableSetId: session.variableSetId,
|
|
751
|
+
environmentId: session.environmentId,
|
|
752
|
+
rigId: session.rigId,
|
|
753
|
+
rigVersionId: session.rigVersionId,
|
|
754
|
+
firstPartyMcpPermissions: permissions.value,
|
|
755
|
+
mcpServers: mcpServers.value,
|
|
756
|
+
parentSessionId: session.parentSessionId,
|
|
757
|
+
createIdempotencyKey: session.createIdempotencyKey === null ? null : modelStringProjection(session.createIdempotencyKey, 512).value,
|
|
758
|
+
temporalWorkflowId: session.temporalWorkflowId === null ? null : modelStringProjection(session.temporalWorkflowId, 512).value,
|
|
759
|
+
activeTurnId: session.activeTurnId,
|
|
760
|
+
lastInputTokens: session.lastInputTokens,
|
|
761
|
+
queueVersion: session.queueVersion,
|
|
762
|
+
queueHeadPosition: session.queueHeadPosition,
|
|
763
|
+
queueTailPosition: session.queueTailPosition,
|
|
764
|
+
effectiveControl: control.value,
|
|
765
|
+
lastSequence: session.lastSequence,
|
|
766
|
+
codexPinnedCredentialId: session.codexPinnedCredentialId,
|
|
767
|
+
codexLastCredentialId: session.codexLastCredentialId,
|
|
768
|
+
pinned: session.pinned,
|
|
769
|
+
pinnedAt: session.pinnedAt,
|
|
770
|
+
pinVersion: session.pinVersion,
|
|
771
|
+
...session.treeStats === void 0 ? {} : { treeStats: session.treeStats },
|
|
772
|
+
createdAt: session.createdAt,
|
|
773
|
+
updatedAt: session.updatedAt,
|
|
774
|
+
projection: {
|
|
775
|
+
truncated: Object.values(fieldFacts).some((fact) => fact.truncated),
|
|
776
|
+
fields: fieldFacts,
|
|
777
|
+
details,
|
|
778
|
+
bytes: 0,
|
|
779
|
+
maxBytes
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
783
|
+
const measured = prettyJsonBytes(result);
|
|
784
|
+
if (result.projection.bytes === measured) break;
|
|
785
|
+
result.projection.bytes = measured;
|
|
786
|
+
}
|
|
787
|
+
const mutable = result;
|
|
788
|
+
const fallbackContainers = [
|
|
789
|
+
["tools", fieldFacts.tools],
|
|
790
|
+
["resources", fieldFacts.resources],
|
|
791
|
+
["metadata", fieldFacts.metadata],
|
|
792
|
+
["mcpServers", fieldFacts.mcpServers],
|
|
793
|
+
["effectiveControl", fieldFacts.effectiveControl],
|
|
794
|
+
["firstPartyMcpPermissions", fieldFacts.firstPartyMcpPermissions]
|
|
795
|
+
];
|
|
796
|
+
for (const [field, fact] of fallbackContainers) {
|
|
797
|
+
if (result.projection.bytes <= maxBytes) break;
|
|
798
|
+
const omission = {
|
|
799
|
+
preview: `[${field} preview omitted at final session_get byte boundary]`,
|
|
800
|
+
...fact.originalCount === void 0 ? {} : { originalCount: fact.originalCount }
|
|
801
|
+
};
|
|
802
|
+
mutable[field] = omission;
|
|
803
|
+
fact.truncated = true;
|
|
804
|
+
fact.deliveredBytes = Buffer.byteLength(JSON.stringify(omission), "utf8");
|
|
805
|
+
fact.deliveredCount = 0;
|
|
806
|
+
result.projection.truncated = true;
|
|
807
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
808
|
+
const measured = prettyJsonBytes(result);
|
|
809
|
+
if (result.projection.bytes === measured) break;
|
|
810
|
+
result.projection.bytes = measured;
|
|
518
811
|
}
|
|
519
812
|
}
|
|
520
|
-
if (
|
|
521
|
-
|
|
522
|
-
session.initialMessage,
|
|
523
|
-
perFieldChars
|
|
524
|
-
);
|
|
525
|
-
changed = true;
|
|
813
|
+
if (result.projection.bytes > maxBytes) {
|
|
814
|
+
throw new RangeError(`session_get projection exceeds its ${maxBytes}-byte envelope`);
|
|
526
815
|
}
|
|
527
|
-
return
|
|
816
|
+
return result;
|
|
817
|
+
}
|
|
818
|
+
function boundRigDetailMcp(rig, versionsPage, changesPage, maxBytes = RIG_DETAIL_MCP_MAX_BYTES) {
|
|
819
|
+
const name = modelStringProjection(rig.name, 512);
|
|
820
|
+
const description = rig.description === null ? null : modelStringProjection(rig.description, 2e3);
|
|
821
|
+
const active = rig.activeVersion;
|
|
822
|
+
const activeSetup = active?.setupScript === null || active?.setupScript === void 0 ? null : modelStringProjection(active.setupScript, 8e3);
|
|
823
|
+
const activeImage = active?.image === null || active?.image === void 0 ? null : modelStringProjection(active.image, 1e3);
|
|
824
|
+
const activeChangelog = active?.changelog === null || active?.changelog === void 0 ? null : modelStringProjection(active.changelog, 2e3);
|
|
825
|
+
const activeChecks = projectMonitoringContainer(active?.checks ?? [], 5e3);
|
|
826
|
+
const activeHooks = projectMonitoringContainer(active?.credentialHooks ?? [], 1500);
|
|
827
|
+
const activeVariableSets = projectMonitoringContainer(active?.defaultVariableSetIds ?? [], 1500);
|
|
828
|
+
const versions = projectMonitoringContainer(versionsPage.versions, 4e3);
|
|
829
|
+
const changes = projectMonitoringContainer(changesPage.changes, 4e3);
|
|
830
|
+
const fieldFacts = {
|
|
831
|
+
name: name.fact,
|
|
832
|
+
description: description?.fact ?? {
|
|
833
|
+
truncated: false,
|
|
834
|
+
originalBytes: 0,
|
|
835
|
+
deliveredBytes: 0
|
|
836
|
+
},
|
|
837
|
+
activeSetupScript: activeSetup?.fact ?? {
|
|
838
|
+
truncated: false,
|
|
839
|
+
originalBytes: 0,
|
|
840
|
+
deliveredBytes: 0
|
|
841
|
+
},
|
|
842
|
+
activeImage: activeImage?.fact ?? {
|
|
843
|
+
truncated: false,
|
|
844
|
+
originalBytes: 0,
|
|
845
|
+
deliveredBytes: 0
|
|
846
|
+
},
|
|
847
|
+
activeChangelog: activeChangelog?.fact ?? {
|
|
848
|
+
truncated: false,
|
|
849
|
+
originalBytes: 0,
|
|
850
|
+
deliveredBytes: 0
|
|
851
|
+
},
|
|
852
|
+
activeChecks: activeChecks.fact,
|
|
853
|
+
activeCredentialHooks: activeHooks.fact,
|
|
854
|
+
activeDefaultVariableSetIds: activeVariableSets.fact,
|
|
855
|
+
versions: versions.fact,
|
|
856
|
+
changes: changes.fact
|
|
857
|
+
};
|
|
858
|
+
const result = {
|
|
859
|
+
rig: {
|
|
860
|
+
id: rig.id,
|
|
861
|
+
accountId: rig.accountId,
|
|
862
|
+
workspaceId: rig.workspaceId,
|
|
863
|
+
name: name.value,
|
|
864
|
+
description: description?.value ?? null,
|
|
865
|
+
createdBy: rig.createdBy === null ? null : modelStringProjection(rig.createdBy, 512).value,
|
|
866
|
+
activeVersion: active ? {
|
|
867
|
+
id: active.id,
|
|
868
|
+
rigId: active.rigId,
|
|
869
|
+
version: active.version,
|
|
870
|
+
image: activeImage?.value ?? null,
|
|
871
|
+
setupScript: activeSetup?.value ?? null,
|
|
872
|
+
checks: activeChecks.value,
|
|
873
|
+
credentialHooks: activeHooks.value,
|
|
874
|
+
defaultVariableSetIds: activeVariableSets.value,
|
|
875
|
+
changelog: activeChangelog?.value ?? null,
|
|
876
|
+
createdBy: active.createdBy === null ? null : modelStringProjection(active.createdBy, 512).value,
|
|
877
|
+
active: active.active,
|
|
878
|
+
createdAt: active.createdAt
|
|
879
|
+
} : null,
|
|
880
|
+
activeVersionHealth: rig.activeVersionHealth,
|
|
881
|
+
versionCount: rig.versionCount,
|
|
882
|
+
createdAt: rig.createdAt,
|
|
883
|
+
updatedAt: rig.updatedAt
|
|
884
|
+
},
|
|
885
|
+
versions: versions.value,
|
|
886
|
+
versionsTotal: versionsPage.total,
|
|
887
|
+
versionsTruncated: versionsPage.hasMore || versions.fact.truncated,
|
|
888
|
+
changes: changes.value,
|
|
889
|
+
changesTotal: changesPage.total,
|
|
890
|
+
changesTruncated: changesPage.hasMore || changes.fact.truncated,
|
|
891
|
+
projection: {
|
|
892
|
+
truncated: versionsPage.hasMore || changesPage.hasMore || Object.values(fieldFacts).some((fact) => fact.truncated),
|
|
893
|
+
fields: fieldFacts,
|
|
894
|
+
details: [
|
|
895
|
+
...activeChecks.details,
|
|
896
|
+
...activeHooks.details,
|
|
897
|
+
...activeVariableSets.details,
|
|
898
|
+
...versions.details,
|
|
899
|
+
...changes.details
|
|
900
|
+
].slice(0, 32),
|
|
901
|
+
bytes: 0,
|
|
902
|
+
maxBytes
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
906
|
+
const measured = prettyJsonBytes(result);
|
|
907
|
+
if (result.projection.bytes === measured) break;
|
|
908
|
+
result.projection.bytes = measured;
|
|
909
|
+
}
|
|
910
|
+
const mutable = result;
|
|
911
|
+
const rigFallbacks = [
|
|
912
|
+
{
|
|
913
|
+
target: mutable.rig.activeVersion ?? {},
|
|
914
|
+
field: "checks",
|
|
915
|
+
fact: fieldFacts.activeChecks
|
|
916
|
+
},
|
|
917
|
+
{ target: mutable, field: "versions", fact: fieldFacts.versions },
|
|
918
|
+
{ target: mutable, field: "changes", fact: fieldFacts.changes },
|
|
919
|
+
{
|
|
920
|
+
target: mutable.rig.activeVersion ?? {},
|
|
921
|
+
field: "credentialHooks",
|
|
922
|
+
fact: fieldFacts.activeCredentialHooks
|
|
923
|
+
},
|
|
924
|
+
{
|
|
925
|
+
target: mutable.rig.activeVersion ?? {},
|
|
926
|
+
field: "defaultVariableSetIds",
|
|
927
|
+
fact: fieldFacts.activeDefaultVariableSetIds
|
|
928
|
+
}
|
|
929
|
+
];
|
|
930
|
+
for (const fallback of rigFallbacks) {
|
|
931
|
+
if (result.projection.bytes <= maxBytes) break;
|
|
932
|
+
const omission = {
|
|
933
|
+
preview: `[${fallback.field} preview omitted at final rig_get byte boundary]`,
|
|
934
|
+
...fallback.fact.originalCount === void 0 ? {} : { originalCount: fallback.fact.originalCount }
|
|
935
|
+
};
|
|
936
|
+
fallback.target[fallback.field] = omission;
|
|
937
|
+
fallback.fact.truncated = true;
|
|
938
|
+
fallback.fact.deliveredBytes = Buffer.byteLength(JSON.stringify(omission), "utf8");
|
|
939
|
+
fallback.fact.deliveredCount = 0;
|
|
940
|
+
result.projection.truncated = true;
|
|
941
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
942
|
+
const measured = prettyJsonBytes(result);
|
|
943
|
+
if (result.projection.bytes === measured) break;
|
|
944
|
+
result.projection.bytes = measured;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
if (result.projection.bytes > maxBytes) {
|
|
948
|
+
throw new RangeError(`rig_get projection exceeds its ${maxBytes}-byte envelope`);
|
|
949
|
+
}
|
|
950
|
+
return result;
|
|
528
951
|
}
|
|
529
952
|
|
|
530
953
|
// src/mcp/server.ts
|
|
@@ -547,8 +970,13 @@ function buildOpenGeniMcpServer(deps, grant, options = {}) {
|
|
|
547
970
|
inputSchema: { title: z4.string().min(1).max(200) }
|
|
548
971
|
},
|
|
549
972
|
async ({ title }) => {
|
|
550
|
-
|
|
551
|
-
|
|
973
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.title.write");
|
|
974
|
+
const result = await updateSessionTitle(deps, grant, sessionId, title, "agent");
|
|
975
|
+
return json({
|
|
976
|
+
ok: true,
|
|
977
|
+
updated: result.updated,
|
|
978
|
+
title: result.title ?? title
|
|
979
|
+
});
|
|
552
980
|
}
|
|
553
981
|
);
|
|
554
982
|
}
|
|
@@ -567,7 +995,7 @@ function buildOpenGeniMcpServer(deps, grant, options = {}) {
|
|
|
567
995
|
registerWorkspaceOrchestrationTools(server, deps, grant, can, sessionId, toolspaceMode, json);
|
|
568
996
|
registerVariableSetTools(server, deps, grant, can, json);
|
|
569
997
|
if (can("github:use")) {
|
|
570
|
-
registerGitHubConnectTool(server, deps,
|
|
998
|
+
registerGitHubConnectTool(server, deps, json);
|
|
571
999
|
if (sessionId !== null) {
|
|
572
1000
|
registerGitHubTokenTool(server, deps, grant, sessionId, json);
|
|
573
1001
|
}
|
|
@@ -587,7 +1015,9 @@ function buildOpenGeniMcpServer(deps, grant, options = {}) {
|
|
|
587
1015
|
if (file.status !== "ready") {
|
|
588
1016
|
throw new Error(`file is ${file.status}`);
|
|
589
1017
|
}
|
|
590
|
-
const signed = await deps.objectStorage.createGetUrl({
|
|
1018
|
+
const signed = await deps.objectStorage.createGetUrl({
|
|
1019
|
+
key: file.objectKey
|
|
1020
|
+
});
|
|
591
1021
|
return json({
|
|
592
1022
|
file: {
|
|
593
1023
|
id: file.id,
|
|
@@ -617,11 +1047,7 @@ function buildOpenGeniMcpServer(deps, grant, options = {}) {
|
|
|
617
1047
|
},
|
|
618
1048
|
async ({ limit }) => {
|
|
619
1049
|
try {
|
|
620
|
-
const
|
|
621
|
-
deps.db,
|
|
622
|
-
grant.workspaceId
|
|
623
|
-
);
|
|
624
|
-
const repositories = await listGitHubAppRepositories(deps.settings, { installationIds });
|
|
1050
|
+
const repositories = await listWorkspaceGitHubRepositories(deps, grant.workspaceId);
|
|
625
1051
|
const visible = typeof limit === "number" ? repositories.slice(0, limit) : repositories;
|
|
626
1052
|
return json({
|
|
627
1053
|
repositories: visible.map(
|
|
@@ -733,7 +1159,9 @@ function buildOpenGeniMcpServer(deps, grant, options = {}) {
|
|
|
733
1159
|
description: "List scheduled tasks.",
|
|
734
1160
|
inputSchema: { limit: z4.number().int().positive().optional() }
|
|
735
1161
|
},
|
|
736
|
-
async ({ limit }) => json({
|
|
1162
|
+
async ({ limit }) => json({
|
|
1163
|
+
tasks: await listScheduledTasks(deps.db, grant.workspaceId, limit ?? 100)
|
|
1164
|
+
})
|
|
737
1165
|
);
|
|
738
1166
|
server.registerTool(
|
|
739
1167
|
"scheduled_tasks_get",
|
|
@@ -780,7 +1208,11 @@ function buildOpenGeniMcpServer(deps, grant, options = {}) {
|
|
|
780
1208
|
payload,
|
|
781
1209
|
toolsProvided: scheduledTaskToolsProvided(args)
|
|
782
1210
|
});
|
|
783
|
-
await syncCreatedScheduledTask({
|
|
1211
|
+
await syncCreatedScheduledTask({
|
|
1212
|
+
db: deps.db,
|
|
1213
|
+
workflowClient: deps.workflowClient,
|
|
1214
|
+
task
|
|
1215
|
+
});
|
|
784
1216
|
return json(task);
|
|
785
1217
|
}
|
|
786
1218
|
);
|
|
@@ -872,7 +1304,10 @@ function buildOpenGeniMcpServer(deps, grant, options = {}) {
|
|
|
872
1304
|
"scheduled_tasks_trigger",
|
|
873
1305
|
{
|
|
874
1306
|
description: "Trigger a scheduled task immediately. Pass a stable triggerId to make a retried trigger idempotent (one charge, one run).",
|
|
875
|
-
inputSchema: {
|
|
1307
|
+
inputSchema: {
|
|
1308
|
+
id: z4.string().uuid(),
|
|
1309
|
+
triggerId: z4.string().min(1).max(128).optional()
|
|
1310
|
+
}
|
|
876
1311
|
},
|
|
877
1312
|
async ({ id, triggerId }) => {
|
|
878
1313
|
const task = await requireScheduledTask(deps.db, grant.workspaceId, id);
|
|
@@ -928,7 +1363,10 @@ function buildOpenGeniMcpServer(deps, grant, options = {}) {
|
|
|
928
1363
|
"scheduled_task_runs_list",
|
|
929
1364
|
{
|
|
930
1365
|
description: "List runs for a scheduled task.",
|
|
931
|
-
inputSchema: {
|
|
1366
|
+
inputSchema: {
|
|
1367
|
+
taskId: z4.string().uuid(),
|
|
1368
|
+
limit: z4.number().int().positive().optional()
|
|
1369
|
+
}
|
|
932
1370
|
},
|
|
933
1371
|
async ({ taskId, limit }) => json({
|
|
934
1372
|
runs: await listScheduledTaskRuns(deps.db, grant.workspaceId, taskId, limit ?? 100)
|
|
@@ -991,6 +1429,7 @@ function registerGoalTools(server, deps, grant, sessionId, json) {
|
|
|
991
1429
|
}
|
|
992
1430
|
},
|
|
993
1431
|
async ({ text, successCriteria, maxAutoContinuations }) => {
|
|
1432
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.goal.write");
|
|
994
1433
|
await requireSession(deps.db, grant.workspaceId, sessionId);
|
|
995
1434
|
const callerTurnId = typeof grant.metadata?.["turnId"] === "string" ? grant.metadata["turnId"] : null;
|
|
996
1435
|
await assertGoalReactivationAllowed(deps, grant.workspaceId, sessionId, callerTurnId);
|
|
@@ -1030,6 +1469,7 @@ function registerGoalTools(server, deps, grant, sessionId, json) {
|
|
|
1030
1469
|
}
|
|
1031
1470
|
},
|
|
1032
1471
|
async ({ text, successCriteria, progressNote }) => {
|
|
1472
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.goal.write");
|
|
1033
1473
|
await requireSession(deps.db, grant.workspaceId, sessionId);
|
|
1034
1474
|
const existing = await getSessionGoal(deps.db, grant.workspaceId, sessionId);
|
|
1035
1475
|
if (!existing) {
|
|
@@ -1065,6 +1505,7 @@ function registerGoalTools(server, deps, grant, sessionId, json) {
|
|
|
1065
1505
|
inputSchema: { evidence: z4.string().min(1) }
|
|
1066
1506
|
},
|
|
1067
1507
|
async ({ evidence }) => {
|
|
1508
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.goal.write");
|
|
1068
1509
|
await requireSession(deps.db, grant.workspaceId, sessionId);
|
|
1069
1510
|
const existing = await getSessionGoal(deps.db, grant.workspaceId, sessionId);
|
|
1070
1511
|
if (!existing) {
|
|
@@ -1092,6 +1533,7 @@ function registerGoalTools(server, deps, grant, sessionId, json) {
|
|
|
1092
1533
|
inputSchema: { rationale: z4.string().min(1) }
|
|
1093
1534
|
},
|
|
1094
1535
|
async ({ rationale }) => {
|
|
1536
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.goal.write");
|
|
1095
1537
|
await requireSession(deps.db, grant.workspaceId, sessionId);
|
|
1096
1538
|
const existing = await getSessionGoal(deps.db, grant.workspaceId, sessionId);
|
|
1097
1539
|
if (!existing) {
|
|
@@ -1121,6 +1563,13 @@ function registerGoalTools(server, deps, grant, sessionId, json) {
|
|
|
1121
1563
|
}
|
|
1122
1564
|
);
|
|
1123
1565
|
}
|
|
1566
|
+
async function authorizeFirstPartySession(deps, grant, sessionId, operation) {
|
|
1567
|
+
return await requireSessionAuthorization(deps, grant, {
|
|
1568
|
+
sessionId,
|
|
1569
|
+
operation,
|
|
1570
|
+
surface: "first_party_mcp"
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1124
1573
|
var MemoryKindSchema = z4.enum(["preference", "semantic", "procedural", "decision", "episodic"]);
|
|
1125
1574
|
function memoryPreview(text) {
|
|
1126
1575
|
const normalized = text.replace(/\s+/g, " ").trim();
|
|
@@ -1235,7 +1684,11 @@ function registerMemoryTools(server, deps, grant, sessionId, json) {
|
|
|
1235
1684
|
);
|
|
1236
1685
|
}
|
|
1237
1686
|
function registerFleetTools(server, deps, grant, sessionId, json) {
|
|
1238
|
-
const services = {
|
|
1687
|
+
const services = {
|
|
1688
|
+
db: deps.db,
|
|
1689
|
+
settings: deps.settings,
|
|
1690
|
+
bus: deps.bus
|
|
1691
|
+
};
|
|
1239
1692
|
const fleetContext = async () => await buildFleetContextForSession(deps, {
|
|
1240
1693
|
accountId: grant.accountId,
|
|
1241
1694
|
workspaceId: grant.workspaceId,
|
|
@@ -1278,7 +1731,11 @@ function registerFleetTools(server, deps, grant, sessionId, json) {
|
|
|
1278
1731
|
workdir: z4.string().optional()
|
|
1279
1732
|
}),
|
|
1280
1733
|
z4.object({ kind: z4.literal("read"), path: z4.string().min(1) }),
|
|
1281
|
-
z4.object({
|
|
1734
|
+
z4.object({
|
|
1735
|
+
kind: z4.literal("write"),
|
|
1736
|
+
path: z4.string().min(1),
|
|
1737
|
+
content: z4.string()
|
|
1738
|
+
})
|
|
1282
1739
|
])
|
|
1283
1740
|
}
|
|
1284
1741
|
},
|
|
@@ -1294,7 +1751,10 @@ function registerFleetTools(server, deps, grant, sessionId, json) {
|
|
|
1294
1751
|
}
|
|
1295
1752
|
},
|
|
1296
1753
|
async ({ kind, name }) => json(
|
|
1297
|
-
await provisionSandbox(services, await fleetContext(), {
|
|
1754
|
+
await provisionSandbox(services, await fleetContext(), {
|
|
1755
|
+
kind,
|
|
1756
|
+
...name ? { name } : {}
|
|
1757
|
+
})
|
|
1298
1758
|
)
|
|
1299
1759
|
);
|
|
1300
1760
|
}
|
|
@@ -1326,24 +1786,30 @@ function registerRigTools(server, deps, grant, can, sessionId, json) {
|
|
|
1326
1786
|
server.registerTool(
|
|
1327
1787
|
"rig_get",
|
|
1328
1788
|
{
|
|
1329
|
-
description: "Get
|
|
1789
|
+
description: "Get one rig's bounded active definition plus compact historical version/change summaries. Historical setup scripts, checks, payloads, and verification logs are represented by counts/byte facts rather than copied into model context; use the access-controlled REST detail endpoints for exact retained definitions.",
|
|
1330
1790
|
inputSchema: {
|
|
1331
1791
|
rigId: z4.string().uuid(),
|
|
1792
|
+
versionLimit: z4.number().int().positive().optional(),
|
|
1332
1793
|
changeLimit: z4.number().int().positive().optional()
|
|
1333
1794
|
}
|
|
1334
1795
|
},
|
|
1335
|
-
async ({ rigId, changeLimit }) => {
|
|
1796
|
+
async ({ rigId, versionLimit, changeLimit }) => {
|
|
1336
1797
|
const rig = await requireRigForApi(deps.db, grant.workspaceId, rigId);
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
changes: await listRigChangesForApi(
|
|
1341
|
-
{ db: deps.db },
|
|
1798
|
+
const [versions, changes] = await Promise.all([
|
|
1799
|
+
listRigVersionMonitoringSummaries(
|
|
1800
|
+
deps.db,
|
|
1342
1801
|
grant.workspaceId,
|
|
1343
1802
|
rig.id,
|
|
1344
|
-
|
|
1803
|
+
boundedRigHistoryLimit(versionLimit)
|
|
1804
|
+
),
|
|
1805
|
+
listRigChangeMonitoringSummaries(
|
|
1806
|
+
deps.db,
|
|
1807
|
+
grant.workspaceId,
|
|
1808
|
+
rig.id,
|
|
1809
|
+
boundedRigHistoryLimit(changeLimit)
|
|
1345
1810
|
)
|
|
1346
|
-
|
|
1811
|
+
]);
|
|
1812
|
+
return json(boundRigDetailMcp(rig, versions, changes));
|
|
1347
1813
|
}
|
|
1348
1814
|
);
|
|
1349
1815
|
server.registerTool(
|
|
@@ -1444,6 +1910,7 @@ function exactAgentCommandContext(grant, callerSessionId) {
|
|
|
1444
1910
|
return {
|
|
1445
1911
|
accountId: grant.accountId,
|
|
1446
1912
|
workspaceId: grant.workspaceId,
|
|
1913
|
+
subjectId: grant.subjectId,
|
|
1447
1914
|
callerSessionId,
|
|
1448
1915
|
callerTurnId: turnId,
|
|
1449
1916
|
callerAttemptId: attemptId,
|
|
@@ -1455,10 +1922,43 @@ function registerWorkspaceOrchestrationTools(server, deps, grant, can, callerSes
|
|
|
1455
1922
|
server.registerTool(
|
|
1456
1923
|
"sessions_list",
|
|
1457
1924
|
{
|
|
1458
|
-
description:
|
|
1459
|
-
inputSchema: {
|
|
1925
|
+
description: `List compact high-level session status in this workspace. Defaults to creation order; use orderBy=updatedAt with decimal activity-revision updatedAfter/updatedThrough tokens for gap-free indexed incremental monitoring independent of application clocks. Cursors are opaque revision-fenced keysets. includeLastMessage is opt-in; its rendered previews share a deterministic ${SESSION_DISCOVERY_PREVIEW_MAX_BYTES}-byte UTF-8 aggregate budget, and omitted previews include a bounded session_events drill-down input (exact message type, direction=before, limit=1, monitoring summary). Use session_get for exact known targets and detailed resources/tools/settings. The list never returns full session objects or history.`,
|
|
1926
|
+
inputSchema: {
|
|
1927
|
+
limit: z4.number().int().positive().max(100).optional(),
|
|
1928
|
+
cursor: z4.string().max(512).optional(),
|
|
1929
|
+
includeLastMessage: z4.boolean().optional(),
|
|
1930
|
+
orderBy: z4.enum(["createdAt", "updatedAt"]).optional(),
|
|
1931
|
+
updatedAfter: z4.string().max(64).optional()
|
|
1932
|
+
}
|
|
1460
1933
|
},
|
|
1461
|
-
async ({ limit
|
|
1934
|
+
async ({ limit, cursor, includeLastMessage, orderBy: requestedOrderBy, updatedAfter }) => {
|
|
1935
|
+
const authorizationScope = await requireSessionAuthorizationListScope(
|
|
1936
|
+
deps,
|
|
1937
|
+
grant,
|
|
1938
|
+
"first_party_mcp"
|
|
1939
|
+
);
|
|
1940
|
+
const decodedCursor = cursor ? decodeSessionDiscoveryCursor(cursor) : void 0;
|
|
1941
|
+
const orderBy = requestedOrderBy ?? decodedCursor?.orderBy ?? "createdAt";
|
|
1942
|
+
if (decodedCursor && decodedCursor.orderBy !== orderBy) {
|
|
1943
|
+
throw new Error("sessions_list cursor order does not match orderBy");
|
|
1944
|
+
}
|
|
1945
|
+
const normalizedUpdatedAfter = updatedAfter !== void 0 ? normalizeSessionDiscoveryRevision(updatedAfter, "updatedAfter") : decodedCursor?.updatedAfter ?? void 0;
|
|
1946
|
+
if (normalizedUpdatedAfter !== void 0 && orderBy !== "updatedAt") {
|
|
1947
|
+
throw new Error("sessions_list updatedAfter requires orderBy=updatedAt");
|
|
1948
|
+
}
|
|
1949
|
+
if (decodedCursor && decodedCursor.updatedAfter !== (normalizedUpdatedAfter ?? null)) {
|
|
1950
|
+
throw new Error("sessions_list cursor does not match updatedAfter");
|
|
1951
|
+
}
|
|
1952
|
+
const page = await listSessionDiscoverySummaries(deps.db, grant.workspaceId, {
|
|
1953
|
+
limit: boundedSessionDiscoveryLimit(limit),
|
|
1954
|
+
...decodedCursor ? { cursor: decodedCursor } : {},
|
|
1955
|
+
includeLastMessage: includeLastMessage === true,
|
|
1956
|
+
orderBy,
|
|
1957
|
+
...normalizedUpdatedAfter ? { updatedAfter: normalizedUpdatedAfter } : {},
|
|
1958
|
+
...authorizationScope ? { authorizationScope } : {}
|
|
1959
|
+
});
|
|
1960
|
+
return json(capSessionDiscoveryPage(page, includeLastMessage === true));
|
|
1961
|
+
}
|
|
1462
1962
|
);
|
|
1463
1963
|
server.registerTool(
|
|
1464
1964
|
"session_get",
|
|
@@ -1467,42 +1967,95 @@ function registerWorkspaceOrchestrationTools(server, deps, grant, can, callerSes
|
|
|
1467
1967
|
inputSchema: { sessionId: z4.string().uuid() }
|
|
1468
1968
|
},
|
|
1469
1969
|
async ({ sessionId }) => {
|
|
1970
|
+
const authorization = await authorizeFirstPartySession(
|
|
1971
|
+
deps,
|
|
1972
|
+
grant,
|
|
1973
|
+
sessionId,
|
|
1974
|
+
"session.read"
|
|
1975
|
+
);
|
|
1470
1976
|
const session = await getSession(deps.db, grant.workspaceId, sessionId);
|
|
1471
1977
|
if (!session) {
|
|
1472
1978
|
throw new Error("session not found");
|
|
1473
1979
|
}
|
|
1474
1980
|
const queue = await getSessionQueueSnapshot(deps.db, grant.workspaceId, sessionId);
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1981
|
+
const projected = projectSessionForRelatedAccess(
|
|
1982
|
+
{
|
|
1983
|
+
...session,
|
|
1984
|
+
effectiveControl: queue?.effectiveControl ?? session.effectiveControl
|
|
1985
|
+
},
|
|
1986
|
+
authorization?.relatedSessionAccess ?? "root"
|
|
1987
|
+
);
|
|
1988
|
+
return json(boundSessionDetailMcp(projected));
|
|
1479
1989
|
}
|
|
1480
1990
|
);
|
|
1481
1991
|
server.registerTool(
|
|
1482
1992
|
"session_events",
|
|
1483
1993
|
{
|
|
1484
|
-
description: "Read a
|
|
1994
|
+
description: "Read a compact semantic tail only when session_get status is insufficient. With no cursor, this returns the newest matching events and excludes raw message/reasoning/command/PTY deltas. Use `latest` as an exclusive lookup for the newest event in exactly one semantic class; it cannot be combined with type or class filters. Use nextBefore to page older or explicit after/nextAfter to page forward. Type/class filters run in the RLS-scoped database query. payloadMode none|summary|full controls retained audit payload projection, but every model result is independently byte-capped with explicit truncation and exact covered sequence bounds. Exact retained forensic payloads require the access-controlled REST/SDK events API with mode=forensic&payloadMode=full; generic source bytes never retained by the audit boundary remain unavailable.",
|
|
1485
1995
|
inputSchema: {
|
|
1486
1996
|
sessionId: z4.string().uuid(),
|
|
1487
1997
|
after: z4.number().int().nonnegative().optional(),
|
|
1488
|
-
|
|
1998
|
+
before: z4.number().int().positive().optional(),
|
|
1999
|
+
limit: z4.number().int().positive().optional(),
|
|
2000
|
+
direction: z4.enum(SessionEventReadDirection.options).optional(),
|
|
2001
|
+
mode: z4.enum(SessionEventReadMode.options).optional(),
|
|
2002
|
+
payloadMode: z4.enum(SessionEventPayloadMode.options).optional(),
|
|
2003
|
+
includeTypes: z4.array(z4.enum(SessionEventType.options)).max(100).optional(),
|
|
2004
|
+
excludeTypes: z4.array(z4.enum(SessionEventType.options)).max(100).optional(),
|
|
2005
|
+
includeClasses: z4.array(z4.enum(SessionEventSemanticClass.options)).max(SessionEventSemanticClass.options.length).optional(),
|
|
2006
|
+
excludeClasses: z4.array(z4.enum(SessionEventSemanticClass.options)).max(SessionEventSemanticClass.options.length).optional(),
|
|
2007
|
+
latest: z4.enum(SessionEventSemanticClass.options).optional()
|
|
1489
2008
|
}
|
|
1490
2009
|
},
|
|
1491
|
-
async ({
|
|
2010
|
+
async ({
|
|
2011
|
+
sessionId,
|
|
2012
|
+
after,
|
|
2013
|
+
before,
|
|
2014
|
+
limit,
|
|
2015
|
+
direction: requestedDirection,
|
|
2016
|
+
mode: requestedMode,
|
|
2017
|
+
payloadMode: requestedPayloadMode,
|
|
2018
|
+
includeTypes,
|
|
2019
|
+
excludeTypes,
|
|
2020
|
+
includeClasses,
|
|
2021
|
+
excludeClasses,
|
|
2022
|
+
latest
|
|
2023
|
+
}) => {
|
|
2024
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.events.read");
|
|
1492
2025
|
await requireSession(deps.db, grant.workspaceId, sessionId);
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
);
|
|
1500
|
-
const
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
2026
|
+
if (latest && [includeTypes, excludeTypes, includeClasses, excludeClasses].some(
|
|
2027
|
+
(filter) => filter !== void 0
|
|
2028
|
+
)) {
|
|
2029
|
+
throw new Error("latest cannot be combined with event filters");
|
|
2030
|
+
}
|
|
2031
|
+
const mode = requestedMode ?? (after !== void 0 ? "forensic" : "monitoring");
|
|
2032
|
+
const direction = latest ? "before" : requestedDirection ?? (before !== void 0 ? "before" : after !== void 0 ? "after" : "before");
|
|
2033
|
+
const payloadMode = requestedPayloadMode ?? (mode === "monitoring" ? "summary" : "full");
|
|
2034
|
+
const dbPage = await listSessionEventPage(deps.db, grant.workspaceId, sessionId, {
|
|
2035
|
+
after: after ?? 0,
|
|
2036
|
+
...before !== void 0 ? { before } : {},
|
|
2037
|
+
direction,
|
|
2038
|
+
limit: latest ? 1 : boundedSessionEventMcpLimit(limit),
|
|
2039
|
+
payloadMode,
|
|
2040
|
+
includeTypes: includeTypes ?? [],
|
|
2041
|
+
excludeTypes: excludeTypes ?? [],
|
|
2042
|
+
includeClasses: latest ? [latest] : includeClasses ?? [],
|
|
2043
|
+
excludeClasses: excludeClasses ?? [],
|
|
2044
|
+
...mode === "monitoring" ? { defaultExcludeTypes: SESSION_EVENT_RAW_DELTA_TYPES } : {},
|
|
2045
|
+
maxBytes: SESSION_EVENT_MCP_MAX_BYTES * 4
|
|
1505
2046
|
});
|
|
2047
|
+
return json(
|
|
2048
|
+
boundSessionEventMcpPage({
|
|
2049
|
+
events: dbPage.events,
|
|
2050
|
+
mode,
|
|
2051
|
+
payloadMode,
|
|
2052
|
+
direction,
|
|
2053
|
+
sourceHasMore: dbPage.hasMore,
|
|
2054
|
+
sourceTruncatedBy: dbPage.truncatedBy,
|
|
2055
|
+
after: after ?? 0,
|
|
2056
|
+
before: before ?? null
|
|
2057
|
+
})
|
|
2058
|
+
);
|
|
1506
2059
|
}
|
|
1507
2060
|
);
|
|
1508
2061
|
}
|
|
@@ -1551,7 +2104,11 @@ function registerWorkspaceOrchestrationTools(server, deps, grant, can, callerSes
|
|
|
1551
2104
|
idempotencyKey: z4.string().min(1).max(200).optional(),
|
|
1552
2105
|
// First-party MCP token permissions for the spawned session; every
|
|
1553
2106
|
// permission must be held by this grant (validated in the domain).
|
|
1554
|
-
|
|
2107
|
+
// A goal requires goals:manage in the resulting set; it is never
|
|
2108
|
+
// silently added beyond the inherited or explicit authority.
|
|
2109
|
+
firstPartyMcpPermissions: z4.array(z4.string()).optional().describe(
|
|
2110
|
+
"Optional first-party capability set for the child. Omit to inherit this session's effective permissions. An explicit set may only narrow capabilities held by this session. A goal-bearing child requires goals:manage in the resulting set; creation fails rather than adding it implicitly."
|
|
2111
|
+
),
|
|
1555
2112
|
// Shared-sandbox placement (addendum 05 §D). OMIT (default) to SHARE the
|
|
1556
2113
|
// creator's box — one filesystem/repo/desktop, N independent conversations;
|
|
1557
2114
|
// this is the SAFE DEFAULT. Pass "new" for a fresh isolated box (a different
|
|
@@ -1582,7 +2139,12 @@ function registerWorkspaceOrchestrationTools(server, deps, grant, can, callerSes
|
|
|
1582
2139
|
// arbitrary session's wake channel without sessions:control on it.
|
|
1583
2140
|
}
|
|
1584
2141
|
},
|
|
1585
|
-
async (args) =>
|
|
2142
|
+
async (args) => {
|
|
2143
|
+
if (callerSessionId !== null) {
|
|
2144
|
+
await authorizeFirstPartySession(deps, grant, callerSessionId, "session.child.create");
|
|
2145
|
+
}
|
|
2146
|
+
return json(await createSessionForRequest(deps, grant, grant.workspaceId, args));
|
|
2147
|
+
}
|
|
1586
2148
|
);
|
|
1587
2149
|
}
|
|
1588
2150
|
if (can("sessions:control") && !toolspaceMode) {
|
|
@@ -1600,6 +2162,7 @@ function registerWorkspaceOrchestrationTools(server, deps, grant, can, callerSes
|
|
|
1600
2162
|
}
|
|
1601
2163
|
},
|
|
1602
2164
|
async ({ sessionId: targetSessionId, text, idempotencyKey, mcpCredentialUpdates }) => {
|
|
2165
|
+
await authorizeFirstPartySession(deps, grant, targetSessionId, "session.append");
|
|
1603
2166
|
if (callerSessionId !== null) {
|
|
1604
2167
|
if ((mcpCredentialUpdates?.length ?? 0) > 0) {
|
|
1605
2168
|
throw new Error("internal session updates cannot change MCP credentials");
|
|
@@ -1649,8 +2212,14 @@ function registerWorkspaceOrchestrationTools(server, deps, grant, can, callerSes
|
|
|
1649
2212
|
}
|
|
1650
2213
|
},
|
|
1651
2214
|
async ({ sessionId, idempotencyKey, reason }) => {
|
|
2215
|
+
const authorization = await authorizeFirstPartySession(
|
|
2216
|
+
deps,
|
|
2217
|
+
grant,
|
|
2218
|
+
sessionId,
|
|
2219
|
+
"session.control"
|
|
2220
|
+
);
|
|
1652
2221
|
if (callerSessionId !== null) {
|
|
1653
|
-
const
|
|
2222
|
+
const controlled2 = await controlAgentSessionWorkstream(
|
|
1654
2223
|
deps,
|
|
1655
2224
|
exactAgentCommandContext(grant, callerSessionId),
|
|
1656
2225
|
{
|
|
@@ -1661,28 +2230,38 @@ function registerWorkspaceOrchestrationTools(server, deps, grant, can, callerSes
|
|
|
1661
2230
|
}
|
|
1662
2231
|
);
|
|
1663
2232
|
return json({
|
|
1664
|
-
receiptId:
|
|
1665
|
-
effectiveControl:
|
|
1666
|
-
|
|
1667
|
-
|
|
2233
|
+
receiptId: controlled2.receipt.id,
|
|
2234
|
+
effectiveControl: projectEffectiveControlForRelatedAccess(
|
|
2235
|
+
serializeEffectiveSessionControl(controlled2.control),
|
|
2236
|
+
sessionId,
|
|
2237
|
+
authorization?.relatedSessionAccess ?? "root"
|
|
2238
|
+
),
|
|
2239
|
+
interruptionCount: controlled2.interruptionCount,
|
|
2240
|
+
replay: controlled2.replay
|
|
1668
2241
|
});
|
|
1669
2242
|
}
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
}
|
|
1684
|
-
)
|
|
2243
|
+
const controlled = await controlHumanSessionWorkstream(
|
|
2244
|
+
deps,
|
|
2245
|
+
{
|
|
2246
|
+
accountId: grant.accountId,
|
|
2247
|
+
workspaceId: grant.workspaceId,
|
|
2248
|
+
sessionId,
|
|
2249
|
+
subjectId: grant.subjectId
|
|
2250
|
+
},
|
|
2251
|
+
{
|
|
2252
|
+
action: "pause",
|
|
2253
|
+
clientEventId: idempotencyKey,
|
|
2254
|
+
...reason ? { reason } : {}
|
|
2255
|
+
}
|
|
1685
2256
|
);
|
|
2257
|
+
return json({
|
|
2258
|
+
...controlled,
|
|
2259
|
+
effectiveControl: projectEffectiveControlForRelatedAccess(
|
|
2260
|
+
controlled.effectiveControl,
|
|
2261
|
+
sessionId,
|
|
2262
|
+
authorization?.relatedSessionAccess ?? "root"
|
|
2263
|
+
)
|
|
2264
|
+
});
|
|
1686
2265
|
}
|
|
1687
2266
|
);
|
|
1688
2267
|
server.registerTool(
|
|
@@ -1696,8 +2275,14 @@ function registerWorkspaceOrchestrationTools(server, deps, grant, can, callerSes
|
|
|
1696
2275
|
}
|
|
1697
2276
|
},
|
|
1698
2277
|
async ({ sessionId, idempotencyKey, reason }) => {
|
|
2278
|
+
const authorization = await authorizeFirstPartySession(
|
|
2279
|
+
deps,
|
|
2280
|
+
grant,
|
|
2281
|
+
sessionId,
|
|
2282
|
+
"session.control"
|
|
2283
|
+
);
|
|
1699
2284
|
if (callerSessionId !== null) {
|
|
1700
|
-
const
|
|
2285
|
+
const controlled2 = await controlAgentSessionWorkstream(
|
|
1701
2286
|
deps,
|
|
1702
2287
|
exactAgentCommandContext(grant, callerSessionId),
|
|
1703
2288
|
{
|
|
@@ -1708,28 +2293,38 @@ function registerWorkspaceOrchestrationTools(server, deps, grant, can, callerSes
|
|
|
1708
2293
|
}
|
|
1709
2294
|
);
|
|
1710
2295
|
return json({
|
|
1711
|
-
receiptId:
|
|
1712
|
-
effectiveControl:
|
|
1713
|
-
|
|
1714
|
-
|
|
2296
|
+
receiptId: controlled2.receipt.id,
|
|
2297
|
+
effectiveControl: projectEffectiveControlForRelatedAccess(
|
|
2298
|
+
serializeEffectiveSessionControl(controlled2.control),
|
|
2299
|
+
sessionId,
|
|
2300
|
+
authorization?.relatedSessionAccess ?? "root"
|
|
2301
|
+
),
|
|
2302
|
+
interruptionCount: controlled2.interruptionCount,
|
|
2303
|
+
replay: controlled2.replay
|
|
1715
2304
|
});
|
|
1716
2305
|
}
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
}
|
|
1731
|
-
)
|
|
2306
|
+
const controlled = await controlHumanSessionWorkstream(
|
|
2307
|
+
deps,
|
|
2308
|
+
{
|
|
2309
|
+
accountId: grant.accountId,
|
|
2310
|
+
workspaceId: grant.workspaceId,
|
|
2311
|
+
sessionId,
|
|
2312
|
+
subjectId: grant.subjectId
|
|
2313
|
+
},
|
|
2314
|
+
{
|
|
2315
|
+
action: "resume",
|
|
2316
|
+
clientEventId: idempotencyKey,
|
|
2317
|
+
...reason ? { reason } : {}
|
|
2318
|
+
}
|
|
1732
2319
|
);
|
|
2320
|
+
return json({
|
|
2321
|
+
...controlled,
|
|
2322
|
+
effectiveControl: projectEffectiveControlForRelatedAccess(
|
|
2323
|
+
controlled.effectiveControl,
|
|
2324
|
+
sessionId,
|
|
2325
|
+
authorization?.relatedSessionAccess ?? "root"
|
|
2326
|
+
)
|
|
2327
|
+
});
|
|
1733
2328
|
}
|
|
1734
2329
|
);
|
|
1735
2330
|
if (callerSessionId !== null) {
|
|
@@ -1744,6 +2339,7 @@ function registerWorkspaceOrchestrationTools(server, deps, grant, can, callerSes
|
|
|
1744
2339
|
}
|
|
1745
2340
|
},
|
|
1746
2341
|
async ({ sessionId, instruction, idempotencyKey }) => {
|
|
2342
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.steer");
|
|
1747
2343
|
const result = await steerAgentSession(
|
|
1748
2344
|
deps,
|
|
1749
2345
|
exactAgentCommandContext(grant, callerSessionId),
|
|
@@ -1769,15 +2365,14 @@ function registerWorkspaceOrchestrationTools(server, deps, grant, can, callerSes
|
|
|
1769
2365
|
}
|
|
1770
2366
|
},
|
|
1771
2367
|
async ({ session_id, title }) => {
|
|
2368
|
+
await authorizeFirstPartySession(deps, grant, session_id, "session.title.write");
|
|
1772
2369
|
await requireSession(deps.db, grant.workspaceId, session_id);
|
|
1773
|
-
const result = await updateSessionTitle(
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
title
|
|
1778
|
-
|
|
1779
|
-
);
|
|
1780
|
-
return json({ ok: true, updated: result.updated, title: result.title ?? title });
|
|
2370
|
+
const result = await updateSessionTitle(deps, grant, session_id, title, "agent");
|
|
2371
|
+
return json({
|
|
2372
|
+
ok: true,
|
|
2373
|
+
updated: result.updated,
|
|
2374
|
+
title: result.title ?? title
|
|
2375
|
+
});
|
|
1781
2376
|
}
|
|
1782
2377
|
);
|
|
1783
2378
|
}
|
|
@@ -1861,7 +2456,11 @@ function registerVariableSetTools(server, deps, grant, can, json) {
|
|
|
1861
2456
|
variableSetId: variableSet.id,
|
|
1862
2457
|
variableName: parsedName.data
|
|
1863
2458
|
});
|
|
1864
|
-
const responseVariableSet = {
|
|
2459
|
+
const responseVariableSet = {
|
|
2460
|
+
id: variableSet.id,
|
|
2461
|
+
name: variableSet.name,
|
|
2462
|
+
created
|
|
2463
|
+
};
|
|
1865
2464
|
return json({
|
|
1866
2465
|
variableSet: responseVariableSet,
|
|
1867
2466
|
environment: responseVariableSet,
|
|
@@ -1906,11 +2505,11 @@ function registerVariableSetTools(server, deps, grant, can, json) {
|
|
|
1906
2505
|
);
|
|
1907
2506
|
}
|
|
1908
2507
|
}
|
|
1909
|
-
function registerGitHubConnectTool(server, deps,
|
|
2508
|
+
function registerGitHubConnectTool(server, deps, json) {
|
|
1910
2509
|
server.registerTool(
|
|
1911
2510
|
"github_connect_link",
|
|
1912
2511
|
{
|
|
1913
|
-
description: "
|
|
2512
|
+
description: "Report GitHub App connection availability. New installation binding is disabled until GitHub installation authority can be proven, so installUrl and linkUrl are null.",
|
|
1914
2513
|
inputSchema: {}
|
|
1915
2514
|
},
|
|
1916
2515
|
async () => {
|
|
@@ -1918,23 +2517,19 @@ function registerGitHubConnectTool(server, deps, grant, options, json) {
|
|
|
1918
2517
|
const missing = githubAppMissingSettings(settings);
|
|
1919
2518
|
const slug = settings.githubAppSlug?.trim() || null;
|
|
1920
2519
|
if (missing.length > 0 || !slug) {
|
|
1921
|
-
return json({
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
);
|
|
2520
|
+
return json({
|
|
2521
|
+
configured: false,
|
|
2522
|
+
appSlug: slug,
|
|
2523
|
+
installUrl: null,
|
|
2524
|
+
linkUrl: null,
|
|
2525
|
+
missing
|
|
2526
|
+
});
|
|
1928
2527
|
}
|
|
1929
|
-
const state = createSignedState(deps.githubStateSecret, {
|
|
1930
|
-
accountId: grant.accountId,
|
|
1931
|
-
workspaceId: grant.workspaceId
|
|
1932
|
-
});
|
|
1933
2528
|
return json({
|
|
1934
2529
|
configured: true,
|
|
1935
2530
|
appSlug: slug,
|
|
1936
|
-
installUrl:
|
|
1937
|
-
|
|
2531
|
+
installUrl: null,
|
|
2532
|
+
linkUrl: null,
|
|
1938
2533
|
missing: []
|
|
1939
2534
|
});
|
|
1940
2535
|
}
|
|
@@ -1964,9 +2559,18 @@ function registerGitHubTokenTool(server, deps, grant, sessionId, json) {
|
|
|
1964
2559
|
if (selected.some((item) => item.installationId !== installationId)) {
|
|
1965
2560
|
throw new Error("GitHub App repository resources must belong to one installation");
|
|
1966
2561
|
}
|
|
2562
|
+
const repositoryIds = selected.map((item) => item.repositoryId);
|
|
2563
|
+
if (!await areGitHubRepositoriesAllowedForWorkspace(
|
|
2564
|
+
deps.db,
|
|
2565
|
+
grant.workspaceId,
|
|
2566
|
+
installationId,
|
|
2567
|
+
repositoryIds
|
|
2568
|
+
)) {
|
|
2569
|
+
throw new Error("this workspace no longer authorizes the session's GitHub repositories");
|
|
2570
|
+
}
|
|
1967
2571
|
const token = await createGitHubAppInstallationToken(deps.settings, {
|
|
1968
2572
|
installationId,
|
|
1969
|
-
repositoryIds
|
|
2573
|
+
repositoryIds
|
|
1970
2574
|
});
|
|
1971
2575
|
return json({
|
|
1972
2576
|
token,
|
|
@@ -1988,19 +2592,18 @@ function repositoryWithScheduledTaskResource(repository) {
|
|
|
1988
2592
|
kind: "repository",
|
|
1989
2593
|
uri,
|
|
1990
2594
|
ref: repository.defaultBranch,
|
|
1991
|
-
mountPath:
|
|
1992
|
-
...repository.private ? {
|
|
2595
|
+
mountPath: defaultRepositoryMountPath(uri),
|
|
2596
|
+
...repository.private ? {
|
|
2597
|
+
githubInstallationId: repository.installationId,
|
|
2598
|
+
githubRepositoryId: repository.id
|
|
2599
|
+
} : {}
|
|
1993
2600
|
}
|
|
1994
2601
|
};
|
|
1995
2602
|
}
|
|
1996
2603
|
function normalizedRepositoryUri(value) {
|
|
1997
2604
|
const url = new URL(value);
|
|
1998
2605
|
const path = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
|
|
1999
|
-
return `https://${url.
|
|
2000
|
-
}
|
|
2001
|
-
function repositoryMountPath(uri) {
|
|
2002
|
-
const url = new URL(uri);
|
|
2003
|
-
return `repos/${url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "")}`;
|
|
2606
|
+
return `https://${url.host.toLowerCase()}/${path}.git`;
|
|
2004
2607
|
}
|
|
2005
2608
|
function boundedMcpLimit(limit) {
|
|
2006
2609
|
if (!limit || !Number.isFinite(limit)) {
|
|
@@ -2008,6 +2611,304 @@ function boundedMcpLimit(limit) {
|
|
|
2008
2611
|
}
|
|
2009
2612
|
return Math.min(500, Math.max(1, Math.floor(limit)));
|
|
2010
2613
|
}
|
|
2614
|
+
function boundedRigHistoryLimit(limit) {
|
|
2615
|
+
if (!limit || !Number.isFinite(limit)) return 20;
|
|
2616
|
+
return Math.min(100, Math.max(1, Math.floor(limit)));
|
|
2617
|
+
}
|
|
2618
|
+
function boundedSessionEventMcpLimit(limit) {
|
|
2619
|
+
if (!limit || !Number.isFinite(limit)) return 40;
|
|
2620
|
+
return Math.min(250, Math.max(1, Math.floor(limit)));
|
|
2621
|
+
}
|
|
2622
|
+
var SESSION_DISCOVERY_DEFAULT_LIMIT = 20;
|
|
2623
|
+
var SESSION_DISCOVERY_MAX_LIMIT = 100;
|
|
2624
|
+
var SESSION_DISCOVERY_TEXT_CHARS = 600;
|
|
2625
|
+
var SESSION_DISCOVERY_PREVIEW_MAX_BYTES = 16384;
|
|
2626
|
+
var SESSION_DISCOVERY_PREVIEW_OMISSION_REASON = "aggregatePreviewBudget";
|
|
2627
|
+
var SESSION_DISCOVERY_PAGE_MAX_BYTES = 128e3;
|
|
2628
|
+
var SESSION_DISCOVERY_PREVIEW_DRILL_DOWN_TOOL = "session_events";
|
|
2629
|
+
var SESSION_DISCOVERY_PREVIEW_DRILL_DOWN_BASE_INPUT = {
|
|
2630
|
+
direction: "before",
|
|
2631
|
+
limit: 1,
|
|
2632
|
+
mode: "monitoring",
|
|
2633
|
+
payloadMode: "summary"
|
|
2634
|
+
};
|
|
2635
|
+
function sessionDiscoveryPreviewDrillDownInput(sessionId, type) {
|
|
2636
|
+
return {
|
|
2637
|
+
sessionId,
|
|
2638
|
+
includeTypes: [type],
|
|
2639
|
+
...SESSION_DISCOVERY_PREVIEW_DRILL_DOWN_BASE_INPUT
|
|
2640
|
+
};
|
|
2641
|
+
}
|
|
2642
|
+
function boundedSessionDiscoveryLimit(limit) {
|
|
2643
|
+
if (!limit || !Number.isFinite(limit)) return SESSION_DISCOVERY_DEFAULT_LIMIT;
|
|
2644
|
+
return Math.min(SESSION_DISCOVERY_MAX_LIMIT, Math.max(1, Math.floor(limit)));
|
|
2645
|
+
}
|
|
2646
|
+
function encodeSessionDiscoveryCursor(cursor) {
|
|
2647
|
+
return Buffer.from(
|
|
2648
|
+
JSON.stringify({
|
|
2649
|
+
v: 2,
|
|
2650
|
+
orderBy: cursor.orderBy,
|
|
2651
|
+
sortRevision: cursor.sortRevision,
|
|
2652
|
+
sortAt: cursor.sortAt,
|
|
2653
|
+
id: cursor.id,
|
|
2654
|
+
snapshotAt: cursor.snapshotAt,
|
|
2655
|
+
snapshotRevision: cursor.snapshotRevision,
|
|
2656
|
+
updatedAfter: cursor.updatedAfter
|
|
2657
|
+
}),
|
|
2658
|
+
"utf8"
|
|
2659
|
+
).toString("base64url");
|
|
2660
|
+
}
|
|
2661
|
+
var SESSION_DISCOVERY_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?Z$/;
|
|
2662
|
+
var SESSION_DISCOVERY_REVISION = /^(?:0|[1-9]\d*)$/;
|
|
2663
|
+
var SESSION_DISCOVERY_REVISION_MAX = 9223372036854775807n;
|
|
2664
|
+
var SESSION_DISCOVERY_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
2665
|
+
function normalizeSessionDiscoveryTimestamp(value, label) {
|
|
2666
|
+
if (!SESSION_DISCOVERY_TIMESTAMP.test(value) || Number.isNaN(new Date(value).getTime())) {
|
|
2667
|
+
throw new Error(`sessions_list ${label} must be an ISO UTC date-time`);
|
|
2668
|
+
}
|
|
2669
|
+
return value;
|
|
2670
|
+
}
|
|
2671
|
+
function normalizeSessionDiscoveryRevision(value, label) {
|
|
2672
|
+
if (!SESSION_DISCOVERY_REVISION.test(value)) {
|
|
2673
|
+
throw new Error(`sessions_list ${label} must be a decimal activity revision`);
|
|
2674
|
+
}
|
|
2675
|
+
const revision = BigInt(value);
|
|
2676
|
+
if (revision > SESSION_DISCOVERY_REVISION_MAX) {
|
|
2677
|
+
throw new Error(`sessions_list ${label} exceeds the database activity revision range`);
|
|
2678
|
+
}
|
|
2679
|
+
return revision.toString();
|
|
2680
|
+
}
|
|
2681
|
+
function decodeSessionDiscoveryCursor(value) {
|
|
2682
|
+
try {
|
|
2683
|
+
const parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
|
|
2684
|
+
if (parsed.v === void 0 && typeof parsed.createdAt === "string" && typeof parsed.id === "string" && SESSION_DISCOVERY_UUID.test(parsed.id)) {
|
|
2685
|
+
const createdAt = normalizeSessionDiscoveryTimestamp(
|
|
2686
|
+
parsed.createdAt,
|
|
2687
|
+
"legacy cursor createdAt"
|
|
2688
|
+
);
|
|
2689
|
+
return {
|
|
2690
|
+
orderBy: "createdAt",
|
|
2691
|
+
sortRevision: "0",
|
|
2692
|
+
sortAt: createdAt,
|
|
2693
|
+
id: parsed.id,
|
|
2694
|
+
snapshotAt: createdAt,
|
|
2695
|
+
snapshotRevision: "0",
|
|
2696
|
+
updatedAfter: null
|
|
2697
|
+
};
|
|
2698
|
+
}
|
|
2699
|
+
if (parsed.v === 1 && parsed.orderBy === "createdAt" && typeof parsed.sortAt === "string" && typeof parsed.snapshotAt === "string" && parsed.updatedAfter === null && typeof parsed.id === "string" && SESSION_DISCOVERY_UUID.test(parsed.id)) {
|
|
2700
|
+
return {
|
|
2701
|
+
orderBy: "createdAt",
|
|
2702
|
+
sortRevision: "0",
|
|
2703
|
+
sortAt: normalizeSessionDiscoveryTimestamp(parsed.sortAt, "cursor sortAt"),
|
|
2704
|
+
id: parsed.id,
|
|
2705
|
+
snapshotAt: normalizeSessionDiscoveryTimestamp(parsed.snapshotAt, "cursor snapshotAt"),
|
|
2706
|
+
snapshotRevision: "0",
|
|
2707
|
+
updatedAfter: null
|
|
2708
|
+
};
|
|
2709
|
+
}
|
|
2710
|
+
if (parsed.v !== 2 || parsed.orderBy !== "createdAt" && parsed.orderBy !== "updatedAt" || typeof parsed.sortRevision !== "string" || typeof parsed.sortAt !== "string" || typeof parsed.snapshotAt !== "string" || typeof parsed.snapshotRevision !== "string" || parsed.updatedAfter !== null && typeof parsed.updatedAfter !== "string" || typeof parsed.id !== "string" || !SESSION_DISCOVERY_UUID.test(parsed.id)) {
|
|
2711
|
+
throw new Error("invalid cursor fields");
|
|
2712
|
+
}
|
|
2713
|
+
const sortAt = normalizeSessionDiscoveryTimestamp(parsed.sortAt, "cursor sortAt");
|
|
2714
|
+
const snapshotAt = normalizeSessionDiscoveryTimestamp(parsed.snapshotAt, "cursor snapshotAt");
|
|
2715
|
+
const sortRevision = normalizeSessionDiscoveryRevision(
|
|
2716
|
+
parsed.sortRevision,
|
|
2717
|
+
"cursor sortRevision"
|
|
2718
|
+
);
|
|
2719
|
+
const snapshotRevision = normalizeSessionDiscoveryRevision(
|
|
2720
|
+
parsed.snapshotRevision,
|
|
2721
|
+
"cursor snapshotRevision"
|
|
2722
|
+
);
|
|
2723
|
+
const normalizedUpdatedAfter = parsed.updatedAfter === null ? null : normalizeSessionDiscoveryRevision(parsed.updatedAfter, "cursor updatedAfter");
|
|
2724
|
+
if (normalizedUpdatedAfter !== null && parsed.orderBy !== "updatedAt") {
|
|
2725
|
+
throw new Error("incremental cursor requires updatedAt order");
|
|
2726
|
+
}
|
|
2727
|
+
if (parsed.orderBy === "createdAt" && (sortRevision !== "0" || snapshotRevision !== "0")) {
|
|
2728
|
+
throw new Error("creation cursor cannot carry activity revisions");
|
|
2729
|
+
}
|
|
2730
|
+
return {
|
|
2731
|
+
orderBy: parsed.orderBy,
|
|
2732
|
+
sortRevision,
|
|
2733
|
+
sortAt,
|
|
2734
|
+
id: parsed.id,
|
|
2735
|
+
snapshotAt,
|
|
2736
|
+
snapshotRevision,
|
|
2737
|
+
updatedAfter: normalizedUpdatedAfter
|
|
2738
|
+
};
|
|
2739
|
+
} catch {
|
|
2740
|
+
throw new Error("sessions_list cursor is invalid");
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
function capSessionDiscoveryText(value, maxChars = SESSION_DISCOVERY_TEXT_CHARS, originalChars) {
|
|
2744
|
+
if (value === null) {
|
|
2745
|
+
return { text: value, truncated: false };
|
|
2746
|
+
}
|
|
2747
|
+
const projectedChars = Array.from(value);
|
|
2748
|
+
const sourceChars = Math.max(projectedChars.length, originalChars ?? projectedChars.length);
|
|
2749
|
+
if (sourceChars <= maxChars) {
|
|
2750
|
+
return { text: value, truncated: false };
|
|
2751
|
+
}
|
|
2752
|
+
let bodyChars = maxChars;
|
|
2753
|
+
let marker = "";
|
|
2754
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
2755
|
+
const omittedChars = Math.max(0, sourceChars - bodyChars);
|
|
2756
|
+
marker = `\u2026[${omittedChars} chars truncated]\u2026`;
|
|
2757
|
+
const nextBodyChars = Math.max(0, maxChars - Array.from(marker).length);
|
|
2758
|
+
if (nextBodyChars === bodyChars) break;
|
|
2759
|
+
bodyChars = nextBodyChars;
|
|
2760
|
+
}
|
|
2761
|
+
return {
|
|
2762
|
+
text: `${projectedChars.slice(0, bodyChars).join("")}${marker}`,
|
|
2763
|
+
truncated: true
|
|
2764
|
+
};
|
|
2765
|
+
}
|
|
2766
|
+
function capSessionDiscoveryPage(page, includeLastMessage) {
|
|
2767
|
+
const projected = page.sessions.map((session) => {
|
|
2768
|
+
const title = capSessionDiscoveryText(session.title, 200, session.titleOriginalChars);
|
|
2769
|
+
const goal = session.goal ? capSessionDiscoveryText(
|
|
2770
|
+
session.goal.text,
|
|
2771
|
+
SESSION_DISCOVERY_TEXT_CHARS,
|
|
2772
|
+
session.goal.textOriginalChars
|
|
2773
|
+
) : null;
|
|
2774
|
+
const preview = includeLastMessage ? capSessionDiscoveryText(
|
|
2775
|
+
session.latestMessage?.preview ?? null,
|
|
2776
|
+
SESSION_DISCOVERY_TEXT_CHARS,
|
|
2777
|
+
session.latestMessage?.previewOriginalChars
|
|
2778
|
+
) : null;
|
|
2779
|
+
const blocker = session.effectiveControl.primaryBlocker;
|
|
2780
|
+
const blockerDisplayName = blocker ? capSessionDiscoveryText(blocker.displayName, 200, blocker.displayNameOriginalChars) : null;
|
|
2781
|
+
return {
|
|
2782
|
+
id: session.id,
|
|
2783
|
+
title: title.text,
|
|
2784
|
+
titleTruncated: title.truncated,
|
|
2785
|
+
parentSessionId: session.parentSessionId,
|
|
2786
|
+
isRoot: session.parentSessionId === null,
|
|
2787
|
+
status: session.status,
|
|
2788
|
+
pause: {
|
|
2789
|
+
state: session.effectiveControl.state,
|
|
2790
|
+
additionalBlockerCount: session.effectiveControl.additionalBlockerCount,
|
|
2791
|
+
source: blocker ? {
|
|
2792
|
+
kind: blocker.kind,
|
|
2793
|
+
...blocker.sessionId ? { sessionId: blocker.sessionId } : {},
|
|
2794
|
+
displayName: blockerDisplayName.text,
|
|
2795
|
+
displayNameTruncated: blockerDisplayName.truncated
|
|
2796
|
+
} : null
|
|
2797
|
+
},
|
|
2798
|
+
goal: session.goal ? {
|
|
2799
|
+
status: session.goal.status,
|
|
2800
|
+
summary: goal.text,
|
|
2801
|
+
summaryTruncated: goal.truncated
|
|
2802
|
+
} : null,
|
|
2803
|
+
queuedPromptCount: session.queuedPromptCount,
|
|
2804
|
+
children: session.treeStats,
|
|
2805
|
+
...includeLastMessage ? {
|
|
2806
|
+
latestMessage: session.latestMessage ? {
|
|
2807
|
+
type: session.latestMessage.type,
|
|
2808
|
+
preview: preview.text,
|
|
2809
|
+
previewTruncated: preview.truncated
|
|
2810
|
+
} : null
|
|
2811
|
+
} : {},
|
|
2812
|
+
createdAt: session.createdAt,
|
|
2813
|
+
updatedAt: session.updatedAt
|
|
2814
|
+
};
|
|
2815
|
+
});
|
|
2816
|
+
let budgetBytes = 0;
|
|
2817
|
+
const budgeted = includeLastMessage ? projected.map((session) => {
|
|
2818
|
+
const latestMessage = session.latestMessage;
|
|
2819
|
+
if (!latestMessage || latestMessage.preview === null) return session;
|
|
2820
|
+
const candidateBytes = Buffer.byteLength(latestMessage.preview, "utf8");
|
|
2821
|
+
if (budgetBytes + candidateBytes <= SESSION_DISCOVERY_PREVIEW_MAX_BYTES) {
|
|
2822
|
+
budgetBytes += candidateBytes;
|
|
2823
|
+
return session;
|
|
2824
|
+
}
|
|
2825
|
+
return {
|
|
2826
|
+
...session,
|
|
2827
|
+
latestMessage: {
|
|
2828
|
+
...latestMessage,
|
|
2829
|
+
preview: null,
|
|
2830
|
+
previewOmitted: true,
|
|
2831
|
+
previewOmissionReason: SESSION_DISCOVERY_PREVIEW_OMISSION_REASON,
|
|
2832
|
+
previewDrillDownTool: SESSION_DISCOVERY_PREVIEW_DRILL_DOWN_TOOL,
|
|
2833
|
+
previewDrillDownInput: sessionDiscoveryPreviewDrillDownInput(
|
|
2834
|
+
session.id,
|
|
2835
|
+
latestMessage.type
|
|
2836
|
+
)
|
|
2837
|
+
}
|
|
2838
|
+
};
|
|
2839
|
+
}) : projected;
|
|
2840
|
+
let kept = budgeted;
|
|
2841
|
+
const build = () => {
|
|
2842
|
+
const previewBytes = includeLastMessage ? kept.reduce(
|
|
2843
|
+
(total, session) => total + (session.latestMessage?.preview === null || !session.latestMessage ? 0 : Buffer.byteLength(session.latestMessage.preview, "utf8")),
|
|
2844
|
+
0
|
|
2845
|
+
) : 0;
|
|
2846
|
+
const previewOmittedCount = includeLastMessage ? kept.filter((session) => {
|
|
2847
|
+
const latestMessage = session.latestMessage;
|
|
2848
|
+
return latestMessage != null && "previewOmitted" in latestMessage && latestMessage.previewOmitted === true;
|
|
2849
|
+
}).length : 0;
|
|
2850
|
+
const lastKept = kept.at(-1);
|
|
2851
|
+
const droppedForByteCap = kept.length < projected.length;
|
|
2852
|
+
const sourceLast = lastKept ? page.sessions.find((session) => session.id === lastKept.id) : void 0;
|
|
2853
|
+
const nextCursor = droppedForByteCap ? sourceLast ? encodeSessionDiscoveryCursor({
|
|
2854
|
+
orderBy: page.orderBy,
|
|
2855
|
+
sortRevision: sourceLast.sortRevision,
|
|
2856
|
+
sortAt: sourceLast.sortAt,
|
|
2857
|
+
id: sourceLast.id,
|
|
2858
|
+
snapshotAt: page.snapshotAt,
|
|
2859
|
+
snapshotRevision: page.snapshotRevision,
|
|
2860
|
+
updatedAfter: page.updatedAfter
|
|
2861
|
+
}) : null : page.nextCursor ? encodeSessionDiscoveryCursor(page.nextCursor) : null;
|
|
2862
|
+
const result2 = {
|
|
2863
|
+
sessions: kept,
|
|
2864
|
+
total: page.total,
|
|
2865
|
+
hasMore: page.hasMore || droppedForByteCap,
|
|
2866
|
+
nextCursor,
|
|
2867
|
+
orderBy: page.orderBy,
|
|
2868
|
+
snapshotAt: page.snapshotAt,
|
|
2869
|
+
snapshotRevision: page.snapshotRevision,
|
|
2870
|
+
updatedAfter: page.updatedAfter,
|
|
2871
|
+
updatedThrough: page.updatedThrough,
|
|
2872
|
+
...includeLastMessage ? {
|
|
2873
|
+
latestMessagePreviewBudget: {
|
|
2874
|
+
bytes: previewBytes,
|
|
2875
|
+
maxBytes: SESSION_DISCOVERY_PREVIEW_MAX_BYTES,
|
|
2876
|
+
omittedCount: previewOmittedCount,
|
|
2877
|
+
truncated: previewOmittedCount > 0,
|
|
2878
|
+
omissionReason: previewOmittedCount > 0 ? SESSION_DISCOVERY_PREVIEW_OMISSION_REASON : null,
|
|
2879
|
+
drillDownTool: SESSION_DISCOVERY_PREVIEW_DRILL_DOWN_TOOL,
|
|
2880
|
+
drillDownInput: {
|
|
2881
|
+
includeTypes: ["user.message", "agent.message.completed"],
|
|
2882
|
+
...SESSION_DISCOVERY_PREVIEW_DRILL_DOWN_BASE_INPUT
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
} : {},
|
|
2886
|
+
responseTruncated: droppedForByteCap,
|
|
2887
|
+
...droppedForByteCap ? {
|
|
2888
|
+
truncationReason: `response exceeded ${SESSION_DISCOVERY_PAGE_MAX_BYTES} bytes; continue with nextCursor`
|
|
2889
|
+
} : {},
|
|
2890
|
+
bytes: 0,
|
|
2891
|
+
maxBytes: SESSION_DISCOVERY_PAGE_MAX_BYTES
|
|
2892
|
+
};
|
|
2893
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
2894
|
+
const measured = Buffer.byteLength(JSON.stringify(result2, null, 2), "utf8");
|
|
2895
|
+
if (result2.bytes === measured) break;
|
|
2896
|
+
result2.bytes = measured;
|
|
2897
|
+
}
|
|
2898
|
+
return result2;
|
|
2899
|
+
};
|
|
2900
|
+
let result = build();
|
|
2901
|
+
while (result.bytes > SESSION_DISCOVERY_PAGE_MAX_BYTES && kept.length > 1) {
|
|
2902
|
+
kept = kept.slice(0, -1);
|
|
2903
|
+
result = build();
|
|
2904
|
+
}
|
|
2905
|
+
if (result.bytes > SESSION_DISCOVERY_PAGE_MAX_BYTES) {
|
|
2906
|
+
throw new RangeError(
|
|
2907
|
+
`sessions_list metadata exceeds its ${SESSION_DISCOVERY_PAGE_MAX_BYTES}-byte envelope`
|
|
2908
|
+
);
|
|
2909
|
+
}
|
|
2910
|
+
return result;
|
|
2911
|
+
}
|
|
2011
2912
|
function parseMcpDate(raw, label) {
|
|
2012
2913
|
const date = new Date(raw);
|
|
2013
2914
|
if (Number.isNaN(date.getTime())) {
|
|
@@ -2020,13 +2921,18 @@ function parseMcpDate(raw, label) {
|
|
|
2020
2921
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2021
2922
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
2022
2923
|
import { environmentsEncryptionKeyBytes } from "@opengeni/config";
|
|
2023
|
-
import {
|
|
2924
|
+
import {
|
|
2925
|
+
prefixedMcpToolName
|
|
2926
|
+
} from "@opengeni/contracts";
|
|
2024
2927
|
import {
|
|
2025
2928
|
hasPermission as hasPermission2,
|
|
2026
2929
|
settingsWithEnabledCapabilityMcpServers
|
|
2027
2930
|
} from "@opengeni/core";
|
|
2028
2931
|
import {
|
|
2029
2932
|
buildConnectionTokenResolver,
|
|
2933
|
+
buildHostConnectionTokenResolver,
|
|
2934
|
+
getSessionRootId,
|
|
2935
|
+
getSessionTurn as getSessionTurn2,
|
|
2030
2936
|
listSessionMcpServerMetadata,
|
|
2031
2937
|
listSessionMcpServersForRun,
|
|
2032
2938
|
requireSession as requireSession2,
|
|
@@ -2051,6 +2957,14 @@ async function prepareToolspaceMcpSurface(input) {
|
|
|
2051
2957
|
}
|
|
2052
2958
|
const sessionId = grant.metadata.sessionId;
|
|
2053
2959
|
const session = await requireSession2(deps.db, grant.workspaceId, sessionId);
|
|
2960
|
+
let rootSessionId = sessionId;
|
|
2961
|
+
if (deps.connectionCredentials?.mcpCredentials) {
|
|
2962
|
+
const resolvedRootSessionId = await getSessionRootId(deps.db, grant.workspaceId, sessionId);
|
|
2963
|
+
if (!resolvedRootSessionId) {
|
|
2964
|
+
throw new Error(`cannot resolve host MCP credentials for missing session ${sessionId}`);
|
|
2965
|
+
}
|
|
2966
|
+
rootSessionId = resolvedRootSessionId;
|
|
2967
|
+
}
|
|
2054
2968
|
const selectedIds = selectedMcpServerIds(
|
|
2055
2969
|
session.tools,
|
|
2056
2970
|
session.mcpServers.map((server) => server.id)
|
|
@@ -2065,12 +2979,13 @@ async function prepareToolspaceMcpSurface(input) {
|
|
|
2065
2979
|
deps,
|
|
2066
2980
|
grant,
|
|
2067
2981
|
sessionId,
|
|
2982
|
+
rootSessionId,
|
|
2068
2983
|
proxyableIds,
|
|
2069
2984
|
activeTurnId: session.activeTurnId ?? null,
|
|
2070
2985
|
getRegistry
|
|
2071
2986
|
});
|
|
2072
2987
|
const tools = listing.map(
|
|
2073
|
-
(entry) => toolspaceToolFor({ deps, grant, sessionId, entry, getRegistry })
|
|
2988
|
+
(entry) => toolspaceToolFor({ deps, grant, sessionId, rootSessionId, entry, getRegistry })
|
|
2074
2989
|
);
|
|
2075
2990
|
return {
|
|
2076
2991
|
sessionId,
|
|
@@ -2101,15 +3016,25 @@ async function buildToolspaceRegistry(deps, workspaceId, sessionId) {
|
|
|
2101
3016
|
return new Map(withSessionServers.mcpServers.map((server) => [server.id, server]));
|
|
2102
3017
|
}
|
|
2103
3018
|
async function resolveToolListing(input) {
|
|
2104
|
-
const { deps, grant, sessionId, proxyableIds, activeTurnId, getRegistry } = input;
|
|
2105
|
-
|
|
3019
|
+
const { deps, grant, sessionId, rootSessionId, proxyableIds, activeTurnId, getRegistry } = input;
|
|
3020
|
+
if (!activeTurnId) {
|
|
3021
|
+
return [];
|
|
3022
|
+
}
|
|
3023
|
+
const activeTurn = await getSessionTurn2(deps.db, grant.workspaceId, activeTurnId);
|
|
3024
|
+
if (!activeTurn || activeTurn.sessionId !== sessionId) {
|
|
3025
|
+
return [];
|
|
3026
|
+
}
|
|
3027
|
+
const cacheKey = await toolListCacheKey(
|
|
3028
|
+
deps,
|
|
3029
|
+
grant.workspaceId,
|
|
3030
|
+
sessionId,
|
|
3031
|
+
proxyableIds,
|
|
3032
|
+
activeTurn
|
|
3033
|
+
);
|
|
2106
3034
|
const cached = readToolListCache(cacheKey);
|
|
2107
3035
|
if (cached) {
|
|
2108
3036
|
return cached;
|
|
2109
3037
|
}
|
|
2110
|
-
if (!activeTurnId) {
|
|
2111
|
-
return [];
|
|
2112
|
-
}
|
|
2113
3038
|
const registry = await getRegistry();
|
|
2114
3039
|
const entries = [];
|
|
2115
3040
|
for (const serverId of proxyableIds) {
|
|
@@ -2117,9 +3042,14 @@ async function resolveToolListing(input) {
|
|
|
2117
3042
|
if (!config || !toolspaceCanProxyServer(config)) {
|
|
2118
3043
|
continue;
|
|
2119
3044
|
}
|
|
2120
|
-
const connection = await connectToolspaceServer({
|
|
2121
|
-
|
|
2122
|
-
|
|
3045
|
+
const connection = await connectToolspaceServer({
|
|
3046
|
+
deps,
|
|
3047
|
+
grant,
|
|
3048
|
+
config,
|
|
3049
|
+
sessionId,
|
|
3050
|
+
rootSessionId,
|
|
3051
|
+
turn: activeTurn
|
|
3052
|
+
}).catch(() => null);
|
|
2123
3053
|
if (!connection) {
|
|
2124
3054
|
continue;
|
|
2125
3055
|
}
|
|
@@ -2138,11 +3068,16 @@ async function resolveToolListing(input) {
|
|
|
2138
3068
|
writeToolListCache(cacheKey, entries);
|
|
2139
3069
|
return entries;
|
|
2140
3070
|
}
|
|
2141
|
-
async function toolListCacheKey(deps, workspaceId, sessionId, proxyableIds) {
|
|
3071
|
+
async function toolListCacheKey(deps, workspaceId, sessionId, proxyableIds, turn) {
|
|
2142
3072
|
const metadata = await listSessionMcpServerMetadata(deps.db, workspaceId, sessionId);
|
|
2143
3073
|
const versions = new Map(metadata.map((server) => [server.id, server.credentialVersion]));
|
|
2144
3074
|
const signature = proxyableIds.slice().sort().map((id) => `${id}@${versions.get(id) ?? 0}`).join(",");
|
|
2145
|
-
|
|
3075
|
+
const authority = JSON.stringify({
|
|
3076
|
+
turnId: turn.id,
|
|
3077
|
+
executionGeneration: turn.executionGeneration,
|
|
3078
|
+
initiator: turn.initiator
|
|
3079
|
+
});
|
|
3080
|
+
return `${workspaceId}:${sessionId}:${signature}:${authority}`;
|
|
2146
3081
|
}
|
|
2147
3082
|
function readToolListCache(key) {
|
|
2148
3083
|
const hit = toolListCache.get(key);
|
|
@@ -2176,9 +3111,18 @@ async function settingsWithSessionMcpServersForToolspace(deps, workspaceId, sess
|
|
|
2176
3111
|
if (metadata.length === 0) {
|
|
2177
3112
|
return settings;
|
|
2178
3113
|
}
|
|
2179
|
-
|
|
3114
|
+
if (metadata.some((server) => server.headerNames.length > 0)) {
|
|
3115
|
+
throw new Error(
|
|
3116
|
+
"session MCP server credentials require OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY"
|
|
3117
|
+
);
|
|
3118
|
+
}
|
|
2180
3119
|
}
|
|
2181
|
-
const servers = await listSessionMcpServersForRun(
|
|
3120
|
+
const servers = await listSessionMcpServersForRun(
|
|
3121
|
+
deps.db,
|
|
3122
|
+
workspaceId,
|
|
3123
|
+
sessionId,
|
|
3124
|
+
encryptionKey ?? null
|
|
3125
|
+
);
|
|
2182
3126
|
if (servers.length === 0) {
|
|
2183
3127
|
return settings;
|
|
2184
3128
|
}
|
|
@@ -2195,6 +3139,7 @@ async function settingsWithSessionMcpServersForToolspace(deps, workspaceId, sess
|
|
|
2195
3139
|
...server.timeoutMs ? { timeoutMs: server.timeoutMs } : {},
|
|
2196
3140
|
cacheToolsList: server.cacheToolsList ?? false,
|
|
2197
3141
|
...server.requireApproval !== void 0 ? { requireApproval: server.requireApproval } : {},
|
|
3142
|
+
...server.connectionRef ? { connectionRef: server.connectionRef } : {},
|
|
2198
3143
|
headers: server.headers
|
|
2199
3144
|
}))
|
|
2200
3145
|
]
|
|
@@ -2222,7 +3167,7 @@ async function connectToolspaceServer(input) {
|
|
|
2222
3167
|
};
|
|
2223
3168
|
}
|
|
2224
3169
|
function toolspaceToolFor(input) {
|
|
2225
|
-
const { deps, grant, sessionId, entry, getRegistry } = input;
|
|
3170
|
+
const { deps, grant, sessionId, rootSessionId, entry, getRegistry } = input;
|
|
2226
3171
|
const { serverId, tool } = entry;
|
|
2227
3172
|
const name = prefixedMcpToolName(serverId, tool.name);
|
|
2228
3173
|
const approvalRequired = mcpToolRequiresApproval(entry.requireApproval, tool.name);
|
|
@@ -2244,7 +3189,7 @@ function toolspaceToolFor(input) {
|
|
|
2244
3189
|
`toolspace call budget exhausted (${deps.settings.toolspaceMaxCallsPerTurn}/turn)`
|
|
2245
3190
|
);
|
|
2246
3191
|
}
|
|
2247
|
-
const turnId = reservation.
|
|
3192
|
+
const turnId = reservation.turn.id;
|
|
2248
3193
|
const registry = await getRegistry();
|
|
2249
3194
|
const config = registry.get(serverId);
|
|
2250
3195
|
if (!config || !toolspaceCanProxyServer(config) || !allowedByConfig(config, tool.name)) {
|
|
@@ -2253,9 +3198,14 @@ function toolspaceToolFor(input) {
|
|
|
2253
3198
|
if (mcpToolRequiresApproval(config.requireApproval, tool.name)) {
|
|
2254
3199
|
return mcpError(APPROVAL_REQUIRED_MESSAGE);
|
|
2255
3200
|
}
|
|
2256
|
-
const connection = await connectToolspaceServer({
|
|
2257
|
-
|
|
2258
|
-
|
|
3201
|
+
const connection = await connectToolspaceServer({
|
|
3202
|
+
deps,
|
|
3203
|
+
grant,
|
|
3204
|
+
config,
|
|
3205
|
+
sessionId,
|
|
3206
|
+
rootSessionId,
|
|
3207
|
+
turn: reservation.turn
|
|
3208
|
+
}).catch(() => null);
|
|
2259
3209
|
if (!connection) {
|
|
2260
3210
|
return mcpError(`upstream tool failed: ${name}`);
|
|
2261
3211
|
}
|
|
@@ -2335,7 +3285,11 @@ async function reserveActiveTurnCall(deps, workspaceId, sessionId) {
|
|
|
2335
3285
|
session.activeTurnId,
|
|
2336
3286
|
deps.settings.toolspaceMaxCallsPerTurn
|
|
2337
3287
|
);
|
|
2338
|
-
|
|
3288
|
+
if (!reservation.reserved) {
|
|
3289
|
+
return { status: "budget_exhausted" };
|
|
3290
|
+
}
|
|
3291
|
+
const turn = await getSessionTurn2(deps.db, workspaceId, session.activeTurnId);
|
|
3292
|
+
return turn && turn.sessionId === sessionId ? { status: "ok", turn } : { status: "no_active_turn" };
|
|
2339
3293
|
}
|
|
2340
3294
|
function selectedMcpServerIds(tools, sessionServerIds) {
|
|
2341
3295
|
const out = new Set(sessionServerIds);
|
|
@@ -2382,7 +3336,18 @@ function connectionBrokerFetch(baseFetch, input) {
|
|
|
2382
3336
|
if (!connectionRef) {
|
|
2383
3337
|
return baseFetch;
|
|
2384
3338
|
}
|
|
2385
|
-
const resolveCredential =
|
|
3339
|
+
const resolveCredential = input.deps.connectionCredentials?.mcpCredentials ? buildHostConnectionTokenResolver(input.deps.connectionCredentials.mcpCredentials, {
|
|
3340
|
+
accountId: input.grant.accountId,
|
|
3341
|
+
workspaceId: input.grant.workspaceId,
|
|
3342
|
+
sessionId: input.sessionId,
|
|
3343
|
+
rootSessionId: input.rootSessionId,
|
|
3344
|
+
turnId: input.turn.id,
|
|
3345
|
+
attemptId: input.turn.activeAttemptId,
|
|
3346
|
+
executionGeneration: input.turn.executionGeneration,
|
|
3347
|
+
initiator: input.turn.initiator,
|
|
3348
|
+
initiatorContext: input.turn.initiatorContext,
|
|
3349
|
+
surface: "toolspace"
|
|
3350
|
+
}) : buildConnectionTokenResolver(input.deps.db, input.deps.settings);
|
|
2386
3351
|
return async (requestInput, init) => {
|
|
2387
3352
|
const request = await mcpRequestInfo(requestInput, init);
|
|
2388
3353
|
const first = await resolveCredential({
|
|
@@ -2390,7 +3355,7 @@ function connectionBrokerFetch(baseFetch, input) {
|
|
|
2390
3355
|
serverId: input.config.id,
|
|
2391
3356
|
connectionRef,
|
|
2392
3357
|
forceRefresh: false,
|
|
2393
|
-
...request.toolName ? {
|
|
3358
|
+
...request.toolName ? { toolName: request.toolName } : {},
|
|
2394
3359
|
subjectId: input.grant.subjectId
|
|
2395
3360
|
});
|
|
2396
3361
|
if (first.status === "auth_needed") {
|
|
@@ -2406,7 +3371,7 @@ function connectionBrokerFetch(baseFetch, input) {
|
|
|
2406
3371
|
serverId: input.config.id,
|
|
2407
3372
|
connectionRef,
|
|
2408
3373
|
forceRefresh: true,
|
|
2409
|
-
...request.toolName ? {
|
|
3374
|
+
...request.toolName ? { toolName: request.toolName } : {},
|
|
2410
3375
|
subjectId: input.grant.subjectId
|
|
2411
3376
|
});
|
|
2412
3377
|
if (refreshed.status === "auth_needed") {
|
|
@@ -2433,9 +3398,11 @@ function authNeededFromStatus(config, first, reason) {
|
|
|
2433
3398
|
status: "auth_needed",
|
|
2434
3399
|
reason,
|
|
2435
3400
|
providerDomain: connectionRef.providerDomain,
|
|
3401
|
+
...connectionRef.provider ? { provider: connectionRef.provider } : {},
|
|
2436
3402
|
connectionId: first.connectionId,
|
|
2437
3403
|
...connectionRef.scopes ? { scopes: connectionRef.scopes } : {},
|
|
2438
|
-
...connectionRef.resource ? { resource: connectionRef.resource } : {}
|
|
3404
|
+
...connectionRef.resource ? { resource: connectionRef.resource } : {},
|
|
3405
|
+
...connectionRef.selectedResources ? { selectedResources: connectionRef.selectedResources } : {}
|
|
2439
3406
|
};
|
|
2440
3407
|
}
|
|
2441
3408
|
async function authNeededFetchResponse(input, request, auth) {
|
|
@@ -2452,10 +3419,12 @@ async function authNeededFetchResponse(input, request, auth) {
|
|
|
2452
3419
|
serverId: input.config.id,
|
|
2453
3420
|
toolName: request.toolName ?? null,
|
|
2454
3421
|
providerDomain: auth.providerDomain,
|
|
3422
|
+
...auth.provider ? { provider: auth.provider } : {},
|
|
2455
3423
|
reason: auth.reason,
|
|
2456
3424
|
...auth.connectionId ? { connectionId: auth.connectionId } : {},
|
|
2457
3425
|
...auth.scopes ? { scopes: auth.scopes } : {},
|
|
2458
3426
|
...auth.resource ? { resource: auth.resource } : {},
|
|
3427
|
+
...auth.selectedResources ? { selectedResources: auth.selectedResources } : {},
|
|
2459
3428
|
...auth.authorizationUrl ? { authorizationUrl: auth.authorizationUrl } : {},
|
|
2460
3429
|
subjectId: input.grant.subjectId
|
|
2461
3430
|
}
|
|
@@ -2632,6 +3601,7 @@ function isInstallRedirectPath(path) {
|
|
|
2632
3601
|
|
|
2633
3602
|
// src/http/auth.ts
|
|
2634
3603
|
var githubConnectPathPattern = /^\/v1\/workspaces\/[^/]+\/github\/connect$/;
|
|
3604
|
+
var githubInstallationLinkPathPattern = /^\/v1\/workspaces\/[^/]+\/github\/installations$/;
|
|
2635
3605
|
function requireAccessKey(settings) {
|
|
2636
3606
|
return async (c, next) => {
|
|
2637
3607
|
if (!settings.authRequired || isAuthExempt(c, settings)) {
|
|
@@ -2671,6 +3641,9 @@ function isAuthExempt(c, settings) {
|
|
|
2671
3641
|
if (githubConnectPathPattern.test(path)) {
|
|
2672
3642
|
return true;
|
|
2673
3643
|
}
|
|
3644
|
+
if (c.req.method === "POST" && githubInstallationLinkPathPattern.test(path)) {
|
|
3645
|
+
return true;
|
|
3646
|
+
}
|
|
2674
3647
|
if (installExactPaths.has(path) || isInstallRedirectPath(path)) {
|
|
2675
3648
|
return true;
|
|
2676
3649
|
}
|
|
@@ -2929,10 +3902,9 @@ import {
|
|
|
2929
3902
|
setInitialActiveCodexCredential,
|
|
2930
3903
|
updateCodexRotationSettings,
|
|
2931
3904
|
upsertCodexSubscriptionCredential,
|
|
2932
|
-
withCodexCapacityMutation
|
|
2933
|
-
CODEX_ROTATION_STRATEGIES
|
|
3905
|
+
withCodexCapacityMutation
|
|
2934
3906
|
} from "@opengeni/db";
|
|
2935
|
-
import { createSignedState
|
|
3907
|
+
import { createSignedState, readSignedState } from "@opengeni/github";
|
|
2936
3908
|
import { HTTPException as HTTPException4 } from "hono/http-exception";
|
|
2937
3909
|
import { requireAccessGrant as requireAccessGrant2 } from "@opengeni/core";
|
|
2938
3910
|
var CODEX_PROVIDER_LABEL = "Codex subscription \xB7 no credits";
|
|
@@ -3018,7 +3990,7 @@ function registerCodexRoutes(app, deps) {
|
|
|
3018
3990
|
message: error instanceof CodexDeviceError ? error.message : "failed to start Codex device login"
|
|
3019
3991
|
});
|
|
3020
3992
|
}
|
|
3021
|
-
const state =
|
|
3993
|
+
const state = createSignedState(githubStateSecret, {
|
|
3022
3994
|
workspaceId,
|
|
3023
3995
|
deviceAuthId: start.deviceAuthId,
|
|
3024
3996
|
userCode: start.userCode
|
|
@@ -3177,7 +4149,9 @@ function registerCodexRoutes(app, deps) {
|
|
|
3177
4149
|
activeAccountId,
|
|
3178
4150
|
settings: {
|
|
3179
4151
|
rotationEnabled: rotation?.rotationEnabled ?? false,
|
|
3180
|
-
|
|
4152
|
+
// sharded-rotation policy: rotation-enabled always behaves as sticky-sharded; report the
|
|
4153
|
+
// effective truth, never the stored legacy residue.
|
|
4154
|
+
rotationStrategy: "sharded",
|
|
3181
4155
|
activeCredentialId: activeAccountId
|
|
3182
4156
|
}
|
|
3183
4157
|
});
|
|
@@ -3209,15 +4183,12 @@ function registerCodexRoutes(app, deps) {
|
|
|
3209
4183
|
if (typeof body.rotationEnabled === "boolean") {
|
|
3210
4184
|
patch.rotationEnabled = body.rotationEnabled;
|
|
3211
4185
|
}
|
|
3212
|
-
if (
|
|
3213
|
-
if (!CODEX_ROTATION_STRATEGIES.includes(body.rotationStrategy)) {
|
|
3214
|
-
throw new HTTPException4(400, { message: "invalid rotation strategy" });
|
|
3215
|
-
}
|
|
3216
|
-
patch.rotationStrategy = body.rotationStrategy;
|
|
3217
|
-
}
|
|
3218
|
-
if (patch.rotationEnabled === void 0 && patch.rotationStrategy === void 0) {
|
|
4186
|
+
if (patch.rotationEnabled === void 0 && body.rotationStrategy === void 0) {
|
|
3219
4187
|
throw new HTTPException4(400, { message: "no settings to update" });
|
|
3220
4188
|
}
|
|
4189
|
+
if (patch.rotationEnabled === void 0) {
|
|
4190
|
+
return c.json({ rotationStrategy: "sharded", rotationStrategyDeprecated: true });
|
|
4191
|
+
}
|
|
3221
4192
|
await ensureCodexRotationSettings(db, grant.accountId, workspaceId);
|
|
3222
4193
|
const mutation = await withCodexCapacityMutation(
|
|
3223
4194
|
db,
|
|
@@ -3234,7 +4205,8 @@ function registerCodexRoutes(app, deps) {
|
|
|
3234
4205
|
await signalCodexCapacityTargets(deps, mutation.wakeTargets);
|
|
3235
4206
|
return c.json({
|
|
3236
4207
|
rotationEnabled: updated.rotationEnabled,
|
|
3237
|
-
|
|
4208
|
+
// sharded-rotation policy: sharded is the only behavior; the stored column is residue.
|
|
4209
|
+
rotationStrategy: "sharded",
|
|
3238
4210
|
activeCredentialId: updated.activeCredentialId
|
|
3239
4211
|
});
|
|
3240
4212
|
});
|
|
@@ -3385,7 +4357,7 @@ import {
|
|
|
3385
4357
|
storeIntegrationOAuthClient,
|
|
3386
4358
|
updateConnection
|
|
3387
4359
|
} from "@opengeni/db";
|
|
3388
|
-
import { createSignedState as
|
|
4360
|
+
import { createSignedState as createSignedState2, readSignedState as readSignedState2 } from "@opengeni/github";
|
|
3389
4361
|
import { Buffer as Buffer2 } from "buffer";
|
|
3390
4362
|
import { createHash, randomBytes } from "crypto";
|
|
3391
4363
|
import { lookup } from "dns/promises";
|
|
@@ -3450,7 +4422,7 @@ async function startMcpOAuth(deps, context) {
|
|
|
3450
4422
|
context.payload.oauthClient
|
|
3451
4423
|
);
|
|
3452
4424
|
const key = requireEnvironmentEncryption(settings);
|
|
3453
|
-
const state =
|
|
4425
|
+
const state = createSignedState2(requireIntegrationsStateSecret(settings), {
|
|
3454
4426
|
accountId: context.accountId,
|
|
3455
4427
|
workspaceId: context.workspaceId,
|
|
3456
4428
|
subjectId: context.subjectId,
|
|
@@ -5439,7 +6411,7 @@ async function buildEnrollmentCredentials(services, input) {
|
|
|
5439
6411
|
// Hand the agent the canonical `/stream` dial base, NOT the raw configured URL.
|
|
5440
6412
|
// The agent's relay producer appends only its routing query and assumes the base
|
|
5441
6413
|
// already carries the relay's `/stream` route; a path-less base 400s the dial and
|
|
5442
|
-
// makes the terminal/desktop streams unreachable
|
|
6414
|
+
// makes the terminal/desktop streams unreachable.
|
|
5443
6415
|
relayUrl: relayDialBaseFromSettings(settings),
|
|
5444
6416
|
relayToken,
|
|
5445
6417
|
// M-AUTH closes the placeholder: there is NO per-machine NATS Account creds
|
|
@@ -6995,46 +7967,44 @@ function looksLikeEmail(value) {
|
|
|
6995
7967
|
|
|
6996
7968
|
// src/routes/github.ts
|
|
6997
7969
|
import { GitHubAppManifestCreate } from "@opengeni/contracts";
|
|
6998
|
-
import {
|
|
7970
|
+
import { deleteGitHubInstallationBinding } from "@opengeni/db";
|
|
6999
7971
|
import {
|
|
7000
7972
|
buildGitHubAppManifest,
|
|
7001
7973
|
convertGitHubAppManifest,
|
|
7002
|
-
createSignedState as
|
|
7974
|
+
createSignedState as createSignedState3,
|
|
7003
7975
|
envLinesFromGitHubManifestConversion,
|
|
7004
7976
|
GitHubAppApiError,
|
|
7005
7977
|
GitHubAppConfigurationError as GitHubAppConfigurationError2,
|
|
7006
|
-
githubOAuthAuthorizeUrl,
|
|
7007
7978
|
githubAppMissingSettings as githubAppMissingSettings2,
|
|
7008
|
-
listGitHubAppRepositories as listGitHubAppRepositories2,
|
|
7009
7979
|
organizationAppManifestUrl,
|
|
7010
7980
|
personalAppManifestUrl,
|
|
7011
7981
|
readSignedState as readSignedState3,
|
|
7012
|
-
stateMaxAgeSeconds
|
|
7013
|
-
verifyGitHubInstallationAccessForUser,
|
|
7982
|
+
stateMaxAgeSeconds,
|
|
7014
7983
|
verifySignedState
|
|
7015
7984
|
} from "@opengeni/github";
|
|
7016
|
-
import {
|
|
7985
|
+
import { setCookie } from "hono/cookie";
|
|
7017
7986
|
import { HTTPException as HTTPException15 } from "hono/http-exception";
|
|
7018
7987
|
import { requireAccessGrant as requireAccessGrant10 } from "@opengeni/core";
|
|
7019
7988
|
var githubStateCookie = "opengeni_github_state";
|
|
7989
|
+
var installationBindingDisabledMessage = "Connecting a GitHub App installation is disabled until GitHub installation authority can be proven";
|
|
7020
7990
|
function registerGitHubRoutes(app, deps) {
|
|
7021
|
-
const { settings, githubStateSecret } = deps;
|
|
7991
|
+
const { db, settings, githubStateSecret } = deps;
|
|
7022
7992
|
app.get("/v1/workspaces/:workspaceId/github/app", async (c) => {
|
|
7023
7993
|
const workspaceId = c.req.param("workspaceId");
|
|
7024
7994
|
const grant = await requireAccessGrant10(c, deps, workspaceId, "github:use");
|
|
7025
7995
|
const missing = githubAppMissingSettings2(settings);
|
|
7026
7996
|
const slug = settings.githubAppSlug?.trim() || null;
|
|
7027
|
-
const state = createSignedState4(githubStateSecret, {
|
|
7028
|
-
accountId: grant.accountId,
|
|
7029
|
-
workspaceId: grant.workspaceId
|
|
7030
|
-
});
|
|
7031
|
-
setGitHubStateCookie(c, deps, state);
|
|
7032
7997
|
return c.json({
|
|
7033
7998
|
configured: missing.length === 0,
|
|
7034
7999
|
appId: settings.githubAppId ?? null,
|
|
7035
8000
|
clientId: settings.githubClientId ?? null,
|
|
7036
8001
|
appSlug: slug,
|
|
7037
|
-
|
|
8002
|
+
// Kept nullable for SDK compatibility. GitHub's setup callback contains
|
|
8003
|
+
// a spoofable installation_id, while user-installation visibility and
|
|
8004
|
+
// repository admin permission do not prove that this human may bind it.
|
|
8005
|
+
installUrl: null,
|
|
8006
|
+
linkUrl: null,
|
|
8007
|
+
installations: await listWorkspaceGitHubInstallationBindings(deps, grant.workspaceId),
|
|
7038
8008
|
missing
|
|
7039
8009
|
});
|
|
7040
8010
|
});
|
|
@@ -7048,19 +8018,7 @@ function registerGitHubRoutes(app, deps) {
|
|
|
7048
8018
|
if (!statePayload || statePayload.workspaceId !== workspaceId) {
|
|
7049
8019
|
throw new HTTPException15(400, { message: "invalid or expired GitHub installation state" });
|
|
7050
8020
|
}
|
|
7051
|
-
|
|
7052
|
-
if (!slug) {
|
|
7053
|
-
throw new HTTPException15(409, {
|
|
7054
|
-
message: JSON.stringify({
|
|
7055
|
-
message: "GitHub App is not configured",
|
|
7056
|
-
missing: githubAppMissingSettings2(settings)
|
|
7057
|
-
})
|
|
7058
|
-
});
|
|
7059
|
-
}
|
|
7060
|
-
setGitHubStateCookie(c, deps, state);
|
|
7061
|
-
return c.redirect(
|
|
7062
|
-
`https://github.com/apps/${slug}/installations/new?state=${encodeURIComponent(state)}`
|
|
7063
|
-
);
|
|
8021
|
+
throw installationBindingDisabled();
|
|
7064
8022
|
});
|
|
7065
8023
|
app.get("/v1/workspaces/:workspaceId/github/repositories", async (c) => {
|
|
7066
8024
|
const workspaceId = c.req.param("workspaceId");
|
|
@@ -7094,6 +8052,23 @@ function registerGitHubRoutes(app, deps) {
|
|
|
7094
8052
|
});
|
|
7095
8053
|
}
|
|
7096
8054
|
});
|
|
8055
|
+
app.delete("/v1/workspaces/:workspaceId/github/installations/:installationId", async (c) => {
|
|
8056
|
+
const workspaceId = c.req.param("workspaceId");
|
|
8057
|
+
const grant = await requireAccessGrant10(c, deps, workspaceId, "github:manage");
|
|
8058
|
+
const installationId = parsePositiveInteger(c.req.param("installationId"));
|
|
8059
|
+
if (installationId === null) {
|
|
8060
|
+
throw new HTTPException15(400, { message: "invalid GitHub installation id" });
|
|
8061
|
+
}
|
|
8062
|
+
const deleted = await deleteGitHubInstallationBinding(db, {
|
|
8063
|
+
accountId: grant.accountId,
|
|
8064
|
+
workspaceId: grant.workspaceId,
|
|
8065
|
+
installationId
|
|
8066
|
+
});
|
|
8067
|
+
if (!deleted) {
|
|
8068
|
+
throw new HTTPException15(404, { message: "GitHub installation binding not found" });
|
|
8069
|
+
}
|
|
8070
|
+
return c.body(null, 204);
|
|
8071
|
+
});
|
|
7097
8072
|
app.post("/v1/workspaces/:workspaceId/github/app-manifest", async (c) => {
|
|
7098
8073
|
const workspaceId = c.req.param("workspaceId");
|
|
7099
8074
|
const grant = await requireAccessGrant10(c, deps, workspaceId, "github:manage");
|
|
@@ -7102,7 +8077,7 @@ function registerGitHubRoutes(app, deps) {
|
|
|
7102
8077
|
/\/+$/,
|
|
7103
8078
|
""
|
|
7104
8079
|
);
|
|
7105
|
-
const state =
|
|
8080
|
+
const state = createSignedState3(githubStateSecret, {
|
|
7106
8081
|
accountId: grant.accountId,
|
|
7107
8082
|
workspaceId: grant.workspaceId
|
|
7108
8083
|
});
|
|
@@ -7134,20 +8109,15 @@ function registerGitHubRoutes(app, deps) {
|
|
|
7134
8109
|
try {
|
|
7135
8110
|
const conversion = await convertGitHubAppManifest(code);
|
|
7136
8111
|
const envLines = envLinesFromGitHubManifestConversion(conversion);
|
|
7137
|
-
const slug = String(conversion.slug ?? "");
|
|
7138
|
-
const installUrl = slug ? `https://github.com/apps/${slug}/installations/new?state=${encodeURIComponent(state)}` : "";
|
|
7139
8112
|
setGitHubStateCookie(c, deps, state);
|
|
7140
|
-
return c.html(githubSuccessHtml(envLines
|
|
8113
|
+
return c.html(githubSuccessHtml(envLines));
|
|
7141
8114
|
} catch (error) {
|
|
7142
8115
|
const message = error instanceof GitHubAppApiError ? error.message : String(error);
|
|
7143
8116
|
throw new HTTPException15(502, { message });
|
|
7144
8117
|
}
|
|
7145
8118
|
});
|
|
7146
8119
|
const handleGitHubInstallCallback = async (c) => {
|
|
7147
|
-
const code = c.req.query("code");
|
|
7148
8120
|
const state = c.req.query("state");
|
|
7149
|
-
const installationIdRaw = c.req.query("installation_id");
|
|
7150
|
-
const setupAction = c.req.query("setup_action") ?? null;
|
|
7151
8121
|
if (!state) {
|
|
7152
8122
|
throw new HTTPException15(400, { message: "missing GitHub installation state" });
|
|
7153
8123
|
}
|
|
@@ -7155,162 +8125,54 @@ function registerGitHubRoutes(app, deps) {
|
|
|
7155
8125
|
if (!statePayload || typeof statePayload.accountId !== "string" || typeof statePayload.workspaceId !== "string") {
|
|
7156
8126
|
throw new HTTPException15(400, { message: "invalid or expired GitHub installation state" });
|
|
7157
8127
|
}
|
|
7158
|
-
|
|
7159
|
-
const grant = await requireAccessGrant10(c, deps, statePayload.workspaceId, "github:manage");
|
|
7160
|
-
if (grant.accountId !== statePayload.accountId) {
|
|
7161
|
-
throw new HTTPException15(403, {
|
|
7162
|
-
message: "GitHub installation state does not match this workspace"
|
|
7163
|
-
});
|
|
7164
|
-
}
|
|
7165
|
-
if (setupAction === "request" && !installationIdRaw) {
|
|
7166
|
-
return c.html(githubSetupPendingHtml());
|
|
7167
|
-
}
|
|
7168
|
-
const installationId = parsePositiveInteger(installationIdRaw);
|
|
7169
|
-
if (installationId === null) {
|
|
7170
|
-
throw new HTTPException15(400, { message: "missing or invalid GitHub installation_id" });
|
|
7171
|
-
}
|
|
7172
|
-
if (!code) {
|
|
7173
|
-
const clientId = settings.githubClientId?.trim();
|
|
7174
|
-
if (!clientId) {
|
|
7175
|
-
throw new HTTPException15(409, {
|
|
7176
|
-
message: JSON.stringify({
|
|
7177
|
-
message: "GitHub App is not configured",
|
|
7178
|
-
missing: ["OPENGENI_GITHUB_CLIENT_ID"]
|
|
7179
|
-
})
|
|
7180
|
-
});
|
|
7181
|
-
}
|
|
7182
|
-
const oauthState = createSignedState4(githubStateSecret, {
|
|
7183
|
-
accountId: grant.accountId,
|
|
7184
|
-
workspaceId: grant.workspaceId,
|
|
7185
|
-
installationId
|
|
7186
|
-
});
|
|
7187
|
-
const baseUrl = (settings.githubAppManifestBaseUrl ?? settings.publicBaseUrl ?? new URL(c.req.url).origin).replace(/\/+$/, "");
|
|
7188
|
-
setGitHubStateCookie(c, deps, oauthState);
|
|
7189
|
-
return c.redirect(
|
|
7190
|
-
githubOAuthAuthorizeUrl({
|
|
7191
|
-
clientId,
|
|
7192
|
-
state: oauthState,
|
|
7193
|
-
redirectUri: `${baseUrl}/v1/github/oauth/callback`
|
|
7194
|
-
})
|
|
7195
|
-
);
|
|
7196
|
-
}
|
|
7197
|
-
return await completeGitHubInstallationBinding(deps, c, {
|
|
7198
|
-
code,
|
|
7199
|
-
statePayload,
|
|
7200
|
-
installationId
|
|
7201
|
-
});
|
|
8128
|
+
throw installationBindingDisabled();
|
|
7202
8129
|
};
|
|
7203
8130
|
app.get("/v1/github/setup", handleGitHubInstallCallback);
|
|
7204
8131
|
app.get("/v1/github/install/callback", handleGitHubInstallCallback);
|
|
7205
8132
|
app.get("/v1/github/oauth/callback", async (c) => {
|
|
7206
|
-
const code = c.req.query("code");
|
|
7207
8133
|
const state = c.req.query("state");
|
|
7208
|
-
if (!code) {
|
|
7209
|
-
throw new HTTPException15(400, { message: "missing GitHub OAuth code" });
|
|
7210
|
-
}
|
|
7211
8134
|
if (!state) {
|
|
7212
8135
|
throw new HTTPException15(400, { message: "missing GitHub OAuth state" });
|
|
7213
8136
|
}
|
|
7214
8137
|
const statePayload = readSignedState3(state, githubStateSecret);
|
|
7215
|
-
|
|
7216
|
-
if (!statePayload || typeof statePayload.accountId !== "string" || typeof statePayload.workspaceId !== "string" || installationId === null) {
|
|
8138
|
+
if (!statePayload || typeof statePayload.accountId !== "string" || typeof statePayload.workspaceId !== "string") {
|
|
7217
8139
|
throw new HTTPException15(400, { message: "invalid or expired GitHub OAuth state" });
|
|
7218
8140
|
}
|
|
7219
|
-
|
|
7220
|
-
return await completeGitHubInstallationBinding(deps, c, {
|
|
7221
|
-
code,
|
|
7222
|
-
statePayload,
|
|
7223
|
-
installationId
|
|
7224
|
-
});
|
|
8141
|
+
throw installationBindingDisabled();
|
|
7225
8142
|
});
|
|
7226
|
-
|
|
7227
|
-
|
|
7228
|
-
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
const grant = await requireAccessGrant10(c, deps, input.statePayload.workspaceId, "github:manage");
|
|
7233
|
-
if (grant.accountId !== input.statePayload.accountId) {
|
|
7234
|
-
throw new HTTPException15(403, {
|
|
7235
|
-
message: "GitHub installation state does not match this workspace"
|
|
7236
|
-
});
|
|
7237
|
-
}
|
|
7238
|
-
try {
|
|
7239
|
-
const installation = await verifyGitHubInstallationAccessForUser(settings, {
|
|
7240
|
-
code: input.code,
|
|
7241
|
-
installationId: input.installationId
|
|
7242
|
-
});
|
|
7243
|
-
if (!installation) {
|
|
7244
|
-
throw new HTTPException15(404, {
|
|
7245
|
-
message: "GitHub App installation was not found for this app"
|
|
7246
|
-
});
|
|
7247
|
-
}
|
|
7248
|
-
if (installation.suspended) {
|
|
7249
|
-
throw new HTTPException15(409, { message: "GitHub App installation is suspended" });
|
|
7250
|
-
}
|
|
7251
|
-
await upsertGitHubInstallation(db, {
|
|
7252
|
-
accountId: grant.accountId,
|
|
7253
|
-
workspaceId: grant.workspaceId,
|
|
7254
|
-
installationId: input.installationId,
|
|
7255
|
-
accountLogin: installation.accountLogin,
|
|
7256
|
-
accountType: installation.accountType
|
|
7257
|
-
});
|
|
7258
|
-
const returnUrl = openGeniReturnUrl(settings, c, input.statePayload.workspaceId);
|
|
7259
|
-
deleteCookie(c, githubStateCookie, { path: "/v1/github" });
|
|
7260
|
-
return c.html(
|
|
7261
|
-
githubSetupSuccessHtml(
|
|
7262
|
-
installation.accountLogin ?? `installation ${input.installationId}`,
|
|
7263
|
-
returnUrl
|
|
7264
|
-
)
|
|
7265
|
-
);
|
|
7266
|
-
} catch (error) {
|
|
7267
|
-
if (error instanceof HTTPException15) {
|
|
7268
|
-
throw error;
|
|
8143
|
+
app.post("/v1/workspaces/:workspaceId/github/installations", async (c) => {
|
|
8144
|
+
const workspaceId = c.req.param("workspaceId");
|
|
8145
|
+
const form = new URLSearchParams(await c.req.text());
|
|
8146
|
+
const state = form.get("oauth_state");
|
|
8147
|
+
if (!state) {
|
|
8148
|
+
throw new HTTPException15(400, { message: "missing GitHub OAuth state" });
|
|
7269
8149
|
}
|
|
7270
|
-
|
|
7271
|
-
|
|
7272
|
-
|
|
7273
|
-
});
|
|
8150
|
+
const statePayload = readSignedState3(state, githubStateSecret);
|
|
8151
|
+
if (!statePayload || typeof statePayload.accountId !== "string" || statePayload.accountId.length === 0 || statePayload.workspaceId !== workspaceId) {
|
|
8152
|
+
throw new HTTPException15(400, { message: "invalid or expired GitHub OAuth state" });
|
|
7274
8153
|
}
|
|
7275
|
-
throw
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
|
|
8154
|
+
throw installationBindingDisabled();
|
|
8155
|
+
});
|
|
8156
|
+
}
|
|
8157
|
+
function installationBindingDisabled() {
|
|
8158
|
+
return new HTTPException15(410, { message: installationBindingDisabledMessage });
|
|
7279
8159
|
}
|
|
7280
8160
|
function setGitHubStateCookie(c, deps, state) {
|
|
7281
8161
|
setCookie(c, githubStateCookie, state, {
|
|
7282
8162
|
httpOnly: true,
|
|
7283
8163
|
sameSite: "Lax",
|
|
7284
8164
|
secure: isSecureRequest(c, deps),
|
|
7285
|
-
path: "/v1
|
|
7286
|
-
maxAge:
|
|
8165
|
+
path: "/v1",
|
|
8166
|
+
maxAge: stateMaxAgeSeconds
|
|
7287
8167
|
});
|
|
7288
8168
|
}
|
|
7289
|
-
function requireGitHubStateCookie(c, state) {
|
|
7290
|
-
if (getCookie(c, githubStateCookie) !== state) {
|
|
7291
|
-
throw new HTTPException15(400, {
|
|
7292
|
-
message: "invalid or expired GitHub installation browser state"
|
|
7293
|
-
});
|
|
7294
|
-
}
|
|
7295
|
-
}
|
|
7296
8169
|
function isSecureRequest(c, deps) {
|
|
7297
8170
|
return deps.settings.publicBaseUrl?.startsWith("https://") || c.req.header("x-forwarded-proto") === "https" || new URL(c.req.url).protocol === "https:";
|
|
7298
8171
|
}
|
|
7299
|
-
|
|
7300
|
-
const installationIds = await listGitHubInstallationIdsForWorkspace2(deps.db, workspaceId);
|
|
7301
|
-
return await listGitHubAppRepositories2(deps.settings, { installationIds });
|
|
7302
|
-
}
|
|
7303
|
-
function githubSuccessHtml(envLines, installUrl) {
|
|
8172
|
+
function githubSuccessHtml(envLines) {
|
|
7304
8173
|
const envText = envLines.join("\n");
|
|
7305
8174
|
const escaped = escapeHtml2(envText);
|
|
7306
|
-
|
|
7307
|
-
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>GitHub App Created</title><style>body{font-family:system-ui,sans-serif;margin:0;min-height:100vh;display:grid;place-items:center;background:#0b0b0d;color:#f4f4f5}main{width:min(760px,calc(100vw - 32px));border:1px solid #27272a;border-radius:8px;padding:28px;background:#111114}h1{margin:0 0 10px;font-size:24px;line-height:1.2}p{margin:0 0 18px;color:#d4d4d8}.env-header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:22px 0 8px}.env-header h2{margin:0;font-size:13px;line-height:1.2;text-transform:uppercase;letter-spacing:.08em;color:#a1a1aa}pre{white-space:pre-wrap;word-break:break-word;max-height:380px;overflow:auto;background:#09090b;border:1px solid #27272a;border-radius:8px;padding:16px;font-size:13px;line-height:1.5}.actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:18px}.button,button{display:inline-flex;align-items:center;justify-content:center;min-height:36px;border-radius:6px;border:1px solid #3f3f46;padding:0 12px;background:#f4f4f5;color:#09090b;font:600 14px system-ui,sans-serif;text-decoration:none;cursor:pointer}.button.secondary{background:transparent;color:#fafafa}.button.secondary:hover,button.secondary:hover{background:#27272a}button:disabled{cursor:not-allowed;opacity:.7}</style></head><body><main><h1>GitHub App created</h1><p>Add these values to .env, then restart API and worker.</p><div class="env-header"><h2>Environment variables</h2><button id="copy-env" type="button">Copy env</button></div><pre id="env-lines">${escaped}</pre><div class="actions">${install}</div><script>(()=>{const button=document.getElementById("copy-env");const env=document.getElementById("env-lines");async function copyText(text){if(navigator.clipboard&&window.isSecureContext){await navigator.clipboard.writeText(text);return;}const area=document.createElement("textarea");area.value=text;area.setAttribute("readonly","");area.style.position="fixed";area.style.inset="-9999px";document.body.append(area);area.select();document.execCommand("copy");area.remove();}button?.addEventListener("click",async()=>{try{await copyText(env?.textContent||"");button.textContent="Copied";setTimeout(()=>button.textContent="Copy env",1600);}catch{button.textContent="Copy failed";setTimeout(()=>button.textContent="Copy env",2200);}});})();</script></main></body></html>`;
|
|
7308
|
-
}
|
|
7309
|
-
function githubSetupSuccessHtml(account, returnUrl) {
|
|
7310
|
-
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>GitHub App Connected</title><style>body{font-family:system-ui,sans-serif;margin:0;min-height:100vh;display:grid;place-items:center;background:#0b0b0d;color:#f4f4f5}main{width:min(640px,calc(100vw - 32px));border:1px solid #27272a;border-radius:8px;padding:28px;background:#111114}h1{margin:0 0 10px;font-size:24px;line-height:1.2}p{margin:0 0 18px;color:#d4d4d8}.button{display:inline-flex;align-items:center;justify-content:center;min-height:36px;border-radius:6px;border:1px solid #3f3f46;padding:0 12px;background:#f4f4f5;color:#09090b;font:600 14px system-ui,sans-serif;text-decoration:none}.button:hover{background:#e4e4e7}</style></head><body><main><h1>GitHub App connected</h1><p>${escapeHtml2(account)} is now available to this OpenGeni workspace.</p><a class="button" href="${escapeHtml2(returnUrl)}">Back to OpenGeni</a></main></body></html>`;
|
|
7311
|
-
}
|
|
7312
|
-
function githubSetupPendingHtml() {
|
|
7313
|
-
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>GitHub App Requested</title><style>body{font-family:system-ui,sans-serif;margin:0;min-height:100vh;display:grid;place-items:center;background:#0b0b0d;color:#f4f4f5}main{width:min(640px,calc(100vw - 32px));border:1px solid #27272a;border-radius:8px;padding:28px;background:#111114}h1{margin:0 0 10px;font-size:24px;line-height:1.2}p{margin:0;color:#d4d4d8}</style></head><body><main><h1>GitHub App request sent</h1><p>An organization administrator must approve the installation before OpenGeni can connect it to this workspace.</p></main></body></html>`;
|
|
8175
|
+
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>GitHub App Created</title><style>body{font-family:system-ui,sans-serif;margin:0;min-height:100vh;display:grid;place-items:center;background:#0b0b0d;color:#f4f4f5}main{width:min(760px,calc(100vw - 32px));border:1px solid #27272a;border-radius:8px;padding:28px;background:#111114}h1{margin:0 0 10px;font-size:24px;line-height:1.2}p{margin:0 0 18px;color:#d4d4d8}.env-header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:22px 0 8px}.env-header h2{margin:0;font-size:13px;line-height:1.2;text-transform:uppercase;letter-spacing:.08em;color:#a1a1aa}pre{white-space:pre-wrap;word-break:break-word;max-height:380px;overflow:auto;background:#09090b;border:1px solid #27272a;border-radius:8px;padding:16px;font-size:13px;line-height:1.5}button{display:inline-flex;align-items:center;justify-content:center;min-height:36px;border-radius:6px;border:1px solid #3f3f46;padding:0 12px;background:#f4f4f5;color:#09090b;font:600 14px system-ui,sans-serif;cursor:pointer}button:disabled{cursor:not-allowed;opacity:.7}</style></head><body><main><h1>GitHub App created</h1><p>Add these values to .env, then restart API and worker.</p><div class="env-header"><h2>Environment variables</h2><button id="copy-env" type="button">Copy env</button></div><pre id="env-lines">${escaped}</pre><script>(()=>{const button=document.getElementById("copy-env");const env=document.getElementById("env-lines");async function copyText(text){if(navigator.clipboard&&window.isSecureContext){await navigator.clipboard.writeText(text);return;}const area=document.createElement("textarea");area.value=text;area.setAttribute("readonly","");area.style.position="fixed";area.style.inset="-9999px";document.body.append(area);area.select();document.execCommand("copy");area.remove();}button?.addEventListener("click",async()=>{try{await copyText(env?.textContent||"");button.textContent="Copied";setTimeout(()=>button.textContent="Copy env",1600);}catch{button.textContent="Copy failed";setTimeout(()=>button.textContent="Copy env",2200);}});})();</script></main></body></html>`;
|
|
7314
8176
|
}
|
|
7315
8177
|
function parsePositiveInteger(value) {
|
|
7316
8178
|
if (!value || !/^\d+$/.test(value)) {
|
|
@@ -7331,14 +8193,6 @@ function escapeHtml2(value) {
|
|
|
7331
8193
|
})[char] ?? char
|
|
7332
8194
|
);
|
|
7333
8195
|
}
|
|
7334
|
-
function openGeniReturnUrl(settings, c, workspaceId) {
|
|
7335
|
-
const base = (settings.publicBaseUrl ?? new URL(c.req.url).origin).replace(/\/+$/, "");
|
|
7336
|
-
const url = new URL(base || new URL(c.req.url).origin);
|
|
7337
|
-
if (workspaceId) {
|
|
7338
|
-
url.searchParams.set("workspaceId", workspaceId);
|
|
7339
|
-
}
|
|
7340
|
-
return url.toString();
|
|
7341
|
-
}
|
|
7342
8196
|
|
|
7343
8197
|
// src/routes/packs.ts
|
|
7344
8198
|
import {
|
|
@@ -7603,8 +8457,8 @@ import {
|
|
|
7603
8457
|
createRigForApi,
|
|
7604
8458
|
createRigVersionForApi,
|
|
7605
8459
|
deleteRigForApi,
|
|
7606
|
-
listRigChangesForApi
|
|
7607
|
-
listRigVersionsForApi
|
|
8460
|
+
listRigChangesForApi,
|
|
8461
|
+
listRigVersionsForApi,
|
|
7608
8462
|
promoteVerifiedDefinitionEditChangeForApi as promoteVerifiedDefinitionEditChangeForApi2,
|
|
7609
8463
|
proposeRigChangeForApi as proposeRigChangeForApi2,
|
|
7610
8464
|
requireRigChangeForApi as requireRigChangeForApi2,
|
|
@@ -7674,7 +8528,7 @@ function registerRigRoutes(app, deps) {
|
|
|
7674
8528
|
const workspaceId = c.req.param("workspaceId");
|
|
7675
8529
|
await requireAccessGrant12(c, deps, workspaceId, "rigs:use");
|
|
7676
8530
|
const rig = await requireRigForApi2(db, workspaceId, c.req.param("rigId"));
|
|
7677
|
-
return c.json(await
|
|
8531
|
+
return c.json(await listRigVersionsForApi({ db }, workspaceId, rig.id));
|
|
7678
8532
|
});
|
|
7679
8533
|
app.post("/v1/workspaces/:workspaceId/rigs/:rigId/versions", async (c) => {
|
|
7680
8534
|
const workspaceId = c.req.param("workspaceId");
|
|
@@ -7695,7 +8549,7 @@ function registerRigRoutes(app, deps) {
|
|
|
7695
8549
|
await requireAccessGrant12(c, deps, workspaceId, "rigs:use");
|
|
7696
8550
|
const rig = await requireRigForApi2(db, workspaceId, c.req.param("rigId"));
|
|
7697
8551
|
return c.json(
|
|
7698
|
-
await
|
|
8552
|
+
await listRigChangesForApi({ db }, workspaceId, rig.id, boundedLimit(c.req.query("limit")))
|
|
7699
8553
|
);
|
|
7700
8554
|
});
|
|
7701
8555
|
app.post("/v1/workspaces/:workspaceId/rigs/:rigId/changes", async (c) => {
|
|
@@ -7914,6 +8768,7 @@ import {
|
|
|
7914
8768
|
FsMoveRequest,
|
|
7915
8769
|
FsReadRequest,
|
|
7916
8770
|
FsWriteRequest,
|
|
8771
|
+
HumanInputRequestStatus,
|
|
7917
8772
|
GitDiffRequest,
|
|
7918
8773
|
GitLogRequest,
|
|
7919
8774
|
GitShowRequest,
|
|
@@ -7924,6 +8779,12 @@ import {
|
|
|
7924
8779
|
PtyResizeRequest,
|
|
7925
8780
|
PtyWriteRequest,
|
|
7926
8781
|
SessionControlRequest,
|
|
8782
|
+
SESSION_EVENT_RAW_DELTA_TYPES as SESSION_EVENT_RAW_DELTA_TYPES2,
|
|
8783
|
+
SessionEventPayloadMode as SessionEventPayloadMode2,
|
|
8784
|
+
SessionEventReadDirection as SessionEventReadDirection2,
|
|
8785
|
+
SessionEventReadMode as SessionEventReadMode2,
|
|
8786
|
+
SessionEventSemanticClass as SessionEventSemanticClass2,
|
|
8787
|
+
SessionEventType as SessionEventType2,
|
|
7927
8788
|
SaveComposerDraftRequest,
|
|
7928
8789
|
SteerSessionQueueItemRequest,
|
|
7929
8790
|
SteerSessionMessageRequest,
|
|
@@ -7931,11 +8792,14 @@ import {
|
|
|
7931
8792
|
UpdateSessionPinRequest,
|
|
7932
8793
|
UpdateSessionGoalRequest,
|
|
7933
8794
|
UpdateSessionRequest,
|
|
7934
|
-
ViewerHeartbeatRequest
|
|
8795
|
+
ViewerHeartbeatRequest,
|
|
8796
|
+
WORKSPACE_CONTROL_ACTOR_MAX_BYTES,
|
|
8797
|
+
workspaceControlUtf8Bytes
|
|
7935
8798
|
} from "@opengeni/contracts";
|
|
7936
8799
|
import { streamTokenDegraded } from "@opengeni/config";
|
|
7937
8800
|
import {
|
|
7938
8801
|
acceptSessionApprovalDecision,
|
|
8802
|
+
acceptSessionHumanInputResponse,
|
|
7939
8803
|
clearSessionGoal,
|
|
7940
8804
|
clearSessionContext,
|
|
7941
8805
|
closePtySession,
|
|
@@ -7944,13 +8808,17 @@ import {
|
|
|
7944
8808
|
getSession as getSession4,
|
|
7945
8809
|
getSessionForSubject,
|
|
7946
8810
|
getSessionGoal as getSessionGoal2,
|
|
8811
|
+
getSessionHumanInputRequest,
|
|
7947
8812
|
getSessionQueueSnapshot as getSessionQueueSnapshot2,
|
|
7948
8813
|
getStreamAcknowledgment,
|
|
7949
8814
|
insertPtySession,
|
|
7950
|
-
|
|
8815
|
+
listSessionEventPage as listSessionEventPage2,
|
|
8816
|
+
listSessionHumanInputRequests,
|
|
7951
8817
|
listSessionIdsInGroup,
|
|
7952
8818
|
listSessionsForSubject,
|
|
7953
8819
|
listSessionTurns,
|
|
8820
|
+
projectEffectiveControlForRelatedAccess as projectEffectiveControlForRelatedAccess2,
|
|
8821
|
+
projectSessionForRelatedAccess as projectSessionForRelatedAccess2,
|
|
7954
8822
|
recordStreamAcknowledgment,
|
|
7955
8823
|
requestSessionCompaction,
|
|
7956
8824
|
setSessionCodexPin,
|
|
@@ -7968,11 +8836,13 @@ import {
|
|
|
7968
8836
|
SessionCommandIdempotencyError,
|
|
7969
8837
|
SessionControlConflictError,
|
|
7970
8838
|
SessionContextBusyError,
|
|
8839
|
+
HumanInputResponseValidationError,
|
|
7971
8840
|
latestWorkspaceCapture,
|
|
7972
8841
|
workspaceCaptureAtRevision
|
|
7973
8842
|
} from "@opengeni/db";
|
|
7974
8843
|
import {
|
|
7975
8844
|
appendAndPublishEvents as appendAndPublishEvents5,
|
|
8845
|
+
boundSessionEventHttpPage,
|
|
7976
8846
|
coalesceSessionEventDeltas,
|
|
7977
8847
|
publishDurableSessionEvents
|
|
7978
8848
|
} from "@opengeni/events";
|
|
@@ -8007,7 +8877,11 @@ import {
|
|
|
8007
8877
|
ChannelAConflictError,
|
|
8008
8878
|
ChannelANotFoundError,
|
|
8009
8879
|
ChannelAUnsupportedError,
|
|
8010
|
-
|
|
8880
|
+
ChannelAUnavailableError,
|
|
8881
|
+
ChannelAValidationError,
|
|
8882
|
+
toolspaceTokenFileFromEnvironment,
|
|
8883
|
+
withToolspaceTokenSession,
|
|
8884
|
+
withRunCredentialsSession
|
|
8011
8885
|
} from "@opengeni/runtime/sandbox";
|
|
8012
8886
|
import { routingEnabled, wrapChannelABoxWithRouting } from "@opengeni/core";
|
|
8013
8887
|
async function withChannelA(services, ctx, fn) {
|
|
@@ -8158,8 +9032,13 @@ async function withChannelA(services, ctx, fn) {
|
|
|
8158
9032
|
{ workspaceId, sessionId: session.id },
|
|
8159
9033
|
established
|
|
8160
9034
|
).session : established.session;
|
|
9035
|
+
const credentialSession = withRunCredentialsSession(routedSession, session.id);
|
|
9036
|
+
const scopedSession = environment.OPENGENI_TOOLSPACE_TOKEN_FILE ? withToolspaceTokenSession(
|
|
9037
|
+
credentialSession,
|
|
9038
|
+
toolspaceTokenFileFromEnvironment(environment, session.id)
|
|
9039
|
+
) : credentialSession;
|
|
8161
9040
|
const service = new SandboxChannelAService({
|
|
8162
|
-
session:
|
|
9041
|
+
session: scopedSession,
|
|
8163
9042
|
leaseEpoch: leaseSnapshot.leaseEpoch,
|
|
8164
9043
|
emit
|
|
8165
9044
|
});
|
|
@@ -8173,6 +9052,8 @@ async function withChannelA(services, ctx, fn) {
|
|
|
8173
9052
|
}
|
|
8174
9053
|
function mapChannelAError(error) {
|
|
8175
9054
|
if (error instanceof HTTPException18) return error;
|
|
9055
|
+
if (error instanceof ChannelAUnavailableError)
|
|
9056
|
+
return new HTTPException18(503, { message: error.message });
|
|
8176
9057
|
if (error instanceof ChannelAValidationError)
|
|
8177
9058
|
return new HTTPException18(400, { message: error.message });
|
|
8178
9059
|
if (error instanceof ChannelANotFoundError)
|
|
@@ -8190,7 +9071,14 @@ async function dropEstablishedHandle(established) {
|
|
|
8190
9071
|
// src/routes/sessions.ts
|
|
8191
9072
|
import { negotiateCapabilities } from "@opengeni/runtime/sandbox";
|
|
8192
9073
|
import { HTTPException as HTTPException21 } from "hono/http-exception";
|
|
8193
|
-
import {
|
|
9074
|
+
import {
|
|
9075
|
+
requireAccessGrant as requireAccessGrant14,
|
|
9076
|
+
requireSessionAuthorization as requireSessionAuthorization2,
|
|
9077
|
+
requireSessionAuthorizationListScope as requireSessionAuthorizationListScope2,
|
|
9078
|
+
SESSION_AUTHORIZATION_DEFAULT_REAUTHORIZE_MS,
|
|
9079
|
+
SessionAuthorizationDeniedError,
|
|
9080
|
+
SessionAuthorizationUnavailableError
|
|
9081
|
+
} from "@opengeni/core";
|
|
8194
9082
|
|
|
8195
9083
|
// src/sandbox/viewer.ts
|
|
8196
9084
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -8772,73 +9660,297 @@ import {
|
|
|
8772
9660
|
} from "@opengeni/core";
|
|
8773
9661
|
|
|
8774
9662
|
// src/http/sse.ts
|
|
8775
|
-
import { listSessionEvents
|
|
8776
|
-
import {
|
|
8777
|
-
|
|
9663
|
+
import { listSessionEvents, listWorkspaceControlEvents } from "@opengeni/db";
|
|
9664
|
+
import {
|
|
9665
|
+
formatSessionEventSse,
|
|
9666
|
+
formatWorkspaceControlEventSse,
|
|
9667
|
+
SESSION_EVENT_SSE_FRAME_MAX_BYTES
|
|
9668
|
+
} from "@opengeni/events";
|
|
9669
|
+
var SESSION_REPLAY_PAGE_SIZE = 100;
|
|
9670
|
+
var WORKSPACE_CONTROL_REPLAY_PAGE_SIZE = 100;
|
|
9671
|
+
var SSE_QUEUED_FRAME_MAX_COUNT = 1;
|
|
9672
|
+
var SSE_WRITE_STALL_TIMEOUT_MS = 3e4;
|
|
9673
|
+
function createByteBoundedSseStream(options = {}) {
|
|
9674
|
+
const maxQueuedBytes = options.maxQueuedBytes ?? SESSION_EVENT_SSE_FRAME_MAX_BYTES;
|
|
9675
|
+
const stallTimeoutMs = options.stallTimeoutMs ?? SSE_WRITE_STALL_TIMEOUT_MS;
|
|
9676
|
+
if (!Number.isSafeInteger(maxQueuedBytes) || maxQueuedBytes <= 0) {
|
|
9677
|
+
throw new RangeError("SSE byte high-water mark must be a positive safe integer");
|
|
9678
|
+
}
|
|
9679
|
+
if (!Number.isSafeInteger(stallTimeoutMs) || stallTimeoutMs <= 0) {
|
|
9680
|
+
throw new RangeError("SSE write stall timeout must be a positive safe integer");
|
|
9681
|
+
}
|
|
8778
9682
|
const encoder = new TextEncoder();
|
|
8779
9683
|
let controller;
|
|
9684
|
+
let stopped = false;
|
|
9685
|
+
let capacityWake = null;
|
|
9686
|
+
let queuedFrames = 0;
|
|
9687
|
+
let queuedBytes = 0;
|
|
9688
|
+
const wakeWriter = () => {
|
|
9689
|
+
const wake = capacityWake;
|
|
9690
|
+
capacityWake = null;
|
|
9691
|
+
wake?.();
|
|
9692
|
+
};
|
|
9693
|
+
const stop = (settle) => {
|
|
9694
|
+
if (stopped) return;
|
|
9695
|
+
stopped = true;
|
|
9696
|
+
wakeWriter();
|
|
9697
|
+
options.onStop?.();
|
|
9698
|
+
try {
|
|
9699
|
+
settle();
|
|
9700
|
+
} catch {
|
|
9701
|
+
}
|
|
9702
|
+
};
|
|
9703
|
+
const stream = new ReadableStream(
|
|
9704
|
+
{
|
|
9705
|
+
start: (rawController) => {
|
|
9706
|
+
controller = rawController;
|
|
9707
|
+
},
|
|
9708
|
+
pull: () => {
|
|
9709
|
+
queuedFrames = 0;
|
|
9710
|
+
queuedBytes = 0;
|
|
9711
|
+
wakeWriter();
|
|
9712
|
+
},
|
|
9713
|
+
cancel: () => {
|
|
9714
|
+
if (stopped) return;
|
|
9715
|
+
stopped = true;
|
|
9716
|
+
wakeWriter();
|
|
9717
|
+
options.onStop?.();
|
|
9718
|
+
}
|
|
9719
|
+
},
|
|
9720
|
+
{
|
|
9721
|
+
highWaterMark: SSE_QUEUED_FRAME_MAX_COUNT,
|
|
9722
|
+
size: () => 1
|
|
9723
|
+
}
|
|
9724
|
+
);
|
|
9725
|
+
return {
|
|
9726
|
+
stream,
|
|
9727
|
+
write: async (frame) => {
|
|
9728
|
+
const chunk = encoder.encode(frame);
|
|
9729
|
+
if (chunk.byteLength > maxQueuedBytes) {
|
|
9730
|
+
const error = new RangeError(
|
|
9731
|
+
`SSE frame cannot fit in the configured queue (${chunk.byteLength} > ${maxQueuedBytes} bytes)`
|
|
9732
|
+
);
|
|
9733
|
+
options.onObservation?.({
|
|
9734
|
+
reason: "frame_too_large",
|
|
9735
|
+
desiredSize: controller.desiredSize,
|
|
9736
|
+
queuedFrames,
|
|
9737
|
+
queuedBytes
|
|
9738
|
+
});
|
|
9739
|
+
stop(() => controller.error(error));
|
|
9740
|
+
throw error;
|
|
9741
|
+
}
|
|
9742
|
+
for (; ; ) {
|
|
9743
|
+
if (stopped) return false;
|
|
9744
|
+
const desired = controller.desiredSize;
|
|
9745
|
+
if (desired === null) return false;
|
|
9746
|
+
if (desired >= 1 && queuedFrames === 0) {
|
|
9747
|
+
controller.enqueue(chunk);
|
|
9748
|
+
queuedFrames = 1;
|
|
9749
|
+
queuedBytes = chunk.byteLength;
|
|
9750
|
+
return true;
|
|
9751
|
+
}
|
|
9752
|
+
options.onObservation?.({
|
|
9753
|
+
reason: "desired_size_non_positive",
|
|
9754
|
+
desiredSize: desired,
|
|
9755
|
+
queuedFrames,
|
|
9756
|
+
queuedBytes
|
|
9757
|
+
});
|
|
9758
|
+
const outcome = await new Promise((resolve) => {
|
|
9759
|
+
let settled = false;
|
|
9760
|
+
const finish = (result) => {
|
|
9761
|
+
if (settled) return;
|
|
9762
|
+
settled = true;
|
|
9763
|
+
clearTimeout(timer);
|
|
9764
|
+
if (capacityWake === wake) capacityWake = null;
|
|
9765
|
+
resolve(result);
|
|
9766
|
+
};
|
|
9767
|
+
const wake = () => finish("capacity");
|
|
9768
|
+
const timer = setTimeout(() => finish("timeout"), stallTimeoutMs);
|
|
9769
|
+
capacityWake = wake;
|
|
9770
|
+
});
|
|
9771
|
+
if (outcome === "timeout" && !stopped) {
|
|
9772
|
+
const error = new TypeError(
|
|
9773
|
+
`SSE consumer did not drain the single-frame queue within ${stallTimeoutMs}ms`
|
|
9774
|
+
);
|
|
9775
|
+
options.onObservation?.({
|
|
9776
|
+
reason: "stall_timeout",
|
|
9777
|
+
desiredSize: controller.desiredSize,
|
|
9778
|
+
queuedFrames,
|
|
9779
|
+
queuedBytes
|
|
9780
|
+
});
|
|
9781
|
+
stop(() => controller.error(error));
|
|
9782
|
+
throw error;
|
|
9783
|
+
}
|
|
9784
|
+
}
|
|
9785
|
+
},
|
|
9786
|
+
close: () => stop(() => controller.close()),
|
|
9787
|
+
fail: (error) => stop(() => controller.error(error)),
|
|
9788
|
+
stopped: () => stopped
|
|
9789
|
+
};
|
|
9790
|
+
}
|
|
9791
|
+
function createLatestWinsDelivery(send, onError) {
|
|
9792
|
+
let newest = null;
|
|
9793
|
+
let running = null;
|
|
9794
|
+
let stopped = false;
|
|
9795
|
+
const start = () => {
|
|
9796
|
+
if (stopped || running || !newest) return;
|
|
9797
|
+
const run = async () => {
|
|
9798
|
+
for (; ; ) {
|
|
9799
|
+
if (stopped || !newest) return;
|
|
9800
|
+
const target = newest;
|
|
9801
|
+
newest = null;
|
|
9802
|
+
await send(target);
|
|
9803
|
+
}
|
|
9804
|
+
};
|
|
9805
|
+
running = run().catch((error) => {
|
|
9806
|
+
stopped = true;
|
|
9807
|
+
newest = null;
|
|
9808
|
+
onError(error);
|
|
9809
|
+
}).finally(() => {
|
|
9810
|
+
running = null;
|
|
9811
|
+
start();
|
|
9812
|
+
});
|
|
9813
|
+
};
|
|
9814
|
+
return {
|
|
9815
|
+
publish: (events) => {
|
|
9816
|
+
if (stopped) return;
|
|
9817
|
+
for (const event of events) {
|
|
9818
|
+
if (!newest || event.sequence > newest.sequence) newest = event;
|
|
9819
|
+
}
|
|
9820
|
+
start();
|
|
9821
|
+
},
|
|
9822
|
+
stop: () => {
|
|
9823
|
+
stopped = true;
|
|
9824
|
+
newest = null;
|
|
9825
|
+
},
|
|
9826
|
+
whenIdle: async () => {
|
|
9827
|
+
for (; ; ) {
|
|
9828
|
+
const pending = running;
|
|
9829
|
+
if (!pending) return;
|
|
9830
|
+
await pending;
|
|
9831
|
+
}
|
|
9832
|
+
},
|
|
9833
|
+
pendingSequence: () => newest?.sequence ?? null
|
|
9834
|
+
};
|
|
9835
|
+
}
|
|
9836
|
+
async function sseSessionStream(db, bus, workspaceId, sessionId, after, signal, options = {}) {
|
|
9837
|
+
if (options.reauthorize && options.reauthorizeAfterMs !== void 0 && (!Number.isSafeInteger(options.reauthorizeAfterMs) || options.reauthorizeAfterMs < 1e3 || options.reauthorizeAfterMs > 6e4)) {
|
|
9838
|
+
throw new RangeError("session stream reauthorization must be between 1000 and 60000ms");
|
|
9839
|
+
}
|
|
8780
9840
|
let lastSent = after;
|
|
8781
|
-
let
|
|
8782
|
-
|
|
9841
|
+
let bootstrapping = true;
|
|
9842
|
+
let newestBuffered = null;
|
|
8783
9843
|
let unsubscribe = null;
|
|
8784
|
-
|
|
8785
|
-
|
|
8786
|
-
|
|
8787
|
-
|
|
8788
|
-
|
|
8789
|
-
|
|
8790
|
-
|
|
8791
|
-
|
|
8792
|
-
|
|
8793
|
-
|
|
8794
|
-
|
|
8795
|
-
|
|
8796
|
-
|
|
8797
|
-
|
|
9844
|
+
let delivery = null;
|
|
9845
|
+
let reauthorizationTimer = null;
|
|
9846
|
+
let detachAbortListener = () => {
|
|
9847
|
+
};
|
|
9848
|
+
const stopUpstream = () => {
|
|
9849
|
+
detachAbortListener();
|
|
9850
|
+
if (reauthorizationTimer) {
|
|
9851
|
+
clearTimeout(reauthorizationTimer);
|
|
9852
|
+
reauthorizationTimer = null;
|
|
9853
|
+
}
|
|
9854
|
+
delivery?.stop();
|
|
9855
|
+
const release = unsubscribe;
|
|
9856
|
+
unsubscribe = null;
|
|
9857
|
+
release?.();
|
|
9858
|
+
};
|
|
9859
|
+
const channel = createByteBoundedSseStream({
|
|
9860
|
+
maxQueuedBytes: options.maxQueuedBytes ?? SESSION_EVENT_SSE_FRAME_MAX_BYTES,
|
|
9861
|
+
...options.stallTimeoutMs === void 0 ? {} : { stallTimeoutMs: options.stallTimeoutMs },
|
|
9862
|
+
onObservation: sseObservationReporter("session", options),
|
|
9863
|
+
onStop: stopUpstream
|
|
9864
|
+
});
|
|
9865
|
+
const fail = (error) => {
|
|
9866
|
+
channel.fail(retryableSseFailure("session event stream delivery failed", error));
|
|
9867
|
+
};
|
|
9868
|
+
const scheduleReauthorization = () => {
|
|
9869
|
+
if (!options.reauthorize || channel.stopped()) return;
|
|
9870
|
+
const interval = options.reauthorizeAfterMs ?? 15e3;
|
|
9871
|
+
reauthorizationTimer = setTimeout(() => {
|
|
9872
|
+
reauthorizationTimer = null;
|
|
9873
|
+
void options.reauthorize().then(scheduleReauthorization).catch((error) => fail(error));
|
|
9874
|
+
}, interval);
|
|
9875
|
+
};
|
|
9876
|
+
scheduleReauthorization();
|
|
9877
|
+
const writeFrame = async (frame) => {
|
|
9878
|
+
if (!await channel.write(frame)) {
|
|
9879
|
+
throw new SseStreamStoppedError();
|
|
9880
|
+
}
|
|
9881
|
+
};
|
|
9882
|
+
const send = async (event) => {
|
|
9883
|
+
if (event.sequence <= lastSent) return;
|
|
9884
|
+
if (event.sequence > lastSent + 1) {
|
|
9885
|
+
while (lastSent + 1 < event.sequence) {
|
|
9886
|
+
const previousLastSent = lastSent;
|
|
9887
|
+
const missing = await listSessionEvents(db, workspaceId, sessionId, {
|
|
9888
|
+
after: lastSent,
|
|
9889
|
+
limit: Math.min(SESSION_REPLAY_PAGE_SIZE, event.sequence - lastSent - 1)
|
|
9890
|
+
});
|
|
9891
|
+
if (missing.length === 0) {
|
|
9892
|
+
throw new Error(
|
|
9893
|
+
`Session event replay stalled before sequence ${event.sequence}; last sent ${lastSent}`
|
|
8798
9894
|
);
|
|
8799
|
-
|
|
8800
|
-
|
|
8801
|
-
|
|
8802
|
-
|
|
8803
|
-
|
|
9895
|
+
}
|
|
9896
|
+
for (const missed of missing) {
|
|
9897
|
+
if (missed.sequence >= event.sequence) break;
|
|
9898
|
+
if (missed.sequence > lastSent) {
|
|
9899
|
+
await writeFrame(formatSessionEventSse(missed));
|
|
9900
|
+
lastSent = missed.sequence;
|
|
8804
9901
|
}
|
|
8805
9902
|
}
|
|
8806
|
-
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
|
|
8810
|
-
if (replaying) {
|
|
8811
|
-
buffered.push(...events);
|
|
8812
|
-
return;
|
|
9903
|
+
if (lastSent === previousLastSent) {
|
|
9904
|
+
throw new Error(
|
|
9905
|
+
`Session event replay made no progress before sequence ${event.sequence}; last sent ${lastSent}`
|
|
9906
|
+
);
|
|
8813
9907
|
}
|
|
8814
|
-
|
|
8815
|
-
|
|
9908
|
+
}
|
|
9909
|
+
}
|
|
9910
|
+
await writeFrame(formatSessionEventSse(event));
|
|
9911
|
+
lastSent = event.sequence;
|
|
9912
|
+
};
|
|
9913
|
+
delivery = createLatestWinsDelivery(send, fail);
|
|
9914
|
+
void (async () => {
|
|
9915
|
+
const release = await bus.subscribe(workspaceId, sessionId, (events) => {
|
|
9916
|
+
if (bootstrapping) {
|
|
9917
|
+
for (const event of events) {
|
|
9918
|
+
if (!newestBuffered || event.sequence > newestBuffered.sequence) {
|
|
9919
|
+
newestBuffered = event;
|
|
9920
|
+
}
|
|
8816
9921
|
}
|
|
8817
|
-
}
|
|
8818
|
-
|
|
8819
|
-
(cursor, limit) => listSessionEvents2(db, workspaceId, sessionId, cursor, limit),
|
|
8820
|
-
send,
|
|
8821
|
-
after
|
|
8822
|
-
);
|
|
8823
|
-
replaying = false;
|
|
8824
|
-
for (const event of buffered.sort((a, b) => a.sequence - b.sequence)) {
|
|
8825
|
-
await send(event);
|
|
9922
|
+
} else {
|
|
9923
|
+
delivery?.publish(events);
|
|
8826
9924
|
}
|
|
8827
|
-
|
|
8828
|
-
|
|
8829
|
-
|
|
8830
|
-
|
|
8831
|
-
unsubscribe?.();
|
|
9925
|
+
});
|
|
9926
|
+
if (channel.stopped()) {
|
|
9927
|
+
release();
|
|
9928
|
+
return;
|
|
8832
9929
|
}
|
|
8833
|
-
|
|
8834
|
-
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
|
|
8838
|
-
|
|
8839
|
-
|
|
8840
|
-
|
|
8841
|
-
|
|
9930
|
+
unsubscribe = release;
|
|
9931
|
+
await replaySessionEvents(
|
|
9932
|
+
(cursor, limit) => listSessionEvents(db, workspaceId, sessionId, cursor, limit),
|
|
9933
|
+
send,
|
|
9934
|
+
after,
|
|
9935
|
+
SESSION_REPLAY_PAGE_SIZE
|
|
9936
|
+
);
|
|
9937
|
+
await writeFrame(": connected\n\n");
|
|
9938
|
+
bootstrapping = false;
|
|
9939
|
+
const buffered = newestBuffered;
|
|
9940
|
+
newestBuffered = null;
|
|
9941
|
+
if (buffered) delivery.publish([buffered]);
|
|
9942
|
+
})().catch((error) => {
|
|
9943
|
+
if (!(error instanceof SseStreamStoppedError)) fail(error);
|
|
9944
|
+
});
|
|
9945
|
+
const abort = () => {
|
|
9946
|
+
channel.close();
|
|
9947
|
+
};
|
|
9948
|
+
if (signal.aborted) abort();
|
|
9949
|
+
else {
|
|
9950
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
9951
|
+
detachAbortListener = () => signal.removeEventListener("abort", abort);
|
|
9952
|
+
}
|
|
9953
|
+
return new Response(channel.stream, {
|
|
8842
9954
|
headers: {
|
|
8843
9955
|
"Content-Type": "text/event-stream; charset=utf-8",
|
|
8844
9956
|
"Cache-Control": "no-cache, no-transform",
|
|
@@ -8846,62 +9958,125 @@ async function sseSessionStream(db, bus, workspaceId, sessionId, after, signal)
|
|
|
8846
9958
|
}
|
|
8847
9959
|
});
|
|
8848
9960
|
}
|
|
8849
|
-
async function replaySessionEvents(loadPage, send, after, pageSize =
|
|
9961
|
+
async function replaySessionEvents(loadPage, send, after, pageSize = SESSION_REPLAY_PAGE_SIZE) {
|
|
8850
9962
|
let cursor = after;
|
|
8851
9963
|
while (true) {
|
|
9964
|
+
const previousCursor = cursor;
|
|
8852
9965
|
const page = await loadPage(cursor, pageSize);
|
|
8853
9966
|
if (page.length === 0) {
|
|
8854
9967
|
return;
|
|
8855
9968
|
}
|
|
8856
9969
|
for (const event of page.sort((a, b) => a.sequence - b.sequence)) {
|
|
9970
|
+
if (event.sequence <= cursor) continue;
|
|
8857
9971
|
await send(event);
|
|
8858
|
-
cursor =
|
|
9972
|
+
cursor = event.sequence;
|
|
8859
9973
|
}
|
|
8860
9974
|
if (page.length < pageSize) {
|
|
8861
9975
|
return;
|
|
8862
9976
|
}
|
|
9977
|
+
if (cursor === previousCursor) {
|
|
9978
|
+
throw new Error(
|
|
9979
|
+
`Session event replay made no progress after sequence ${cursor}; refusing to repeat a full stale page`
|
|
9980
|
+
);
|
|
9981
|
+
}
|
|
8863
9982
|
}
|
|
8864
9983
|
}
|
|
8865
|
-
async function sseWorkspaceControlStream(db, bus, workspaceId, after, signal) {
|
|
8866
|
-
const encoder = new TextEncoder();
|
|
9984
|
+
async function sseWorkspaceControlStream(db, bus, workspaceId, after, signal, options = {}) {
|
|
8867
9985
|
let lastSent = after;
|
|
8868
|
-
let
|
|
8869
|
-
|
|
9986
|
+
let bootstrapping = true;
|
|
9987
|
+
let newestBuffered = null;
|
|
8870
9988
|
let unsubscribe = null;
|
|
8871
|
-
|
|
8872
|
-
|
|
8873
|
-
|
|
8874
|
-
|
|
8875
|
-
|
|
8876
|
-
|
|
8877
|
-
|
|
8878
|
-
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
|
|
9989
|
+
let delivery = null;
|
|
9990
|
+
let detachAbortListener = () => {
|
|
9991
|
+
};
|
|
9992
|
+
const stopUpstream = () => {
|
|
9993
|
+
detachAbortListener();
|
|
9994
|
+
delivery?.stop();
|
|
9995
|
+
const release = unsubscribe;
|
|
9996
|
+
unsubscribe = null;
|
|
9997
|
+
release?.();
|
|
9998
|
+
};
|
|
9999
|
+
const channel = createByteBoundedSseStream({
|
|
10000
|
+
maxQueuedBytes: options.maxQueuedBytes ?? SESSION_EVENT_SSE_FRAME_MAX_BYTES,
|
|
10001
|
+
...options.stallTimeoutMs === void 0 ? {} : { stallTimeoutMs: options.stallTimeoutMs },
|
|
10002
|
+
onObservation: sseObservationReporter("workspace_control", options),
|
|
10003
|
+
onStop: stopUpstream
|
|
10004
|
+
});
|
|
10005
|
+
const fail = (error) => {
|
|
10006
|
+
channel.fail(retryableSseFailure("workspace control stream delivery failed", error));
|
|
10007
|
+
};
|
|
10008
|
+
const writeFrame = async (frame) => {
|
|
10009
|
+
if (!await channel.write(frame)) throw new SseStreamStoppedError();
|
|
10010
|
+
};
|
|
10011
|
+
const send = async (event) => {
|
|
10012
|
+
if (event.sequence <= lastSent) return;
|
|
10013
|
+
if (event.sequence > lastSent + 1) {
|
|
10014
|
+
while (lastSent < event.sequence) {
|
|
10015
|
+
const previousLastSent = lastSent;
|
|
10016
|
+
const limit = Math.min(
|
|
10017
|
+
WORKSPACE_CONTROL_REPLAY_PAGE_SIZE,
|
|
10018
|
+
Math.max(1, event.sequence - lastSent)
|
|
10019
|
+
);
|
|
10020
|
+
const missing = await listWorkspaceControlEvents(db, workspaceId, lastSent, limit);
|
|
10021
|
+
let reachedIncoming = false;
|
|
10022
|
+
for (const missed of missing.sort((a, b) => a.sequence - b.sequence)) {
|
|
10023
|
+
if (missed.sequence >= event.sequence) {
|
|
10024
|
+
reachedIncoming = true;
|
|
10025
|
+
break;
|
|
10026
|
+
}
|
|
10027
|
+
if (missed.sequence > lastSent) {
|
|
10028
|
+
await writeFrame(formatWorkspaceControlEventSse(missed));
|
|
10029
|
+
lastSent = missed.sequence;
|
|
10030
|
+
}
|
|
8883
10031
|
}
|
|
8884
|
-
|
|
8885
|
-
|
|
8886
|
-
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
send(event);
|
|
8890
|
-
cursor = Math.max(cursor, event.sequence);
|
|
10032
|
+
if (reachedIncoming || missing.length < limit) break;
|
|
10033
|
+
if (lastSent === previousLastSent) {
|
|
10034
|
+
throw new Error(
|
|
10035
|
+
`Workspace control gap fill returned a full stale page before sequence ${event.sequence}; last sent ${lastSent}`
|
|
10036
|
+
);
|
|
8891
10037
|
}
|
|
8892
|
-
if (page.length < 1e3) break;
|
|
8893
10038
|
}
|
|
8894
|
-
|
|
8895
|
-
|
|
8896
|
-
|
|
10039
|
+
}
|
|
10040
|
+
await writeFrame(formatWorkspaceControlEventSse(event));
|
|
10041
|
+
lastSent = event.sequence;
|
|
10042
|
+
};
|
|
10043
|
+
delivery = createLatestWinsDelivery(send, fail);
|
|
10044
|
+
void (async () => {
|
|
10045
|
+
const release = await bus.subscribeWorkspaceControl(workspaceId, (event) => {
|
|
10046
|
+
if (bootstrapping) {
|
|
10047
|
+
if (!newestBuffered || event.sequence > newestBuffered.sequence) newestBuffered = event;
|
|
10048
|
+
} else {
|
|
10049
|
+
delivery?.publish([event]);
|
|
8897
10050
|
}
|
|
8898
|
-
|
|
8899
|
-
|
|
8900
|
-
|
|
8901
|
-
|
|
8902
|
-
|
|
8903
|
-
|
|
8904
|
-
|
|
10051
|
+
});
|
|
10052
|
+
if (channel.stopped()) {
|
|
10053
|
+
release();
|
|
10054
|
+
return;
|
|
10055
|
+
}
|
|
10056
|
+
unsubscribe = release;
|
|
10057
|
+
await replayWorkspaceControlEvents(
|
|
10058
|
+
(cursor, limit) => listWorkspaceControlEvents(db, workspaceId, cursor, limit),
|
|
10059
|
+
send,
|
|
10060
|
+
after,
|
|
10061
|
+
WORKSPACE_CONTROL_REPLAY_PAGE_SIZE
|
|
10062
|
+
);
|
|
10063
|
+
await writeFrame(": connected\n\n");
|
|
10064
|
+
bootstrapping = false;
|
|
10065
|
+
const buffered = newestBuffered;
|
|
10066
|
+
newestBuffered = null;
|
|
10067
|
+
if (buffered) delivery.publish([buffered]);
|
|
10068
|
+
})().catch((error) => {
|
|
10069
|
+
if (!(error instanceof SseStreamStoppedError)) fail(error);
|
|
10070
|
+
});
|
|
10071
|
+
const abort = () => {
|
|
10072
|
+
channel.close();
|
|
10073
|
+
};
|
|
10074
|
+
if (signal.aborted) abort();
|
|
10075
|
+
else {
|
|
10076
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
10077
|
+
detachAbortListener = () => signal.removeEventListener("abort", abort);
|
|
10078
|
+
}
|
|
10079
|
+
return new Response(channel.stream, {
|
|
8905
10080
|
headers: {
|
|
8906
10081
|
"Content-Type": "text/event-stream; charset=utf-8",
|
|
8907
10082
|
"Cache-Control": "no-cache, no-transform",
|
|
@@ -8909,6 +10084,49 @@ async function sseWorkspaceControlStream(db, bus, workspaceId, after, signal) {
|
|
|
8909
10084
|
}
|
|
8910
10085
|
});
|
|
8911
10086
|
}
|
|
10087
|
+
async function replayWorkspaceControlEvents(loadPage, send, after, pageSize) {
|
|
10088
|
+
let cursor = after;
|
|
10089
|
+
while (true) {
|
|
10090
|
+
const previousCursor = cursor;
|
|
10091
|
+
const page = await loadPage(cursor, pageSize);
|
|
10092
|
+
if (page.length === 0) return;
|
|
10093
|
+
for (const event of page.sort((a, b) => a.sequence - b.sequence)) {
|
|
10094
|
+
if (event.sequence <= cursor) continue;
|
|
10095
|
+
await send(event);
|
|
10096
|
+
cursor = event.sequence;
|
|
10097
|
+
}
|
|
10098
|
+
if (page.length < pageSize) return;
|
|
10099
|
+
if (cursor === previousCursor) {
|
|
10100
|
+
throw new Error(
|
|
10101
|
+
`Workspace control replay made no progress after sequence ${cursor}; refusing to repeat a full stale page`
|
|
10102
|
+
);
|
|
10103
|
+
}
|
|
10104
|
+
}
|
|
10105
|
+
}
|
|
10106
|
+
var SseStreamStoppedError = class extends Error {
|
|
10107
|
+
};
|
|
10108
|
+
function sseObservationReporter(stream, options) {
|
|
10109
|
+
return (observation) => {
|
|
10110
|
+
options.onObservation?.(observation);
|
|
10111
|
+
options.observability?.incrementCounter({
|
|
10112
|
+
name: "opengeni_sse_delivery_bound_events_total",
|
|
10113
|
+
help: "SSE writes that encountered a configured queue, frame, or stall bound.",
|
|
10114
|
+
labels: { stream, reason: observation.reason }
|
|
10115
|
+
});
|
|
10116
|
+
if (observation.reason !== "desired_size_non_positive") {
|
|
10117
|
+
options.observability?.warn("SSE delivery terminated at a bounded stream seam", {
|
|
10118
|
+
stream,
|
|
10119
|
+
reason: observation.reason,
|
|
10120
|
+
desiredSize: observation.desiredSize,
|
|
10121
|
+
queuedFrames: observation.queuedFrames,
|
|
10122
|
+
queuedBytes: observation.queuedBytes
|
|
10123
|
+
});
|
|
10124
|
+
}
|
|
10125
|
+
};
|
|
10126
|
+
}
|
|
10127
|
+
function retryableSseFailure(message, error) {
|
|
10128
|
+
return error instanceof TypeError ? error : new TypeError(message, { cause: error });
|
|
10129
|
+
}
|
|
8912
10130
|
|
|
8913
10131
|
// src/routes/workspace-capture.ts
|
|
8914
10132
|
import {
|
|
@@ -8927,6 +10145,13 @@ function signedUrl(signed) {
|
|
|
8927
10145
|
}
|
|
8928
10146
|
async function loadManifest(row, storage) {
|
|
8929
10147
|
if (!row.manifestKey) return null;
|
|
10148
|
+
const stats = WorkspaceCaptureStats.safeParse(row.stats);
|
|
10149
|
+
if (!stats.success) {
|
|
10150
|
+
console.warn(
|
|
10151
|
+
`workspace capture read \u2014 row stats failed schema validation (session=${row.sessionId} rev=${row.revision})`
|
|
10152
|
+
);
|
|
10153
|
+
return null;
|
|
10154
|
+
}
|
|
8930
10155
|
const blob = await storage.getObjectBytes(row.manifestKey);
|
|
8931
10156
|
if (!blob) return null;
|
|
8932
10157
|
let json;
|
|
@@ -8945,7 +10170,16 @@ async function loadManifest(row, storage) {
|
|
|
8945
10170
|
);
|
|
8946
10171
|
return null;
|
|
8947
10172
|
}
|
|
8948
|
-
|
|
10173
|
+
const manifest = parsed.data;
|
|
10174
|
+
const servedStats = stats.data;
|
|
10175
|
+
const statsMatch = manifest.stats.repoCount === servedStats.repoCount && manifest.stats.fileCount === servedStats.fileCount && manifest.stats.additions === servedStats.additions && manifest.stats.deletions === servedStats.deletions && manifest.stats.totalBytes === servedStats.totalBytes && manifest.stats.tooLargeCount === servedStats.tooLargeCount && manifest.stats.binaryCount === servedStats.binaryCount && manifest.stats.treeEntryCount === servedStats.treeEntryCount && manifest.stats.treeTruncated === servedStats.treeTruncated && manifest.stats.durationMs === servedStats.durationMs && (manifest.stats.fingerprint ?? null) === (servedStats.fingerprint ?? null);
|
|
10176
|
+
if (manifest.revision !== row.revision || manifest.capturedAt !== row.capturedAt || manifest.turnId !== row.turnId || manifest.leaseEpoch !== row.leaseEpoch || !statsMatch || manifest.repos.length !== manifest.stats.repoCount || manifest.files.length !== manifest.stats.fileCount || manifest.treeTruncated !== manifest.stats.treeTruncated) {
|
|
10177
|
+
console.warn(
|
|
10178
|
+
`workspace capture read \u2014 manifest identity did not match row (session=${row.sessionId} rev=${row.revision})`
|
|
10179
|
+
);
|
|
10180
|
+
return null;
|
|
10181
|
+
}
|
|
10182
|
+
return { manifest, byteLength: blob.bytes.byteLength, stats: servedStats };
|
|
8949
10183
|
}
|
|
8950
10184
|
async function serveWorkspaceCapture(row, storage) {
|
|
8951
10185
|
if (!row) {
|
|
@@ -8966,13 +10200,8 @@ async function serveWorkspaceCapture(row, storage) {
|
|
|
8966
10200
|
});
|
|
8967
10201
|
}
|
|
8968
10202
|
if (row.state !== "available" || !row.manifestKey) return { available: false };
|
|
8969
|
-
const
|
|
8970
|
-
if (!
|
|
8971
|
-
console.warn(
|
|
8972
|
-
`workspace capture read \u2014 row stats failed schema validation (session=${row.sessionId} rev=${row.revision})`
|
|
8973
|
-
);
|
|
8974
|
-
return { available: false };
|
|
8975
|
-
}
|
|
10203
|
+
const loaded = await loadManifest(row, storage);
|
|
10204
|
+
if (!loaded) return { available: false };
|
|
8976
10205
|
const meta = {
|
|
8977
10206
|
available: true,
|
|
8978
10207
|
revision: row.revision,
|
|
@@ -8980,32 +10209,12 @@ async function serveWorkspaceCapture(row, storage) {
|
|
|
8980
10209
|
turnId: row.turnId,
|
|
8981
10210
|
leaseEpoch: row.leaseEpoch,
|
|
8982
10211
|
sizeBytes: row.sizeBytes ?? 0,
|
|
8983
|
-
stats: stats
|
|
10212
|
+
stats: loaded.stats
|
|
8984
10213
|
};
|
|
8985
|
-
|
|
8986
|
-
if (!blob) {
|
|
8987
|
-
return { available: false };
|
|
8988
|
-
}
|
|
8989
|
-
if (blob.bytes.byteLength <= CAPTURE_INLINE_MANIFEST_MAX_BYTES) {
|
|
8990
|
-
let json;
|
|
8991
|
-
try {
|
|
8992
|
-
json = JSON.parse(new TextDecoder().decode(blob.bytes));
|
|
8993
|
-
} catch {
|
|
8994
|
-
console.warn(
|
|
8995
|
-
`workspace capture read \u2014 manifest blob is not valid JSON (session=${row.sessionId} rev=${row.revision})`
|
|
8996
|
-
);
|
|
8997
|
-
return { available: false };
|
|
8998
|
-
}
|
|
8999
|
-
const manifest = WorkspaceCaptureManifest.safeParse(json);
|
|
9000
|
-
if (!manifest.success) {
|
|
9001
|
-
console.warn(
|
|
9002
|
-
`workspace capture read \u2014 manifest failed schema validation (session=${row.sessionId} rev=${row.revision})`
|
|
9003
|
-
);
|
|
9004
|
-
return { available: false };
|
|
9005
|
-
}
|
|
10214
|
+
if (loaded.byteLength <= CAPTURE_INLINE_MANIFEST_MAX_BYTES) {
|
|
9006
10215
|
return GetWorkspaceCaptureResponse.parse({
|
|
9007
10216
|
...meta,
|
|
9008
|
-
manifest: manifest
|
|
10217
|
+
manifest: loaded.manifest,
|
|
9009
10218
|
manifestUrl: null
|
|
9010
10219
|
});
|
|
9011
10220
|
}
|
|
@@ -9079,6 +10288,49 @@ async function serveWorkspaceCaptureFile(row, path, storage) {
|
|
|
9079
10288
|
// src/routes/sessions.ts
|
|
9080
10289
|
function registerSessionRoutes(app, deps) {
|
|
9081
10290
|
const { settings, db, bus, workflowClient, objectStorage } = deps;
|
|
10291
|
+
const requestSessionAuthorization = /* @__PURE__ */ new WeakMap();
|
|
10292
|
+
const relatedSessionAccessFor = (c) => requestSessionAuthorization.get(c.req.raw)?.relatedSessionAccess ?? "root";
|
|
10293
|
+
const projectQueueSnapshot = (snapshot, sessionId, access) => ({
|
|
10294
|
+
...snapshot,
|
|
10295
|
+
effectiveControl: projectEffectiveControlForRelatedAccess2(
|
|
10296
|
+
snapshot.effectiveControl,
|
|
10297
|
+
sessionId,
|
|
10298
|
+
access
|
|
10299
|
+
)
|
|
10300
|
+
});
|
|
10301
|
+
const authorizeSessionHttp = async (c, next) => {
|
|
10302
|
+
if (!deps.sessionAuthorization) {
|
|
10303
|
+
await next();
|
|
10304
|
+
return;
|
|
10305
|
+
}
|
|
10306
|
+
const workspaceId = c.req.param("workspaceId") ?? "";
|
|
10307
|
+
const sessionId = c.req.param("sessionId") ?? "";
|
|
10308
|
+
const operation = sessionAuthorizationOperationForHttp(
|
|
10309
|
+
c.req.method,
|
|
10310
|
+
new URL(c.req.url).pathname,
|
|
10311
|
+
sessionId
|
|
10312
|
+
);
|
|
10313
|
+
if (operation === "session.stream.read") {
|
|
10314
|
+
await next();
|
|
10315
|
+
return;
|
|
10316
|
+
}
|
|
10317
|
+
if (!operation) {
|
|
10318
|
+
throw sessionAuthorizationHttpError(new SessionAuthorizationUnavailableError());
|
|
10319
|
+
}
|
|
10320
|
+
const grant = await requireAccessGrant14(c, deps, workspaceId);
|
|
10321
|
+
try {
|
|
10322
|
+
const authorization = await requireSessionAuthorization2(deps, grant, {
|
|
10323
|
+
sessionId,
|
|
10324
|
+
operation,
|
|
10325
|
+
surface: "http"
|
|
10326
|
+
});
|
|
10327
|
+
if (authorization) requestSessionAuthorization.set(c.req.raw, authorization);
|
|
10328
|
+
} catch (error) {
|
|
10329
|
+
throw sessionAuthorizationHttpError(error);
|
|
10330
|
+
}
|
|
10331
|
+
await next();
|
|
10332
|
+
};
|
|
10333
|
+
app.use("/v1/workspaces/:workspaceId/sessions/:sessionId/*", authorizeSessionHttp);
|
|
9082
10334
|
app.post("/v1/workspaces/:workspaceId/sessions", async (c) => {
|
|
9083
10335
|
const workspaceId = c.req.param("workspaceId");
|
|
9084
10336
|
const grant = await requireAccessGrant14(c, deps, workspaceId, "sessions:create");
|
|
@@ -9088,6 +10340,12 @@ function registerSessionRoutes(app, deps) {
|
|
|
9088
10340
|
app.get("/v1/workspaces/:workspaceId/sessions", async (c) => {
|
|
9089
10341
|
const workspaceId = c.req.param("workspaceId");
|
|
9090
10342
|
const grant = await requireAccessGrant14(c, deps, workspaceId, "sessions:read");
|
|
10343
|
+
let authorizationScope;
|
|
10344
|
+
try {
|
|
10345
|
+
authorizationScope = await requireSessionAuthorizationListScope2(deps, grant, "http");
|
|
10346
|
+
} catch (error) {
|
|
10347
|
+
throw sessionAuthorizationHttpError(error);
|
|
10348
|
+
}
|
|
9091
10349
|
const pageView = c.req.query("view") === "page";
|
|
9092
10350
|
const query = sessionListQuery(c.req.query(), pageView);
|
|
9093
10351
|
let page;
|
|
@@ -9097,7 +10355,8 @@ function registerSessionRoutes(app, deps) {
|
|
|
9097
10355
|
limit: boundedLimit(query.limit),
|
|
9098
10356
|
...query.cursor ? { cursor: query.cursor } : {},
|
|
9099
10357
|
...query.search ? { search: query.search } : {},
|
|
9100
|
-
...query.parentSessionId !== void 0 ? { parentSessionId: query.parentSessionId } : {}
|
|
10358
|
+
...query.parentSessionId !== void 0 ? { parentSessionId: query.parentSessionId } : {},
|
|
10359
|
+
...authorizationScope ? { authorizationScope } : {}
|
|
9101
10360
|
});
|
|
9102
10361
|
} catch (error) {
|
|
9103
10362
|
if (error instanceof SessionListAccessError) {
|
|
@@ -9108,6 +10367,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
9108
10367
|
}
|
|
9109
10368
|
throw error;
|
|
9110
10369
|
}
|
|
10370
|
+
c.header("x-opengeni-pinned-truncated", page.pinnedTruncated === true ? "true" : "false");
|
|
9111
10371
|
if (pageView) {
|
|
9112
10372
|
return c.json(page);
|
|
9113
10373
|
}
|
|
@@ -9120,7 +10380,13 @@ function registerSessionRoutes(app, deps) {
|
|
|
9120
10380
|
if (!z2.string().uuid().safeParse(sessionId).success) {
|
|
9121
10381
|
throw new HTTPException21(404, { message: "session not found" });
|
|
9122
10382
|
}
|
|
9123
|
-
const session = await getSessionForSubject(
|
|
10383
|
+
const session = await getSessionForSubject(
|
|
10384
|
+
db,
|
|
10385
|
+
workspaceId,
|
|
10386
|
+
sessionId,
|
|
10387
|
+
grant.subjectId,
|
|
10388
|
+
relatedSessionAccessFor(c)
|
|
10389
|
+
);
|
|
9124
10390
|
if (!session) {
|
|
9125
10391
|
throw new HTTPException21(404, { message: "session not found" });
|
|
9126
10392
|
}
|
|
@@ -9147,14 +10413,17 @@ function registerSessionRoutes(app, deps) {
|
|
|
9147
10413
|
if (!session) {
|
|
9148
10414
|
throw new HTTPException21(404, { message: "session not found" });
|
|
9149
10415
|
}
|
|
9150
|
-
return c.json(session);
|
|
10416
|
+
return c.json(projectSessionForRelatedAccess2(session, relatedSessionAccessFor(c)));
|
|
9151
10417
|
} catch (error) {
|
|
9152
10418
|
if (error instanceof SessionPinAccessError) {
|
|
9153
10419
|
throw new HTTPException21(403, { message: error.message });
|
|
9154
10420
|
}
|
|
9155
10421
|
if (error instanceof SessionPinVersionConflictError) {
|
|
9156
10422
|
return c.json(
|
|
9157
|
-
{
|
|
10423
|
+
{
|
|
10424
|
+
message: "session pin changed in another client",
|
|
10425
|
+
current: error.current
|
|
10426
|
+
},
|
|
9158
10427
|
409
|
|
9159
10428
|
);
|
|
9160
10429
|
}
|
|
@@ -9163,8 +10432,8 @@ function registerSessionRoutes(app, deps) {
|
|
|
9163
10432
|
});
|
|
9164
10433
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/lineage", async (c) => {
|
|
9165
10434
|
const workspaceId = c.req.param("workspaceId");
|
|
9166
|
-
await requireAccessGrant14(c, deps, workspaceId, "sessions:read");
|
|
9167
|
-
return c.json(await readSessionLineage(
|
|
10435
|
+
const grant = await requireAccessGrant14(c, deps, workspaceId, "sessions:read");
|
|
10436
|
+
return c.json(await readSessionLineage(deps, grant, c.req.param("sessionId")));
|
|
9168
10437
|
});
|
|
9169
10438
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/codex-account", async (c) => {
|
|
9170
10439
|
const workspaceId = c.req.param("workspaceId");
|
|
@@ -9173,7 +10442,9 @@ function registerSessionRoutes(app, deps) {
|
|
|
9173
10442
|
const body = await c.req.json();
|
|
9174
10443
|
const target = typeof body.target === "string" ? body.target : "";
|
|
9175
10444
|
if (!target) {
|
|
9176
|
-
throw new HTTPException21(400, {
|
|
10445
|
+
throw new HTTPException21(400, {
|
|
10446
|
+
message: 'target is required ("auto" or an account id)'
|
|
10447
|
+
});
|
|
9177
10448
|
}
|
|
9178
10449
|
const pinned = target === "auto" ? null : target;
|
|
9179
10450
|
const mutation = await withCodexCapacityMutation2(
|
|
@@ -9186,7 +10457,9 @@ function registerSessionRoutes(app, deps) {
|
|
|
9186
10457
|
);
|
|
9187
10458
|
const ok = mutation.result;
|
|
9188
10459
|
if (!ok) {
|
|
9189
|
-
throw new HTTPException21(404, {
|
|
10460
|
+
throw new HTTPException21(404, {
|
|
10461
|
+
message: "session or codex account not found"
|
|
10462
|
+
});
|
|
9190
10463
|
}
|
|
9191
10464
|
await Promise.allSettled(
|
|
9192
10465
|
mutation.wakeTargets.map(
|
|
@@ -9214,8 +10487,14 @@ function registerSessionRoutes(app, deps) {
|
|
|
9214
10487
|
const sessionId = c.req.param("sessionId");
|
|
9215
10488
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
9216
10489
|
const payload = UpdateSessionRequest.parse(await c.req.json());
|
|
9217
|
-
await updateSessionTitle2(
|
|
9218
|
-
const session = await getSessionForSubject(
|
|
10490
|
+
const titleUpdate = await updateSessionTitle2(deps, grant, sessionId, payload.title, "user");
|
|
10491
|
+
const session = await getSessionForSubject(
|
|
10492
|
+
db,
|
|
10493
|
+
workspaceId,
|
|
10494
|
+
sessionId,
|
|
10495
|
+
grant.subjectId,
|
|
10496
|
+
titleUpdate.relatedSessionAccess
|
|
10497
|
+
);
|
|
9219
10498
|
if (!session) {
|
|
9220
10499
|
throw new HTTPException21(404, { message: "session not found" });
|
|
9221
10500
|
}
|
|
@@ -9373,28 +10652,132 @@ function registerSessionRoutes(app, deps) {
|
|
|
9373
10652
|
workflowId: requested.temporalWorkflowId,
|
|
9374
10653
|
wakeRevision: requested.wakeRevision
|
|
9375
10654
|
});
|
|
9376
|
-
return c.json({
|
|
10655
|
+
return c.json({
|
|
10656
|
+
status: "pending",
|
|
10657
|
+
message: "Compaction will run at the next safe boundary."
|
|
10658
|
+
});
|
|
9377
10659
|
});
|
|
9378
10660
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/events", async (c) => {
|
|
9379
10661
|
const workspaceId = c.req.param("workspaceId");
|
|
9380
10662
|
await requireAccessGrant14(c, deps, workspaceId, "sessions:read");
|
|
9381
10663
|
const sessionId = c.req.param("sessionId");
|
|
9382
10664
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
9383
|
-
const
|
|
9384
|
-
const
|
|
10665
|
+
const rawAfter = c.req.query("after");
|
|
10666
|
+
const rawBefore = c.req.query("before");
|
|
10667
|
+
const after = eventSequence(rawAfter, 0);
|
|
10668
|
+
const before = optionalEventSequence(rawBefore);
|
|
9385
10669
|
const compact = compactEvents(c.req.query("compact"));
|
|
9386
|
-
const
|
|
9387
|
-
const
|
|
10670
|
+
const explicitReplay = rawAfter !== void 0 || rawBefore !== void 0 || compact;
|
|
10671
|
+
const mode = eventEnumValue(
|
|
10672
|
+
c.req.query("mode"),
|
|
10673
|
+
SessionEventReadMode2,
|
|
10674
|
+
"mode",
|
|
10675
|
+
explicitReplay ? "forensic" : "monitoring"
|
|
10676
|
+
);
|
|
10677
|
+
const latestClass = eventEnumValue(
|
|
10678
|
+
c.req.query("latest"),
|
|
10679
|
+
SessionEventSemanticClass2,
|
|
10680
|
+
"latest",
|
|
10681
|
+
void 0
|
|
10682
|
+
);
|
|
10683
|
+
if (latestClass && ["includeTypes", "excludeTypes", "includeClasses", "excludeClasses"].some(
|
|
10684
|
+
(name) => c.req.query(name) !== void 0
|
|
10685
|
+
)) {
|
|
10686
|
+
throw new HTTPException21(400, {
|
|
10687
|
+
message: "latest cannot be combined with event filters"
|
|
10688
|
+
});
|
|
10689
|
+
}
|
|
10690
|
+
const direction = latestClass ? "before" : eventEnumValue(
|
|
10691
|
+
c.req.query("direction"),
|
|
10692
|
+
SessionEventReadDirection2,
|
|
10693
|
+
"direction",
|
|
10694
|
+
before !== void 0 ? "before" : rawAfter !== void 0 ? "after" : mode === "monitoring" ? "before" : "after"
|
|
10695
|
+
);
|
|
10696
|
+
const payloadMode = eventEnumValue(
|
|
10697
|
+
c.req.query("payloadMode"),
|
|
10698
|
+
SessionEventPayloadMode2,
|
|
10699
|
+
"payloadMode",
|
|
10700
|
+
mode === "monitoring" ? "summary" : "full"
|
|
10701
|
+
);
|
|
10702
|
+
const includeTypes = eventEnumList(
|
|
10703
|
+
c.req.query("includeTypes"),
|
|
10704
|
+
SessionEventType2,
|
|
10705
|
+
"includeTypes"
|
|
10706
|
+
);
|
|
10707
|
+
const excludeTypes = eventEnumList(
|
|
10708
|
+
c.req.query("excludeTypes"),
|
|
10709
|
+
SessionEventType2,
|
|
10710
|
+
"excludeTypes"
|
|
10711
|
+
);
|
|
10712
|
+
const includeClasses = eventEnumList(
|
|
10713
|
+
c.req.query("includeClasses"),
|
|
10714
|
+
SessionEventSemanticClass2,
|
|
10715
|
+
"includeClasses"
|
|
10716
|
+
);
|
|
10717
|
+
const excludeClasses = eventEnumList(
|
|
10718
|
+
c.req.query("excludeClasses"),
|
|
10719
|
+
SessionEventSemanticClass2,
|
|
10720
|
+
"excludeClasses"
|
|
10721
|
+
);
|
|
10722
|
+
const limit = latestClass ? 1 : eventListLimit(
|
|
10723
|
+
c.req.query("limit"),
|
|
10724
|
+
compact ? 5e3 : mode === "monitoring" ? 250 : 2e3,
|
|
10725
|
+
mode === "monitoring" ? 40 : 500
|
|
10726
|
+
);
|
|
10727
|
+
const dbPage = await listSessionEventPage2(db, workspaceId, sessionId, {
|
|
9388
10728
|
after,
|
|
9389
10729
|
...before !== void 0 ? { before } : {},
|
|
9390
|
-
limit
|
|
10730
|
+
limit,
|
|
10731
|
+
direction,
|
|
10732
|
+
payloadMode,
|
|
10733
|
+
includeTypes,
|
|
10734
|
+
excludeTypes,
|
|
10735
|
+
includeClasses: latestClass ? [latestClass] : includeClasses,
|
|
10736
|
+
excludeClasses,
|
|
10737
|
+
...mode === "monitoring" ? { defaultExcludeTypes: SESSION_EVENT_RAW_DELTA_TYPES2 } : {}
|
|
9391
10738
|
});
|
|
9392
|
-
|
|
10739
|
+
const events = dbPage.events;
|
|
10740
|
+
const projected = compact ? coalesceSessionEventDeltas(events) : events;
|
|
10741
|
+
const page = boundSessionEventHttpPage(projected, {
|
|
10742
|
+
direction
|
|
10743
|
+
});
|
|
10744
|
+
const hasMore = dbPage.hasMore || page.truncated;
|
|
10745
|
+
c.header("X-OpenGeni-Page-Bytes", String(page.bytes));
|
|
10746
|
+
c.header("X-OpenGeni-Page-Max-Bytes", String(1024 * 1024));
|
|
10747
|
+
c.header("X-OpenGeni-Page-Truncated", String(hasMore));
|
|
10748
|
+
c.header("X-OpenGeni-Has-More", String(hasMore));
|
|
10749
|
+
c.header("X-OpenGeni-Event-Mode", mode);
|
|
10750
|
+
c.header("X-OpenGeni-Event-Direction", direction);
|
|
10751
|
+
c.header("X-OpenGeni-Payload-Mode", payloadMode);
|
|
10752
|
+
c.header("X-OpenGeni-Forensic-Exact", String(mode === "forensic" && payloadMode === "full"));
|
|
10753
|
+
const coveredFirst = page.events[0]?.sequence;
|
|
10754
|
+
const coveredLast = page.events.at(-1)?.sequence;
|
|
10755
|
+
if (coveredFirst !== void 0) c.header("X-OpenGeni-Covered-First", String(coveredFirst));
|
|
10756
|
+
if (coveredLast !== void 0) c.header("X-OpenGeni-Covered-Last", String(coveredLast));
|
|
10757
|
+
const truncatedBy = page.truncated ? "http_bytes" : dbPage.truncatedBy;
|
|
10758
|
+
if (truncatedBy) c.header("X-OpenGeni-Truncated-By", truncatedBy);
|
|
10759
|
+
if (page.nextSequence !== null) {
|
|
10760
|
+
c.header(
|
|
10761
|
+
direction === "before" ? "X-OpenGeni-Next-Before" : "X-OpenGeni-Next-After",
|
|
10762
|
+
String(page.nextSequence)
|
|
10763
|
+
);
|
|
10764
|
+
}
|
|
10765
|
+
return c.json(page.events);
|
|
9393
10766
|
});
|
|
9394
10767
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/events/stream", async (c) => {
|
|
9395
10768
|
const workspaceId = c.req.param("workspaceId");
|
|
9396
|
-
await requireAccessGrant14(c, deps, workspaceId, "sessions:read");
|
|
10769
|
+
const grant = await requireAccessGrant14(c, deps, workspaceId, "sessions:read");
|
|
9397
10770
|
const sessionId = c.req.param("sessionId");
|
|
10771
|
+
let authorization;
|
|
10772
|
+
try {
|
|
10773
|
+
authorization = await requireSessionAuthorization2(deps, grant, {
|
|
10774
|
+
sessionId,
|
|
10775
|
+
operation: "session.stream.read",
|
|
10776
|
+
surface: "stream"
|
|
10777
|
+
});
|
|
10778
|
+
} catch (error) {
|
|
10779
|
+
throw sessionAuthorizationHttpError(error);
|
|
10780
|
+
}
|
|
9398
10781
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
9399
10782
|
const after = Number(c.req.query("after") ?? c.req.header("Last-Event-ID") ?? 0);
|
|
9400
10783
|
return sseSessionStream(
|
|
@@ -9403,7 +10786,20 @@ function registerSessionRoutes(app, deps) {
|
|
|
9403
10786
|
workspaceId,
|
|
9404
10787
|
sessionId,
|
|
9405
10788
|
Number.isFinite(after) ? after : 0,
|
|
9406
|
-
c.req.raw.signal
|
|
10789
|
+
c.req.raw.signal,
|
|
10790
|
+
{
|
|
10791
|
+
observability: deps.observability,
|
|
10792
|
+
...authorization ? {
|
|
10793
|
+
reauthorizeAfterMs: authorization.reauthorizeAfterMs ?? SESSION_AUTHORIZATION_DEFAULT_REAUTHORIZE_MS,
|
|
10794
|
+
reauthorize: async () => {
|
|
10795
|
+
await requireSessionAuthorization2(deps, grant, {
|
|
10796
|
+
sessionId,
|
|
10797
|
+
operation: "session.stream.read",
|
|
10798
|
+
surface: "stream"
|
|
10799
|
+
});
|
|
10800
|
+
}
|
|
10801
|
+
} : {}
|
|
10802
|
+
}
|
|
9407
10803
|
);
|
|
9408
10804
|
});
|
|
9409
10805
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/turns", async (c) => {
|
|
@@ -9421,7 +10817,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
9421
10817
|
const sessionId = c.req.param("sessionId");
|
|
9422
10818
|
const snapshot = await getSessionQueueSnapshot2(db, workspaceId, sessionId);
|
|
9423
10819
|
if (!snapshot) throw new HTTPException21(404, { message: "session not found" });
|
|
9424
|
-
return c.json(snapshot);
|
|
10820
|
+
return c.json(projectQueueSnapshot(snapshot, sessionId, relatedSessionAccessFor(c)));
|
|
9425
10821
|
});
|
|
9426
10822
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/queue/:turnId/move", async (c) => {
|
|
9427
10823
|
const workspaceId = c.req.param("workspaceId");
|
|
@@ -9430,14 +10826,21 @@ function registerSessionRoutes(app, deps) {
|
|
|
9430
10826
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
9431
10827
|
const payload = MoveSessionQueueItemRequest.parse(await c.req.json());
|
|
9432
10828
|
try {
|
|
9433
|
-
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
|
|
9439
|
-
|
|
10829
|
+
const response = await moveHumanQueuePrompt(
|
|
10830
|
+
deps,
|
|
10831
|
+
{
|
|
10832
|
+
accountId: grant.accountId,
|
|
10833
|
+
workspaceId,
|
|
10834
|
+
sessionId,
|
|
10835
|
+
subjectId: grant.subjectId
|
|
10836
|
+
},
|
|
10837
|
+
c.req.param("turnId"),
|
|
10838
|
+
payload
|
|
9440
10839
|
);
|
|
10840
|
+
return c.json({
|
|
10841
|
+
...response,
|
|
10842
|
+
snapshot: projectQueueSnapshot(response.snapshot, sessionId, relatedSessionAccessFor(c))
|
|
10843
|
+
});
|
|
9441
10844
|
} catch (error) {
|
|
9442
10845
|
return commandConflictResponse(c, error);
|
|
9443
10846
|
}
|
|
@@ -9449,14 +10852,21 @@ function registerSessionRoutes(app, deps) {
|
|
|
9449
10852
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
9450
10853
|
const payload = EditSessionQueueItemRequest.parse(await c.req.json());
|
|
9451
10854
|
try {
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
|
|
9458
|
-
|
|
10855
|
+
const response = await editHumanQueuePrompt(
|
|
10856
|
+
deps,
|
|
10857
|
+
{
|
|
10858
|
+
accountId: grant.accountId,
|
|
10859
|
+
workspaceId,
|
|
10860
|
+
sessionId,
|
|
10861
|
+
subjectId: grant.subjectId
|
|
10862
|
+
},
|
|
10863
|
+
c.req.param("turnId"),
|
|
10864
|
+
payload
|
|
9459
10865
|
);
|
|
10866
|
+
return c.json({
|
|
10867
|
+
...response,
|
|
10868
|
+
snapshot: projectQueueSnapshot(response.snapshot, sessionId, relatedSessionAccessFor(c))
|
|
10869
|
+
});
|
|
9460
10870
|
} catch (error) {
|
|
9461
10871
|
return commandConflictResponse(c, error);
|
|
9462
10872
|
}
|
|
@@ -9468,14 +10878,21 @@ function registerSessionRoutes(app, deps) {
|
|
|
9468
10878
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
9469
10879
|
const payload = SteerSessionQueueItemRequest.parse(await c.req.json());
|
|
9470
10880
|
try {
|
|
9471
|
-
|
|
9472
|
-
|
|
9473
|
-
|
|
9474
|
-
|
|
9475
|
-
|
|
9476
|
-
|
|
9477
|
-
|
|
10881
|
+
const response = await steerHumanQueuePrompt(
|
|
10882
|
+
deps,
|
|
10883
|
+
{
|
|
10884
|
+
accountId: grant.accountId,
|
|
10885
|
+
workspaceId,
|
|
10886
|
+
sessionId,
|
|
10887
|
+
subjectId: grant.subjectId
|
|
10888
|
+
},
|
|
10889
|
+
c.req.param("turnId"),
|
|
10890
|
+
payload
|
|
9478
10891
|
);
|
|
10892
|
+
return c.json({
|
|
10893
|
+
...response,
|
|
10894
|
+
snapshot: projectQueueSnapshot(response.snapshot, sessionId, relatedSessionAccessFor(c))
|
|
10895
|
+
});
|
|
9479
10896
|
} catch (error) {
|
|
9480
10897
|
return commandConflictResponse(c, error);
|
|
9481
10898
|
}
|
|
@@ -9487,14 +10904,21 @@ function registerSessionRoutes(app, deps) {
|
|
|
9487
10904
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
9488
10905
|
const payload = DeleteSessionQueueItemRequest.parse(await c.req.json());
|
|
9489
10906
|
try {
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
|
|
9493
|
-
|
|
9494
|
-
|
|
9495
|
-
|
|
9496
|
-
|
|
10907
|
+
const response = await deleteHumanQueuePrompt(
|
|
10908
|
+
deps,
|
|
10909
|
+
{
|
|
10910
|
+
accountId: grant.accountId,
|
|
10911
|
+
workspaceId,
|
|
10912
|
+
sessionId,
|
|
10913
|
+
subjectId: grant.subjectId
|
|
10914
|
+
},
|
|
10915
|
+
c.req.param("turnId"),
|
|
10916
|
+
payload
|
|
9497
10917
|
);
|
|
10918
|
+
return c.json({
|
|
10919
|
+
...response,
|
|
10920
|
+
snapshot: projectQueueSnapshot(response.snapshot, sessionId, relatedSessionAccessFor(c))
|
|
10921
|
+
});
|
|
9498
10922
|
} catch (error) {
|
|
9499
10923
|
return commandConflictResponse(c, error);
|
|
9500
10924
|
}
|
|
@@ -9504,7 +10928,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
9504
10928
|
const grant = await requireAccessGrant14(c, deps, workspaceId, "sessions:read");
|
|
9505
10929
|
const sessionId = c.req.param("sessionId");
|
|
9506
10930
|
return c.json(
|
|
9507
|
-
await getHumanComposerDraft(
|
|
10931
|
+
await getHumanComposerDraft(deps, {
|
|
9508
10932
|
accountId: grant.accountId,
|
|
9509
10933
|
workspaceId,
|
|
9510
10934
|
sessionId,
|
|
@@ -9520,8 +10944,13 @@ function registerSessionRoutes(app, deps) {
|
|
|
9520
10944
|
try {
|
|
9521
10945
|
return c.json(
|
|
9522
10946
|
await saveHumanComposerDraft(
|
|
9523
|
-
|
|
9524
|
-
{
|
|
10947
|
+
deps,
|
|
10948
|
+
{
|
|
10949
|
+
accountId: grant.accountId,
|
|
10950
|
+
workspaceId,
|
|
10951
|
+
sessionId,
|
|
10952
|
+
subjectId: grant.subjectId
|
|
10953
|
+
},
|
|
9525
10954
|
payload
|
|
9526
10955
|
)
|
|
9527
10956
|
);
|
|
@@ -9532,16 +10961,33 @@ function registerSessionRoutes(app, deps) {
|
|
|
9532
10961
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/control", async (c) => {
|
|
9533
10962
|
const workspaceId = c.req.param("workspaceId");
|
|
9534
10963
|
const grant = await requireAccessGrant14(c, deps, workspaceId, "sessions:control");
|
|
10964
|
+
if (workspaceControlUtf8Bytes(grant.subjectId) > WORKSPACE_CONTROL_ACTOR_MAX_BYTES) {
|
|
10965
|
+
throw new HTTPException21(400, { message: "workspace-control actor is too large" });
|
|
10966
|
+
}
|
|
9535
10967
|
const sessionId = c.req.param("sessionId");
|
|
9536
|
-
const
|
|
10968
|
+
const parsed = SessionControlRequest.safeParse(await c.req.json().catch(() => null));
|
|
10969
|
+
if (!parsed.success) {
|
|
10970
|
+
throw new HTTPException21(400, { message: "invalid session control request" });
|
|
10971
|
+
}
|
|
9537
10972
|
try {
|
|
9538
|
-
|
|
9539
|
-
|
|
9540
|
-
|
|
9541
|
-
|
|
9542
|
-
|
|
9543
|
-
|
|
10973
|
+
const response = await controlHumanSessionWorkstream2(
|
|
10974
|
+
deps,
|
|
10975
|
+
{
|
|
10976
|
+
accountId: grant.accountId,
|
|
10977
|
+
workspaceId,
|
|
10978
|
+
sessionId,
|
|
10979
|
+
subjectId: grant.subjectId
|
|
10980
|
+
},
|
|
10981
|
+
parsed.data
|
|
9544
10982
|
);
|
|
10983
|
+
return c.json({
|
|
10984
|
+
...response,
|
|
10985
|
+
effectiveControl: projectEffectiveControlForRelatedAccess2(
|
|
10986
|
+
response.effectiveControl,
|
|
10987
|
+
sessionId,
|
|
10988
|
+
relatedSessionAccessFor(c)
|
|
10989
|
+
)
|
|
10990
|
+
});
|
|
9545
10991
|
} catch (error) {
|
|
9546
10992
|
return commandConflictResponse(c, error);
|
|
9547
10993
|
}
|
|
@@ -9555,6 +11001,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
9555
11001
|
const payload = SteerSessionMessageRequest.parse(raw);
|
|
9556
11002
|
const result = await acceptSessionUserMessage2(deps, grant, workspaceId, sessionId, {
|
|
9557
11003
|
text: payload.text,
|
|
11004
|
+
turnInstructions: payload.turnInstructions ?? null,
|
|
9558
11005
|
resources: payload.resources,
|
|
9559
11006
|
tools: payload.tools,
|
|
9560
11007
|
toolsProvided: userMessagePayloadHasOwnProperty({ payload: raw }, "tools"),
|
|
@@ -9575,9 +11022,22 @@ function registerSessionRoutes(app, deps) {
|
|
|
9575
11022
|
const sessionId = c.req.param("sessionId");
|
|
9576
11023
|
const rawEvent = await c.req.json();
|
|
9577
11024
|
const event = ClientSessionEvent.parse(rawEvent);
|
|
11025
|
+
const refinedOperation = event.type === "user.approvalDecision" ? "session.approval.write" : event.type === "user.humanInputResponse" ? "session.human_input.write" : null;
|
|
11026
|
+
if (refinedOperation) {
|
|
11027
|
+
try {
|
|
11028
|
+
await requireSessionAuthorization2(deps, grant, {
|
|
11029
|
+
sessionId,
|
|
11030
|
+
operation: refinedOperation,
|
|
11031
|
+
surface: "http"
|
|
11032
|
+
});
|
|
11033
|
+
} catch (error) {
|
|
11034
|
+
throw sessionAuthorizationHttpError(error);
|
|
11035
|
+
}
|
|
11036
|
+
}
|
|
9578
11037
|
if (event.type === "user.message") {
|
|
9579
11038
|
const { accepted } = await acceptSessionUserMessage2(deps, grant, workspaceId, sessionId, {
|
|
9580
11039
|
text: event.payload.text,
|
|
11040
|
+
turnInstructions: event.payload.turnInstructions ?? null,
|
|
9581
11041
|
resources: event.payload.resources ?? [],
|
|
9582
11042
|
tools: event.payload.tools ?? [],
|
|
9583
11043
|
toolsProvided: userMessagePayloadHasOwnProperty(rawEvent, "tools"),
|
|
@@ -9615,7 +11075,79 @@ function registerSessionRoutes(app, deps) {
|
|
|
9615
11075
|
});
|
|
9616
11076
|
return c.json(accepted.event, 202);
|
|
9617
11077
|
}
|
|
11078
|
+
if (event.type === "user.humanInputResponse") {
|
|
11079
|
+
let accepted;
|
|
11080
|
+
try {
|
|
11081
|
+
accepted = await acceptSessionHumanInputResponse(db, {
|
|
11082
|
+
accountId: grant.accountId,
|
|
11083
|
+
workspaceId,
|
|
11084
|
+
sessionId,
|
|
11085
|
+
requestId: event.payload.requestId,
|
|
11086
|
+
response: event.payload.response,
|
|
11087
|
+
respondedBy: grant.subjectId,
|
|
11088
|
+
clientEventId: event.clientEventId ?? null
|
|
11089
|
+
});
|
|
11090
|
+
} catch (error) {
|
|
11091
|
+
if (error instanceof HumanInputResponseValidationError) {
|
|
11092
|
+
throw new HTTPException21(error.code === "SKIP_NOT_ALLOWED" ? 409 : 422, {
|
|
11093
|
+
message: error.message
|
|
11094
|
+
});
|
|
11095
|
+
}
|
|
11096
|
+
throw error;
|
|
11097
|
+
}
|
|
11098
|
+
if (accepted.action === "not_found") {
|
|
11099
|
+
throw new HTTPException21(404, { message: "human-input request not found" });
|
|
11100
|
+
}
|
|
11101
|
+
await publishDurableSessionEvents(bus, workspaceId, sessionId, accepted.events);
|
|
11102
|
+
if (accepted.workflowWakeRevision !== null) {
|
|
11103
|
+
await workflowClient.signalApprovalDecision({
|
|
11104
|
+
accountId: grant.accountId,
|
|
11105
|
+
workspaceId,
|
|
11106
|
+
sessionId,
|
|
11107
|
+
eventId: accepted.events[0]?.id ?? event.payload.requestId,
|
|
11108
|
+
workflowId: workflowIdForSession(sessionId),
|
|
11109
|
+
workflowWakeRevision: accepted.workflowWakeRevision
|
|
11110
|
+
});
|
|
11111
|
+
}
|
|
11112
|
+
if (accepted.action === "conflict") {
|
|
11113
|
+
throw new HTTPException21(409, {
|
|
11114
|
+
message: `human-input request is ${accepted.request.status}`
|
|
11115
|
+
});
|
|
11116
|
+
}
|
|
11117
|
+
return c.json(accepted.event, 202);
|
|
11118
|
+
}
|
|
11119
|
+
});
|
|
11120
|
+
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/human-input-requests", async (c) => {
|
|
11121
|
+
const workspaceId = c.req.param("workspaceId");
|
|
11122
|
+
await requireAccessGrant14(c, deps, workspaceId, "sessions:read");
|
|
11123
|
+
const sessionId = c.req.param("sessionId");
|
|
11124
|
+
await assertSessionExists(db, workspaceId, sessionId);
|
|
11125
|
+
const rawStatus = c.req.query("status");
|
|
11126
|
+
const status = rawStatus ? HumanInputRequestStatus.safeParse(rawStatus) : null;
|
|
11127
|
+
if (status && !status.success) {
|
|
11128
|
+
throw new HTTPException21(400, { message: "invalid human-input request status" });
|
|
11129
|
+
}
|
|
11130
|
+
const requests = await listSessionHumanInputRequests(db, workspaceId, sessionId, {
|
|
11131
|
+
...status?.success ? { status: status.data } : {}
|
|
11132
|
+
});
|
|
11133
|
+
return c.json({ requests });
|
|
9618
11134
|
});
|
|
11135
|
+
app.get(
|
|
11136
|
+
"/v1/workspaces/:workspaceId/sessions/:sessionId/human-input-requests/:requestId",
|
|
11137
|
+
async (c) => {
|
|
11138
|
+
const workspaceId = c.req.param("workspaceId");
|
|
11139
|
+
await requireAccessGrant14(c, deps, workspaceId, "sessions:read");
|
|
11140
|
+
const sessionId = c.req.param("sessionId");
|
|
11141
|
+
const request = await getSessionHumanInputRequest(
|
|
11142
|
+
db,
|
|
11143
|
+
workspaceId,
|
|
11144
|
+
sessionId,
|
|
11145
|
+
c.req.param("requestId")
|
|
11146
|
+
);
|
|
11147
|
+
if (!request) throw new HTTPException21(404, { message: "human-input request not found" });
|
|
11148
|
+
return c.json(request);
|
|
11149
|
+
}
|
|
11150
|
+
);
|
|
9619
11151
|
function assertOwnershipEnabled() {
|
|
9620
11152
|
if (!settings.sandboxOwnershipEnabled) {
|
|
9621
11153
|
throw new HTTPException21(404, {
|
|
@@ -9642,6 +11174,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
9642
11174
|
{ workspaceId, sandboxGroupId: session.sandboxGroupId }
|
|
9643
11175
|
);
|
|
9644
11176
|
const { shared, sharedSessionIds } = await resolveSharedExposure(workspaceId, session);
|
|
11177
|
+
const visibleSharedSessionIds = relatedSessionAccessFor(c) === "root" ? sharedSessionIds : [];
|
|
9645
11178
|
const ack = await getStreamAcknowledgment(db, {
|
|
9646
11179
|
workspaceId,
|
|
9647
11180
|
sandboxGroupId: session.sandboxGroupId,
|
|
@@ -9696,13 +11229,13 @@ function registerSessionRoutes(app, deps) {
|
|
|
9696
11229
|
// tracks the desktop tier + a desktop-capable backend.
|
|
9697
11230
|
computerUseEnabled: settings.computerUseEnabled,
|
|
9698
11231
|
computerUseReadOnly: settings.computerUseReadOnly,
|
|
9699
|
-
// Graceful degrade (
|
|
11232
|
+
// Graceful degrade (stream-token availability contract): if desktop is enabled but no stream-token
|
|
9700
11233
|
// secret is resolvable, the desktop cell reports transport:null rather
|
|
9701
11234
|
// than advertising a plane we can never authorize.
|
|
9702
11235
|
streamTokenSecretAvailable: !streamTokenDegraded(settings),
|
|
9703
11236
|
desktopAcknowledged: acknowledged,
|
|
9704
11237
|
shared,
|
|
9705
|
-
sharedSessionIds,
|
|
11238
|
+
sharedSessionIds: visibleSharedSessionIds,
|
|
9706
11239
|
// The minted live address (null when not unlocked/degraded). The resolver
|
|
9707
11240
|
// only folds it in when the desktop gates pass + the ack is present.
|
|
9708
11241
|
...desktopStream ? {
|
|
@@ -9752,7 +11285,9 @@ function registerSessionRoutes(app, deps) {
|
|
|
9752
11285
|
}
|
|
9753
11286
|
const parsed = AcknowledgeStreamRequest.safeParse(await c.req.json().catch(() => ({})));
|
|
9754
11287
|
if (!parsed.success) {
|
|
9755
|
-
throw new HTTPException21(400, {
|
|
11288
|
+
throw new HTTPException21(400, {
|
|
11289
|
+
message: "invalid stream acknowledgment request"
|
|
11290
|
+
});
|
|
9756
11291
|
}
|
|
9757
11292
|
const recorded = await recordStreamAcknowledgment(db, {
|
|
9758
11293
|
accountId: grant.accountId,
|
|
@@ -9779,7 +11314,9 @@ function registerSessionRoutes(app, deps) {
|
|
|
9779
11314
|
}
|
|
9780
11315
|
const parsed = AttachViewerRequest.safeParse(await c.req.json().catch(() => ({})));
|
|
9781
11316
|
if (!parsed.success) {
|
|
9782
|
-
throw new HTTPException21(400, {
|
|
11317
|
+
throw new HTTPException21(400, {
|
|
11318
|
+
message: "invalid viewer attach request"
|
|
11319
|
+
});
|
|
9783
11320
|
}
|
|
9784
11321
|
const wantDesktop = parsed.data.desktop ?? false;
|
|
9785
11322
|
const { shared } = await resolveSharedExposure(workspaceId, session);
|
|
@@ -9790,10 +11327,14 @@ function registerSessionRoutes(app, deps) {
|
|
|
9790
11327
|
subjectId: grant.subjectId
|
|
9791
11328
|
});
|
|
9792
11329
|
if (!ack?.acknowledgedUnredacted) {
|
|
9793
|
-
throw new HTTPException21(409, {
|
|
11330
|
+
throw new HTTPException21(409, {
|
|
11331
|
+
message: "stream_acknowledgment_required"
|
|
11332
|
+
});
|
|
9794
11333
|
}
|
|
9795
11334
|
if (shared && !ack.acknowledgedShared) {
|
|
9796
|
-
throw new HTTPException21(409, {
|
|
11335
|
+
throw new HTTPException21(409, {
|
|
11336
|
+
message: "shared_acknowledgment_required"
|
|
11337
|
+
});
|
|
9797
11338
|
}
|
|
9798
11339
|
}
|
|
9799
11340
|
const activeSandbox = session.activeSandboxId ? await getSandbox2(db, workspaceId, session.activeSandboxId) : null;
|
|
@@ -9916,7 +11457,9 @@ function registerSessionRoutes(app, deps) {
|
|
|
9916
11457
|
}
|
|
9917
11458
|
const parsed = ViewerHeartbeatRequest.safeParse(await c.req.json().catch(() => ({})));
|
|
9918
11459
|
if (!parsed.success) {
|
|
9919
|
-
throw new HTTPException21(400, {
|
|
11460
|
+
throw new HTTPException21(400, {
|
|
11461
|
+
message: "viewer heartbeat requires { leaseEpoch }"
|
|
11462
|
+
});
|
|
9920
11463
|
}
|
|
9921
11464
|
const alive = await heartbeatViewer(
|
|
9922
11465
|
{ db, settings },
|
|
@@ -9969,7 +11512,10 @@ function registerSessionRoutes(app, deps) {
|
|
|
9969
11512
|
viewerId: c.req.param("viewerId"),
|
|
9970
11513
|
idleGraceMs: settings.sandboxIdleGraceMs
|
|
9971
11514
|
});
|
|
9972
|
-
return c.json({
|
|
11515
|
+
return c.json({
|
|
11516
|
+
liveness: result?.liveness ?? null,
|
|
11517
|
+
refcount: result?.refcount ?? null
|
|
11518
|
+
});
|
|
9973
11519
|
}
|
|
9974
11520
|
);
|
|
9975
11521
|
async function channelAPreamble(c, permission) {
|
|
@@ -9981,7 +11527,12 @@ function registerSessionRoutes(app, deps) {
|
|
|
9981
11527
|
if (!session) {
|
|
9982
11528
|
throw new HTTPException21(404, { message: "session not found" });
|
|
9983
11529
|
}
|
|
9984
|
-
return {
|
|
11530
|
+
return {
|
|
11531
|
+
accountId: grant.accountId,
|
|
11532
|
+
workspaceId,
|
|
11533
|
+
session,
|
|
11534
|
+
subjectId: grant.subjectId
|
|
11535
|
+
};
|
|
9985
11536
|
}
|
|
9986
11537
|
async function parseChannelABody(c, schema) {
|
|
9987
11538
|
const raw = await c.req.json().catch(() => void 0);
|
|
@@ -10111,7 +11662,9 @@ function registerSessionRoutes(app, deps) {
|
|
|
10111
11662
|
const sessionId = c.req.param("sessionId") ?? "";
|
|
10112
11663
|
const path = c.req.query("path");
|
|
10113
11664
|
if (!path) {
|
|
10114
|
-
throw new HTTPException21(400, {
|
|
11665
|
+
throw new HTTPException21(400, {
|
|
11666
|
+
message: "path query parameter is required"
|
|
11667
|
+
});
|
|
10115
11668
|
}
|
|
10116
11669
|
const session = await getSession4(db, workspaceId, sessionId);
|
|
10117
11670
|
if (!session) {
|
|
@@ -10125,7 +11678,9 @@ function registerSessionRoutes(app, deps) {
|
|
|
10125
11678
|
if (revisionParam !== void 0 && revisionParam !== "") {
|
|
10126
11679
|
const revision = Number(revisionParam);
|
|
10127
11680
|
if (!Number.isInteger(revision) || revision < 0) {
|
|
10128
|
-
throw new HTTPException21(400, {
|
|
11681
|
+
throw new HTTPException21(400, {
|
|
11682
|
+
message: "revision must be a non-negative integer"
|
|
11683
|
+
});
|
|
10129
11684
|
}
|
|
10130
11685
|
row = await workspaceCaptureAtRevision(db, workspaceId, sessionId, revision);
|
|
10131
11686
|
} else {
|
|
@@ -10192,7 +11747,9 @@ function registerSessionRoutes(app, deps) {
|
|
|
10192
11747
|
throw new HTTPException21(404, { message: "pty not found or closed" });
|
|
10193
11748
|
}
|
|
10194
11749
|
if (pty.execSessionId === null) {
|
|
10195
|
-
throw new HTTPException21(409, {
|
|
11750
|
+
throw new HTTPException21(409, {
|
|
11751
|
+
message: "interactive terminal unsupported on this backend"
|
|
11752
|
+
});
|
|
10196
11753
|
}
|
|
10197
11754
|
let seq = 1;
|
|
10198
11755
|
await withChannelA({ db, settings, bus }, ctx, async ({ service }) => {
|
|
@@ -10255,7 +11812,11 @@ function registerSessionRoutes(app, deps) {
|
|
|
10255
11812
|
workspaceId: ctx.workspaceId,
|
|
10256
11813
|
ptyId: req.ptyId
|
|
10257
11814
|
});
|
|
10258
|
-
const exited = {
|
|
11815
|
+
const exited = {
|
|
11816
|
+
ptyId: req.ptyId,
|
|
11817
|
+
exitCode: 0,
|
|
11818
|
+
reason: "exit"
|
|
11819
|
+
};
|
|
10259
11820
|
await appendAndPublishEvents5(db, bus, ctx.workspaceId, ctx.session.id, [
|
|
10260
11821
|
{ type: "terminal.pty.exited", payload: exited }
|
|
10261
11822
|
]);
|
|
@@ -10263,13 +11824,114 @@ function registerSessionRoutes(app, deps) {
|
|
|
10263
11824
|
return c.body(null, 204);
|
|
10264
11825
|
});
|
|
10265
11826
|
}
|
|
10266
|
-
function eventListLimit(raw, max = 2e3) {
|
|
10267
|
-
const limit = Number(raw ??
|
|
11827
|
+
function eventListLimit(raw, max = 2e3, fallback = 500) {
|
|
11828
|
+
const limit = Number(raw ?? fallback);
|
|
10268
11829
|
if (!Number.isFinite(limit)) {
|
|
10269
|
-
return
|
|
11830
|
+
return fallback;
|
|
10270
11831
|
}
|
|
10271
11832
|
return Math.min(max, Math.max(1, Math.floor(limit)));
|
|
10272
11833
|
}
|
|
11834
|
+
function sessionAuthorizationOperationForHttp(method, pathname, sessionId) {
|
|
11835
|
+
const marker = `/sessions/${sessionId}`;
|
|
11836
|
+
const markerAt = pathname.indexOf(marker);
|
|
11837
|
+
if (markerAt < 0) return null;
|
|
11838
|
+
const suffix = pathname.slice(markerAt + marker.length);
|
|
11839
|
+
const verb = method.toUpperCase();
|
|
11840
|
+
if (suffix === "") {
|
|
11841
|
+
if (verb === "GET") return "session.read";
|
|
11842
|
+
if (verb === "PATCH") return "session.title.write";
|
|
11843
|
+
return null;
|
|
11844
|
+
}
|
|
11845
|
+
if (suffix === "/pin" && verb === "PUT") return "session.pin.write";
|
|
11846
|
+
if (suffix === "/lineage" && verb === "GET") return "session.lineage.read";
|
|
11847
|
+
if (suffix === "/codex-account" && verb === "POST") {
|
|
11848
|
+
return "session.codex_account.write";
|
|
11849
|
+
}
|
|
11850
|
+
if (suffix === "/goal") {
|
|
11851
|
+
return verb === "GET" ? "session.goal.read" : ["PATCH", "DELETE"].includes(verb) ? "session.goal.write" : null;
|
|
11852
|
+
}
|
|
11853
|
+
if (suffix === "/context/clear" || suffix === "/context/compact") {
|
|
11854
|
+
return verb === "POST" ? "session.context.write" : null;
|
|
11855
|
+
}
|
|
11856
|
+
if (suffix === "/events/stream" && verb === "GET") return "session.stream.read";
|
|
11857
|
+
if (suffix === "/events") {
|
|
11858
|
+
if (verb === "GET") return "session.events.read";
|
|
11859
|
+
if (verb === "POST") return "session.append";
|
|
11860
|
+
return null;
|
|
11861
|
+
}
|
|
11862
|
+
if (suffix === "/turns" && verb === "GET") return "session.turns.read";
|
|
11863
|
+
if (suffix === "/queue" && verb === "GET") return "session.queue.read";
|
|
11864
|
+
if (suffix.startsWith("/queue/") && verb === "POST") return "session.queue.control";
|
|
11865
|
+
if (suffix === "/composer-draft") {
|
|
11866
|
+
if (verb === "GET") return "session.composer.read";
|
|
11867
|
+
if (verb === "PUT") return "session.composer.write";
|
|
11868
|
+
return null;
|
|
11869
|
+
}
|
|
11870
|
+
if (suffix === "/control" && verb === "POST") return "session.control";
|
|
11871
|
+
if (suffix === "/steer" && verb === "POST") return "session.steer";
|
|
11872
|
+
if (suffix === "/human-input-requests" && verb === "GET") {
|
|
11873
|
+
return "session.human_input.read";
|
|
11874
|
+
}
|
|
11875
|
+
if (suffix.startsWith("/human-input-requests/") && verb === "GET") {
|
|
11876
|
+
return "session.human_input.read";
|
|
11877
|
+
}
|
|
11878
|
+
if (suffix === "/stream-capabilities" && verb === "GET") return "session.viewer.read";
|
|
11879
|
+
if (suffix === "/stream-capabilities/acknowledge" && verb === "POST") {
|
|
11880
|
+
return "session.stream.acknowledge";
|
|
11881
|
+
}
|
|
11882
|
+
if (suffix === "/viewers" && verb === "POST") return "session.viewer.control";
|
|
11883
|
+
if (suffix.startsWith("/viewers/") && ["POST", "DELETE"].includes(verb)) {
|
|
11884
|
+
return "session.viewer.control";
|
|
11885
|
+
}
|
|
11886
|
+
if (suffix === "/fs/list" || suffix === "/fs/read") {
|
|
11887
|
+
return verb === "POST" ? "session.files.read" : null;
|
|
11888
|
+
}
|
|
11889
|
+
if (["/fs/write", "/fs/delete", "/fs/move", "/fs/mkdir"].includes(suffix)) {
|
|
11890
|
+
return verb === "POST" ? "session.files.write" : null;
|
|
11891
|
+
}
|
|
11892
|
+
if (suffix.startsWith("/git/") && verb === "POST") return "session.git.read";
|
|
11893
|
+
if ((suffix === "/workspace/capture" || suffix === "/workspace/capture/file") && verb === "GET") {
|
|
11894
|
+
return "session.capture.read";
|
|
11895
|
+
}
|
|
11896
|
+
if (suffix === "/terminal/exec" && verb === "POST") return "session.terminal.control";
|
|
11897
|
+
if (suffix === "/terminal/pty" && verb === "POST") return "session.terminal.control";
|
|
11898
|
+
if (suffix.startsWith("/terminal/pty/") && verb === "POST") {
|
|
11899
|
+
return "session.terminal.control";
|
|
11900
|
+
}
|
|
11901
|
+
return null;
|
|
11902
|
+
}
|
|
11903
|
+
function sessionAuthorizationHttpError(error) {
|
|
11904
|
+
if (error instanceof SessionAuthorizationDeniedError) {
|
|
11905
|
+
return new HTTPException21(404, { message: "session not found" });
|
|
11906
|
+
}
|
|
11907
|
+
if (error instanceof SessionAuthorizationUnavailableError) {
|
|
11908
|
+
return new HTTPException21(503, { message: "session authorization is unavailable" });
|
|
11909
|
+
}
|
|
11910
|
+
if (error instanceof HTTPException21) return error;
|
|
11911
|
+
throw error;
|
|
11912
|
+
}
|
|
11913
|
+
function eventEnumValue(raw, schema, name, fallback) {
|
|
11914
|
+
if (raw === void 0) return fallback;
|
|
11915
|
+
const parsed = schema.safeParse(raw);
|
|
11916
|
+
if (!parsed.success) {
|
|
11917
|
+
throw new HTTPException21(400, { message: `${name} is invalid` });
|
|
11918
|
+
}
|
|
11919
|
+
return parsed.data;
|
|
11920
|
+
}
|
|
11921
|
+
function eventEnumList(raw, schema, name) {
|
|
11922
|
+
if (raw === void 0 || raw.trim() === "") return [];
|
|
11923
|
+
const values = raw.split(",").map((value) => value.trim()).filter(Boolean);
|
|
11924
|
+
if (values.length > 100) {
|
|
11925
|
+
throw new HTTPException21(400, { message: `${name} accepts at most 100 values` });
|
|
11926
|
+
}
|
|
11927
|
+
return values.map((value) => {
|
|
11928
|
+
const parsed = schema.safeParse(value);
|
|
11929
|
+
if (!parsed.success) {
|
|
11930
|
+
throw new HTTPException21(400, { message: `${name} contains an invalid value` });
|
|
11931
|
+
}
|
|
11932
|
+
return parsed.data;
|
|
11933
|
+
});
|
|
11934
|
+
}
|
|
10273
11935
|
function sessionListQuery(query, allowCursor = true) {
|
|
10274
11936
|
const parentSessionId = query.parentSessionId;
|
|
10275
11937
|
if (parentSessionId !== void 0 && parentSessionId !== "null" && !z2.string().uuid().safeParse(parentSessionId).success) {
|
|
@@ -10284,7 +11946,9 @@ function sessionListQuery(query, allowCursor = true) {
|
|
|
10284
11946
|
}
|
|
10285
11947
|
const search = query.search?.trim();
|
|
10286
11948
|
if (search && search.length > 200) {
|
|
10287
|
-
throw new HTTPException21(400, {
|
|
11949
|
+
throw new HTTPException21(400, {
|
|
11950
|
+
message: "search must be at most 200 characters"
|
|
11951
|
+
});
|
|
10288
11952
|
}
|
|
10289
11953
|
return {
|
|
10290
11954
|
limit: query.limit,
|
|
@@ -10467,9 +12131,11 @@ import {
|
|
|
10467
12131
|
UpdateWorkspaceModelPolicyRequest,
|
|
10468
12132
|
UpdateWorkspaceRequest,
|
|
10469
12133
|
UpdateWorkspaceSettingsRequest,
|
|
12134
|
+
WORKSPACE_CONTROL_ACTOR_MAX_BYTES as WORKSPACE_CONTROL_ACTOR_MAX_BYTES2,
|
|
10470
12135
|
WorkspaceInferenceControlRequest,
|
|
10471
12136
|
Workspace,
|
|
10472
|
-
WorkspaceMember
|
|
12137
|
+
WorkspaceMember,
|
|
12138
|
+
workspaceControlUtf8Bytes as workspaceControlUtf8Bytes2
|
|
10473
12139
|
} from "@opengeni/contracts";
|
|
10474
12140
|
import {
|
|
10475
12141
|
allWorkspacePermissions,
|
|
@@ -10492,6 +12158,7 @@ import {
|
|
|
10492
12158
|
updateWorkspaceSettings,
|
|
10493
12159
|
upsertWorkspaceModelPolicy
|
|
10494
12160
|
} from "@opengeni/db";
|
|
12161
|
+
import { boundWorkspaceControlHttpPage } from "@opengeni/events";
|
|
10495
12162
|
import { HTTPException as HTTPException23 } from "hono/http-exception";
|
|
10496
12163
|
import { hasPermission as hasPermission3, requireAccessContext as requireAccessContext2, requireAccessGrant as requireAccessGrant16 } from "@opengeni/core";
|
|
10497
12164
|
import { requireLimit as requireLimit7 } from "@opengeni/core";
|
|
@@ -10601,12 +12268,18 @@ function registerWorkspaceRoutes(app, deps) {
|
|
|
10601
12268
|
app.post("/v1/workspaces/:workspaceId/inference-control", async (c) => {
|
|
10602
12269
|
const workspaceId = c.req.param("workspaceId");
|
|
10603
12270
|
const grant = await requireAccessGrant16(c, deps, workspaceId, "workspace:admin");
|
|
10604
|
-
|
|
12271
|
+
if (workspaceControlUtf8Bytes2(grant.subjectId) > WORKSPACE_CONTROL_ACTOR_MAX_BYTES2) {
|
|
12272
|
+
throw new HTTPException23(400, { message: "workspace-control actor is too large" });
|
|
12273
|
+
}
|
|
12274
|
+
const parsed = WorkspaceInferenceControlRequest.safeParse(await c.req.json().catch(() => null));
|
|
12275
|
+
if (!parsed.success) {
|
|
12276
|
+
throw new HTTPException23(400, { message: "invalid workspace inference-control request" });
|
|
12277
|
+
}
|
|
10605
12278
|
return c.json(
|
|
10606
12279
|
await controlHumanWorkspace(
|
|
10607
12280
|
{ db: deps.db, bus: deps.bus, workflowClient: deps.workflowClient },
|
|
10608
12281
|
{ accountId: grant.accountId, workspaceId, subjectId: grant.subjectId },
|
|
10609
|
-
|
|
12282
|
+
parsed.data
|
|
10610
12283
|
)
|
|
10611
12284
|
);
|
|
10612
12285
|
});
|
|
@@ -10614,20 +12287,30 @@ function registerWorkspaceRoutes(app, deps) {
|
|
|
10614
12287
|
const workspaceId = c.req.param("workspaceId");
|
|
10615
12288
|
await requireAccessGrant16(c, deps, workspaceId, "workspace:read");
|
|
10616
12289
|
const after = Math.max(0, Number.parseInt(c.req.query("after") ?? "0", 10) || 0);
|
|
10617
|
-
|
|
10618
|
-
|
|
10619
|
-
|
|
10620
|
-
|
|
10621
|
-
|
|
10622
|
-
|
|
10623
|
-
|
|
10624
|
-
)
|
|
12290
|
+
const limit = boundedLimit(c.req.query("limit"));
|
|
12291
|
+
const fetched = await listWorkspaceControlEvents2(deps.db, workspaceId, after, limit + 1);
|
|
12292
|
+
const countHasMore = fetched.length > limit;
|
|
12293
|
+
const page = boundWorkspaceControlHttpPage(fetched.slice(0, limit));
|
|
12294
|
+
const truncated = countHasMore || page.truncated;
|
|
12295
|
+
c.header("X-OpenGeni-Page-Bytes", String(page.bytes));
|
|
12296
|
+
c.header("X-OpenGeni-Page-Truncated", String(truncated));
|
|
12297
|
+
if (page.nextSequence !== null) {
|
|
12298
|
+
c.header("X-OpenGeni-Next-After", String(page.nextSequence));
|
|
12299
|
+
}
|
|
12300
|
+
return c.json(page.events);
|
|
10625
12301
|
});
|
|
10626
12302
|
app.get("/v1/workspaces/:workspaceId/control-events/stream", async (c) => {
|
|
10627
12303
|
const workspaceId = c.req.param("workspaceId");
|
|
10628
12304
|
await requireAccessGrant16(c, deps, workspaceId, "workspace:read");
|
|
10629
12305
|
const after = Math.max(0, Number.parseInt(c.req.query("after") ?? "0", 10) || 0);
|
|
10630
|
-
return await sseWorkspaceControlStream(
|
|
12306
|
+
return await sseWorkspaceControlStream(
|
|
12307
|
+
deps.db,
|
|
12308
|
+
deps.bus,
|
|
12309
|
+
workspaceId,
|
|
12310
|
+
after,
|
|
12311
|
+
c.req.raw.signal,
|
|
12312
|
+
{ observability: deps.observability }
|
|
12313
|
+
);
|
|
10631
12314
|
});
|
|
10632
12315
|
app.put("/v1/workspaces/:workspaceId/default-rig", async (c) => {
|
|
10633
12316
|
const workspaceId = c.req.param("workspaceId");
|
|
@@ -10743,6 +12426,7 @@ import {
|
|
|
10743
12426
|
validateFileResources,
|
|
10744
12427
|
validateGitHubRepositorySelection,
|
|
10745
12428
|
validateGitHubRepositorySelectionShape,
|
|
12429
|
+
validateGitHubRepositorySelectionShapes,
|
|
10746
12430
|
validateToolRefs,
|
|
10747
12431
|
withDefaultEnabledCapabilityMcpTools
|
|
10748
12432
|
} from "@opengeni/core";
|
|
@@ -10788,8 +12472,10 @@ function createApp(deps) {
|
|
|
10788
12472
|
};
|
|
10789
12473
|
const sandboxClient = deps.sandboxClient ?? createApiSandboxClient(deps.settings);
|
|
10790
12474
|
const resumeBoxById = deps.resumeBoxById ?? makeResumeBoxById(sandboxClient);
|
|
12475
|
+
const observability = deps.observability ?? createObservability(deps.settings, { component: "api" });
|
|
10791
12476
|
const routeDeps = {
|
|
10792
12477
|
...deps,
|
|
12478
|
+
observability,
|
|
10793
12479
|
githubStateSecret: deps.githubStateSecret ?? deps.settings.githubAppManifestStateSecret ?? crypto.randomUUID(),
|
|
10794
12480
|
managedAuth,
|
|
10795
12481
|
objectStorage,
|
|
@@ -10799,7 +12485,6 @@ function createApp(deps) {
|
|
|
10799
12485
|
resumeBoxById
|
|
10800
12486
|
};
|
|
10801
12487
|
const app = new Hono();
|
|
10802
|
-
const observability = deps.observability ?? createObservability(deps.settings, { component: "api" });
|
|
10803
12488
|
app.use(
|
|
10804
12489
|
"*",
|
|
10805
12490
|
cors({
|
|
@@ -10970,7 +12655,29 @@ function createApp(deps) {
|
|
|
10970
12655
|
app.all("/v1/workspaces/:workspaceId/mcp", async (c) => {
|
|
10971
12656
|
const workspaceId = c.req.param("workspaceId");
|
|
10972
12657
|
const grant = await requireMcpAccessGrant(c, routeDeps, workspaceId);
|
|
10973
|
-
const
|
|
12658
|
+
const toolspaceGrant = isToolspaceGrant(routeDeps.settings, grant);
|
|
12659
|
+
const boundSessionId = grant.metadata?.sessionId;
|
|
12660
|
+
if (toolspaceGrant || typeof boundSessionId === "string") {
|
|
12661
|
+
if (typeof boundSessionId !== "string") {
|
|
12662
|
+
throw new HTTPException24(404, { message: "session not found" });
|
|
12663
|
+
}
|
|
12664
|
+
try {
|
|
12665
|
+
await requireSessionAuthorization3(routeDeps, grant, {
|
|
12666
|
+
sessionId: boundSessionId,
|
|
12667
|
+
operation: toolspaceGrant ? "session.toolspace.call" : "session.first_party_mcp.call",
|
|
12668
|
+
surface: toolspaceGrant ? "toolspace" : "first_party_mcp"
|
|
12669
|
+
});
|
|
12670
|
+
} catch (error) {
|
|
12671
|
+
if (error instanceof SessionAuthorizationDeniedError2) {
|
|
12672
|
+
throw new HTTPException24(404, { message: "session not found" });
|
|
12673
|
+
}
|
|
12674
|
+
if (error instanceof SessionAuthorizationUnavailableError2) {
|
|
12675
|
+
throw new HTTPException24(503, { message: "session authorization is unavailable" });
|
|
12676
|
+
}
|
|
12677
|
+
throw error;
|
|
12678
|
+
}
|
|
12679
|
+
}
|
|
12680
|
+
const toolspace = toolspaceGrant ? await prepareToolspaceMcpSurface({ deps: routeDeps, grant }) : null;
|
|
10974
12681
|
const workspace = await getWorkspace2(routeDeps.db, workspaceId);
|
|
10975
12682
|
const workspaceMemoryEnabled = resolveWorkspaceMemoryEnabled(workspace?.settings);
|
|
10976
12683
|
const transport = new WebStandardStreamableHTTPServerTransport2({
|
|
@@ -11319,6 +13026,18 @@ var routeLabelPatterns = [
|
|
|
11319
13026
|
pattern: /^\/v1\/workspaces\/[^/]+\/github\/repositories\/sync$/,
|
|
11320
13027
|
label: "/v1/workspaces/:workspaceId/github/repositories/sync"
|
|
11321
13028
|
},
|
|
13029
|
+
{
|
|
13030
|
+
pattern: /^\/v1\/workspaces\/[^/]+\/github\/connect$/,
|
|
13031
|
+
label: "/v1/workspaces/:workspaceId/github/connect"
|
|
13032
|
+
},
|
|
13033
|
+
{
|
|
13034
|
+
pattern: /^\/v1\/workspaces\/[^/]+\/github\/installations$/,
|
|
13035
|
+
label: "/v1/workspaces/:workspaceId/github/installations"
|
|
13036
|
+
},
|
|
13037
|
+
{
|
|
13038
|
+
pattern: /^\/v1\/workspaces\/[^/]+\/github\/installations\/[^/]+$/,
|
|
13039
|
+
label: "/v1/workspaces/:workspaceId/github/installations/:installationId"
|
|
13040
|
+
},
|
|
11322
13041
|
{
|
|
11323
13042
|
pattern: /^\/v1\/workspaces\/[^/]+\/github\/app-manifest$/,
|
|
11324
13043
|
label: "/v1/workspaces/:workspaceId/github/app-manifest"
|
|
@@ -11478,8 +13197,9 @@ export {
|
|
|
11478
13197
|
validateFileResources,
|
|
11479
13198
|
validateGitHubRepositorySelection,
|
|
11480
13199
|
validateGitHubRepositorySelectionShape,
|
|
13200
|
+
validateGitHubRepositorySelectionShapes,
|
|
11481
13201
|
validateToolRefs,
|
|
11482
13202
|
withDefaultEnabledCapabilityMcpTools,
|
|
11483
13203
|
workflowIdForSession2 as workflowIdForSession
|
|
11484
13204
|
};
|
|
11485
|
-
//# sourceMappingURL=chunk-
|
|
13205
|
+
//# sourceMappingURL=chunk-EYYTFA7N.js.map
|