@opengeni/api-router 2.6.4 → 2.10.0-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/access-grant-rls.d.ts +3 -0
  2. package/dist/app.d.ts +2 -1
  3. package/dist/app.js +3 -1
  4. package/dist/auth/organization-user-setup.d.ts +23 -0
  5. package/dist/{chunk-XTLI3CBH.js → chunk-XXH7DW34.js} +6866 -3554
  6. package/dist/chunk-XXH7DW34.js.map +1 -0
  7. package/dist/codemode.d.ts +6 -0
  8. package/dist/github-access.d.ts +25 -2
  9. package/dist/http/request-source.d.ts +14 -0
  10. package/dist/index.js +19 -3
  11. package/dist/index.js.map +1 -1
  12. package/dist/integrations/oauth-client.d.ts +2 -11
  13. package/dist/integrations/personal-github-repositories.d.ts +41 -2
  14. package/dist/integrations/slack-interactions.d.ts +1 -1
  15. package/dist/mcp/server.d.ts +158 -1
  16. package/dist/mcp/session-view.d.ts +56 -2
  17. package/dist/mcp/session-wait.d.ts +1 -1
  18. package/dist/mcp-oauth.d.ts +19 -0
  19. package/dist/model-catalog.d.ts +5 -31
  20. package/dist/routes/codex.d.ts +27 -2
  21. package/dist/routes/github.d.ts +2 -0
  22. package/dist/routes/organization-model-providers.d.ts +3 -0
  23. package/dist/routes/workspace-artifacts.d.ts +2 -1
  24. package/dist/work-discovery-observability.d.ts +1 -1
  25. package/dist/workspace-artifact-content.d.ts +28 -0
  26. package/dist/workspace-artifact-provenance.d.ts +7 -0
  27. package/dist/workspace-deletion.d.ts +6 -0
  28. package/dist/workspace-tool-gateway-observability.d.ts +17 -0
  29. package/dist/workspace-tool-gateway.d.ts +118 -0
  30. package/package.json +19 -18
  31. package/src/access-grant-rls.ts +40 -0
  32. package/src/app.ts +296 -53
  33. package/src/auth/organization-user-setup.ts +95 -5
  34. package/src/codemode.ts +33 -4
  35. package/src/github-access.ts +117 -1
  36. package/src/http/auth.ts +11 -0
  37. package/src/http/request-source.ts +37 -0
  38. package/src/http/sse.ts +15 -7
  39. package/src/index.ts +18 -2
  40. package/src/integrations/oauth-client.ts +168 -203
  41. package/src/integrations/personal-github-repositories.ts +238 -9
  42. package/src/integrations/slack-app-home.ts +2 -2
  43. package/src/integrations/slack-interactions.ts +77 -37
  44. package/src/mcp/company-brain-governed-writes.ts +2 -2
  45. package/src/mcp/company-profile-agent-admin.ts +1 -1
  46. package/src/mcp/remember.ts +1 -1
  47. package/src/mcp/server.ts +504 -133
  48. package/src/mcp/session-view.ts +20 -1
  49. package/src/mcp/session-wait.ts +3 -0
  50. package/src/mcp-oauth.ts +539 -0
  51. package/src/model-catalog.ts +45 -337
  52. package/src/routes/automations.ts +178 -19
  53. package/src/routes/codex.ts +242 -73
  54. package/src/routes/connections.ts +271 -16
  55. package/src/routes/documents.ts +67 -19
  56. package/src/routes/files.ts +54 -6
  57. package/src/routes/github.ts +116 -15
  58. package/src/routes/organization-memberships.ts +59 -16
  59. package/src/routes/organization-model-providers.ts +231 -0
  60. package/src/routes/packs.ts +1 -0
  61. package/src/routes/personal-github.ts +39 -0
  62. package/src/routes/pr-review.ts +72 -4
  63. package/src/routes/scheduled-tasks.ts +40 -13
  64. package/src/routes/sessions.ts +153 -65
  65. package/src/routes/supergrok.ts +3 -2
  66. package/src/routes/workspace-artifacts.ts +176 -67
  67. package/src/routes/workspaces.ts +405 -62
  68. package/src/sandbox/channel-a.ts +17 -4
  69. package/src/work-discovery-observability.ts +3 -3
  70. package/src/workspace-artifact-content.ts +163 -0
  71. package/src/workspace-artifact-provenance.ts +104 -0
  72. package/src/workspace-deletion.ts +64 -0
  73. package/src/workspace-tool-gateway-observability.ts +100 -0
  74. package/src/workspace-tool-gateway.ts +691 -0
  75. package/dist/chunk-XTLI3CBH.js.map +0 -1
