@opengeni/api-router 3.0.1 → 4.0.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 (96) hide show
  1. package/dist/admission-trace.d.ts +8 -0
  2. package/dist/app.js +1 -1
  3. package/dist/auth/managed-auth-attempt-context.d.ts +2 -0
  4. package/dist/auth/managed-auth-session-adapter.d.ts +2 -0
  5. package/dist/auth/managed-auth.d.ts +1 -0
  6. package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
  7. package/dist/{chunk-AJ62T5QN.js → chunk-E7TZFJ65.js} +22300 -21113
  8. package/dist/{chunk-AJ62T5QN.js.map → chunk-E7TZFJ65.js.map} +1 -1
  9. package/dist/connection-ownership.d.ts +1 -2
  10. package/dist/fatal-process-boundary.d.ts +1 -1
  11. package/dist/index.js +24 -6
  12. package/dist/index.js.map +1 -1
  13. package/dist/integrations/atlassian.d.ts +8 -3
  14. package/dist/integrations/fiken.d.ts +3 -1
  15. package/dist/integrations/github-lens-connect.d.ts +5 -65
  16. package/dist/integrations/google-drive.d.ts +2 -0
  17. package/dist/integrations/integration-commit-authority.d.ts +13 -0
  18. package/dist/integrations/oauth-client.d.ts +119 -11
  19. package/dist/integrations/oauth-profiles.d.ts +8 -13
  20. package/dist/integrations/slack-interactions.d.ts +1 -1
  21. package/dist/mcp/capability-account-readiness.d.ts +7 -0
  22. package/dist/mcp/first-party-tool-permissions.d.ts +4 -0
  23. package/dist/mcp-oauth.d.ts +22 -1
  24. package/dist/routes/api-integrations.d.ts +1 -0
  25. package/dist/routes/codex.d.ts +28 -0
  26. package/dist/routes/managed-sign-in-methods.d.ts +6 -0
  27. package/dist/routes/organization-integration-policy.d.ts +3 -0
  28. package/dist/routes/sessions.d.ts +24 -2
  29. package/dist/scheduled-task-deletion.d.ts +3 -4
  30. package/dist/workspace-tool-gateway.d.ts +3 -1
  31. package/package.json +15 -15
  32. package/src/admission-trace.ts +14 -0
  33. package/src/app.ts +141 -94
  34. package/src/auth/managed-auth-attempt-context.ts +9 -0
  35. package/src/auth/managed-auth-session-adapter.ts +11 -0
  36. package/src/auth/managed-auth.ts +39 -2
  37. package/src/auth/managed-sign-in-notifications.ts +81 -0
  38. package/src/connection-ownership.ts +2 -9
  39. package/src/editable-artifact-production.ts +6 -0
  40. package/src/fatal-process-boundary.ts +17 -2
  41. package/src/index.ts +3 -4
  42. package/src/integrations/atlassian.ts +184 -76
  43. package/src/integrations/fiken.ts +57 -35
  44. package/src/integrations/github-app-connect.ts +51 -15
  45. package/src/integrations/github-lens-connect.ts +7 -28
  46. package/src/integrations/google-drive.ts +296 -125
  47. package/src/integrations/integration-commit-authority.ts +69 -0
  48. package/src/integrations/oauth-client.ts +231 -132
  49. package/src/integrations/oauth-profiles.ts +10 -45
  50. package/src/integrations/personal-github.ts +55 -31
  51. package/src/integrations/provider-oauth.ts +57 -82
  52. package/src/integrations/social-oauth.ts +83 -56
  53. package/src/mcp/capability-account-readiness.ts +17 -0
  54. package/src/mcp/editable-artifacts.ts +7 -2
  55. package/src/mcp/first-party-tool-permissions.ts +1 -0
  56. package/src/mcp/knowledge-entries.ts +25 -8
  57. package/src/mcp/server.ts +135 -43
  58. package/src/mcp-oauth.ts +336 -34
  59. package/src/routes/api-integrations.ts +116 -4
  60. package/src/routes/automations.ts +5 -13
  61. package/src/routes/billing.ts +52 -0
  62. package/src/routes/canonical-human-identities.ts +61 -0
  63. package/src/routes/capabilities.ts +51 -1
  64. package/src/routes/codex.ts +4 -2
  65. package/src/routes/connect.ts +90 -31
  66. package/src/routes/connections.ts +487 -307
  67. package/src/routes/files.ts +21 -12
  68. package/src/routes/github.ts +48 -19
  69. package/src/routes/insights.ts +17 -7
  70. package/src/routes/integration-facets.ts +66 -25
  71. package/src/routes/knowledge.ts +48 -1
  72. package/src/routes/managed-sign-in-methods.ts +433 -0
  73. package/src/routes/organization-integration-policy.ts +113 -0
  74. package/src/routes/organization-memberships.ts +22 -2
  75. package/src/routes/plugins.ts +42 -4
  76. package/src/routes/pr-review-github.ts +67 -57
  77. package/src/routes/pr-review.ts +74 -71
  78. package/src/routes/rigs.ts +32 -13
  79. package/src/routes/scheduled-tasks.ts +6 -6
  80. package/src/routes/sessions.ts +233 -11
  81. package/src/routes/skill-content.ts +19 -3
  82. package/src/routes/social.ts +24 -13
  83. package/src/sandbox/channel-a.ts +7 -1
  84. package/src/sandbox/rematerialize.ts +2 -1
  85. package/src/scheduled-task-deletion.ts +13 -3
  86. package/src/workspace-tool-gateway.ts +31 -31
  87. package/dist/connection-authority-owner.d.ts +0 -59
  88. package/dist/routes/connection-authorities.d.ts +0 -3
  89. package/dist/routes/host-mcp-bindings.d.ts +0 -5
  90. package/dist/routes/host-mcp-resolvers.d.ts +0 -3
  91. package/dist/routes/packs.d.ts +0 -3
  92. package/src/connection-authority-owner.ts +0 -61
  93. package/src/routes/connection-authorities.ts +0 -139
  94. package/src/routes/host-mcp-bindings.ts +0 -122
  95. package/src/routes/host-mcp-resolvers.ts +0 -41
  96. package/src/routes/packs.ts +0 -784
