@opengeni/api-router 2.2.0-canary.0 → 2.3.2-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 (39) hide show
  1. package/dist/app.js +1 -1
  2. package/dist/auth/managed-auth.d.ts +5 -2
  3. package/dist/auth/managed-email.d.ts +29 -0
  4. package/dist/auth/organization-user-setup.d.ts +57 -0
  5. package/dist/{chunk-3TP54PPX.js → chunk-IBV7Z6F4.js} +3545 -2009
  6. package/dist/chunk-IBV7Z6F4.js.map +1 -0
  7. package/dist/index.js +1 -1
  8. package/dist/integrations/slack-app-home.d.ts +1 -1
  9. package/dist/mcp/server.d.ts +1 -1
  10. package/dist/mcp/session-view.d.ts +1 -0
  11. package/dist/routes/automations.d.ts +13 -0
  12. package/dist/routes/insights.d.ts +2 -1
  13. package/dist/routes/managed-onboarding.d.ts +29 -0
  14. package/dist/routes/pr-review-github.d.ts +3 -0
  15. package/package.json +18 -18
  16. package/src/app.ts +56 -5
  17. package/src/auth/managed-auth.ts +29 -34
  18. package/src/auth/managed-email.ts +174 -0
  19. package/src/auth/organization-user-setup.ts +217 -0
  20. package/src/http/auth.ts +15 -0
  21. package/src/http/sse.ts +62 -13
  22. package/src/integrations/slack-app-home.ts +2 -2
  23. package/src/mcp/company-brain-governed-writes.ts +4 -4
  24. package/src/mcp/company-profile-agent-admin.ts +11 -18
  25. package/src/mcp/remember.ts +4 -4
  26. package/src/mcp/server.ts +50 -4
  27. package/src/mcp/session-view.ts +8 -2
  28. package/src/routes/automations.ts +3 -3
  29. package/src/routes/documents.ts +2 -0
  30. package/src/routes/insights.ts +61 -19
  31. package/src/routes/managed-onboarding.ts +317 -0
  32. package/src/routes/organization-memberships.ts +212 -155
  33. package/src/routes/pr-review-github.ts +844 -0
  34. package/src/routes/pr-review.ts +20 -0
  35. package/src/routes/rigs.ts +37 -4
  36. package/src/routes/sessions.ts +101 -0
  37. package/src/sandbox/channel-a.ts +34 -7
  38. package/src/sandbox/viewer.ts +47 -11
  39. package/dist/chunk-3TP54PPX.js.map +0 -1
@@ -44,9 +44,11 @@ import {
44
44
  PrReviewProviderRepositoryError,
45
45
  verifyPrReviewProviderRepository,
46
46
  } from "../integrations/pr-review-provider";
47
+ import { registerPrReviewGitHubRoutes } from "./pr-review-github";
47
48
 
48
49
  export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
49
50
  const { db, settings } = deps;
51
+ registerPrReviewGitHubRoutes(app, deps);
50
52
 