@@ -13,12 +13,15 @@ import {
13
13
  canonicalConfiguredModel,
14
14
  defaultPrReviewProviderBaseUrl,
15
15
  getCapabilityPack,
16
+ workspaceCustomModelReference,
17
+ lockActiveCustomModelForAdmission,
16
18
  prReviewWebhookAuthKind,
17
19
  normalizePrReviewProviderBaseUrl,
18
20
  prReviewPackConnectorId,
19
21
  PR_REVIEW_AUTOMATION_TEMPLATE_ID,
20
22
  requireAccessGrant,
21
23
  requirePermission,
24
+ resolveWorkspaceCatalogSettings,
22
25
  type ApiRouteDeps,
23
26
  } from "@opengeni/core";
24
27
  import {
@@ -385,10 +388,24 @@ export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
385
388
  throw error;
386
389
  }
387
390
  }
391
+ const catalogSettings = (
392
+ await resolveWorkspaceCatalogSettings(db, deps.settings, {
393
+ accountId: grant.accountId,
394
+ workspaceId,
395
+ })
396
+ ).settings;
388
397
  const model = payload.model
389
- ? (canonicalConfiguredModel(settings, payload.model) ?? null)
398
+ ? (canonicalConfiguredModel(catalogSettings, payload.model) ?? null)
390
399
  : null;
391
- if (model) await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, model);
400
+ if (model) await assertWorkspaceModelPolicyAllows(db, catalogSettings, workspaceId, model);
401
+ const beforeCreateCommit = model
402
+ ? prReviewCustomModelCommitGuard({
403
+ settings: catalogSettings,
404
+ accountId: grant.accountId,
405
+ workspaceId,
406
+ modelId: model,
407
+ })
408
+ : undefined;
392
409
  const template = getCapabilityPack(OPENGENI_PR_REVIEW_PACK_ID)?.automationTemplates?.find(
393
410
  (candidate) => candidate.id === PR_REVIEW_AUTOMATION_TEMPLATE_ID,
394
411
  );
@@ -417,6 +434,7 @@ export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
417
434
  eventTypes: template.eventTypes,
418
435
  configuration: template.configuration,
419
436
  sessionTemplate: template.sessionTemplate,
437
+ ...(beforeCreateCommit ? { beforeCreateCommit } : {}),
420
438
  }),
421
439
  "This repository is already bound to the selected PR Review registration",
422
440
  );
@@ -441,13 +459,40 @@ export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
441
459
  const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
442
460
  await requirePrReviewPackActive(db, workspaceId);
443
461
  const payload = UpdatePrReviewRepositoryBindingRequest.parse(await c.req.json());
462
+ const catalogSettings = (
463
+ await resolveWorkspaceCatalogSettings(db, deps.settings, {
464
+ accountId: grant.accountId,
465
+ workspaceId,
466
+ })
467
+ ).settings;
444
468
  const model =
445
469
  payload.model === undefined
446
470
  ? undefined
447
471
  : payload.model === null
448
472
  ? null
449
- : (canonicalConfiguredModel(settings, payload.model) ?? null);
450
- if (model) await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, model);
473
+ : (canonicalConfiguredModel(catalogSettings, payload.model) ?? null);
474
+ if (model) await assertWorkspaceModelPolicyAllows(db, catalogSettings, workspaceId, model);
475
+ const beforeUpdateCommit = async (
476
+ tx: ApiRouteDeps["db"],
477
+ context: {
478
+ currentModel: string | null;
479
+ currentStatus: "active" | "disabled";
480
+ nextModel: string | null;
481
+ nextStatus: "active" | "disabled";
482
+ },
483
+ ): Promise<void> => {
484
+ const materialExecutionChange =
485
+ payload.model !== undefined ||
486
+ payload.additionalInstructions !== undefined ||
487
+ (context.currentStatus === "disabled" && context.nextStatus === "active");
488
+ if (!materialExecutionChange || !context.nextModel) return;
489
+ await prReviewCustomModelCommitGuard({
490
+ settings: catalogSettings,
491
+ accountId: grant.accountId,
492
+ workspaceId,
493
+ modelId: context.nextModel,
494
+ })?.(tx);
495
+ };
451
496
  const binding = await updatePrReviewRepositoryBinding(db, {
452
497
  accountId: grant.accountId,
453
498
  workspaceId,
@@ -458,6 +503,7 @@ export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
458
503
  ? { additionalInstructions: payload.additionalInstructions }
459
504
  : {}),
460
505
  ...(payload.status !== undefined ? { status: payload.status } : {}),
506
+ beforeUpdateCommit,
461
507
  });
