@opengeni/api-router 2.4.2-canary.0 → 2.6.3-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 (63) hide show
  1. package/dist/app.js +1 -1
  2. package/dist/auth/managed-auth-attempt-context.d.ts +5 -1
  3. package/dist/auth/managed-auth.d.ts +24 -1
  4. package/dist/{chunk-L7GVVSSQ.js → chunk-XTLI3CBH.js} +2884 -483
  5. package/dist/chunk-XTLI3CBH.js.map +1 -0
  6. package/dist/http/sse.d.ts +9 -0
  7. package/dist/index.js +104 -4
  8. package/dist/index.js.map +1 -1
  9. package/dist/integrations/slack-interactions.d.ts +1 -1
  10. package/dist/interaction-metrics.d.ts +2 -0
  11. package/dist/mcp/company-brain-governed-writes.d.ts +1 -1
  12. package/dist/mcp/company-profile-agent-admin.d.ts +6 -6
  13. package/dist/mcp/remember.d.ts +2 -2
  14. package/dist/mcp/server.d.ts +34 -0
  15. package/dist/mcp/session-view.d.ts +1 -0
  16. package/dist/mcp/session-wait.d.ts +19 -0
  17. package/dist/routes/api-keys.d.ts +2 -0
  18. package/dist/routes/browser-sessions.d.ts +1 -0
  19. package/dist/routes/computer-sessions.d.ts +12 -0
  20. package/dist/routes/managed-auth-session-sets.d.ts +7 -0
  21. package/dist/routes/sessions.d.ts +17 -5
  22. package/dist/routes/workspaces.d.ts +1 -1
  23. package/dist/sandbox/metrics-ingestion.d.ts +16 -0
  24. package/dist/work-discovery-observability.d.ts +33 -0
  25. package/dist/workspace-delete-observability.d.ts +10 -0
  26. package/package.json +19 -19
  27. package/src/app.ts +172 -20
  28. package/src/auth/managed-auth-attempt-context.ts +40 -3
  29. package/src/auth/managed-auth-session-adapter.ts +1 -0
  30. package/src/auth/managed-auth.ts +164 -4
  31. package/src/http/sse.ts +279 -45
  32. package/src/integrations/oauth-client.ts +8 -1
  33. package/src/integrations/provider-oauth.ts +12 -2
  34. package/src/interaction-metrics.ts +30 -0
  35. package/src/mcp/company-brain-governed-writes.ts +38 -23
  36. package/src/mcp/company-profile-agent-admin.ts +7 -7
  37. package/src/mcp/remember.ts +19 -8
  38. package/src/mcp/server.ts +621 -54
  39. package/src/mcp/session-wait.ts +56 -8
  40. package/src/routes/api-integrations.ts +2 -2
  41. package/src/routes/api-keys.ts +149 -7
  42. package/src/routes/browser-sessions.ts +10 -2
  43. package/src/routes/capabilities.ts +3 -3
  44. package/src/routes/codex.ts +483 -74
  45. package/src/routes/company-profile.ts +64 -0
  46. package/src/routes/computer-sessions.ts +103 -1
  47. package/src/routes/integration-facets.ts +8 -5
  48. package/src/routes/interaction-resources.ts +7 -1
  49. package/src/routes/managed-auth-session-sets.ts +199 -2
  50. package/src/routes/organization-memberships.ts +28 -8
  51. package/src/routes/packs.ts +5 -5
  52. package/src/routes/plugins.ts +2 -2
  53. package/src/routes/scheduled-tasks.ts +9 -0
  54. package/src/routes/sessions.ts +380 -87
  55. package/src/routes/skills.ts +3 -3
  56. package/src/routes/workspaces.ts +293 -54
  57. package/src/sandbox/channel-a.ts +10 -4
  58. package/src/sandbox/machines.ts +13 -6
  59. package/src/sandbox/metrics-ingestion.ts +157 -3
  60. package/src/sandbox/viewer.ts +20 -1
  61. package/src/work-discovery-observability.ts +121 -0
  62. package/src/workspace-delete-observability.ts +75 -0
  63. package/dist/chunk-L7GVVSSQ.js.map +0 -1
@@ -81,7 +81,51 @@ export const SESSION_WAIT_EVENT_TYPES = [
81
81
  "goal.continuation",
82
82
  ] as const satisfies readonly SessionEventType[];
83
83
 