@@ -51,7 +51,7 @@ async function parseRigRequest<T>(c: Context, schema: ZodType<T>, label: string)
51
51
  throw new ApiHttpError(422, {
52
52
  code: "validation_failed",
53
53
  message: imageOverrideUnsupported
54
- ? "Rig base-image overrides are not supported; Rigs use the deployment-managed platform sandbox."
54
+ ? "Sandbox Environment base-image overrides are not supported; Sandbox Environments use the deployment-managed platform sandbox."
55
55
  : `Invalid ${label}.`,
56
56
  retryable: false,
57
57
  details: {
@@ -77,7 +77,7 @@ export function registerRigRoutes(app: Hono, deps: ApiRouteDeps): void {
77
77
  const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId, permission);
78
78
  const rigId = c.req.param("rigId");
79
79
  if (!rigId) {
80
- throw new HTTPException(400, { message: "rig id is required" });
80
+ throw new HTTPException(400, { message: "sandbox environment id is required" });
81
81
  }
82
82
  const rig = await requireRigForApi(db, authorization.grant, rigId);
83
83
  if (
@@ -126,7 +126,7 @@ export function registerRigRoutes(app: Hono, deps: ApiRouteDeps): void {
126
126
  // The verifying transition already committed. Keep its deterministic
127
127
  // attempt identity retryable instead of reporting an opaque 5xx after a
128
128
  // durable mutation or inventing a second attempt after an ambiguous start.
129
- deps.observability?.warn("rig change verification start failed", {
129
+ deps.observability?.warn("sandbox environment change verification start failed", {
130
130
  workspaceId,
131
131
  changeId,
132
132
  attempt,
@@ -163,11 +163,14 @@ export function registerRigRoutes(app: Hono, deps: ApiRouteDeps): void {
163
163
  // Creation already committed. The rig remains fully usable through its
164
164
  // runtime setup fallback and can be re-verified explicitly; never turn a
165
165
  // successful create into an unretryable 5xx because Temporal was down.
166
- deps.observability?.warn("initial rig provider-image verification start failed", {
167
- workspaceId,
168
- versionId,
169
- error: error instanceof Error ? error.message : String(error),
170
- });
166
+ deps.observability?.warn(
167
+ "initial sandbox environment provider-image verification start failed",
168
+ {
169
+ workspaceId,
170
+ versionId,
171
+ error: error instanceof Error ? error.message : String(error),
172
+ },
173
+ );
171
174
  return false;
172
175
  }
173
176
  }
@@ -183,7 +186,11 @@ export function registerRigRoutes(app: Hono, deps: ApiRouteDeps): void {
183
186
  const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
184
187
  const grant = authorization.grant;
185
188
  requirePermission(grant, "rigs:manage");
186
- const payload = await parseRigRequest(c, CreateRigRequest, "Rig create request");
189
+ const payload = await parseRigRequest(
190
+ c,
191
+ CreateRigRequest,
192
+ "Sandbox Environment create request",
193
+ );
187
194
  const allowOrganization =
188
195
  payload.scope === "organization" &&
189
196
  authorization.accountGrant?.permissions.includes("account:admin") === true;
@@ -222,7 +229,11 @@ export function registerRigRoutes(app: Hono, deps: ApiRouteDeps): void {
222
229
  message: "missing permission: account:admin",
223
230
  });
224
231
  }
225
- const payload = await parseRigRequest(c, UpdateRigRequest, "Rig update request");
232
+ const payload = await parseRigRequest(
233
+ c,
234
+ UpdateRigRequest,
235
+ "Sandbox Environment update request",
236
+ );
226
237
  return c.json(await updateRigForApi({ db }, grant, rig, payload, { allowOrganization }));
227
238
  });
228
239
 
@@ -254,7 +265,11 @@ export function registerRigRoutes(app: Hono, deps: ApiRouteDeps): void {
254
265
  app.post("/v1/workspaces/:workspaceId/rigs/:rigId/versions", async (c) => {
255
266
  const workspaceId = c.req.param("workspaceId");
256
267
  const { grant, rig } = await requireRigMutation(c, workspaceId, "rigs:manage");
257
- const payload = await parseRigRequest(c, RigDefinitionEditPayload, "Rig version request");
268
+ const payload = await parseRigRequest(
269
+ c,
270
+ RigDefinitionEditPayload,
271
+ "Sandbox Environment version request",
272
+ );
258
273
  const version = await createRigVersionForApi({ db }, grant, rig, payload);
259
274
  const started = await tryStartInitialVersionVerification(rig.workspaceId, version.id);
260
275
  if (!started) c.header("OpenGeni-Rig-Verification", "deferred");
@@ -289,7 +304,11 @@ export function registerRigRoutes(app: Hono, deps: ApiRouteDeps): void {
289
304
  app.post("/v1/workspaces/:workspaceId/rigs/:rigId/changes", async (c) => {
290
305
  const workspaceId = c.req.param("workspaceId");
291
306
  const { grant, rig } = await requireRigMutation(c, workspaceId, "rigs:use");
292
- const request = await parseRigRequest(c, ProposeRigChangeRequest, "Rig change request");
307
+ const request = await parseRigRequest(
308
+ c,
309
+ ProposeRigChangeRequest,
310
+ "Sandbox Environment change request",
311
+ );
293
312
  const change = await proposeRigChangeForApi({ db }, grant, rig, request);
294
313
  const verification = await startChangeVerification(rig.workspaceId, change.id);
295
314
  if (!verification.started) c.header("OpenGeni-Rig-Verification", "deferred");
@@ -336,7 +355,7 @@ export function registerRigRoutes(app: Hono, deps: ApiRouteDeps): void {
336
355
  const workspaceId = c.req.param("workspaceId");
337
356
  const { rig } = await requireRigMutation(c, workspaceId, "rigs:use");
338
357
  if (!rig.activeVersion) {
339
- return c.json({ error: "rig has no active version" }, 422);
358
+ return c.json({ error: "sandbox environment has no active version" }, 422);
340
359
  }
341
360
  await startVersionVerification(rig.workspaceId, rig.activeVersion.id);
342
361
  return c.json({ ok: true, versionId: rig.activeVersion.id }, 202);
@@ -27,6 +27,7 @@ import {
27
27
  syncCreatedScheduledTask,
28
28
  syncUpdatedScheduledTask,
29
29
  updateScheduledTaskForApi,
30
+ triggerScheduledTaskForGrant,
30
31
  validateScheduledTaskMachineTarget,
31
32
  validateScheduledTaskTarget,
32
33
  validatedScheduledTaskUpdate,
@@ -152,7 +153,7 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
152
153
  });
153
154
  const task = await updateScheduledTaskForApi(
154
155
  db,
155
- workspaceId,
156
+ grant,
156
157
  taskId,
157
158
  update,
158
159
  payload.agentLearning
@@ -168,7 +169,7 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
168
169
  const grant = await requireAccessGrant(c, deps, workspaceId, "scheduled_tasks:manage");
169
170
  const existing = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
170
171
  const previous = await captureScheduledTaskRestoreState(db, existing);
171
- const task = await updateScheduledTaskForApi(db, workspaceId, existing.id, {
172
+ const task = await updateScheduledTaskForApi(db, grant, existing.id, {
172
173
  status: "paused",
173
174
  });
174
175
  await syncUpdatedScheduledTask({ db, workflowClient, previous, task });
@@ -203,7 +204,7 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
203
204
  sessionAuthorization: deps.sessionAuthorization,
204
205
  authorizationSurface: "http",
205
206
  });
206
- const task = await updateScheduledTaskForApi(db, workspaceId, existing.id, update);
207
+ const task = await updateScheduledTaskForApi(db, grant, existing.id, update);
207
208
  await syncUpdatedScheduledTask({ db, workflowClient, previous, task });
208
209
  return c.json(scheduledTaskForGrant(task, grant));
209
210
  });
@@ -262,7 +263,7 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
262
263
  ? manualScheduledTaskTriggerUsageKey(workspaceId, task.id, triggerToken)
263
264
  : `knowledge-source-sync:manual:${workspaceId}:${task.id}:${triggerToken}`;
264
265
  const triggerWorkflowId = manualScheduledTaskTriggerWorkflowId(task.id, triggerToken);
265
- await workflowClient.triggerScheduledTask({
266
+ await triggerScheduledTaskForGrant(db, grant, workflowClient, {
266
267
  task,
267
268
  agentRunUsageIdempotencyKey,
268
269
  triggerWorkflowId,
@@ -288,9 +289,8 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
288
289
  const workspaceId = c.req.param("workspaceId");
289
290
  const grant = await requireAccessGrant(c, deps, workspaceId, "scheduled_tasks:manage");
290
291
  await deleteScheduledTaskWithDurableCleanup(deps, {
291
- workspaceId,
292
+ grant,
292
293
  taskId: c.req.param("taskId"),
293
- subjectId: grant.subjectId,
294
294
  });
295
295
  return c.json({ ok: true });
296
296
  });
@@ -1,7 +1,14 @@
1
1
  import { getRetainedProviderCommand } from "@opengeni/db/retained-provider-commands";
2
+ import { searchSessionMessagesForSubject, SessionMessageSearchCursorError } from "@opengeni/db";
3
+ import { SessionMessageSearchRequest } from "@opengeni/contracts";
2
4
  import { scheduledSessionIds } from "@opengeni/db";
3
5
  import { withSiteSessionOrigin } from "@opengeni/core";
4
6
  import { resolveSiteSessionOrigin } from "../site-session-origin";
7
+ import { SandboxRecoveryRequest } from "@opengeni/contracts";
8
+ import { getManagedHumanSandboxRecovery, consentManagedHumanSandboxRecovery } from "@opengeni/core";
9
+ import { SandboxRecoveryConflictError } from "@opengeni/db";
10
+ import { codexAccountJson } from "./codex";
11
+ import { getSessionCodexAccounts } from "@opengeni/db";
5
12
  import {
6
13
  AcknowledgeStreamRequest,
7
14
  ApplySessionGoalRevisionRequest,
@@ -43,6 +50,7 @@ import {
43
50
  RenewSessionRealtimeRequest,
44
51
  SyncSessionRealtimeLedgerRequest,
45
52
  SessionControlRequest,
53
+ SessionRetryRequest,
46
54
  SESSION_EVENT_RAW_DELTA_TYPES,
47
55
  SessionEventPayloadMode,
48
56
  SessionEventReadDirection,
@@ -257,6 +265,7 @@ import {
257
265
  type ResolvedSessionAuthorization,
258
266
  } from "@opengeni/core";
259
267
  import type { ApiRouteDeps } from "@opengeni/core";
268
+ import { SessionRetryConflictError } from "@opengeni/db";
260
269
  import {
261
270
  attachViewer,
262
271
  detachViewer,
@@ -274,6 +283,7 @@ import { buildSessionCodexRealtimeBroker, CodexRealtimeBrokerError } from "../co
274
283
  import {
275
284
  acceptSessionUserMessage,
276
285
  controlHumanSessionWorkstream,
286
+ retryFailedSession,
277
287
  createSessionForRequest,
278
288
  deleteHumanQueuePrompt,
279
289
  editHumanQueuePrompt,
@@ -310,6 +320,7 @@ import {
310
320
  import { publishSandboxFileArtifact } from "../sandbox-file-artifacts";
311
321
  import { ApiHttpError } from "../http/api-error";
312
322
  import { observeWorkDiscovery, summarizeWorkDiscoveryRows } from "../work-discovery-observability";
323
+ import { recordAcceptedApiAdmission } from "../admission-trace";
313
324
 
314
325
  type SessionRouteDeps = ApiRouteDeps & Pick<ViewerServices, "establishSandboxSession">;
315
326
 
@@ -681,6 +692,59 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
681
692
  }
682
693
  });
683
694
 
695
+ app.get("/v1/workspaces/:workspaceId/session-message-search", async (c) => {
696
+ const workspaceId = c.req.param("workspaceId");
697
+ const authorization = await requireAccessGrantAuthorization(
698
+ c,
699
+ deps,
700
+ workspaceId,
701
+ "sessions:read",
702
+ );
703
+ const grant = authorization.grant;
704
+ // A single-session filter is not authority: both searches use the complete
705
+ // live host/agent list scope, plus ordinary private/archive/RLS boundaries.
706
+ let authorizationScope;
707
+ try {
708
+ authorizationScope = await requireSessionAuthorizationListScope(deps, grant, "http");
709
+ } catch (error) {
710
+ throw sessionAuthorizationHttpError(error);
711
+ }
712
+ const raw = c.req.query();
713
+ const parsed = SessionMessageSearchRequest.safeParse({
714
+ ...raw,
715
+ ...(raw.limit !== undefined ? { limit: Number(raw.limit) } : {}),
716
+ });
717
+ if (!parsed.success)
718
+ throw new HTTPException(400, { message: "Invalid session message search request" });
719
+ try {
720
+ return c.json(
721
+ await searchSessionMessagesForSubject(
722
+ db,
723
+ workspaceId,
724
+ parsed.data,
725
+ {
726
+ subjectId: grant.subjectId,
727
+ ...(authorizationScope ? { authorizationScope } : {}),
728
+ personalWorkspaceOwnerException: hasVerifiedOwningUserAuthorization(authorization),
729
+ },
730
+ { signal: c.req.raw.signal },
731
+ ),
732
+ );
733
+ } catch (error) {
734
+ if (error instanceof SessionListAccessError)
735
+ throw new HTTPException(403, { message: error.message });
736
+ if (error instanceof SessionMessageSearchCursorError)
737
+ throw new HTTPException(400, { message: error.message });
738
+ // Clients are told to cancel superseded searches, so an abort here is a
739
+ // routine client disconnect, never a server failure. Match the existing
740
+ // client-gone convention (mcp/request-abort.ts): 499 with an empty body,
741
+ // which a departed client never reads and monitors never count as 5xx.
742
+ if (c.req.raw.signal.aborted || (error instanceof Error && error.name === "AbortError"))
743
+ return new Response(null, { status: 499 });
744
+ throw error;
745
+ }
746
+ });
747
+
684
748
  app.get("/v1/workspaces/:workspaceId/sessions", async (c) => {
685
749
  const workspaceId = c.req.param("workspaceId");
686
750
  const authorization = await requireAccessGrantAuthorization(
@@ -708,6 +772,8 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
708
772
  ...(query.search ? { search: query.search } : {}),
709
773
  ...(query.pinsOnly ? { pinsOnly: true } : {}),
710
774
  ...(query.archivedOnly ? { archivedOnly: true } : {}),
775
+ ...(query.sortBy ? { sortBy: query.sortBy } : {}),
776
+ ...(query.archiveStatus ? { archiveStatus: query.archiveStatus } : {}),
711
777
  ...(query.parentSessionId !== undefined ? { parentSessionId: query.parentSessionId } : {}),
712
778
  ...(query.channelId !== undefined ? { channelId: query.channelId } : {}),
713
779
  ...(query.originSiteId ? { originSiteId: query.originSiteId } : {}),
@@ -1944,6 +2010,31 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
1944
2010
  });
1945
2011
  });
1946
2012
 
2013
+ app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/codex-accounts", async (c) => {
2014
+ const workspaceId = c.req.param("workspaceId");
2015
+ await requireAccessGrant(c, deps, workspaceId, "sessions:read");
2016
+ await requireAccessGrant(c, deps, workspaceId, "workspace:read");
2017
+ // authorizeSessionHttp has already enforced private-session and agent scope.
2018
+ const projection = await getSessionCodexAccounts(db, workspaceId, c.req.param("sessionId"));
2019
+ if (!projection) throw new HTTPException(404, { message: "session not found" });
2020
+ const activeAccountId = projection.rotation?.activeCredentialId ?? null;
2021
+ return c.json({
2022
+ accounts: projection.accounts.map((account) => codexAccountJson(account)),
2023
+ activeAccountId,
2024
+ settings: {
2025
+ rotationEnabled: projection.rotation?.rotationEnabled ?? false,
2026
+ rotationStrategy: "sharded",
2027
+ activeCredentialId: activeAccountId,
2028
+ },
2029
+ currentSelection: projection.currentSelection,
2030
+ currentAccount: projection.currentAccount
2031
+ ? codexAccountJson(projection.currentAccount)
2032
+ : null,
2033
+ pinnedAccountId: projection.pinnedAccountId,
2034
+ lastAccountId: projection.lastAccountId,
2035
+ });
2036
+ });
2037
+
1947
2038
  // Pin (or unpin) the session's Codex account. body { target: "auto" | "<id>" }:
1948
2039
  // "auto" clears the pin (the session follows the workspace active pointer); a
1949
2040
  // uuid pins the session to that specific account. Overrides a capacity-blocked
@@ -2569,10 +2660,17 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2569
2660
 
2570
2661
  app.delete("/v1/workspaces/:workspaceId/sessions/:sessionId/goal", async (c) => {
2571
2662
  const workspaceId = c.req.param("workspaceId");
2572
- await requireAccessGrant(c, deps, workspaceId, "sessions:control");
2663
+ const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
2573
2664
  const sessionId = c.req.param("sessionId");
2574
2665
  await assertSessionExists(db, workspaceId, sessionId);
2575
- const { event } = await clearSessionGoal(db, workspaceId, sessionId);
2666
+ const { event } = await clearSessionGoal(db, workspaceId, sessionId, {
2667
+ actor: grant.principalKind === "agent_attempt" ? "agent" : "api",
2668
+ }).catch((error: unknown) => {
2669
+ if (error instanceof SessionControlConflictError) {
2670
+ throw new HTTPException(409, { message: error.message, cause: error });
2671
+ }
2672
+ throw error;
2673
+ });
2576
2674
  if (event) {
2577
2675
  try {
2578
2676
  await bus.publish(workspaceId, sessionId, [event]);
@@ -3059,6 +3157,112 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
3059
3157
  }
3060
3158
  });
3061
3159
 
3160
+ app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/sandbox-recovery", async (c) => {
3161
+ const workspaceId = c.req.param("workspaceId");
3162
+ const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
3163
+ try {
3164
+ return c.json(
3165
+ await getManagedHumanSandboxRecovery(
3166
+ deps,
3167
+ authorization,
3168
+ workspaceId,
3169
+ c.req.param("sessionId"),
3170
+ ),
3171
+ );
3172
+ } catch (error) {
3173
+ throw sessionTenancyHttpError(error);
3174
+ }
3175
+ });
3176
+
3177
+ app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/sandbox-recovery", async (c) => {
3178
+ const workspaceId = c.req.param("workspaceId");
3179
+ const sessionId = c.req.param("sessionId");
3180
+ const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
3181
+ const parsed = SandboxRecoveryRequest.safeParse(await c.req.json().catch(() => null));
3182
+ if (!parsed.success) throw new HTTPException(400, { message: "invalid checkpoint consent" });
3183
+ let consentCommitted = false;
3184
+ try {
3185
+ const receipt = await consentManagedHumanSandboxRecovery(
3186
+ deps,
3187
+ authorization,
3188
+ workspaceId,
3189
+ sessionId,
3190
+ parsed.data,
3191
+ );
3192
+ consentCommitted = true;
3193
+ const projection = await getManagedHumanSandboxRecovery(
3194
+ deps,
3195
+ authorization,
3196
+ workspaceId,
3197
+ sessionId,
3198
+ );
3199
+ if (
3200
+ projection.operationId === receipt.operationId &&
3201
+ projection.status === "consent_accepted"
3202
+ ) {
3203
+ // Establish/verify only. No failed-turn Retry, Send, tool callback or
3204
+ // command replay. The ordinary cold->warming election owns creation.
3205
+ const session = await getSession(db, workspaceId, sessionId);
3206
+ if (session) {
3207
+ try {
3208
+ await withChannelARead(
3209
+ channelAServices,
3210
+ {
3211
+ accountId: authorization.grant.accountId,
3212
+ workspaceId,
3213
+ session,
3214
+ subjectId: authorization.grant.subjectId,
3215
+ },
3216
+ async () => undefined,
3217
+ );
3218
+ } catch {
3219
+ // Consent committed. Never turn a provider failure into a false
3220
+ // rejection of consent or leak native provider bindings/messages.
3221
+ }
3222
+ }
3223
+ }
3224
+ return c.json({
3225
+ ...receipt,
3226
+ recovery: await getManagedHumanSandboxRecovery(deps, authorization, workspaceId, sessionId),
3227
+ });
3228
+ } catch (error) {
3229
+ if (consentCommitted) {
3230
+ // A later authorization/read failure cannot revoke the committed
3231
+ // receipt. Return no session state, checkpoint or provider details.
3232
+ return c.json(
3233
+ {
3234
+ code: "upstream_unavailable",
3235
+ message:
3236
+ "Recovery status could not be confirmed. Check status without resubmitting consent.",
3237
+ outcomeUnknown: true,
3238
+ retryable: false,
3239
+ },
3240
+ 503,
3241
+ );
3242
+ }
3243
+ if (error instanceof SandboxRecoveryConflictError)
3244
+ return c.json({ code: error.code, message: error.message }, 409);
3245
+ if (error instanceof SessionCommandIdempotencyError) return commandConflictResponse(c, error);
3246
+ throw sessionTenancyHttpError(error);
3247
+ }
3248
+ });
3249
+
3250
+ app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/retry", async (c) => {
3251
+ const workspaceId = c.req.param("workspaceId");
3252
+ const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
3253
+ const parsed = SessionRetryRequest.safeParse(await c.req.json().catch(() => null));
3254
+ if (!parsed.success) throw new HTTPException(400, { message: "invalid session retry request" });
3255
+ try {
3256
+ return c.json(
3257
+ await retryFailedSession(deps, grant, workspaceId, c.req.param("sessionId"), parsed.data),
3258
+ );
3259
+ } catch (error) {
3260
+ if (error instanceof SessionRetryConflictError)
3261
+ return c.json({ code: error.code, message: error.message }, 409);
3262
+ return commandConflictResponse(c, error);
3263
+ }
3264
+ });
3265
+
3062
3266
  app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/control", async (c) => {
3063
3267
  const workspaceId = c.req.param("workspaceId");
3064
3268
  const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
@@ -3121,10 +3325,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
3121
3325
  reasoningEffort: payload.reasoningEffort ?? null,
3122
3326
  latencyMode: payload.latencyMode ?? null,
3123
3327
  mcpCredentialUpdates: payload.mcpCredentialUpdates ?? [],
3124
- connectionAuthorities: payload.connectionAuthorities,
3125
- ...(payload.selectedHostMcpDelegations
3126
- ? { selectedHostMcpDelegations: payload.selectedHostMcpDelegations }
3127
- : {}),
3328
+ connectionAccounts: payload.connectionAccounts,
3128
3329
  ...(payload.personalResourceAttachment
3129
3330
  ? { personalResourceAttachment: payload.personalResourceAttachment }
3130
3331
  : {}),
@@ -3140,6 +3341,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
3140
3341
  } catch (error) {
3141
3342
  return commandConflictResponse(c, error);
3142
3343
  }
3344
+ recordAcceptedApiAdmission(deps.observability, result);
3143
3345
  return c.json(result, 202);
3144
3346
  });
3145
3347
 
@@ -3163,6 +3365,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
3163
3365
  } catch (error) {
3164
3366
  return commandConflictResponse(c, error);
3165
3367
  }
3368
+ recordAcceptedApiAdmission(deps.observability, result);
3166
3369
  return c.json(result, 202);
3167
3370
  });