462
508
  if (!binding)
463
509
  throw new HTTPException(404, {
@@ -501,6 +547,28 @@ export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
501
547
  });
502
548
  }
503
549
 
550
+ function prReviewCustomModelCommitGuard(input: {
551
+ settings: ApiRouteDeps["settings"];
552
+ accountId: string;
553
+ workspaceId: string;
554
+ modelId: string;
555
+ }): ((tx: ApiRouteDeps["db"]) => Promise<void>) | undefined {
556
+ const reference = workspaceCustomModelReference(input.settings, input.modelId);
557
+ if (!reference) return undefined;
558
+ return async (tx): Promise<void> => {
559
+ const active = await lockActiveCustomModelForAdmission(tx, {
560
+ accountId: input.accountId,
561
+ workspaceId: input.workspaceId,
562
+ reference,
563
+ });
564
+ if (!active) {
565
+ throw new HTTPException(422, {
566
+ message: `model is not available: ${input.modelId}`,
567
+ });
568
+ }
569
+ };
570
+ }
571
+
504
572
  function assertPrReviewSandboxBackend(deps: ApiRouteDeps): void {
505
573
  if (deps.settings.sandboxBackend === "selfhosted") {
506
574
  throw new HTTPException(409, {
@@ -6,7 +6,7 @@ import {
6
6
  import { listScheduledTaskRuns, listScheduledTasks } from "@opengeni/db";
7
7
  import type { Hono } from "hono";
8
8
  import { HTTPException } from "hono/http-exception";
9
- import { requireAccessGrant } from "@opengeni/core";
9
+ import { requireAccessGrant, resolveWorkspaceCatalogSettings } from "@opengeni/core";
10
10
  import { recordWorkspaceUsage, requireLimit } from "@opengeni/core";
11
11
  import type { ApiRouteDeps } from "@opengeni/core";
12
12
  import {
@@ -30,7 +30,7 @@ import { boundedLimit } from "../http/common";
30
30
  import { deleteScheduledTaskWithDurableCleanup } from "../scheduled-task-deletion";
31
31
 
32
32
  export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void {
33
- const { settings, db, workflowClient, objectStorage } = deps;
33
+ const { db, workflowClient, objectStorage } = deps;
34
34
 
35
35
  app.post("/v1/workspaces/:workspaceId/scheduled-tasks", async (c) => {
36
36
  const workspaceId = c.req.param("workspaceId");
@@ -43,6 +43,12 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
43
43
  });
44
44
  }
45
45
  const payload = parsedPayload.data;
46
+ const catalogSettings = (
47
+ await resolveWorkspaceCatalogSettings(db, deps.settings, {
48
+ accountId: grant.accountId,
49
+ workspaceId,
50
+ })
51
+ ).settings;
46
52
  await requireLimit(deps, {
47
53
  accountId: grant.accountId,
48
54
  workspaceId,
@@ -50,7 +56,7 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
50
56
  quantity: 1,
51
57
  });
52
58
  const task = await createValidatedScheduledTask({
53
- settings,
59
+ settings: catalogSettings,
54
60
  db,
55
61
  objectStorage,
56
62
  grant,
@@ -91,8 +97,14 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
91
97
  });
92
98
  }
93
99
  const payload = parsedPayload.data;
100
+ const catalogSettings = (
101
+ await resolveWorkspaceCatalogSettings(db, deps.settings, {
102
+ accountId: grant.accountId,
103
+ workspaceId,
104
+ })
105
+ ).settings;
94
106
  const update = await validatedScheduledTaskUpdate({
95
- settings,
107
+ settings: catalogSettings,
96
108
  db,
97
109
  objectStorage,
98
110
  grant,
@@ -124,8 +136,14 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
124
136
  const grant = await requireAccessGrant(c, deps, workspaceId, "scheduled_tasks:manage");
125
137
  const existing = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
126
138
  const previous = await captureScheduledTaskRestoreState(db, existing);
139
+ const catalogSettings = (
140
+ await resolveWorkspaceCatalogSettings(db, deps.settings, {
141
+ accountId: grant.accountId,
142
+ workspaceId,
143
+ })
144
+ ).settings;
127
145
  const update = await validatedScheduledTaskUpdate({
128
- settings,
146
+ settings: catalogSettings,
129
147
  db,
130
148
  objectStorage,
131
149
  grant,
@@ -146,6 +164,12 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
146
164
  // recognised as codex-billed and skip the credit/cost gates at the edge.
147
165
  const task = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
148
166
  if (task.action.kind === "agent_turn") {
167
+ const catalogSettings = (
168
+ await resolveWorkspaceCatalogSettings(db, deps.settings, {
169
+ accountId: grant.accountId,
170
+ workspaceId,
171
+ })
172
+ ).settings;
149
173
  await validateScheduledTaskTarget({
150
174
  db,
151
175
  sessionAuthorization: deps.sessionAuthorization,
@@ -159,20 +183,23 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
159
183
  missingTargetStatus: 404,
160
184
  });
161
185
  await validateScheduledTaskMachineTarget({
162
- settings,
186
+ settings: catalogSettings,
163
187
  db,
164
188
  grant,
165
189
  runMode: task.runMode,
166
190
  agentConfig: task.agentConfig,
167
191
  requireOnline: true,
168
192
  });
169
- await requireLimit(deps, {
170
- accountId: grant.accountId,
171
- workspaceId,
172
- action: "agent_run:create",
173
- quantity: 1,
174
- model: task.agentConfig.model ?? deps.settings.openaiModel,
175
- });
193
+ await requireLimit(
194
+ { ...deps, settings: catalogSettings },
195
+ {
196
+ accountId: grant.accountId,
197
+ workspaceId,
198
+ action: "agent_run:create",
199
+ quantity: 1,
200
+ model: task.agentConfig.model ?? catalogSettings.openaiModel,
201
+ },
202
+ );
176
203
  }
177
204
  // Body is optional (a bare POST is still a valid trigger); only a present,
178
205
  // non-empty body must parse against the contract.
@@ -12,6 +12,7 @@ import {
12
12
  GatewayRealtimeConnectRequest,
13
13
  ClientSessionEvent,
14
14
  CompactSessionContextRequest,
15
+ CreateSessionRequest,
15
16
  DeleteSessionQueueItemRequest,
16
17
  EditSessionQueueItemRequest,
17
18
  EndSessionRealtimeRequest,
@@ -47,6 +48,7 @@ import {
47
48
  SessionEventSemanticClass,
48
49
  SessionEventType,
49
50
  SessionMcpServerId,
51
+ SaveNewSessionDraftRequest,
50
52
  compactSessionEventResult,
51
53
  sessionEventLatestClassToSemanticClass,
52
54
  SaveComposerDraftRequest,
@@ -106,10 +108,12 @@ import {
106
108
  getRetainedProcess,
107
109
  getSandbox,
108
110
  getSession,
111
+ readActiveSandbox,
109
112
  deleteSessionTreeIfQuiescent,
110
113
  getSessionEvent,
111
114
  getSessionForSubject,
112
115
  getSessionGoal,
116
+ getLatestSessionModelContext,
113
117
  getSessionHumanInputRequest,
114
118
  getSessionGoalWithContinuation,
115
119
  getSessionGoalRevision,
@@ -195,7 +199,7 @@ import {
195
199
  import {
196
200
  appendAndPublishEvents,
197
201
  boundSessionEventHttpPage,
198
- coalesceSessionEventDeltas,
202
+ coalesceSessionEventDeltasWithCoverage,
199
203
  publishDurableSessionEvents,
200
204
  } from "@opengeni/events";
201
205
  import {
@@ -216,6 +220,7 @@ import {
216
220
  NatsControlRpc,
217
221
  negotiateCapabilities,
218
222
  negotiateSelfhostedCapabilities,
223
+ resolveConnectedMachineWorkspaceRoot,
219
224
  selectBackend,
220
225
  SelfhostedSession,
221
226
  } from "@opengeni/runtime/sandbox";
@@ -231,6 +236,7 @@ import {
231
236
  requirePermission,
232
237
  requireSessionAuthorization,
233
238
  requireSessionAuthorizationListScope,
239
+ resolveWorkspaceCatalogSettings,
234
240
  withResolvedSessionAuthorization,
235
241
  SESSION_AUTHORIZATION_DEFAULT_REAUTHORIZE_MS,
236
242
  SessionAuthorizationDeniedError,
@@ -549,6 +555,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
549
555
  }
550
556
  let session: Session;
551
557
  try {
558
+ CreateSessionRequest.parse(payload);
552
559
  session = await createSessionForRequest(deps, grant, workspaceId, payload, authorization);
553
560
  } catch (error) {
554
561
  return sessionCreateErrorResponse(c, error);
@@ -562,7 +569,13 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
562
569
  app.get("/v1/workspaces/:workspaceId/new-session-draft", async (c) => {
563
570
  const workspaceId = c.req.param("workspaceId");
564
571
  const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:read");
565
- return c.json(await getActorNewSessionDraft({ settings, db }, grant, workspaceId));
572
+ const catalog = await resolveWorkspaceCatalogSettings(db, settings, {
573
+ accountId: grant.accountId,
574
+ workspaceId,
575
+ });
576
+ return c.json(
577
+ await getActorNewSessionDraft({ settings: catalog.settings, db }, grant, workspaceId),
578
+ );
566
579
  });
567
580
 
568
581
  app.put("/v1/workspaces/:workspaceId/new-session-draft", async (c) => {
@@ -587,9 +600,14 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
587
600
  );
588
601
  }
589
602
  try {
603
+ SaveNewSessionDraftRequest.parse(payload);
604
+ const catalog = await resolveWorkspaceCatalogSettings(db, settings, {
605
+ accountId: grant.accountId,
606
+ workspaceId,
607
+ });
590
608
  return c.json(
591
609
  await saveActorNewSessionDraft(
592
- { settings, db, objectStorage },
610
+ { settings: catalog.settings, db, objectStorage },
593
611
  grant,
594
612
  workspaceId,
595
613
  payload,
@@ -873,6 +891,32 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
873
891
  return c.json(await withEffectivePolicy(deps, workspaceId, grant.subjectId, session));
874
892
  });
875
893
 
894
+ app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/model-context", async (c) => {
895
+ const workspaceId = c.req.param("workspaceId");
896
+ const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:read");
897
+ const sessionId = c.req.param("sessionId");
898
+ if (!z.string().uuid().safeParse(sessionId).success) {
899
+ throw new HTTPException(404, { message: "session not found" });
900
+ }
901
+ const session = await getSessionForSubject(
902
+ db,
903
+ workspaceId,
904
+ sessionId,
905
+ grant.subjectId,
906
+ relatedSessionAccessFor(c),
907
+ );
908
+ if (!session) {
909
+ throw new HTTPException(404, { message: "session not found" });
910
+ }
911
+ return c.json(
912
+ await getLatestSessionModelContext(db, {
913
+ accountId: grant.accountId,
914
+ workspaceId,
915
+ sessionId,
916
+ }),
917
+ );
918
+ });
919
+
876
920
  app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/background-commands", async (c) => {
877
921
  const workspaceId = c.req.param("workspaceId");
878
922
  const sessionId = c.req.param("sessionId");
@@ -2606,10 +2650,16 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2606
2650
  c.header("X-OpenGeni-Covered-Last", String(result.coveredSequence.last));
2607
2651
  return c.json(result);
2608
2652
  }
2609
- const projected = compact ? coalesceSessionEventDeltas(events) : events;
2653
+ const compactProjection = compact ? coalesceSessionEventDeltasWithCoverage(events) : null;
2654
+ const projected = compactProjection?.events ?? events;
2655
+ const forensicExact =
2656
+ !compact && mode === "forensic" && payloadMode === "full" && dbPage.fullPayloadsExact;
2610
2657
  const page = boundSessionEventHttpPage(projected, {
2611
2658
  direction,
2612
- eventProjection: mode === "forensic" && payloadMode === "full" ? "exact" : "bounded",
2659
+ eventProjection: forensicExact ? "exact" : "bounded",
2660
+ ...(compactProjection
2661
+ ? { coveredThroughBySequence: compactProjection.coveredThroughBySequence }
2662
+ : {}),
2613
2663
  });
2614
2664
  const hasMore = dbPage.hasMore || page.truncated;
2615
2665
  c.header("X-OpenGeni-Page-Bytes", String(page.bytes));
@@ -2619,9 +2669,14 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2619
2669
  c.header("X-OpenGeni-Event-Mode", mode);
2620
2670
  c.header("X-OpenGeni-Event-Direction", direction);
2621
2671
  c.header("X-OpenGeni-Payload-Mode", payloadMode);
2622
- c.header("X-OpenGeni-Forensic-Exact", String(mode === "forensic" && payloadMode === "full"));
2672
+ c.header("X-OpenGeni-Forensic-Exact", String(forensicExact));
2623
2673
  const coveredFirst = page.events[0]?.sequence;
2624
- const coveredLast = page.events.at(-1)?.sequence;
2674
+ const coveredLastEvent = page.events.at(-1);
2675
+ const coveredLast =
2676
+ coveredLastEvent === undefined
2677
+ ? undefined
2678
+ : (compactProjection?.coveredThroughBySequence.get(coveredLastEvent.sequence) ??
2679
+ coveredLastEvent.sequence);
2625
2680
  if (coveredFirst !== undefined) c.header("X-OpenGeni-Covered-First", String(coveredFirst));
2626
2681
  if (coveredLast !== undefined) c.header("X-OpenGeni-Covered-Last", String(coveredLast));
2627
2682
  const truncatedBy = page.truncated ? "http_bytes" : dbPage.truncatedBy;
@@ -2898,28 +2953,33 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2898
2953
  const sessionId = c.req.param("sessionId");
2899
2954
  await assertSessionExists(db, workspaceId, sessionId);
2900
2955
  const payload = parseSteerSessionAdmission(await c.req.json().catch(() => null));
2901
- const result = await acceptSessionUserMessage(deps, grant, workspaceId, sessionId, {
2902
- text: payload.text,
2903
- annotations: payload.annotations,
2904
- modelContext: payload.modelContext ?? null,
2905
- resources: payload.resources,
2906
- model: payload.model ?? null,
2907
- reasoningEffort: payload.reasoningEffort ?? null,
2908
- latencyMode: payload.latencyMode ?? null,
2909
- mcpCredentialUpdates: payload.mcpCredentialUpdates ?? [],
2910
- connectionAuthorities: payload.connectionAuthorities,
2911
- ...(payload.personalResourceAttachment
2912
- ? { personalResourceAttachment: payload.personalResourceAttachment }
2913
- : {}),
2914
- authorization,
2915
- delivery: "steer",
2916
- origin: "human",
2917
- ...(payload.controlEtag !== undefined ? { controlEtag: payload.controlEtag } : {}),
2918
- ...(payload.expectedDraftRevision !== undefined
2919
- ? { expectedDraftRevision: payload.expectedDraftRevision }
2920
- : {}),
2921
- ...(payload.clientEventId ? { clientEventId: payload.clientEventId } : {}),
2922
- });
2956
+ let result: Awaited<ReturnType<typeof acceptSessionUserMessage>>;
2957
+ try {
2958
+ result = await acceptSessionUserMessage(deps, grant, workspaceId, sessionId, {
2959
+ text: payload.text,
2960
+ annotations: payload.annotations,
2961
+ modelContext: payload.modelContext ?? null,
2962
+ resources: payload.resources,
2963
+ model: payload.model ?? null,
2964
+ reasoningEffort: payload.reasoningEffort ?? null,
2965
+ latencyMode: payload.latencyMode ?? null,
2966
+ mcpCredentialUpdates: payload.mcpCredentialUpdates ?? [],
2967
+ connectionAuthorities: payload.connectionAuthorities,
2968
+ ...(payload.personalResourceAttachment
2969
+ ? { personalResourceAttachment: payload.personalResourceAttachment }
2970
+ : {}),
2971
+ authorization,
2972
+ delivery: "steer",
2973
+ origin: "human",
2974
+ ...(payload.controlEtag !== undefined ? { controlEtag: payload.controlEtag } : {}),
2975
+ ...(payload.expectedDraftRevision !== undefined
2976
+ ? { expectedDraftRevision: payload.expectedDraftRevision }
2977
+ : {}),
2978
+ ...(payload.clientEventId ? { clientEventId: payload.clientEventId } : {}),
2979
+ });
2980
+ } catch (error) {
2981
+ return commandConflictResponse(c, error);
2982
+ }
2923
2983
  return c.json(result, 202);
2924
2984
  });
2925
2985
 
@@ -2975,29 +3035,34 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2975
3035
  }
2976
3036
  }
2977
3037
  if (event.type === "user.message") {
2978
- const { accepted } = await acceptSessionUserMessage(deps, grant, workspaceId, sessionId, {
2979
- text: event.payload.text,
2980
- annotations: event.payload.annotations,
2981
- modelContext: event.payload.modelContext ?? null,
2982
- resources: event.payload.resources ?? [],
2983
- model: event.payload.model ?? null,
2984
- reasoningEffort: event.payload.reasoningEffort ?? null,
2985
- latencyMode: event.payload.latencyMode ?? null,
2986
- mcpCredentialUpdates: event.payload.mcpCredentialUpdates ?? [],
2987
- connectionAuthorities: event.payload.connectionAuthorities,
2988
- ...(event.payload.personalResourceAttachment
2989
- ? { personalResourceAttachment: event.payload.personalResourceAttachment }
2990
- : {}),
2991
- authorization,
2992
- ...(event.payload.controlEtag !== undefined
2993
- ? { controlEtag: event.payload.controlEtag }
2994
- : {}),
2995
- ...(event.payload.expectedDraftRevision !== undefined
2996
- ? { expectedDraftRevision: event.payload.expectedDraftRevision }
2997
- : {}),
2998
- ...(event.clientEventId ? { clientEventId: event.clientEventId } : {}),
2999
- });
3000
- return c.json(accepted, 202);
3038
+ let result: Awaited<ReturnType<typeof acceptSessionUserMessage>>;
3039
+ try {
3040
+ result = await acceptSessionUserMessage(deps, grant, workspaceId, sessionId, {
3041
+ text: event.payload.text,
3042
+ annotations: event.payload.annotations,
3043
+ modelContext: event.payload.modelContext ?? null,
3044
+ resources: event.payload.resources ?? [],
3045
+ model: event.payload.model ?? null,
3046
+ reasoningEffort: event.payload.reasoningEffort ?? null,
3047
+ latencyMode: event.payload.latencyMode ?? null,
3048
+ mcpCredentialUpdates: event.payload.mcpCredentialUpdates ?? [],
3049
+ connectionAuthorities: event.payload.connectionAuthorities,
3050
+ ...(event.payload.personalResourceAttachment
3051
+ ? { personalResourceAttachment: event.payload.personalResourceAttachment }
3052
+ : {}),
3053
+ authorization,
3054
+ ...(event.payload.controlEtag !== undefined
3055
+ ? { controlEtag: event.payload.controlEtag }
3056
+ : {}),
3057
+ ...(event.payload.expectedDraftRevision !== undefined
3058
+ ? { expectedDraftRevision: event.payload.expectedDraftRevision }
3059
+ : {}),
3060
+ ...(event.clientEventId ? { clientEventId: event.clientEventId } : {}),
3061
+ });
3062
+ } catch (error) {
3063
+ return commandConflictResponse(c, error);
3064
+ }
3065
+ return c.json(result.accepted, 202);
3001
3066
  }
3002
3067
 
3003
3068
  if (event.type === "user.approvalDecision") {
@@ -3218,6 +3283,10 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
3218
3283
  getEnrollment(db, grant, activeSandbox.enrollmentId),
3219
3284
  getLiveEnrollmentConnection(db, grant, activeSandbox.enrollmentId),
3220
3285
  ]);
3286
+ const reportedWorkspaceRoot = liveConnection?.workspaceRoot ?? enrollment?.workspaceRoot;
3287
+ const effectiveWorkspaceRoot = reportedWorkspaceRoot
3288
+ ? resolveConnectedMachineWorkspaceRoot(reportedWorkspaceRoot, session.workingDir)
3289
+ : null;
3221
3290
  let probeResponded = false;
3222
3291
  if (liveConnection?.connectionInstanceId) {
3223
3292
  const machine = new SelfhostedSession({
@@ -3226,7 +3295,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
3226
3295
  connectionInstanceId: liveConnection.connectionInstanceId,
3227
3296
  // Capability negotiation only pings, so a pre-root agent may still
3228
3297
  // report upgrade guidance without exposing a false filesystem root.
3229
- workspaceRoot: liveConnection.workspaceRoot ?? "/",
3298
+ workspaceRoot: effectiveWorkspaceRoot ?? liveConnection.workspaceRoot ?? "/",
3230
3299
  controlRpc: new NatsControlRpc(async () => bus.getRequestConnection()),
3231
3300
  relay: relayConfigFromSettings(settings),
3232
3301
  epoch: session.activeEpoch,
@@ -3242,7 +3311,10 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
3242
3311
  ...commonNegotiation,
3243
3312
  os: enrollment?.os ?? session.sandboxOs,
3244
3313
  leaseEpoch: session.activeEpoch,
3245
- enrollment,
3314
+ enrollment:
3315
+ enrollment && effectiveWorkspaceRoot
3316
+ ? { ...enrollment, workspaceRoot: effectiveWorkspaceRoot }
3317
+ : enrollment,
3246
3318
  probeResponded,
3247
3319
  });
3248
3320
  } else {
@@ -3291,17 +3363,17 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
3291
3363
  ...capabilities,
3292
3364
  FileSystem: {
3293
3365
  ...capabilities.FileSystem,
3294
- root: selectBackend(
3295
- (selfhostedActive
3296
- ? "selfhosted"
3297
- : activeSandbox?.kind === "modal"
3298
- ? "modal"
3299
- : session.sandboxBackend === "selfhosted" &&
3300
- settings.sandboxBackend !== "selfhosted" &&
3301
- settings.sandboxBackend !== "none"
3302
- ? settings.sandboxBackend
3303
- : session.sandboxBackend) as SandboxBackend,
3304
- ).workspaceRoot,
3366
+ root: selfhostedActive
3367
+ ? capabilities.FileSystem.root
3368
+ : selectBackend(
3369
+ (activeSandbox?.kind === "modal"
3370
+ ? "modal"
3371
+ : session.sandboxBackend === "selfhosted" &&
3372
+ settings.sandboxBackend !== "selfhosted" &&
3373
+ settings.sandboxBackend !== "none"
3374
+ ? settings.sandboxBackend
3375
+ : session.sandboxBackend) as SandboxBackend,
3376
+ ).workspaceRoot,
3305
3377
  },
3306
3378
  Git: {
3307
3379
  ...capabilities.Git,
@@ -3318,6 +3390,21 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
3318
3390
  DesktopStream: { ...capabilities.DesktopStream, ...wire },
3319
3391
  };
3320
3392
  }
3393
+ if (selfhostedActive) {
3394
+ const currentPointer = await readActiveSandbox(db, workspaceId, sessionId);
3395
+ if (
3396
+ !currentPointer ||
3397
+ currentPointer.activeSandboxId !== session.activeSandboxId ||
3398
+ currentPointer.activeEpoch !== session.activeEpoch ||
3399
+ currentPointer.workingDir !== session.workingDir
3400
+ ) {
3401
+ throw new ApiHttpError(409, {
3402
+ code: "conflict",
3403
+ message: "sandbox route changed while capabilities were being negotiated; retry",
3404
+ retryable: true,
3405
+ });
3406
+ }
3407
+ }
3321
3408
  return c.json(responseCapabilities);
3322
3409
  });
3323
3410
 
@@ -4290,6 +4377,7 @@ export function sessionAuthorizationOperationForHttp(
4290
4377
  }
4291
4378
  if (suffix === "/lineage" && verb === "GET") return "session.lineage.read";
4292
4379
  if (suffix === "/background-commands" && verb === "GET") return "session.read";
4380
+ if (suffix === "/model-context" && verb === "GET") return "session.read";
4293
4381
  if (/^\/background-commands\/[^/]+$/.test(suffix) && verb === "DELETE") {
4294
4382
  return "session.control";
4295
4383
  }
@@ -552,8 +552,9 @@ export function registerSuperGrokRoutes(app: Hono, deps: ApiRouteDeps): void {
552
552
  } catch {
553
553
  valid = false;
554
554
  }
555
- const catalog = valid
556
- ? configuredModels(withXaiSubscriptionCatalogProvider(deps.settings))
555
+ const catalogSettings = valid ? (await deps.resolveCatalogSettings()).settings : null;
556
+ const catalog = catalogSettings
557
+ ? configuredModels(withXaiSubscriptionCatalogProvider(catalogSettings))
557
558
  .filter(
558
559
  (model) =>
559
560
  model.credentialSource.kind === "connected_subscription" &&