84
- const SESSION_WAIT_EVENT_TYPE_SET: ReadonlySet<string> = new Set(SESSION_WAIT_EVENT_TYPES);
84
+ /**
85
+ * Settlement and blocking events that make a child result usable.
86
+ * Goal facts are deliberately absent: an agent can complete its durable goal
87
+ * before it emits the final assistant message and settles the turn. Completed
88
+ * agent messages are also absent because commentary messages use the same
89
+ * event type; an ordinary result-bearing `turn.completed` carries the
90
+ * authoritative final output.
91
+ */
92
+ export const SESSION_WAIT_COMPLETION_EVENT_TYPES = [
93
+ "turn.completed",
94
+ "turn.failed",
95
+ "turn.cancelled",
96
+ "turn.superseded",
97
+ "turn.capacity_waiting",
98
+ "session.requiresAction",
99
+ "session.humanInput.requested",
100
+ "session.control.paused",
101
+ "tool.auth_needed",
102
+ "credential.auth_needed",
103
+ "rig.setup.failed",
104
+ "goal.paused",
105
+ ] as const satisfies readonly SessionEventType[];
106
+
107
+ const SESSION_WAIT_COMPLETION_EVENT_TYPE_SET: ReadonlySet<string> = new Set(
108
+ SESSION_WAIT_COMPLETION_EVENT_TYPES,
109
+ );
110
+
111
+ /**
112
+ * A completed turn is result-bearing only when it carries the ordinary final
113
+ * output. Segment-limit and maintenance turns settle one execution segment
114
+ * while the session still has work to do, so they must not release a parent.
115
+ */
116
+ export function sessionWaitCompletionEventMatches(event: SessionEvent): boolean {
117
+ if (!SESSION_WAIT_COMPLETION_EVENT_TYPE_SET.has(event.type)) return false;
118
+ if (event.type !== "turn.completed") return true;
119
+ if (event.payload === null || typeof event.payload !== "object" || Array.isArray(event.payload)) {
120
+ return false;
121
+ }
122
+ const payload = event.payload as Record<string, unknown>;
123
+ return (
124
+ Object.prototype.hasOwnProperty.call(payload, "output") &&
125
+ !Object.prototype.hasOwnProperty.call(payload, "segmentLimit") &&
126
+ !Object.prototype.hasOwnProperty.call(payload, "maintenance")
127
+ );
128
+ }
85
129
 
86
130
  /** The self-session event that announces a newly pending machine input. */
87
131
  export const SESSION_WAIT_OWN_PENDING_EVENT_TYPE =
