@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
@@ -8,11 +8,17 @@ import {
8
8
  type PersonalGitHubRepository as PersonalGitHubRepositoryContract,
9
9
  type PersonalGitHubRepositorySelectionInput,
10
10
  } from "@opengeni/contracts/personal-github";
11
+ import {
12
+ GitHubRepositoryBranchesResponse,
13
+ type GitHubRepositoryBranchesResponse as GitHubRepositoryBranchesResponseContract,
14
+ type ListGitHubRepositoryBranchesQuery,
15
+ } from "@opengeni/contracts/github-repository-contracts";
11
16
  import type { ApiRouteDeps } from "@opengeni/core";
12
17
  import {
13
18
  buildConnectionTokenResolver,
14
19
  getConnectionMetadata,
15
20
  getPersonalGitHubRepositorySelectionState,
21
+ type PersonalGitHubRepositorySelectionState as DbPersonalGitHubRepositorySelectionState,
16
22
  } from "@opengeni/db";
17
23
  import { readResponseTextBounded } from "@opengeni/network";
18
24
  import { HTTPException } from "hono/http-exception";
@@ -21,6 +27,7 @@ import JSONBig from "json-bigint";
21
27
  const GITHUB_API_VERSION = "2022-11-28";
22
28
  const GITHUB_REQUEST_TIMEOUT_MS = 10_000;
23
29
  const GITHUB_REPOSITORIES_RESPONSE_MAX_BYTES = 1024 * 1024;
