@opengeni/api-router 2.6.4 → 2.8.1-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.
- package/dist/app.d.ts +2 -1
- package/dist/app.js +3 -1
- package/dist/{chunk-XTLI3CBH.js → chunk-HHKQMVY7.js} +3009 -1699
- package/dist/chunk-HHKQMVY7.js.map +1 -0
- package/dist/github-access.d.ts +25 -2
- package/dist/index.js +14 -2
- package/dist/index.js.map +1 -1
- package/dist/integrations/personal-github-repositories.d.ts +41 -2
- package/dist/mcp/scheduled-task-view.d.ts +3 -3
- package/dist/model-catalog.d.ts +5 -31
- package/dist/routes/codex.d.ts +8 -1
- package/dist/routes/github.d.ts +2 -0
- package/dist/routes/organization-model-providers.d.ts +3 -0
- package/dist/workspace-deletion.d.ts +6 -0
- package/package.json +18 -18
- package/src/app.ts +55 -9
- package/src/auth/organization-user-setup.ts +3 -3
- package/src/github-access.ts +117 -1
- package/src/http/sse.ts +15 -7
- package/src/index.ts +13 -1
- package/src/integrations/personal-github-repositories.ts +238 -9
- package/src/integrations/slack-app-home.ts +2 -2
- package/src/integrations/slack-interactions.ts +77 -37
- package/src/mcp/company-brain-governed-writes.ts +2 -2
- package/src/mcp/company-profile-agent-admin.ts +1 -1
- package/src/mcp/remember.ts +1 -1
- package/src/mcp/server.ts +11 -2
- package/src/model-catalog.ts +45 -337
- package/src/routes/automations.ts +178 -19
- package/src/routes/codex.ts +136 -32
- package/src/routes/connections.ts +271 -16
- package/src/routes/github.ts +116 -15
- package/src/routes/organization-memberships.ts +50 -3
- package/src/routes/organization-model-providers.ts +231 -0
- package/src/routes/packs.ts +1 -0
- package/src/routes/personal-github.ts +39 -0
- package/src/routes/pr-review.ts +72 -4
- package/src/routes/scheduled-tasks.ts +40 -13
- package/src/routes/sessions.ts +88 -52
- package/src/routes/supergrok.ts +3 -2
- package/src/routes/workspaces.ts +405 -62
- package/src/workspace-deletion.ts +64 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 &&
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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 {
|
|
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
|
|
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(
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
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(
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
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(
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
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(
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
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;
|
|
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;
|
|
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.
|
|
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,
|
package/src/mcp/remember.ts
CHANGED
|
@@ -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
|
|
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,
|
package/src/mcp/server.ts
CHANGED
|
@@ -154,6 +154,7 @@ import {
|
|
|
154
154
|
requireLiveAgentAttemptAuthorization,
|
|
155
155
|
requireSessionAuthorization,
|
|
156
156
|
requireSessionAuthorizationListScope,
|
|
157
|
+
resolveWorkspaceCatalogSettings,
|
|
157
158
|
SessionAuthorizationDeniedError,
|
|
158
159
|
SessionAuthorizationUnavailableError,
|
|
159
160
|
saveWorkspaceMemoryWithSlackPublication,
|
|
@@ -1718,7 +1719,7 @@ export function buildOpenGeniMcpServer(
|
|
|
1718
1719
|
async ({ id, triggerId }) => {
|
|
1719
1720
|
const task = await requireScheduledTask(deps.db, grant.workspaceId, id);
|
|
1720
1721
|
if (task.action.kind === "agent_turn") {
|
|
1721
|
-
await validateScheduledTaskTarget({
|
|
1722
|
+
const targetSession = await validateScheduledTaskTarget({
|
|
1722
1723
|
db: deps.db,
|
|
1723
1724
|
sessionAuthorization: deps.sessionAuthorization,
|
|
1724
1725
|
authorizationSurface: "first_party_mcp",
|
|
@@ -1730,8 +1731,16 @@ export function buildOpenGeniMcpServer(
|
|
|
1730
1731
|
agentConfig: task.agentConfig,
|
|
1731
1732
|
missingTargetStatus: 404,
|
|
1732
1733
|
});
|
|
1734
|
+
const catalogSourceSettings = deps.catalogSourceSettings ?? deps.settings;
|
|
1735
|
+
const catalogSettings = (
|
|
1736
|
+
await resolveWorkspaceCatalogSettings(deps.db, catalogSourceSettings, {
|
|
1737
|
+
accountId: grant.accountId,
|
|
1738
|
+
workspaceId: grant.workspaceId,
|
|
1739
|
+
...(targetSession ? { retainedProductModelId: targetSession.model } : {}),
|
|
1740
|
+
})
|
|
1741
|
+
).settings;
|
|
1733
1742
|
await validateScheduledTaskMachineTarget({
|
|
1734
|
-
settings:
|
|
1743
|
+
settings: catalogSettings,
|
|
1735
1744
|
db: deps.db,
|
|
1736
1745
|
grant,
|
|
1737
1746
|
runMode: task.runMode,
|