@@ -188,6 +232,10 @@ export type SessionWaitInput = {
188
232
  targets: readonly SessionWaitTarget[];
189
233
  ownSessionId: string | null;
190
234
  maxWaitMs: number;
235
+ /** Target events that end the wait. Defaults to the ordinary activity set. */
236
+ targetEventTypes?: readonly SessionEventType[] | undefined;
237
+ /** Optional payload-aware refinement applied after the event-type filter. */
238
+ targetEventMatches?: ((event: SessionEvent) => boolean) | undefined;
191
239
  source: SessionWaitSource;
192
240
  signal?: AbortSignal | undefined;
193
241
  now?: (() => number) | undefined;
@@ -206,6 +254,11 @@ export async function waitForSessionChanges(input: SessionWaitInput): Promise<Se
206
254
  let liveFanout = true;
207
255
  let waited = false;
208
256
  const ownSessionId = input.source.readOwnPendingUpdateKinds ? input.ownSessionId : null;
257
+ const targetEventTypeSet: ReadonlySet<string> = new Set(
258
+ input.targetEventTypes ?? SESSION_WAIT_EVENT_TYPES,
259
+ );
260
+ const targetEventMatches = (event: SessionEvent): boolean =>
261
+ targetEventTypeSet.has(event.type) && (input.targetEventMatches?.(event) ?? true);
209
262
 
210
263
  // One subscription per distinct session; a session may be both a target and
211
264
  // the caller's own session, in which case either condition wakes the wait.
@@ -230,11 +283,7 @@ export async function waitForSessionChanges(input: SessionWaitInput): Promise<Se
230
283
  const after = targetAfter.get(sessionId);
231
284
  for (const event of events) {
232
285
  if (event.sessionId !== sessionId) continue;
233
- if (
234
- after !== undefined &&
235
- event.sequence > after &&
236
- SESSION_WAIT_EVENT_TYPE_SET.has(event.type)
237
- ) {
286
+ if (after !== undefined && event.sequence > after && targetEventMatches(event)) {
238
287
  return true;
239
288
  }
240
289
  if (sessionId === ownSessionId && event.type === SESSION_WAIT_OWN_PENDING_EVENT_TYPE) {
@@ -274,8 +323,7 @@ export async function waitForSessionChanges(input: SessionWaitInput): Promise<Se
274
323
  const changed: SessionWaitTargetResult[] = [];
275
324
  for (const { target, read } of targetReads) {
276
325
  const events = read.events.filter(
277
- (event) =>
278
- event.sequence > target.afterSequence && SESSION_WAIT_EVENT_TYPE_SET.has(event.type),
326
+ (event) => event.sequence > target.afterSequence && targetEventMatches(event),
279
327
  );
280
328
  if (events.length === 0) continue;
281
329
  changed.push({
@@ -148,7 +148,7 @@ export function registerApiIntegrationRoutes(
148
148
 
149
149
  app.post("/v1/workspaces/:workspaceId/integrations/install", async (c) => {
150
150
  const workspaceId = c.req.param("workspaceId");
151
- const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
151
+ const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
152
152
  const payload = InstallApiIntegrationRequest.parse(await c.req.json());
153
153
  const resolved = await resolveForRoute({
154
154
  deps,
@@ -261,7 +261,7 @@ export function registerApiIntegrationRoutes(
261
261
  "/v1/workspaces/:workspaceId/integrations/:capabilityId/instances/:instanceKey",
262
262
  async (c) => {
263
263
  const workspaceId = c.req.param("workspaceId");
264
- const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
264
+ const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
265
265
  const capabilityId = decodeURIComponent(c.req.param("capabilityId"));
266
266
  const instanceKey = decodeURIComponent(c.req.param("instanceKey"));
267
267
  const payload = UninstallApiIntegrationRequest.parse(await c.req.json());
@@ -1,12 +1,41 @@
1
- import { CreateApiKeyRequest, CreateApiKeyResponse, Permission } from "@opengeni/contracts";
2
- import { createApiKey, listApiKeys, revokeApiKey } from "@opengeni/db";
1
+ import {
2
+ CreateApiKeyRequest,
3
+ CreateApiKeyResponse,
4
+ CreateOrganizationApiKeyRequest,
5
+ Permission,
6
+ type AccessContext,
7
+ } from "@opengeni/contracts";
8
+ import {
9
+ createApiKey,
10
+ createOrganizationApiKey as createOrganizationApiKeyRecord,
11
+ listApiKeys,
12
+ listOrganizationApiKeys,
13
+ OrganizationApiKeyLimitExceededError,
14
+ revokeApiKey,
15
+ revokeOrganizationApiKey,
16
+ } from "@opengeni/db";
17
+ import { configuredStaticUsageLimits } from "@opengeni/config";
3
18
  import { zValidator } from "@hono/zod-validator";
4
19
  import type { Hono } from "hono";
5
20
  import { HTTPException } from "hono/http-exception";
6
21
  import type { ApiRouteDeps } from "@opengeni/core";
7
- import { requireAccessGrant } from "@opengeni/core";
22
+ import {
23
+ accountScopedApiKeyWorkspaceAuthority,
24
+ requireAccessContext,
25
+ requireAccessGrant,
26
+ requireAccessGrantAuthorization,
27
+ type AccessGrantAuthorization,
28
+ } from "@opengeni/core";
8
29
  import { requireLimit } from "@opengeni/core";
9
30
 
31
+ export const organizationApiKeyPermissions: Permission[] = [
32
+ "account:read",
33
+ "workspace:create",
34
+ "workspace:read",
35
+ "workspace:admin",
36
+ "api_keys:manage",
37
+ ];
38
+
10
39
  export function registerApiKeyRoutes(app: Hono, deps: ApiRouteDeps): void {
11
40
  app.get("/v1/workspaces/:workspaceId/api-keys", async (c) => {
12
41
  const workspaceId = c.req.param("workspaceId");
@@ -19,11 +48,17 @@ export function registerApiKeyRoutes(app: Hono, deps: ApiRouteDeps): void {
19
48
  zValidator("json", CreateApiKeyRequest.omit({ workspaceId: true })),
20
49
  async (c) => {
21
50
  const workspaceId = c.req.param("workspaceId");
22
- const grant = await requireAccessGrant(c, deps, workspaceId, "api_keys:manage");
51
+ const authorization = await requireAccessGrantAuthorization(
52
+ c,
53
+ deps,
54
+ workspaceId,
55
+ "api_keys:manage",
56
+ );
57
+ const grant = authorization.grant;
23
58
  const body = c.req.valid("json");
24
59
  const permissions: Permission[] =
25
60
  body.permissions.length > 0 ? (body.permissions as Permission[]) : ["workspace:read"];
26
- ensureDelegablePermissions(grant.permissions, permissions);
61
+ ensureDelegablePermissions(authorization, permissions);
27
62
  await requireLimit(deps, {
28
63
  accountId: grant.accountId,
29
64
  workspaceId,
@@ -51,12 +86,90 @@ export function registerApiKeyRoutes(app: Hono, deps: ApiRouteDeps): void {
51
86
  await requireAccessGrant(c, deps, workspaceId, "api_keys:manage");
52
87
  return c.json(await revokeApiKey(deps.db, workspaceId, c.req.param("apiKeyId")));
53
88
  });
89
+
90
+ app.get("/v1/organizations/:organizationId/api-keys", async (c) => {
91
+ const organizationId = c.req.param("organizationId");
92
+ const context = await requireAccessContext(c, deps);
93
+ requireOrganizationApiKeyControlPermission(context, organizationId);
94
+ return c.json({ apiKeys: await listOrganizationApiKeys(deps.db, organizationId) });
95
+ });
96
+
97
+ app.post(
98
+ "/v1/organizations/:organizationId/api-keys",
99
+ zValidator("json", CreateOrganizationApiKeyRequest),
100
+ async (c) => {
101
+ const organizationId = c.req.param("organizationId");
102
+ const context = await requireAccessContext(c, deps);
103
+ requireOrganizationApiKeyControlPermission(context, organizationId);
104
+ const body = c.req.valid("json");
105
+ const token = generateApiKeyToken();
106
+ try {
107
+ const apiKey = await createOrganizationApiKeyRecord(deps.db, {
108
+ accountId: organizationId,
109
+ name: body.name,
110
+ description: body.description ?? null,
111
+ prefix: token.slice(0, 14),
112
+ keyHash: await sha256Hex(token),
113
+ permissions: organizationApiKeyPermissions,
114
+ expiresAt: body.expiresAt ? new Date(body.expiresAt) : null,
115
+ maxActiveKeys: organizationApiKeyLimit(deps),
116
+ rotationSourceApiKeyId: authenticatedApiKeyId(context),
117
+ });
118
+ return c.json(CreateApiKeyResponse.parse({ apiKey, token }), 201);
119
+ } catch (error) {
120
+ if (error instanceof OrganizationApiKeyLimitExceededError) {
121
+ throw new HTTPException(429, { message: error.message });
122
+ }
123
+ throw error;
124
+ }
125
+ },
126
+ );
127
+
128
+ app.delete("/v1/organizations/:organizationId/api-keys/:apiKeyId", async (c) => {
129
+ const organizationId = c.req.param("organizationId");
130
+ const context = await requireAccessContext(c, deps);
131
+ requireOrganizationApiKeyControlPermission(context, organizationId);
132
+ const apiKey = await revokeOrganizationApiKey(deps.db, organizationId, c.req.param("apiKeyId"));
133
+ if (!apiKey) {
134
+ throw new HTTPException(404, { message: "API key not found" });
135
+ }
136
+ return c.json(apiKey);
137
+ });
138
+ }
139
+
140
+ function requireAccountPermission(
141
+ context: AccessContext,
142
+ accountId: string,
143
+ permission: Permission,
144
+ ): void {
145
+ const grant = context.accountGrants.find((candidate) => candidate.accountId === accountId);
146
+ if (
147
+ !grant ||
148
+ (!grant.permissions.includes(permission) && !grant.permissions.includes("account:admin"))
149
+ ) {
150
+ throw new HTTPException(403, { message: `missing permission: ${permission}` });
151
+ }
54
152
  }
55
153
 
56
- function ensureDelegablePermissions(grantPermissions: Permission[], requested: Permission[]): void {
154
+ function ensureDelegablePermissions(
155
+ authorization: AccessGrantAuthorization,
156
+ requested: Permission[],
157
+ ): void {
158
+ const grantPermissions = authorization.grant.permissions;
57
159
  if (grantPermissions.includes("workspace:admin")) {
160
+ const accountLiteralPermissions = new Set<Permission>([
161
+ "account:read",
162
+ "account:admin",
163
+ "workspace:create",
164
+ "billing:read",
165
+ "billing:manage",
166
+ ]);
167
+ const workspaceLiteralPermissions = new Set<Permission>(["members:manage", "secrets:read"]);
58
168
  const highTrustMissing = requested.filter(
59
- (permission) => permission === "secrets:read" && !grantPermissions.includes("secrets:read"),
169
+ (permission) =>
170
+ (accountLiteralPermissions.has(permission) &&
171
+ !authorization.accountGrant?.permissions.includes(permission)) ||
172
+ (workspaceLiteralPermissions.has(permission) && !grantPermissions.includes(permission)),
60
173
  );
61
174
  if (highTrustMissing.length === 0) return;
62
175
  throw new HTTPException(403, {
@@ -71,6 +184,35 @@ function ensureDelegablePermissions(grantPermissions: Permission[], requested: P
71
184
  }
72
185
  }
73
186
 
187
+ function requireOrganizationApiKeyControlPermission(
188
+ context: AccessContext,
189
+ organizationId: string,
190
+ ): void {
191
+ requireAccountPermission(context, organizationId, "api_keys:manage");
192
+ if (!context.subjectId.startsWith("api_key:")) return;
193
+ const authority = accountScopedApiKeyWorkspaceAuthority(context);
194
+ if (
195
+ !authority ||
196
+ authority.accountId !== organizationId ||
197
+ !authority.permissions.includes("api_keys:manage")
198
+ ) {
199
+ throw new HTTPException(403, { message: "organization API key authority required" });
200
+ }
201
+ }
202
+
203
+ function authenticatedApiKeyId(context: AccessContext): string | null {
204
+ return context.subjectId.startsWith("api_key:")
205
+ ? context.subjectId.slice("api_key:".length)
206
+ : null;
207
+ }
208
+
209
+ function organizationApiKeyLimit(deps: ApiRouteDeps): number | null {
210
+ if (deps.settings.usageLimitsMode !== "static" && deps.settings.usageLimitsMode !== "managed") {
211
+ return null;
212
+ }
213
+ return configuredStaticUsageLimits(deps.settings).maxApiKeysPerWorkspace ?? null;
214
+ }
215
+
74
216
  function generateApiKeyToken(): string {
75
217
  const bytes = new Uint8Array(32);
76
218
  crypto.getRandomValues(bytes);
@@ -3435,7 +3435,14 @@ async function ensureHeadedBrowserDisplayStack(
3435
3435
  headless: boolean,
3436
3436
  linkedComputer: boolean,
3437
3437
  ): Promise<void> {
3438
- if (!browserNeedsStandaloneDisplayStack({ headless, linkedComputer })) return;
3438
+ if (
3439
+ !browserNeedsStandaloneDisplayStack({
3440
+ headless,
3441
+ linkedComputer,
3442
+ nativeBrowserControl: typeof session.ensureBrowserControl === "function",
3443
+ })
3444
+ )
3445
+ return;
3439
3446
  if (typeof session.exec !== "function" && typeof session.execCommand !== "function") return;
3440
3447
  await ensureDisplayStack(session, {
3441
3448
  telemetryContext: { callerKind: "viewer" },
@@ -3445,8 +3452,9 @@ async function ensureHeadedBrowserDisplayStack(
3445
3452
  export function browserNeedsStandaloneDisplayStack(input: {
3446
3453
  headless: boolean;
3447
3454
  linkedComputer: boolean;
3455
+ nativeBrowserControl: boolean;
3448
3456
  }): boolean {
3449
- return !input.headless && !input.linkedComputer;
3457
+ return !input.headless && !input.linkedComputer && !input.nativeBrowserControl;
3450
3458
  }
3451
3459
 
3452
3460
  async function ensureLinkedComputerController(
@@ -32,7 +32,7 @@ export function registerCapabilityRoutes(app: Hono, deps: ApiRouteDeps): void {
32
32
 
33
33
  app.post("/v1/workspaces/:workspaceId/capabilities", async (c) => {
34
34
  const workspaceId = c.req.param("workspaceId");
35
- const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
35
+ const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
36
36
  const payload = CreateCapabilityCatalogItemRequest.parse(await c.req.json());
37
37
  return c.json(
38
38
  await createCatalogItem({ db, accountId: grant.accountId, workspaceId, payload }),
@@ -62,7 +62,7 @@ export function registerCapabilityRoutes(app: Hono, deps: ApiRouteDeps): void {
62
62
 
63
63
  app.post("/v1/workspaces/:workspaceId/capabilities/:capabilityId/enable", async (c) => {
64
64
  const workspaceId = c.req.param("workspaceId");
65
- const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
65
+ const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
66
66
  const payload = EnableCapabilityRequest.parse(await c.req.json());
67
67
  const installation = await enableCapability({
68
68
  db,
@@ -78,7 +78,7 @@ export function registerCapabilityRoutes(app: Hono, deps: ApiRouteDeps): void {
78
78
 
79
79
  app.post("/v1/workspaces/:workspaceId/capabilities/:capabilityId/disable", async (c) => {
80
80
  const workspaceId = c.req.param("workspaceId");
81
- const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
81
+ const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
82
82
  const installation = await disableCapability({
83
83
  db,
84
84
  accountId: grant.accountId,