3168
3371
 
@@ -3206,10 +3409,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
3206
3409
  reasoningEffort: event.payload.reasoningEffort ?? null,
3207
3410
  latencyMode: event.payload.latencyMode ?? null,
3208
3411
  mcpCredentialUpdates: event.payload.mcpCredentialUpdates ?? [],
3209
- connectionAuthorities: event.payload.connectionAuthorities,
3210
- ...(event.payload.selectedHostMcpDelegations
3211
- ? { selectedHostMcpDelegations: event.payload.selectedHostMcpDelegations }
3212
- : {}),
3412
+ connectionAccounts: event.payload.connectionAccounts,
3213
3413
  ...(event.payload.personalResourceAttachment
3214
3414
  ? { personalResourceAttachment: event.payload.personalResourceAttachment }
3215
3415
  : {}),
@@ -3225,6 +3425,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
3225
3425
  } catch (error) {
3226
3426
  return commandConflictResponse(c, error);
3227
3427
  }
3428
+ recordAcceptedApiAdmission(deps.observability, result);
3228
3429
  return c.json(result.accepted, 202);
3229
3430
  }
3230
3431
 
@@ -4566,6 +4767,7 @@ export function sessionAuthorizationOperationForHttp(
4566
4767
  if (suffix === "/lineage" && verb === "GET") return "session.lineage.read";
4567
4768
  if (suffix === "/background-commands" && verb === "GET") return "session.read";
4568
4769
  if (suffix === "/model-context" && verb === "GET") return "session.read";
4770
+ if (suffix === "/codex-accounts" && verb === "GET") return "session.read";
4569
4771
  if (/^\/background-commands\/[^/]+$/.test(suffix) && verb === "DELETE") {
4570
4772
  return "session.control";
4571
4773
  }
@@ -4632,6 +4834,8 @@ export function sessionAuthorizationOperationForHttp(
4632
4834
  // acceptSessionUserMessageWithOutcome after the body is parsed.
4633
4835
  if (suffix === "/composer-draft/submit" && verb === "POST") return "session.append";
4634
4836
  if (suffix === "/control" && verb === "POST") return "session.control";
4837
+ if (suffix === "/retry" && verb === "POST") return "session.control";
4838
+ if (suffix === "/sandbox-recovery" && ["GET", "POST"].includes(verb)) return "session.control";
4635
4839
  if (suffix === "/steer" && verb === "POST") return "session.steer";
4636
4840
  if (suffix === "/human-input-requests" && verb === "GET") {
4637
4841
  return "session.human_input.read";
@@ -4795,7 +4999,7 @@ function eventEnumList<T extends string>(
4795
4999
  });
4796
5000
  }
4797
5001
 
4798
- function sessionListQuery(
5002
+ export function sessionListQuery(
4799
5003
  query: Record<string, string>,
4800
5004
  allowCursor = true,
4801
5005
  ): {
@@ -4806,6 +5010,8 @@ function sessionListQuery(
4806
5010
  search: string | undefined;
4807
5011
  pinsOnly: boolean;
4808
5012
  archivedOnly: boolean;
5013
+ sortBy: "updatedAt" | "createdAt" | "name" | undefined;
5014
+ archiveStatus: "active" | "archived" | "all" | undefined;
4809
5015
  channelId: string | null | undefined;
4810
5016
  createdBy: { kind: "subject" | "service"; subjectId: string } | undefined;
4811
5017
  updatedFrom: Date | undefined;
@@ -4852,6 +5058,20 @@ function sessionListQuery(
4852
5058
  throw new HTTPException(400, { message: 'archivedOnly must be the literal "true"' });
4853
5059
  }
4854
5060
  const archivedOnly = query.archivedOnly === "true";
5061
+ const sortBy = z.enum(["updatedAt", "createdAt", "name"]).optional().safeParse(query.sortBy);
5062
+ const archiveStatus = z
5063
+ .enum(["active", "archived", "all"])
5064
+ .optional()
5065
+ .safeParse(query.archiveStatus);
5066
+ if (!sortBy.success || !archiveStatus.success) {
5067
+ throw new HTTPException(400, { message: "Invalid session sortBy or archiveStatus" });
5068
+ }
5069
+ if (archivedOnly && archiveStatus.data !== undefined && archiveStatus.data !== "archived") {
5070
+ throw new HTTPException(400, { message: "archivedOnly conflicts with archiveStatus" });
5071
+ }
5072
+ if (pinsOnly && archiveStatus.data === "archived") {
5073
+ throw new HTTPException(400, { message: "pinsOnly cannot be combined with archived status" });
5074
+ }
4855
5075
  const channelId = query.channelId;
4856
5076
  if (
4857
5077
  channelId !== undefined &&
@@ -4956,6 +5176,8 @@ function sessionListQuery(
4956
5176
  search: search || undefined,
4957
5177
  pinsOnly,
4958
5178
  archivedOnly,
5179
+ sortBy: sortBy.data,
5180
+ archiveStatus: archiveStatus.data,
4959
5181
  channelId: channelId === undefined ? undefined : channelId === "null" ? null : channelId,
4960
5182
  createdBy:
4961
5183
  createdByKind && createdBySubjectId
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import type { Context, Hono } from "hono";
3
3
  import { HTTPException } from "hono/http-exception";
4
4
  import { SkillFile, type SkillWriteReceipt } from "@opengeni/contracts";
5
+ import { readSkillRemovalScope } from "@opengeni/db";
5
6
  import {
6
7
  approveSkill,
7
8
  rejectSkill,
@@ -33,6 +34,7 @@ const saveRequest = z
33
34
  .strict();
34
35
  const revisionRequest = z
35
36
  .object({
37
+ removalOperationId: z.uuid().optional(),
36
38
  operationId: z.uuid(),
37
39
  revisionId: z.uuid(),
38
40
  expectedRevisionId: z.uuid().nullable(),
@@ -180,8 +182,22 @@ export function registerSkillContentRoutes(app: Hono, deps: ApiRouteDeps): void
180
182
  { accountId: access.grant.accountId, workspaceId, subjectId: access.grant.subjectId },
181
183
  skillId,
182
184
  );
183
- if (!current) throw new HTTPException(404, { message: "Skill not found" });
184
- authorizePreferenceRegistryScopeMutation(access, current.scope);
185
+ const scope =
186
+ current?.scope ??
187
+ (operation === "approve" && request.removalOperationId
188
+ ? await readSkillRemovalScope(
189
+ deps.db,
190
+ {
191
+ accountId: access.grant.accountId,
192
+ workspaceId,
193
+ subjectId: access.grant.subjectId,
194
+ },
195
+ skillId,
196
+ request.operationId,
197
+ )
198
+ : null);
199
+ if (!scope) throw new HTTPException(404, { message: "Skill not found" });
200
+ authorizePreferenceRegistryScopeMutation(access, scope);
185
201
  const apply =
186
202
  operation === "approve"
187
203
  ? approveSkill
@@ -214,7 +230,7 @@ async function skillMutation(run: () => Promise<SkillWriteReceipt>): Promise<Ski
214
230
  let current: unknown = error;
215
231
  for (let depth = 0; depth < 4 && current && typeof current === "object"; depth++) {
216
232
  const candidate = current as { code?: unknown; cause?: unknown };
217
- if (candidate.code === "23505" || candidate.code === "40001")
233
+ if (candidate.code === "23505" || candidate.code === "40001" || candidate.code === "55P03")
218
234
  throw new HTTPException(409, {
219
235
  message:
220
236
  "Skill changed or this operation key was reused. Reload and reconcile the change.",
@@ -2,8 +2,10 @@ import {
2
2
  CreateSocialConnectionRequest,
3
3
  CreateSocialPostRequest,
4
4
  OAuthStartResponse,
5
+ OrganizationIntegrationDeniedError,
5
6
  SocialOAuthStartRequest,
6
7
  } from "@opengeni/contracts";
8
+ import { withOrganizationIntegrationAcquisition } from "@opengeni/db/organization-integration-policy";
7
9
  import {
8
10
  createSocialConnection,
9
11
  createSocialPost,
@@ -47,19 +49,25 @@ export function registerSocialRoutes(app: Hono, deps: ApiRouteDeps): void {
47
49
  const payload = CreateSocialConnectionRequest.parse(await c.req.json());
48
50
  try {
49
51
  return c.json(
50
- await createSocialConnection(db, {
51
- accountId: grant.accountId,
52
- workspaceId,
53
- provider: payload.provider,
54
- accountHandle: payload.accountHandle,
55
- accountName: payload.accountName ?? null,
56
- externalAccountId: payload.externalAccountId ?? null,
57
- status: payload.status,
58
- scopes: payload.scopes,
59
- credentialRef: payload.credentialRef ?? null,
60
- tokenMetadata: payload.tokenMetadata,
61
- metadata: payload.metadata,
62
- }),
52
+ await withOrganizationIntegrationAcquisition(
53
+ db,
54
+ { accountId: grant.accountId, workspaceId },
55
+ [payload.provider === "x" || payload.provider === "reddit" ? payload.provider : null],
56
+ (tx) =>
57
+ createSocialConnection(tx, {
58
+ accountId: grant.accountId,
59
+ workspaceId,
60
+ provider: payload.provider,
61
+ accountHandle: payload.accountHandle,
62
+ accountName: payload.accountName ?? null,
63
+ externalAccountId: payload.externalAccountId ?? null,
64
+ status: payload.status,
65
+ scopes: payload.scopes,
66
+ credentialRef: payload.credentialRef ?? null,
67
+ tokenMetadata: payload.tokenMetadata,
68
+ metadata: payload.metadata,
69
+ }),
70
+ ),
63
71
  201,
64
72
  );
65
73
  } catch (error) {
@@ -222,6 +230,9 @@ function parseConnectionIds(raw: string | undefined): string[] | undefined {
222
230
  }
223
231
 
224
232
  function socialHttpException(error: unknown): HTTPException {
233
+ if (error instanceof OrganizationIntegrationDeniedError) {
234
+ return new HTTPException(403, { message: error.message });
235
+ }
225
236
  const message = error instanceof Error ? error.message : String(error);
226
237
  if (message.includes("not found")) {
227
238
  return new HTTPException(404, { message });