30
+ const GITHUB_BRANCHES_RESPONSE_MAX_BYTES = 256 * 1024;
24
31
  const personalGitHubProviderJsonParser = JSONBig({
25
32
  storeAsString: true,
26
33
  protoAction: "error",
@@ -31,6 +38,60 @@ export type PersonalGitHubRepositoryConnection = NonNullable<
31
38
  Awaited<ReturnType<typeof getConnectionMetadata>>
32
39
  >;
33
40
 
41
+ type PersonalGitHubRepositoryProviderOperation =
42
+ | "repositories_list"
43
+ | "repository_branches_list"
44
+ | "repository_verify";
45
+
46
+ type ExpectedPersonalGitHubRepositoryAuthority = {
47
+ selectionGeneration: number;
48
+ repositoryId: string;
49
+ fullName: string;
50
+ };
51
+
52
+ type PersonalGitHubProviderJsonOptions = {
53
+ operation: PersonalGitHubRepositoryProviderOperation;
54
+ expectedRepositoryAuthority?: ExpectedPersonalGitHubRepositoryAuthority;
55
+ responseLabel?: string;
56
+ responseMaxBytes?: number;
57
+ };
58
+
59
+ export type PersonalGitHubRepositoryBranchServices = {
60
+ requireConnection: typeof requirePersonalGitHubRepositoryConnection;
61
+ getSelectionState: (
62
+ deps: ApiRouteDeps,
63
+ input: {
64
+ accountId: string;
65
+ originWorkspaceId: string;
66
+ subjectId: string;
67
+ connectionId: string;
68
+ },
69
+ ) => Promise<DbPersonalGitHubRepositorySelectionState | null>;
70
+ providerJson: (input: {
71
+ deps: ApiRouteDeps;
72
+ connection: PersonalGitHubRepositoryConnection;
73
+ subjectId: string;
74
+ url: URL;
75
+ expectedConnectionAuthorityGeneration: number;
76
+ options: PersonalGitHubProviderJsonOptions;
77
+ }) => Promise<unknown>;
78
+ };
79
+
80
+ const personalGitHubRepositoryBranchServices: PersonalGitHubRepositoryBranchServices = {
81
+ requireConnection: requirePersonalGitHubRepositoryConnection,
82
+ getSelectionState: async (deps, input) =>
83
+ await getPersonalGitHubRepositorySelectionState(deps.db, input),
84
+ providerJson: async (input) =>
85
+ await personalGitHubProviderJson(
86
+ input.deps,
87
+ input.connection,
88
+ input.subjectId,
89
+ input.url,
90
+ input.expectedConnectionAuthorityGeneration,
91
+ input.options,
92
+ ),
93
+ };
94
+
34
95
  export type PersonalGitHubRepositoryProviderErrorCode =
35
96
  | "connection_changed"
36
97
  | "connection_inactive"
@@ -40,7 +101,9 @@ export type PersonalGitHubRepositoryProviderErrorCode =
40
101
  | "provider_unavailable"
41
102
  | "repository_archived"
42
103
  | "repository_not_found"
104
+ | "repository_not_selected"
43
105
  | "repository_read_denied"
106
+ | "repository_selection_changed"
44
107
  | "repository_write_denied";
45
108
 
46
109
  export class PersonalGitHubRepositoryProviderError extends Error {
@@ -104,6 +167,7 @@ export async function listLivePersonalGitHubRepositories(
104
167
  input.subjectId,
105
168
  url,
106
169
  input.expectedConnectionAuthorityGeneration,
170
+ { operation: "repositories_list" },
107
171
  );
108
172
  if (!Array.isArray(payload) || payload.length > PERSONAL_GITHUB_REPOSITORY_CATALOG_MAX) {
109
173
  throw new PersonalGitHubRepositoryProviderError("invalid_provider_response");
@@ -124,6 +188,93 @@ export async function listLivePersonalGitHubRepositories(
124
188
  };
125
189
  }
126
190
 
191
+ export async function listLivePersonalGitHubRepositoryBranches(
192
+ deps: ApiRouteDeps,
193
+ input: {
194
+ workspaceId: string;
195
+ accountId: string;
196
+ subjectId: string;
197
+ connectionId: string;
198
+ repositoryId: string;
199
+ query: ListGitHubRepositoryBranchesQuery;
200
+ },
201
+ services: PersonalGitHubRepositoryBranchServices = personalGitHubRepositoryBranchServices,
202
+ ): Promise<GitHubRepositoryBranchesResponseContract> {
203
+ const connection = await services.requireConnection(deps, input);
204
+ const current = await requireSelectedPersonalGitHubRepository(
205
+ deps,
206
+ connection,
207
+ input,
208
+ null,
209
+ services.getSelectionState,
210
+ );
211
+ const [owner, repositoryName] = current.repository.fullName.split("/");
212
+ if (!owner || !repositoryName) {
213
+ throw new PersonalGitHubRepositoryProviderError("invalid_provider_response");
214
+ }
215
+ const url = new URL(
216
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repositoryName)}/branches`,
217
+ PERSONAL_GITHUB_API_ORIGIN,
218
+ );
219
+ url.searchParams.set("page", String(input.query.cursor));
220
+ url.searchParams.set("per_page", String(input.query.limit));
221
+ const expectedRepositoryAuthority = {
222
+ selectionGeneration: current.selectionGeneration,
223
+ repositoryId: current.repository.repositoryId,
224
+ fullName: current.repository.fullName,
225
+ };
226
+ const payload = await services.providerJson({
227
+ deps,
228
+ connection,
229
+ subjectId: input.subjectId,
230
+ url,
231
+ expectedConnectionAuthorityGeneration: current.connectionAuthorityGeneration,
232
+ options: {
233
+ operation: "repository_branches_list",
234
+ expectedRepositoryAuthority,
235
+ responseLabel: "GitHub repository branches response",
236
+ responseMaxBytes: GITHUB_BRANCHES_RESPONSE_MAX_BYTES,
237
+ },
238
+ });
239
+ if (!Array.isArray(payload) || payload.length > input.query.limit) {
240
+ throw new PersonalGitHubRepositoryProviderError("invalid_provider_response");
241
+ }
242
+ const branches: string[] = [];
243
+ const seen = new Set<string>();
244
+ for (const value of payload) {
245
+ const name =
246
+ value && typeof value === "object" && !Array.isArray(value)
247
+ ? (value as Record<string, unknown>).name
248
+ : null;
249
+ if (typeof name !== "string" || name.length === 0 || name.length > 1024 || seen.has(name)) {
250
+ throw new PersonalGitHubRepositoryProviderError("invalid_provider_response");
251
+ }
252
+ seen.add(name);
253
+ branches.push(name);
254
+ }
255
+ await requireSelectedPersonalGitHubRepository(
256
+ deps,
257
+ connection,
258
+ input,
259
+ expectedRepositoryAuthority,
260
+ services.getSelectionState,
261
+ );
262
+ const response = GitHubRepositoryBranchesResponse.safeParse({
263
+ branches: branches.map((name) => ({
264
+ name,
265
+ isDefault: name === current.repository.defaultBranch,
266
+ })),
267
+ nextCursor:
268
+ branches.length === input.query.limit && input.query.cursor < 10_000
269
+ ? input.query.cursor + 1
270
+ : null,
271
+ });
272
+ if (!response.success) {
273
+ throw new PersonalGitHubRepositoryProviderError("invalid_provider_response");
274
+ }
275
+ return response.data;
276
+ }
277
+
127
278
  /** Serial by design: one bounded provider request and one authority check at a time. */
128
279
  export async function verifyLivePersonalGitHubRepositories(
129
280
  deps: ApiRouteDeps,
@@ -154,7 +305,7 @@ export async function verifyLivePersonalGitHubRepositories(
154
305
  input.subjectId,
155
306
  url,
156
307
  input.expectedConnectionAuthorityGeneration,
157
- true,
308
+ { operation: "repository_verify" },
158
309
  );
159
310
  const repository = parsePersonalGitHubRepository(payload);
160
311
  if (
@@ -179,6 +330,58 @@ export async function verifyLivePersonalGitHubRepositories(
179
330
  return verified;
180
331
  }
181
332
 
333
+ async function requireSelectedPersonalGitHubRepository(
334
+ deps: ApiRouteDeps,
335
+ connection: PersonalGitHubRepositoryConnection,
336
+ input: {
337
+ accountId: string;
338
+ subjectId: string;
339
+ connectionId: string;
340
+ repositoryId: string;
341
+ },
342
+ expected: ExpectedPersonalGitHubRepositoryAuthority | null,
343
+ getSelectionState: PersonalGitHubRepositoryBranchServices["getSelectionState"],
344
+ ): Promise<{
345
+ connectionAuthorityGeneration: number;
346
+ selectionGeneration: number;
347
+ repository: NonNullable<
348
+ Awaited<ReturnType<typeof getPersonalGitHubRepositorySelectionState>>
349
+ >["repositories"][number];
350
+ }> {
351
+ const selection = await getSelectionState(deps, {
352
+ accountId: input.accountId,
353
+ originWorkspaceId: connection.workspaceId,
354
+ subjectId: input.subjectId,
355
+ connectionId: input.connectionId,
356
+ });
357
+ if (!selection) {
358
+ throw new PersonalGitHubRepositoryProviderError(
359
+ expected ? "repository_selection_changed" : "repository_not_selected",
360
+ );
361
+ }
362
+ const repository = selection.repositories.find(
363
+ (candidate) => candidate.repositoryId === input.repositoryId,
364
+ );
365
+ if (!repository) {
366
+ throw new PersonalGitHubRepositoryProviderError(
367
+ expected ? "repository_selection_changed" : "repository_not_selected",
368
+ );
369
+ }
370
+ if (
371
+ expected &&
372
+ (selection.selectionGeneration !== expected.selectionGeneration ||
373
+ repository.repositoryId !== expected.repositoryId ||
374
+ repository.fullName !== expected.fullName)
375
+ ) {
376
+ throw new PersonalGitHubRepositoryProviderError("repository_selection_changed");
377
+ }
378
+ return {
379
+ connectionAuthorityGeneration: selection.connectionAuthorityGeneration,
380
+ selectionGeneration: selection.selectionGeneration,
381
+ repository,
382
+ };
383
+ }
384
+
182
385
  export function personalGitHubRepositoryProviderHttpError(
183
386
  error: PersonalGitHubRepositoryProviderError,
184
387
  ): HTTPException {
@@ -194,6 +397,14 @@ export function personalGitHubRepositoryProviderHttpError(
194
397
  return new HTTPException(503, { message: "GitHub is temporarily rate limited" });
195
398
  case "repository_not_found":
196
399
  return new HTTPException(422, { message: "a selected GitHub repository is unavailable" });
400
+ case "repository_not_selected":
401
+ return new HTTPException(404, {
402
+ message: "personal GitHub repository is not selected for this connection",
403
+ });
404
+ case "repository_selection_changed":
405
+ return new HTTPException(409, {
406
+ message: "personal GitHub repository selection changed; refresh and try again",
407
+ });
197
408
  case "repository_archived":
198
409
  return new HTTPException(422, {
199
410
  message: "an archived or disabled GitHub repository cannot be selected for write access",
@@ -218,7 +429,7 @@ async function personalGitHubProviderJson(
218
429
  subjectId: string,
219
430
  url: URL,
220
431
  expectedConnectionAuthorityGeneration: number,
221
- repositoryLookup = false,
432
+ options: PersonalGitHubProviderJsonOptions,
222
433
  ): Promise<unknown> {
223
434
  if (url.origin !== PERSONAL_GITHUB_API_ORIGIN) {
224
435
  throw new PersonalGitHubRepositoryProviderError("invalid_provider_response");
@@ -229,7 +440,8 @@ async function personalGitHubProviderJson(
229
440
  subjectId,
230
441
  url,
231
442
  expectedConnectionAuthorityGeneration,
232
- repositoryLookup,
443
+ options.operation,
444
+ options.expectedRepositoryAuthority,
233
445
  false,
234
446
  );
235
447
  if (response.status === 401) {
@@ -240,7 +452,8 @@ async function personalGitHubProviderJson(
240
452
  subjectId,
241
453
  url,
242
454
  expectedConnectionAuthorityGeneration,
243
- repositoryLookup,
455
+ options.operation,
456
+ options.expectedRepositoryAuthority,
244
457
  true,
245
458
  );
246
459
  }
@@ -264,7 +477,7 @@ async function personalGitHubProviderJson(
264
477
  if (status === 429) {
265
478
  throw new PersonalGitHubRepositoryProviderError("provider_rate_limited");
266
479
  }
267
- if (status === 404 && repositoryLookup) {
480
+ if (status === 404 && options.operation !== "repositories_list") {
268
481
  throw new PersonalGitHubRepositoryProviderError("repository_not_found");
269
482
  }
270
483
  throw new PersonalGitHubRepositoryProviderError("provider_unavailable");
@@ -273,8 +486,11 @@ async function personalGitHubProviderJson(
273
486
  return parsePersonalGitHubProviderJson(
274
487
  await readResponseTextBounded(
275
488
  response,
276
- GITHUB_REPOSITORIES_RESPONSE_MAX_BYTES,
277
- repositoryLookup ? "GitHub repository response" : "GitHub repositories response",
489
+ options.responseMaxBytes ?? GITHUB_REPOSITORIES_RESPONSE_MAX_BYTES,
490
+ options.responseLabel ??
491
+ (options.operation === "repositories_list"
492
+ ? "GitHub repositories response"
493
+ : "GitHub repository response"),
278
494
  ),
279
495
  );
280
496
  } catch {
@@ -293,7 +509,8 @@ async function personalGitHubProviderFetch(
293
509
  subjectId: string,
294
510
  url: URL,
295
511
  expectedConnectionAuthorityGeneration: number,
296
- repositoryLookup: boolean,
512
+ operation: PersonalGitHubRepositoryProviderOperation,
513
+ expectedRepositoryAuthority: ExpectedPersonalGitHubRepositoryAuthority | undefined,
297
514
  forceRefresh: boolean,
298
515
  ): Promise<Response> {
299
516
  const resolver = buildConnectionTokenResolver(deps.db, deps.settings, undefined, {
@@ -305,7 +522,7 @@ async function personalGitHubProviderFetch(
305
522
  workspaceId: connection.workspaceId,
306
523
  subjectId,
307
524
  serverId: "github-personal-repository-picker",
308
- toolName: repositoryLookup ? "repository_verify" : "repositories_list",
525
+ toolName: operation,
309
526
  connectionRef: {
310
527
  connectionId: connection.id,
311
528
  providerDomain: PERSONAL_GITHUB_PROVIDER_DOMAIN,
@@ -350,6 +567,18 @@ async function personalGitHubProviderFetch(
350
567
  ) {
351
568
  throw new PersonalGitHubRepositoryProviderError("connection_changed");
352
569
  }
570
+ if (expectedRepositoryAuthority) {
571
+ const repository = authority.repositories.find(
572
+ (candidate) => candidate.repositoryId === expectedRepositoryAuthority.repositoryId,
573
+ );
574
+ if (
575
+ authority.selectionGeneration !== expectedRepositoryAuthority.selectionGeneration ||
576
+ !repository ||
577
+ repository.fullName !== expectedRepositoryAuthority.fullName
578
+ ) {
579
+ throw new PersonalGitHubRepositoryProviderError("repository_selection_changed");
580
+ }
581
+ }
353
582
  const authorization = Object.entries(credential.headers).find(
354
583
  ([name]) => name.toLowerCase() === "authorization",
355
584
  )?.[1];
@@ -1,4 +1,4 @@
1
- import { AUTOMATIC_SESSION_TITLE_FALLBACK, type Session } from "@opengeni/contracts";
1
+ import { deriveSessionDisplayTitle, type Session } from "@opengeni/contracts";
2
2
  import type { SlackHomeBlock } from "./slack-bot";
3
3
 
4
4
  const ATTENTION_LIMIT = 5;
@@ -186,7 +186,7 @@ function appendSessionGroup(
186
186
  );
187
187
  for (const session of sessions) {
188
188
  const url = sessionUrl(session.id);
189
- const title = (session.title?.trim() || AUTOMATIC_SESSION_TITLE_FALLBACK).slice(0, 180);
189
+ const title = deriveSessionDisplayTitle(session).slice(0, 180);
190
190
  blocks.push({
191
191
  type: "section",
192
192
  block_id: `opengeni_home_session_${session.id}`,
@@ -123,6 +123,7 @@ import {
123
123
  requireAccessContext,
124
124
  requireAccessGrant,
125
125
  requireSessionAuthorizationListScope,
126
+ resolveWorkspaceCatalogSettings,
126
127
  type ApiRouteDeps,
127
128
  } from "@opengeni/core";
128
129
  import { publishDurableSessionEvents } from "@opengeni/events";
@@ -1058,6 +1059,23 @@ export function registerSlackInteractionRoutes(app: Hono, deps: ApiRouteDeps): v
1058
1059
  });
1059
1060
  }
1060
1061
 
1062
+ async function withCatalogSettings(
1063
+ deps: ApiRouteDeps,
1064
+ grant: Pick<AccessGrant, "accountId" | "workspaceId">,
1065
+ ): Promise<ApiRouteDeps> {
1066
+ const catalogSourceSettings = deps.catalogSourceSettings ?? deps.settings;
1067
+ return {
1068
+ ...deps,
1069
+ catalogSourceSettings,
1070
+ settings: (
1071
+ await resolveWorkspaceCatalogSettings(deps.db, catalogSourceSettings, {
1072
+ accountId: grant.accountId,
1073
+ workspaceId: grant.workspaceId,
1074
+ })
1075
+ ).settings,
1076
+ };
1077
+ }
1078
+
1061
1079
  async function publishSlackAppHome(
1062
1080
  deps: ApiRouteDeps,
1063
1081
  installation: SlackInstallationRoute,
@@ -2152,17 +2170,22 @@ async function processSlackInboxEntry(deps: ApiRouteDeps, entry: SlackInteractio
2152
2170
  preparedAttachments,
2153
2171
  preparedModelContext,
2154
2172
  );
2155
- session = await createSessionForRequest(deps, grant, interaction.workspaceId, {
2156
- requestedSessionId: interaction.sessionReservationId,
2157
- initialMessage: prepared.entry.text,
2158
- ...(prepared.modelContext ? { modelContext: prepared.modelContext } : {}),
2159
- instructions: SLACK_SESSION_INSTRUCTIONS,
2160
- firstPartyMcpTools: slackTaskFirstPartyMcpTools(deps.settings),
2161
- resources: preparedAttachments.resources,
2162
- ...(preferredModel ? { model: preferredModel } : {}),
2163
- idempotencyKey: `slack:${entry.connectionId}:${entry.providerEventId}`,
2164
- clientEventId: `slack:${entry.providerEventId}`,
2165
- });
2173
+ session = await createSessionForRequest(
2174
+ await withCatalogSettings(deps, grant),
2175
+ grant,
2176
+ interaction.workspaceId,
2177
+ {
2178
+ requestedSessionId: interaction.sessionReservationId,
2179
+ initialMessage: prepared.entry.text,
2180
+ ...(prepared.modelContext ? { modelContext: prepared.modelContext } : {}),
2181
+ instructions: SLACK_SESSION_INSTRUCTIONS,
2182
+ firstPartyMcpTools: slackTaskFirstPartyMcpTools(deps.settings),
2183
+ resources: preparedAttachments.resources,
2184
+ ...(preferredModel ? { model: preferredModel } : {}),
2185
+ idempotencyKey: `slack:${entry.connectionId}:${entry.providerEventId}`,
2186
+ clientEventId: `slack:${entry.providerEventId}`,
2187
+ },
2188
+ );
2166
2189
  } catch (error) {
2167
2190
  if (error instanceof HTTPException) {
2168
2191
  await client.postMessage({
@@ -2741,21 +2764,26 @@ async function processSlackReactionInboxEntry(
2741
2764
  const preparedEntry = slackReactionPreparedEntry(entry, context, preparedTask);
2742
2765
  let session: Awaited<ReturnType<typeof createSessionForRequest>>;
2743
2766
  try {
2744
- session = await createSessionForRequest(deps, grant, interaction.workspaceId, {
2745
- requestedSessionId: interaction.sessionReservationId,
2746
- initialMessage: preparedEntry.text,
2747
- instructions: SLACK_SESSION_INSTRUCTIONS,
2748
- // The exact reacted message and bounded containing thread are already in
2749
- // the prompt; do not expose general Slack history tools for this trigger.
2750
- firstPartyMcpTools: resolveFirstPartyMcpToolPolicy(deps.settings).default,
2751
- resources: preparedTask.resources,
2752
- ...(preferredModel ? { model: preferredModel } : {}),
2753
- // Every reaction entry converging on this route must use the same create
2754
- // key. This closes the same-owner multi-event race while the owner check
2755
- // above prevents a different subject from winning creation authority.
2756
- idempotencyKey: `slack-interaction:${interaction.id}`,
2757
- clientEventId: `slack:${entry.providerEventId}`,
2758
- });
2767
+ session = await createSessionForRequest(
2768
+ await withCatalogSettings(deps, grant),
2769
+ grant,
2770
+ interaction.workspaceId,
2771
+ {
2772
+ requestedSessionId: interaction.sessionReservationId,
2773
+ initialMessage: preparedEntry.text,
2774
+ instructions: SLACK_SESSION_INSTRUCTIONS,
2775
+ // The exact reacted message and bounded containing thread are already in
2776
+ // the prompt; do not expose general Slack history tools for this trigger.
2777
+ firstPartyMcpTools: resolveFirstPartyMcpToolPolicy(deps.settings).default,
2778
+ resources: preparedTask.resources,
2779
+ ...(preferredModel ? { model: preferredModel } : {}),
2780
+ // Every reaction entry converging on this route must use the same create
2781
+ // key. This closes the same-owner multi-event race while the owner check
2782
+ // above prevents a different subject from winning creation authority.
2783
+ idempotencyKey: `slack-interaction:${interaction.id}`,
2784
+ clientEventId: `slack:${entry.providerEventId}`,
2785
+ },
2786
+ );
2759
2787
  // The route-wide create key converges every replica on one reserved
2760
2788
  // session, but its first writer's initial message is the only event created
2761
2789
  // by that operation. Replay this exact Slack event through the normal
@@ -3143,11 +3171,17 @@ async function acceptSlackReactionTask(
3143
3171
  }
3144
3172
  return;
3145
3173
  }
3146
- await acceptSessionUserMessage(deps, grant, grant.workspaceId, sessionId, {
3147
- text: entry.text,
3148
- resources,
3149
- clientEventId,
3150
- });
3174
+ await acceptSessionUserMessage(
3175
+ await withCatalogSettings(deps, grant),
3176
+ grant,
3177
+ grant.workspaceId,
3178
+ sessionId,
3179
+ {
3180
+ text: entry.text,
3181
+ resources,
3182
+ clientEventId,
3183
+ },
3184
+ );
3151
3185
  }
3152
3186
 
3153
3187
  async function continueSlackSession(
@@ -3230,12 +3264,18 @@ async function continueSlackSession(
3230
3264
  if (!hasPermission(grant.permissions, "sessions:control")) {
3231
3265
  throw new SlackInteractionPermanentError("sessions_control_denied");
3232
3266
  }
3233
- await acceptSessionUserMessage(deps, grant, interaction.workspaceId, interaction.sessionId, {
3234
- text: entry.text,
3235
- ...(options.modelContext ? { modelContext: options.modelContext } : {}),
3236
- resources,
3237
- clientEventId: `slack:${entry.providerEventId}`,
3238
- });
3267
+ await acceptSessionUserMessage(
3268
+ await withCatalogSettings(deps, grant),
3269
+ grant,
3270
+ interaction.workspaceId,
3271
+ interaction.sessionId,
3272
+ {
3273
+ text: entry.text,
3274
+ ...(options.modelContext ? { modelContext: options.modelContext } : {}),
3275
+ resources,
3276
+ clientEventId: `slack:${entry.providerEventId}`,
3277
+ },
3278
+ );
3239
3279
  }
3240
3280
 
3241
3281
  const SLACK_ACTION_ID_BY_KIND: Record<SlackInteractionActionKind, string> = {
@@ -141,7 +141,7 @@ export function registerCompanyBrainGovernedWriteTools(
141
141
  description:
142
142
  "Atomically promote one still-active note from this exact root task tree into a workspace instruction-policy proposal. The note bytes remain exact evidence and draft content. " +
143
143
  `Use this only for a universal always-on rule, never for an incident, fact, decision, outcome, or conditional procedure. Once active, those bytes are composed verbatim into the prompt of every session the target applies to, so a note over ${AGENT_AUTHORED_INSTRUCTION_POLICY_CONTENT_MAX_CHARS} characters is rejected here rather than truncated: write a fresh minimal imperative note instead of promoting a long working note. ` +
144
- "Off creates nothing; Review first keeps the proposal inactive; Autonomous may activate an eligible proposal through the governed instruction lifecycle with an undoable receipt. This never widens scope.",
144
+ "Off creates nothing; Require approval keeps the proposal inactive; Autonomous may activate an eligible proposal through the governed instruction lifecycle with an undoable receipt. This never widens scope.",
145
145
  inputSchema: {
146
146
  ...taskNotePromotion,
147
147
  target: WorkspaceInstructionPolicyTarget,
@@ -202,7 +202,7 @@ export function registerCompanyBrainGovernedWriteTools(
202
202
  description:
203
203
  "Materialize an evidence-backed workspace instruction-policy proposal. " +
204
204
  `Use this only for a minimal universal rule, never for an incident, fact, decision, outcome, or conditional procedure. Once active, this content is composed verbatim into the prompt of every session the target applies to (every session in this workspace for a global charter or policy, every session bound to the role for a role policy), so keep it under ${AGENT_AUTHORED_INSTRUCTION_POLICY_CONTENT_MAX_CHARS} characters. ${AGENT_AUTHORED_INSTRUCTION_POLICY_STYLE} ` +
205
- "Off creates nothing; Review first keeps the proposal inactive; Autonomous may activate an eligible proposal through the governed instruction lifecycle with an undoable receipt.",
205
+ "Off creates nothing; Require approval keeps the proposal inactive; Autonomous may activate an eligible proposal through the governed instruction lifecycle with an undoable receipt.",
206
206
  inputSchema: {
207
207
  ...evidence,
208
208
  target: WorkspaceInstructionPolicyTarget,
@@ -127,7 +127,7 @@ export function registerCompanyProfileAgentAdminTools(
127
127
  "Prepare the organization's small, stable identity: identity says who the organization is, and mission says why it exists. " +
128
128
  "Once activated, both fields are mandatory prompt context in every root session for the whole organization, so use one plain descriptive statement per field with no products, customers, goals, constraints, procedures, or marketing copy. Those details belong in organization-scoped Documents and are retrieved only when relevant. " +
129
129
  `Each field is bounded to ${AGENT_AUTHORED_COMPANY_PROFILE_SCALAR_MAX_CHARS} characters for agent-authored proposals. ` +
130
- "This is independent of workspace learning policy and follows the organization owner's Agent-managed identity mode. Off creates nothing. Review first returns status=confirmation_required with the exact `humanInput` payload; call `request_human_input` with it verbatim, then call `company_profile_confirm` with the returned requestId. Autonomous may return status=activated immediately after exact live-owner, stale-head, and compare-and-swap checks; do not ask for another confirmation in that case.",
130
+ "This is independent of workspace learning policy and follows the organization owner's Agent-managed identity mode. Off creates nothing. Require approval returns status=confirmation_required with the exact `humanInput` payload; call `request_human_input` with it verbatim, then call `company_profile_confirm` with the returned requestId. Autonomous may return status=activated immediately after exact live-owner, stale-head, and compare-and-swap checks; do not ask for another confirmation in that case.",
131
131
  inputSchema: {
132
132
  operationId: z.string().uuid(),
133
133
  identity: scalar,
@@ -72,7 +72,7 @@ export function registerRememberTools(input: RegisterRememberToolsInput): void {
72
72
  description:
73
73
  "Create governed durable knowledge, a Skill, or a mandatory workspace instruction. When the agent-only memory_save tool is available, use it instead for ordinary durable facts, decisions, incidents, bug fixes, and confirmed outcomes; those Memory writes are autonomous and independent of Learning mode. Use lane=knowledge only when memory_save is unavailable and the user explicitly requests reviewed workspace knowledge; lane=preference creates a Skill for reusable conditional how-to guidance; lane=instruction_policy is only for a universal always/never rule that should apply to nearly every task. " +
74
74
  `Write the instruction lane as the shortest complete rule, at most ${AGENT_AUTHORED_INSTRUCTION_POLICY_CONTENT_MAX_CHARS} characters and normally 1-3 imperative sentences, with no numbered steps, examples, rationale, or restated defaults. Keep a Skill under ${AGENT_AUTHORED_PREFERENCE_CONTENT_MAX_CHARS} characters with one clear trigger and outcome, only the necessary steps, checks, and exceptions, and a one-sentence descriptor. Do not copy one item into multiple lanes. ` +
75
- "Under Autonomous learning, an eligible Skill or workspace instruction may activate immediately through its governed lifecycle. Under Review first, it remains inactive and the receipt returns status=confirmation_required with the exact `humanInput` payload: call `request_human_input` with it verbatim, then call `remember_confirm` with the returned requestId. Off creates no governed change. Reviewed Knowledge always needs confirmation. Do not use lane=knowledge for facts you merely inferred; use memory_save when available, otherwise knowledge_propose or task notes. Confirmed lane=knowledge content keeps its reviewed claim provenance and materializes its exact approved text into Memory for later `memory_search` retrieval.",
75
+ "Under Autonomous learning, an eligible Skill or workspace instruction may activate immediately through its governed lifecycle. Under Require approval, it remains inactive and the receipt returns status=confirmation_required with the exact `humanInput` payload: call `request_human_input` with it verbatim, then call `remember_confirm` with the returned requestId. Off creates no governed change. Reviewed Knowledge always needs confirmation. Do not use lane=knowledge for facts you merely inferred; use memory_save when available, otherwise knowledge_propose or task notes. Confirmed lane=knowledge content keeps its reviewed claim provenance and materializes its exact approved text into Memory for later `memory_search` retrieval.",
76
76
  inputSchema: {
77
77
  lane: z.enum(["preference", "instruction_policy", "knowledge"]),
78
78
  ...laneFields,