51
53
  app.get("/v1/workspaces/:workspaceId/pr-review/registrations", async (c) => {
52
54
  const workspaceId = c.req.param("workspaceId");
@@ -144,6 +146,24 @@ export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
144
146
  throw new HTTPException(404, {
145
147
  message: "PR Review app registration not found",
146
148
  });
149
+ if (existing.credentialKind === "managed_github_app") {
150
+ if (
151
+ payload.accessToken !== undefined ||
152
+ payload.privateKey !== undefined ||
153
+ payload.webhookSecret !== undefined ||
154
+ payload.webhookUsername !== undefined ||
155
+ payload.accessTokenExpiresAt !== undefined
156
+ ) {
157
+ throw new HTTPException(422, {
158
+ message: "OpenGeni Lens credentials are managed by the deployment",
159
+ });
160
+ }
161
+ if (payload.status === "active") {
162
+ throw new HTTPException(409, {
163
+ message: "Reconnect OpenGeni Lens to reactivate and resync its repositories",
164
+ });
165
+ }
166
+ }
147
167
  if (payload.accessToken && existing.credentialKind !== "provider_token") {
148
168
  throw new HTTPException(422, {
149
169
  message: "GitHub App registrations accept private keys, not access tokens",
@@ -12,6 +12,7 @@ import {
12
12
  import type { Hono } from "hono";
13
13
  import type { Context } from "hono";
14
14
  import { HTTPException } from "hono/http-exception";
15
+ import type { ZodType } from "zod";
15
16
  import {
16
17
  requireAccessGrant,
17
18
  requireAccessGrantAuthorization,
@@ -32,6 +33,38 @@ import {
32
33
  updateRigForApi,
33
34
  } from "@opengeni/core";
34
35
  import { boundedLimit } from "../http/common";
36
+ import { ApiHttpError } from "../http/api-error";
37
+
38
+ async function parseRigRequest<T>(c: Context, schema: ZodType<T>, label: string): Promise<T> {
39
+ const body: unknown = await c.req.json().catch(() => null);
40
+ const bodyRecord = body && typeof body === "object" ? (body as Record<string, unknown>) : null;
41
+ const payloadRecord =
42
+ bodyRecord?.payload && typeof bodyRecord.payload === "object"
43
+ ? (bodyRecord.payload as Record<string, unknown>)
44
+ : null;
45
+ const imageOverrideUnsupported = Boolean(
46
+ (bodyRecord && Object.hasOwn(bodyRecord, "image")) ||
47
+ (payloadRecord && Object.hasOwn(payloadRecord, "image")),
48
+ );
49
+ const parsed = schema.safeParse(body);
50
+ if (parsed.success && !imageOverrideUnsupported) return parsed.data;
51
+ throw new ApiHttpError(422, {
52
+ code: "validation_failed",
53
+ message: imageOverrideUnsupported
54
+ ? "Rig base-image overrides are not supported; Rigs use the deployment-managed platform sandbox."
55
+ : `Invalid ${label}.`,
56
+ retryable: false,
57
+ details: {
58
+ ...(imageOverrideUnsupported ? { code: "RIG_IMAGE_OVERRIDE_UNSUPPORTED" } : {}),
59
+ fields: parsed.success
60
+ ? []
61
+ : parsed.error.issues.slice(0, 16).map((issue) => ({
62
+ path: issue.path,
63
+ code: issue.code,
64
+ })),
65
+ },
66
+ });
67
+ }
35
68
 
36
69
  export function registerRigRoutes(app: Hono, deps: ApiRouteDeps): void {
37
70
  const { db, workflowClient } = deps;
@@ -150,7 +183,7 @@ export function registerRigRoutes(app: Hono, deps: ApiRouteDeps): void {
150
183
  const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
151
184
  const grant = authorization.grant;
152
185
  requirePermission(grant, "rigs:manage");
153
- const payload = CreateRigRequest.parse(await c.req.json());
186
+ const payload = await parseRigRequest(c, CreateRigRequest, "Rig create request");
154
187
  const allowOrganization =
155
188
  payload.scope === "organization" &&
156
189
  authorization.accountGrant?.permissions.includes("account:admin") === true;
@@ -189,7 +222,7 @@ export function registerRigRoutes(app: Hono, deps: ApiRouteDeps): void {
189
222
  message: "missing permission: account:admin",
190
223
  });
191
224
  }
192
- const payload = UpdateRigRequest.parse(await c.req.json());
225
+ const payload = await parseRigRequest(c, UpdateRigRequest, "Rig update request");
193
226
  return c.json(await updateRigForApi({ db }, grant, rig, payload, { allowOrganization }));
194
227
  });
195
228
 
@@ -221,7 +254,7 @@ export function registerRigRoutes(app: Hono, deps: ApiRouteDeps): void {
221
254
  app.post("/v1/workspaces/:workspaceId/rigs/:rigId/versions", async (c) => {
222
255
  const workspaceId = c.req.param("workspaceId");
223
256
  const { grant, rig } = await requireRigMutation(c, workspaceId, "rigs:manage");
224
- const payload = RigDefinitionEditPayload.parse(await c.req.json());
257
+ const payload = await parseRigRequest(c, RigDefinitionEditPayload, "Rig version request");
225
258
  const version = await createRigVersionForApi({ db }, grant, rig, payload);
226
259
  const started = await tryStartInitialVersionVerification(rig.workspaceId, version.id);
227
260
  if (!started) c.header("OpenGeni-Rig-Verification", "deferred");
@@ -256,7 +289,7 @@ export function registerRigRoutes(app: Hono, deps: ApiRouteDeps): void {
256
289
  app.post("/v1/workspaces/:workspaceId/rigs/:rigId/changes", async (c) => {
257
290
  const workspaceId = c.req.param("workspaceId");
258
291
  const { grant, rig } = await requireRigMutation(c, workspaceId, "rigs:use");
259
- const request = ProposeRigChangeRequest.parse(await c.req.json());
292
+ const request = await parseRigRequest(c, ProposeRigChangeRequest, "Rig change request");
260
293
  const change = await proposeRigChangeForApi({ db }, grant, rig, request);
261
294
  const verification = await startChangeVerification(rig.workspaceId, change.id);
262
295
  if (!verification.started) c.header("OpenGeni-Rig-Verification", "deferred");
@@ -61,6 +61,7 @@ import {
61
61
  UpdateSessionGoalRequest,
62
62
  UpdateSessionMcpApprovalPolicyRequest,
63
63
  UpdateSessionRequest,
64
+ UpdateSessionVariableSetsRequest,
64
65
  UpdateSessionVisibilityRequest,
65
66
  UpdateSessionToolPolicyRequest,
66
67
  ViewerHeartbeatRequest,
@@ -79,6 +80,7 @@ import {
79
80
  type TerminalPtyExitedPayload,
80
81
  type TerminalPtyOutputDeltaPayload,
81
82
  type TerminalPtyStartedPayload,
83
+ type VariableSet,
82
84
  } from "@opengeni/contracts";
83
85
  import { streamTokenDegraded } from "@opengeni/config";
84
86
  import {
@@ -121,6 +123,7 @@ import {
121
123
  setSessionCodexPinInTransaction,
122
124
  withSessionCodexCapacityMutation,
123
125
  setSessionChannel,
126
+ updateSessionVariableSets,
124
127
  ChannelNotFoundError,
125
128
  setSessionAttention,
126
129
  setSessionArchive,
@@ -150,6 +153,7 @@ import {
150
153
  SessionRealtimeConflictError,
151
154
  SessionToolPolicyVersionConflictError,
152
155
  SessionContextBusyError,
156
+ SessionVariableSetSelectionUnavailableError,
153
157
  workspaceControlRequestLockTimeoutMs,
154
158
  SessionTenancyAccessError,
155
159
  SessionTenancyConflictError,
@@ -264,6 +268,7 @@ import {
264
268
  workspaceSessionToolPolicyDefaultServerIds,
265
269
  workspaceSessionToolPolicyServerIds,
266
270
  relayConfigFromSettings,
271
+ validateVariableSetAttachment,
267
272
  } from "@opengeni/core";
268
273
  import { assertSessionExists, boundedLimit } from "../http/common";
269
274
  import { sseSessionStream } from "../http/sse";
@@ -1769,6 +1774,91 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
1769
1774
  return c.json(await withEffectivePolicy(deps, workspaceId, grant.subjectId, session));
1770
1775
  });
1771
1776
 
1777
+ // Replace the complete ordered Variable Set selection. The DB mutation
1778
+ // serializes with turn claim, rejects live/shared sandbox use, and requests a
1779
+ // cold rotation before the new environment can be materialized.
1780
+ app.put("/v1/workspaces/:workspaceId/sessions/:sessionId/variable-sets", async (c) => {
1781
+ const workspaceId = c.req.param("workspaceId");
1782
+ const sessionId = c.req.param("sessionId");
1783
+ const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
1784
+ try {
1785
+ await requireSessionAuthorization(deps, grant, {
1786
+ sessionId,
1787
+ operation: "session.variable_sets.write",
1788
+ surface: "http",
1789
+ });
1790
+ } catch (error) {
1791
+ throw sessionAuthorizationHttpError(error);
1792
+ }
1793
+ // Detach still requires attach authority: it changes which protected
1794
+ // resources the session may materialize. Non-empty selections additionally
1795
+ // require use authority through validateVariableSetAttachment.
1796
+ requirePermission(grant, "variable-sets:attach");
1797
+ const parsed = UpdateSessionVariableSetsRequest.safeParse(await c.req.json().catch(() => null));
1798
+ if (!parsed.success) {
1799
+ throw new ApiHttpError(422, {
1800
+ code: "validation_failed",
1801
+ message: "Invalid session Variable Set request.",
1802
+ retryable: false,
1803
+ details: { fields: zodErrorFields(parsed.error) },
1804
+ });
1805
+ }
1806
+ const variableSets: VariableSet[] = [];
1807
+ for (const variableSetId of parsed.data.variableSetIds) {
1808
+ variableSets.push(
1809
+ await validateVariableSetAttachment({ settings, db }, grant, workspaceId, variableSetId),
1810
+ );
1811
+ }
1812
+ const result = await updateSessionVariableSets(db, {
1813
+ accountId: grant.accountId,
1814
+ workspaceId,
1815
+ sessionId,
1816
+ subjectId: grant.subjectId,
1817
+ variableSets: variableSets.map((variableSet) => ({
1818
+ id: variableSet.id,
1819
+ name: variableSet.name,
1820
+ scope: variableSet.scope,
1821
+ })),
1822
+ });
1823
+ if (result.status === "not_found") {
1824
+ throw new HTTPException(404, { message: "session not found" });
1825
+ }
1826
+ if (result.status === "invalid_variable_sets") {
1827
+ throw new ApiHttpError(422, {
1828
+ code: "validation_failed",
1829
+ message: "One or more selected Variable Sets are no longer available.",
1830
+ retryable: false,
1831
+ outcomeUnknown: false,
1832
+ details: { variableSetIds: result.variableSetIds },
1833
+ });
1834
+ }
1835
+ if (result.status === "blocked") {
1836
+ const messages = {
1837
+ turn_in_flight:
1838
+ "Variable Sets can be changed only when the session has no accepted, queued, claimed, or pending work.",
1839
+ shared_sandbox_group:
1840
+ "Variable Sets cannot be changed while this session shares a sandbox; fork it into a separate session first.",
1841
+ live_sandbox_holders:
1842
+ "Close active terminal, desktop, and sandbox operations before changing Variable Sets.",
1843
+ } as const;
1844
+ throw new HTTPException(409, { message: messages[result.reason] });
1845
+ }
1846
+ if (result.status === "updated") {
1847
+ await publishDurableSessionEvents(bus, workspaceId, sessionId, [result.event]);
1848
+ }
1849
+ const session = await getSessionForSubject(
1850
+ db,
1851
+ workspaceId,
1852
+ sessionId,
1853
+ grant.subjectId,
1854
+ relatedSessionAccessFor(c),
1855
+ );
1856
+ if (!session) {
1857
+ throw new HTTPException(404, { message: "session not found" });
1858
+ }
1859
+ return c.json(await withEffectivePolicy(deps, workspaceId, grant.subjectId, session));
1860
+ });
1861
+
1772
1862
  // Manual rename. A user-set title is permanent: the db write is
1773
1863
  // unconditional (source='user'), so it always pins the session over later
1774
1864
  // agent writes. Returns the refreshed session, mirroring GET detail.
@@ -4125,6 +4215,7 @@ export function sessionAuthorizationOperationForHttp(
4125
4215
  if (suffix === "/visibility" && verb === "PUT") return "session.visibility.write";
4126
4216
  if (suffix === "/forks" && verb === "POST") return "session.fork.create";
4127
4217
  if (suffix === "/channel" && verb === "PUT") return "session.channel.write";
4218
+ if (suffix === "/variable-sets" && verb === "PUT") return "session.variable_sets.write";
4128
4219
  if (suffix === "/tool-policy" && verb === "PUT") return "session.tool_policy.write";
4129
4220
  if (/^\/mcp-servers\/[^/]+\/approval-policy$/.test(suffix) && verb === "PATCH") {
4130
4221
  return "session.mcp.approval_policy.write";
@@ -4584,6 +4675,16 @@ export function sessionCreateErrorResponse(c: Context, error: unknown): Response
4584
4675
  // insert's FK rejection surfaces as the same 422 an unknown id gets.
4585
4676
  return c.json({ code: "SESSION_CREATE_REJECTED", message: error.message }, 422);
4586
4677
  }
4678
+ if (error instanceof SessionVariableSetSelectionUnavailableError) {
4679
+ return c.json(
4680
+ {
4681
+ code: "SESSION_CREATE_REJECTED",
4682
+ message: error.message,
4683
+ details: { variableSetIds: error.variableSetIds },
4684
+ },
4685
+ 422,
4686
+ );
4687
+ }
4587
4688
  if (error instanceof SessionSpawnDeniedError) {
4588
4689
  return c.json(
4589
4690
  sessionSpawnDenialEnvelope(error),
@@ -32,6 +32,7 @@ import {
32
32
  getSandboxSessionEnvelope,
33
33
  getLiveEnrollmentConnection,
34
34
  getSandbox,
35
+ getScheduledScopedRigVersionMetadata,
35
36
  touchLeaseHolder,
36
37
  loadWorkspaceEnvironmentForRun,
37
38
  markWarmLeaseInstanceLost,
@@ -539,19 +540,45 @@ async function withChannelAOperation<T>(
539
540
 
540
541
  // The STABLE run-environment used by both a cloud home and a machine home.
541
542
  // It also carries the per-session Codemode pointer selected below.
542
- const workspaceEnvironment = await loadWorkspaceEnvironmentForRun(db, settings, {
543
- accountId,
544
- workspaceId,
545
- variableSetId: session.environmentId,
546
- authority: { kind: "session_attach", sessionId: session.id, subjectId: ctx.subjectId },
547
- });
543
+ const workspaceEnvironmentValues: Record<string, string> = {};
544
+ const rigVersion =
545
+ session.rigId && session.rigVersionId
546
+ ? await getScheduledScopedRigVersionMetadata(
547
+ db,
548
+ {
549
+ accountId,
550
+ workspaceId,
551
+ subjectId: ctx.subjectId ?? "session-attach",
552
+ },
553
+ session.rigId,
554
+ session.rigVersionId,
555
+ )
556
+ : null;
557
+ for (const variableSetId of rigVersion?.version.defaultVariableSetIds ?? []) {
558
+ const workspaceEnvironment = await loadWorkspaceEnvironmentForRun(db, settings, {
559
+ accountId,
560
+ workspaceId,
561
+ variableSetId,
562
+ authority: { kind: "session_attach", sessionId: session.id, subjectId: ctx.subjectId },
563
+ });
564
+ Object.assign(workspaceEnvironmentValues, workspaceEnvironment?.values ?? {});
565
+ }
566
+ for (const variableSetId of session.variableSetIds) {
567
+ const workspaceEnvironment = await loadWorkspaceEnvironmentForRun(db, settings, {
568
+ accountId,
569
+ workspaceId,
570
+ variableSetId,
571
+ authority: { kind: "session_attach", sessionId: session.id, subjectId: ctx.subjectId },
572
+ });
573
+ Object.assign(workspaceEnvironmentValues, workspaceEnvironment?.values ?? {});
574
+ }
548
575
  const settingsForSession =
549
576
  session.sandboxBackend !== settings.sandboxBackend
550
577
  ? { ...settings, sandboxBackend: session.sandboxBackend }
551
578
  : settings;
552
579
  const environment = stableSandboxEnvironmentForRun(
553
580
  settingsForSession,
554
- workspaceEnvironment?.values ?? {},
581
+ workspaceEnvironmentValues,
555
582
  { workspaceId },
556
583
  );
557
584
  if (hasGitCredentialRepositorySelection(session.resources)) {
@@ -35,6 +35,7 @@ import {
35
35
  acquireLease,
36
36
  getLiveEnrollmentConnection,
37
37
  getSandbox,
38
+ getScheduledScopedRigVersionMetadata,
38
39
  getSandboxSessionEnvelope,
39
40
  heartbeatLeaseHolder,
40
41
  loadWorkspaceEnvironmentForRun,
@@ -149,16 +150,51 @@ export async function sessionAttachEnvironment(
149
150
  * the materialization audit fact. Null records the legacy service sentinel. */
150
151
  attachSubjectId: string | null,
151
152
  ): Promise<Record<string, string>> {
152
- const workspaceEnvironment = await loadWorkspaceEnvironmentForRun(
153
- services.db,
154
- services.settings,
155
- {
156
- accountId: session.accountId,
157
- workspaceId,
158
- variableSetId: session.environmentId,
159
- authority: { kind: "session_attach", sessionId: session.id, subjectId: attachSubjectId },
160
- },
161
- );
153
+ const workspaceEnvironmentValues: Record<string, string> = {};
154
+ const rigVersion =
155
+ session.rigId && session.rigVersionId
156
+ ? await getScheduledScopedRigVersionMetadata(
157
+ services.db,
158
+ {
159
+ accountId: session.accountId,
160
+ workspaceId,
161
+ subjectId: attachSubjectId ?? "session-attach",
162
+ },
163
+ session.rigId,
164
+ session.rigVersionId,
165
+ )
166
+ : null;
167
+ for (const variableSetId of rigVersion?.version.defaultVariableSetIds ?? []) {
168
+ const workspaceEnvironment = await loadWorkspaceEnvironmentForRun(
169
+ services.db,
170
+ services.settings,
171
+ {
172
+ accountId: session.accountId,
173
+ workspaceId,
174
+ variableSetId,
175
+ authority: { kind: "session_attach", sessionId: session.id, subjectId: attachSubjectId },
176
+ },
177
+ );
178
+ Object.assign(workspaceEnvironmentValues, workspaceEnvironment?.values ?? {});
179
+ }
180
+ // Older persisted/test projections can omit the plural field. Preserve the
181
+ // legacy final alias as the single explicit selection until every caller is
182
+ // guaranteed to have crossed the plural contract boundary.
183
+ const explicitVariableSetIds =
184
+ session.variableSetIds ?? (session.variableSetId ? [session.variableSetId] : []);
185
+ for (const variableSetId of explicitVariableSetIds) {
186
+ const workspaceEnvironment = await loadWorkspaceEnvironmentForRun(
187
+ services.db,
188
+ services.settings,
189
+ {
190
+ accountId: session.accountId,
191
+ workspaceId,
192
+ variableSetId,
193
+ authority: { kind: "session_attach", sessionId: session.id, subjectId: attachSubjectId },
194
+ },
195
+ );
196
+ Object.assign(workspaceEnvironmentValues, workspaceEnvironment?.values ?? {});
197
+ }
162
198
  // Build the env with the SESSION's backend, not the deployment default: the
163
199
  // stable base is backend-aware (HOME = the descriptor workspaceRoot, and the
164
200
  // git token-file/askpass pointers derive from HOME), the box is established
@@ -173,7 +209,7 @@ export async function sessionAttachEnvironment(
173
209
  : services.settings;
174
210
  const environment = stableSandboxEnvironmentForRun(
175
211
  settingsForSession,
176
- workspaceEnvironment?.values ?? {},
212
+ workspaceEnvironmentValues,
177
213
  { workspaceId },
178
214
  );
179
215
  if (hasGitCredentialRepositorySelection(session.resources)) {