@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.
@@ -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 { hasPermission as hasPermission4, requireAccessGrant as requireAccessGrant17, requirePermission } from "@opengeni/core";
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
- listGitHubInstallationIdsForWorkspace,
314
+ areGitHubRepositoriesAllowedForWorkspace,
301
315
  listScheduledTaskRuns,
302
316
  listScheduledTasks,
303
- listSessionEvents,
304
- listSessions,
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 { hasPermission } from "@opengeni/core";
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
- var CHARS_PER_TOKEN = 4;
380
- function estimateTokensFromChars(chars) {
381
- return Math.ceil(chars / CHARS_PER_TOKEN);
382
- }
383
- function estimateValueTokens(value) {
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 truncated \u2014 page with after/limit on session_events, or read the session notebook for the full content]`;
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 `${head}${truncationMarker(dropped)}${tail}`;
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
- return clampString(value, perFieldChars);
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
- if (safeStringify(mapped).length <= perFieldChars * 2) {
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
- if (safeStringify(out).length <= perFieldChars * 4) {
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
- if (cappedPayload === event.payload) {
450
- return event;
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 { ...event, payload: cappedPayload };
544
+ return page;
453
545
  }
454
- function buildTruncationEvent(template, droppedCount, firstDroppedSequence, lastDroppedSequence, markerSequence) {
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
- id: "00000000-0000-0000-0000-000000000000",
457
- workspaceId: template.workspaceId,
458
- sessionId: template.sessionId,
459
- sequence: markerSequence,
460
- type: "session.status.changed",
461
- payload: {
462
- _truncated: true,
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 capEventPage(events, config = DEFAULT_EVENT_CAP) {
473
- const realLast = events[events.length - 1];
474
- const nextAfter = realLast ? realLast.sequence : null;
475
- const trimmed = events.map((event) => capEventPayload(event, config.perFieldChars));
476
- let runningTokens = 0;
477
- let overBudget = false;
478
- for (const event of trimmed) {
479
- runningTokens += estimateValueTokens(event);
480
- if (runningTokens > config.pageTokenBudget) {
481
- overBudget = true;
482
- break;
483
- }
484
- }
485
- const keepCount = config.headEvents + config.tailEvents;
486
- if (!overBudget || trimmed.length <= keepCount + 1) {
487
- return { events: trimmed, nextAfter, truncated: overBudget && trimmed.length > keepCount + 1 };
488
- }
489
- const head = trimmed.slice(0, config.headEvents);
490
- const tail = trimmed.slice(trimmed.length - config.tailEvents);
491
- const droppedStart = config.headEvents;
492
- const droppedEnd = trimmed.length - config.tailEvents - 1;
493
- const droppedCount = droppedEnd - droppedStart + 1;
494
- const firstDroppedSequence = trimmed[droppedStart].sequence;
495
- const lastDroppedSequence = trimmed[droppedEnd].sequence;
496
- const markerSequence = head[head.length - 1].sequence;
497
- const marker = buildTruncationEvent(
498
- realLast,
499
- droppedCount,
500
- firstDroppedSequence,
501
- lastDroppedSequence,
502
- markerSequence
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
- events: [...head, marker, ...tail],
506
- nextAfter,
507
- truncated: true
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 capSessionDetail(session, perFieldChars = DEFAULT_SESSION_DETAIL_CHARS) {
511
- let changed = false;
512
- const out = { ...session };
513
- if (session.metadata !== void 0) {
514
- const capped = capPayloadValue(session.metadata, perFieldChars);
515
- if (capped !== session.metadata) {
516
- out.metadata = capped;
517
- changed = true;
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 (typeof session.initialMessage === "string" && session.initialMessage.length > perFieldChars) {
521
- out.initialMessage = clampString(
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 changed ? out : session;
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
- const result = await updateSessionTitle(deps, grant.workspaceId, sessionId, title, "agent");
551
- return json({ ok: true, updated: result.updated, title: result.title ?? title });
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, grant, options, json);
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({ key: file.objectKey });
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 installationIds = await listGitHubInstallationIdsForWorkspace(
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({ tasks: await listScheduledTasks(deps.db, grant.workspaceId, limit ?? 100) })
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({ db: deps.db, workflowClient: deps.workflowClient, task });
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: { id: z4.string().uuid(), triggerId: z4.string().min(1).max(128).optional() }
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: { taskId: z4.string().uuid(), limit: z4.number().int().positive().optional() }
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 = { db: deps.db, settings: deps.settings, bus: deps.bus };
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({ kind: z4.literal("write"), path: z4.string().min(1), content: z4.string() })
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(), { kind, ...name ? { name } : {} })
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 a rig, its versions, and recent changes.",
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
- return json({
1338
- rig,
1339
- versions: await listRigVersionsForApi({ db: deps.db }, grant.workspaceId, rig.id),
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
- boundedMcpLimit(changeLimit)
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: "List sessions in this workspace, newest first.",
1459
- inputSchema: { limit: z4.number().int().positive().optional() }
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 }) => json({ sessions: await listSessions(deps.db, grant.workspaceId, boundedMcpLimit(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
- return json({
1476
- ...capSessionDetail(session),
1477
- effectiveControl: queue?.effectiveControl ?? null
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 session's event timeline (oldest first), to monitor another session's progress. Pass `after` = the highest event `sequence` already seen to page forward; the response's `nextAfter` is that cursor. The response is BYTE-CAPPED for a monitoring glance: fat per-event payloads (a worker's verbatim tool outputs, message/reasoning bodies) are clamped, and an over-budget page is reduced to its head + tail with a marker \u2014 page the gap with `after`/`limit`, or read the worker's session notebook, if you need omitted content verbatim. `nextAfter` always advances past every event the page covered, so paging never skips real events.",
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
- limit: z4.number().int().positive().optional()
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 ({ sessionId, after, limit }) => {
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
- const events = await listSessionEvents(
1494
- deps.db,
1495
- grant.workspaceId,
1496
- sessionId,
1497
- after ?? 0,
1498
- boundedMcpLimit(limit)
1499
- );
1500
- const capped = capEventPage(events);
1501
- return json({
1502
- events: capped.events,
1503
- nextAfter: capped.nextAfter ?? after ?? 0,
1504
- ...capped.truncated ? { truncated: true } : {}
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
- firstPartyMcpPermissions: z4.array(z4.string()).optional(),
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) => json(await createSessionForRequest(deps, grant, grant.workspaceId, 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 controlled = await controlAgentSessionWorkstream(
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: controlled.receipt.id,
1665
- effectiveControl: controlled.control,
1666
- interruptionCount: controlled.interruptionCount,
1667
- replay: controlled.replay
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
- return json(
1671
- await controlHumanSessionWorkstream(
1672
- deps,
1673
- {
1674
- accountId: grant.accountId,
1675
- workspaceId: grant.workspaceId,
1676
- sessionId,
1677
- subjectId: grant.subjectId
1678
- },
1679
- {
1680
- action: "pause",
1681
- clientEventId: idempotencyKey,
1682
- ...reason ? { reason } : {}
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 controlled = await controlAgentSessionWorkstream(
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: controlled.receipt.id,
1712
- effectiveControl: controlled.control,
1713
- interruptionCount: controlled.interruptionCount,
1714
- replay: controlled.replay
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
- return json(
1718
- await controlHumanSessionWorkstream(
1719
- deps,
1720
- {
1721
- accountId: grant.accountId,
1722
- workspaceId: grant.workspaceId,
1723
- sessionId,
1724
- subjectId: grant.subjectId
1725
- },
1726
- {
1727
- action: "resume",
1728
- clientEventId: idempotencyKey,
1729
- ...reason ? { reason } : {}
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
- deps,
1775
- grant.workspaceId,
1776
- session_id,
1777
- title,
1778
- "agent"
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 = { id: variableSet.id, name: variableSet.name, created };
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, grant, options, json) {
2508
+ function registerGitHubConnectTool(server, deps, json) {
1910
2509
  server.registerTool(
1911
2510
  "github_connect_link",
1912
2511
  {
1913
- description: "Create a workspace-bound GitHub App install link to share with a human. Opening it redirects to GitHub to install the app and select repositories for this workspace; completing the connection requires the person to be signed in to this OpenGeni deployment with github:manage. The link expires.",
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({ configured: false, appSlug: slug, installUrl: null, missing });
1922
- }
1923
- const base = (settings.publicBaseUrl ?? settings.githubAppManifestBaseUrl ?? options.requestOrigin ?? "").replace(/\/+$/, "");
1924
- if (!base) {
1925
- throw new Error(
1926
- "github_connect_link requires OPENGENI_PUBLIC_BASE_URL (or OPENGENI_GITHUB_APP_MANIFEST_BASE_URL) so the install link can route through this deployment"
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: `${base}/v1/workspaces/${grant.workspaceId}/github/connect?state=${encodeURIComponent(state)}`,
1937
- expiresInSeconds: stateMaxAgeSeconds,
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: selected.map((item) => item.repositoryId)
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: repositoryMountPath(uri),
1992
- ...repository.private ? { githubInstallationId: repository.installationId, githubRepositoryId: repository.id } : {}
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.hostname.toLowerCase()}/${path}.git`;
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 { prefixedMcpToolName } from "@opengeni/contracts";
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
- const cacheKey = await toolListCacheKey(deps, grant.workspaceId, sessionId, proxyableIds);
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({ deps, grant, config, sessionId }).catch(
2121
- () => null
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
- return `${workspaceId}:${sessionId}:${signature}`;
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
- throw new Error("session MCP server credentials require OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY");
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(deps.db, workspaceId, sessionId, encryptionKey);
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.turnId;
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({ deps, grant, config, sessionId }).catch(
2257
- () => null
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
- return reservation.reserved ? { status: "ok", turnId: session.activeTurnId } : { status: "budget_exhausted" };
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 = buildConnectionTokenResolver(input.deps.db, input.deps.settings);
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 ? { toolId: 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 ? { toolId: 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 as createSignedState2, readSignedState } from "@opengeni/github";
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 = createSignedState2(githubStateSecret, {
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
- rotationStrategy: rotation?.rotationStrategy ?? "most_remaining",
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 (typeof body.rotationStrategy === "string") {
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
- rotationStrategy: updated.rotationStrategy,
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 createSignedState3, readSignedState as readSignedState2 } from "@opengeni/github";
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 = createSignedState3(requireIntegrationsStateSecret(settings), {
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 (dossier §V5/§V6).
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 { listGitHubInstallationIdsForWorkspace as listGitHubInstallationIdsForWorkspace2, upsertGitHubInstallation } from "@opengeni/db";
7970
+ import { deleteGitHubInstallationBinding } from "@opengeni/db";
6999
7971
  import {
7000
7972
  buildGitHubAppManifest,
7001
7973
  convertGitHubAppManifest,
7002
- createSignedState as createSignedState4,
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 as stateMaxAgeSeconds2,
7013
- verifyGitHubInstallationAccessForUser,
7982
+ stateMaxAgeSeconds,
7014
7983
  verifySignedState
7015
7984
  } from "@opengeni/github";
7016
- import { deleteCookie, getCookie, setCookie } from "hono/cookie";
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
- installUrl: slug ? `https://github.com/apps/${slug}/installations/new?state=${encodeURIComponent(state)}` : null,
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
- const slug = settings.githubAppSlug?.trim();
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 = createSignedState4(githubStateSecret, {
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, installUrl));
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
- requireGitHubStateCookie(c, state);
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
- const installationId = parsePositiveInteger(String(statePayload?.installationId ?? ""));
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
- requireGitHubStateCookie(c, state);
7220
- return await completeGitHubInstallationBinding(deps, c, {
7221
- code,
7222
- statePayload,
7223
- installationId
7224
- });
8141
+ throw installationBindingDisabled();
7225
8142
  });
7226
- }
7227
- async function completeGitHubInstallationBinding(deps, c, input) {
7228
- const { db, settings } = deps;
7229
- if (!input.statePayload.workspaceId || !input.statePayload.accountId) {
7230
- throw new HTTPException15(400, { message: "invalid or expired GitHub installation state" });
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
- if (error instanceof GitHubAppConfigurationError2) {
7271
- throw new HTTPException15(409, {
7272
- message: JSON.stringify({ message: error.message, missing: error.missing })
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 new HTTPException15(502, {
7276
- message: error instanceof Error ? error.message : String(error)
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/github",
7286
- maxAge: stateMaxAgeSeconds2
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
- async function listWorkspaceGitHubRepositories(deps, workspaceId) {
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
- const install = installUrl ? `<a class="button secondary" href="${escapeHtml2(installUrl)}">Install on repositories</a>` : "";
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 as listRigChangesForApi2,
7607
- listRigVersionsForApi as listRigVersionsForApi2,
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 listRigVersionsForApi2({ db }, workspaceId, rig.id));
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 listRigChangesForApi2({ db }, workspaceId, rig.id, boundedLimit(c.req.query("limit")))
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
- listSessionEvents as listSessionEvents3,
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
- ChannelAValidationError
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: routedSession,
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 { requireAccessGrant as requireAccessGrant14 } from "@opengeni/core";
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 as listSessionEvents2, listWorkspaceControlEvents } from "@opengeni/db";
8776
- import { formatSse } from "@opengeni/events";
8777
- async function sseSessionStream(db, bus, workspaceId, sessionId, after, signal) {
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 replaying = true;
8782
- const buffered = [];
9841
+ let bootstrapping = true;
9842
+ let newestBuffered = null;
8783
9843
  let unsubscribe = null;
8784
- const stream = new ReadableStream({
8785
- start: async (rawController) => {
8786
- controller = rawController;
8787
- const send = async (event) => {
8788
- if (event.sequence <= lastSent) {
8789
- return;
8790
- }
8791
- if (event.sequence > lastSent + 1) {
8792
- const missing = await listSessionEvents2(
8793
- db,
8794
- workspaceId,
8795
- sessionId,
8796
- lastSent,
8797
- event.sequence - lastSent - 1
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
- for (const missed of missing) {
8800
- if (missed.sequence > lastSent) {
8801
- controller.enqueue(encoder.encode(formatSse(missed)));
8802
- lastSent = missed.sequence;
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
- controller.enqueue(encoder.encode(formatSse(event)));
8807
- lastSent = event.sequence;
8808
- };
8809
- unsubscribe = await bus.subscribe(workspaceId, sessionId, async (events) => {
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
- for (const event of events.sort((a, b) => a.sequence - b.sequence)) {
8815
- await send(event);
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
- await replaySessionEvents(
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
- buffered.length = 0;
8828
- controller.enqueue(encoder.encode(": connected\n\n"));
8829
- },
8830
- cancel: () => {
8831
- unsubscribe?.();
9925
+ });
9926
+ if (channel.stopped()) {
9927
+ release();
9928
+ return;
8832
9929
  }
8833
- });
8834
- signal.addEventListener(
8835
- "abort",
8836
- () => {
8837
- unsubscribe?.();
8838
- },
8839
- { once: true }
8840
- );
8841
- return new Response(stream, {
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 = 1e3) {
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 = Math.max(cursor, event.sequence);
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 replaying = true;
8869
- const buffered = [];
9986
+ let bootstrapping = true;
9987
+ let newestBuffered = null;
8870
9988
  let unsubscribe = null;
8871
- const stream = new ReadableStream({
8872
- start: async (controller) => {
8873
- const send = (event) => {
8874
- if (event.sequence <= lastSent) return;
8875
- controller.enqueue(encoder.encode(formatSse(event)));
8876
- lastSent = event.sequence;
8877
- };
8878
- unsubscribe = await bus.subscribeWorkspaceControl(workspaceId, async (event) => {
8879
- if (replaying) {
8880
- buffered.push(event);
8881
- } else {
8882
- send(event);
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
- let cursor = after;
8886
- while (true) {
8887
- const page = await listWorkspaceControlEvents(db, workspaceId, cursor, 1e3);
8888
- for (const event of page) {
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
- replaying = false;
8895
- for (const event of buffered.sort((left, right) => left.sequence - right.sequence)) {
8896
- send(event);
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
- buffered.length = 0;
8899
- controller.enqueue(encoder.encode(": connected\n\n"));
8900
- },
8901
- cancel: () => unsubscribe?.()
8902
- });
8903
- signal.addEventListener("abort", () => unsubscribe?.(), { once: true });
8904
- return new Response(stream, {
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
- return { manifest: parsed.data, byteLength: blob.bytes.byteLength };
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 stats = WorkspaceCaptureStats.safeParse(row.stats);
8970
- if (!stats.success) {
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.data
10212
+ stats: loaded.stats
8984
10213
  };
8985
- const blob = await storage.getObjectBytes(row.manifestKey);
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.data,
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(db, workspaceId, sessionId, grant.subjectId);
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
- { message: "session pin changed in another client", current: error.current },
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(db, workspaceId, c.req.param("sessionId")));
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, { message: 'target is required ("auto" or an account id)' });
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, { message: "session or codex account not found" });
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({ db, bus }, workspaceId, sessionId, payload.title, "user");
9218
- const session = await getSessionForSubject(db, workspaceId, sessionId, grant.subjectId);
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({ status: "pending", message: "Compaction will run at the next safe boundary." });
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 after = eventSequence(c.req.query("after"), 0);
9384
- const before = optionalEventSequence(c.req.query("before"));
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 limit = eventListLimit(c.req.query("limit"), compact ? 5e3 : 2e3);
9387
- const events = await listSessionEvents3(db, workspaceId, sessionId, {
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
- return c.json(compact ? coalesceSessionEventDeltas(events) : events);
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
- return c.json(
9434
- await moveHumanQueuePrompt(
9435
- { db, bus },
9436
- { accountId: grant.accountId, workspaceId, sessionId, subjectId: grant.subjectId },
9437
- c.req.param("turnId"),
9438
- payload
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
- return c.json(
9453
- await editHumanQueuePrompt(
9454
- { db, bus },
9455
- { accountId: grant.accountId, workspaceId, sessionId, subjectId: grant.subjectId },
9456
- c.req.param("turnId"),
9457
- payload
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
- return c.json(
9472
- await steerHumanQueuePrompt(
9473
- { db, bus },
9474
- { accountId: grant.accountId, workspaceId, sessionId, subjectId: grant.subjectId },
9475
- c.req.param("turnId"),
9476
- payload
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
- return c.json(
9491
- await deleteHumanQueuePrompt(
9492
- { db, bus },
9493
- { accountId: grant.accountId, workspaceId, sessionId, subjectId: grant.subjectId },
9494
- c.req.param("turnId"),
9495
- payload
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(db, {
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
- db,
9524
- { accountId: grant.accountId, workspaceId, sessionId, subjectId: grant.subjectId },
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 payload = SessionControlRequest.parse(await c.req.json());
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
- return c.json(
9539
- await controlHumanSessionWorkstream2(
9540
- { db, bus, workflowClient },
9541
- { accountId: grant.accountId, workspaceId, sessionId, subjectId: grant.subjectId },
9542
- payload
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 (I8/OD-8): if desktop is enabled but no stream-token
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, { message: "invalid stream acknowledgment request" });
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, { message: "invalid viewer attach request" });
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, { message: "stream_acknowledgment_required" });
11330
+ throw new HTTPException21(409, {
11331
+ message: "stream_acknowledgment_required"
11332
+ });
9794
11333
  }
9795
11334
  if (shared && !ack.acknowledgedShared) {
9796
- throw new HTTPException21(409, { message: "shared_acknowledgment_required" });
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, { message: "viewer heartbeat requires { leaseEpoch }" });
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({ liveness: result?.liveness ?? null, refcount: result?.refcount ?? null });
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 { accountId: grant.accountId, workspaceId, session, subjectId: grant.subjectId };
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, { message: "path query parameter is required" });
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, { message: "revision must be a non-negative integer" });
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, { message: "interactive terminal unsupported on this backend" });
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 = { ptyId: req.ptyId, exitCode: 0, reason: "exit" };
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 ?? 500);
11827
+ function eventListLimit(raw, max = 2e3, fallback = 500) {
11828
+ const limit = Number(raw ?? fallback);
10268
11829
  if (!Number.isFinite(limit)) {
10269
- return 500;
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, { message: "search must be at most 200 characters" });
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
- const payload = WorkspaceInferenceControlRequest.parse(await c.req.json());
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
- payload
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
- return c.json(
10618
- await listWorkspaceControlEvents2(
10619
- deps.db,
10620
- workspaceId,
10621
- after,
10622
- boundedLimit(c.req.query("limit"))
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(deps.db, deps.bus, workspaceId, after, c.req.raw.signal);
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 toolspace = isToolspaceGrant(routeDeps.settings, grant) ? await prepareToolspaceMcpSurface({ deps: routeDeps, grant }) : null;
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-HBEJMWD3.js.map
13205
+ //# sourceMappingURL=chunk-EYYTFA7N.js.map