@opengeni/api-router 3.0.0-canary.0 → 3.0.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.js +1 -1
- package/dist/{chunk-7IAWEYBH.js → chunk-XZGTKYOA.js} +3626 -2525
- package/dist/{chunk-7IAWEYBH.js.map → chunk-XZGTKYOA.js.map} +1 -1
- package/dist/index.js +1 -1
- package/dist/integrations/fiken.d.ts +3 -1
- package/dist/integrations/github-lens-connect.d.ts +2 -2
- package/dist/integrations/integration-commit-authority.d.ts +13 -0
- package/dist/integrations/oauth-client.d.ts +118 -1
- package/dist/integrations/slack-interactions.d.ts +1 -1
- package/dist/mcp/first-party-tool-permissions.d.ts +4 -0
- package/dist/mcp/scheduled-task-view.d.ts +3 -3
- package/dist/routes/api-integrations.d.ts +1 -0
- package/dist/routes/organization-integration-policy.d.ts +3 -0
- package/package.json +19 -19
- package/src/app.ts +7 -2
- package/src/integrations/atlassian.ts +135 -65
- package/src/integrations/fiken.ts +57 -35
- package/src/integrations/github-app-connect.ts +34 -15
- package/src/integrations/google-drive.ts +295 -124
- package/src/integrations/integration-commit-authority.ts +69 -0
- package/src/integrations/oauth-client.ts +220 -65
- package/src/integrations/personal-github.ts +55 -31
- package/src/integrations/provider-oauth.ts +54 -31
- package/src/integrations/social-oauth.ts +83 -56
- package/src/mcp/first-party-tool-permissions.ts +1 -0
- package/src/mcp/knowledge-entries.ts +23 -6
- package/src/routes/api-integrations.ts +116 -4
- package/src/routes/billing.ts +52 -0
- package/src/routes/capabilities.ts +51 -1
- package/src/routes/connect.ts +161 -57
- package/src/routes/connections.ts +430 -305
- package/src/routes/github.ts +48 -19
- package/src/routes/integration-facets.ts +66 -25
- package/src/routes/knowledge.ts +48 -1
- package/src/routes/organization-integration-policy.ts +113 -0
- package/src/routes/plugins.ts +12 -2
- package/src/routes/pr-review-github.ts +60 -30
- package/src/routes/pr-review.ts +69 -45
- package/src/routes/social.ts +24 -13
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { withOrganizationIntegrationAcquisition } from "@opengeni/db/organization-integration-policy";
|
|
2
|
+
import { claimOAuthAcquisition, finishOAuthAcquisition } from "./oauth-client";
|
|
1
3
|
import { Buffer } from "node:buffer";
|
|
2
4
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
5
|
|
|
@@ -18,7 +20,11 @@ import {
|
|
|
18
20
|
type ApiIntegrationOAuthStartRequest,
|
|
19
21
|
type ConnectionOwnership,
|
|
20
22
|
} from "@opengeni/contracts";
|
|
21
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
integrationKeyForConnectProvider,
|
|
25
|
+
requireEnvironmentEncryption,
|
|
26
|
+
type ApiRouteDeps,
|
|
27
|
+
} from "@opengeni/core";
|
|
22
28
|
import { ExternalActorContinuation } from "@opengeni/contracts/external-identities";
|
|
23
29
|
import {
|
|
24
30
|
consumeIntegrationOAuthStateNonce,
|
|
@@ -28,7 +34,6 @@ import {
|
|
|
28
34
|
loadConnectionCredentialForBroker,
|
|
29
35
|
persistProviderOAuthConnection,
|
|
30
36
|
getConnectAttempt,
|
|
31
|
-
claimConnectOperation,
|
|
32
37
|
finishConnectOperation,
|
|
33
38
|
} from "@opengeni/db";
|
|
34
39
|
import { createSignedState, readSignedState } from "@opengeni/github";
|
|
@@ -149,6 +154,7 @@ export async function startApiIntegrationProviderOAuth(
|
|
|
149
154
|
},
|
|
150
155
|
): Promise<OAuthStartResponse> {
|
|
151
156
|
const definition = requiredDefinition(input.payload.definitionId);
|
|
157
|
+
await withOrganizationIntegrationAcquisition(deps.db, input, [definition.id], async () => {});
|
|
152
158
|
const providerDomain = integrationDefinitionProviderDomain(definition);
|
|
153
159
|
const existing = input.payload.connectionId
|
|
154
160
|
? await getConnectionMetadata(
|
|
@@ -307,12 +313,18 @@ export async function completeApiIntegrationProviderOAuth(
|
|
|
307
313
|
operationId: `oauth:${state.nonce}`,
|
|
308
314
|
inputDigest: createHash("sha256").update(input.state!).digest("hex"),
|
|
309
315
|
};
|
|
310
|
-
const claim = await
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
+
const claim = await claimOAuthAcquisition(
|
|
317
|
+
deps.db,
|
|
318
|
+
state,
|
|
319
|
+
{
|
|
320
|
+
...connectOperation,
|
|
321
|
+
expectedRevision: stored.attempt.revision,
|
|
322
|
+
authorize: (tx, _attempt, origin) =>
|
|
323
|
+
requireConnectOwnerAuthority(tx, state!, "connections:write", origin),
|
|
324
|
+
},
|
|
325
|
+
integrationKeyForConnectProvider(state.definitionId),
|
|
326
|
+
Boolean(input.code && !input.error),
|
|
327
|
+
);
|
|
316
328
|
if (claim.status === "replayed") return { redirectTo: exactReturnUrl, exactReturn: true };
|
|
317
329
|
}
|
|
318
330
|
requireProviderOAuthOwner(state);
|
|
@@ -350,6 +362,7 @@ export async function completeApiIntegrationProviderOAuth(
|
|
|
350
362
|
|
|
351
363
|
const definition = integrationDefinitionById(state.definitionId);
|
|
352
364
|
if (!definition) throw new ProviderOAuthCallbackError("state_invalid");
|
|
365
|
+
await withOrganizationIntegrationAcquisition(deps.db, state, [definition.id], async () => {});
|
|
353
366
|
if (
|
|
354
367
|
state.definitionFingerprint !== providerDefinitionFingerprint(definition) ||
|
|
355
368
|
state.providerDomain !== integrationDefinitionProviderDomain(definition) ||
|
|
@@ -480,32 +493,42 @@ export async function completeApiIntegrationProviderOAuth(
|
|
|
480
493
|
const persist = (database: import("@opengeni/db").Database) =>
|
|
481
494
|
persistProviderOAuthConnection(database, persistenceInput);
|
|
482
495
|
if (connectOperation) {
|
|
483
|
-
await
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
496
|
+
await finishOAuthAcquisition(
|
|
497
|
+
deps.db,
|
|
498
|
+
state,
|
|
499
|
+
{
|
|
500
|
+
...connectOperation,
|
|
501
|
+
authorize: (tx, _attempt, origin) =>
|
|
502
|
+
requireConnectOwnerAuthority(tx, state!, "connections:write", origin),
|
|
503
|
+
commit: async (tx, current) => {
|
|
504
|
+
const connection = await persist(tx);
|
|
505
|
+
if (!connection) throw new ProviderOAuthCallbackError("connection_conflict");
|
|
506
|
+
return {
|
|
507
|
+
...current,
|
|
508
|
+
revision: current.revision + 1,
|
|
509
|
+
state: "connected_but_incomplete",
|
|
510
|
+
credentialsCommitted: true,
|
|
511
|
+
nextAction: { type: "none" },
|
|
512
|
+
account: {
|
|
513
|
+
id: connection.id,
|
|
514
|
+
providerId: current.providerId,
|
|
515
|
+
label: identity.displayName ?? identity.email ?? identity.principalId,
|
|
516
|
+
ownership: current.ownership,
|
|
517
|
+
status: "connected",
|
|
518
|
+
},
|
|
519
|
+
};
|
|
520
|
+
},
|
|
504
521
|
},
|
|
505
|
-
|
|
522
|
+
definition.id,
|
|
523
|
+
);
|
|
506
524
|
return { redirectTo: exactReturnUrl!, exactReturn: true };
|
|
507
525
|
}
|
|
508
|
-
const connection = await
|
|
526
|
+
const connection = await withOrganizationIntegrationAcquisition(
|
|
527
|
+
deps.db,
|
|
528
|
+
state,
|
|
529
|
+
[definition.id],
|
|
530
|
+
persist,
|
|
531
|
+
);
|
|
509
532
|
if (!connection) throw new ProviderOAuthCallbackError("connection_conflict");
|
|
510
533
|
return {
|
|
511
534
|
redirectTo: providerOAuthReturnUrl(returnBaseUrl, state.returnPath, "success", {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { withOrganizationIntegrationAcquisition } from "@opengeni/db/organization-integration-policy";
|
|
2
|
+
import { claimOAuthAcquisition, finishOAuthAcquisition } from "./oauth-client";
|
|
1
3
|
import { parseSocialOauthClientsJson, type Settings } from "@opengeni/config";
|
|
2
4
|
import {
|
|
3
5
|
OAuthStartResponse,
|
|
@@ -15,7 +17,6 @@ import {
|
|
|
15
17
|
loadSocialConnectionCredential,
|
|
16
18
|
updateSocialConnectionCredential,
|
|
17
19
|
upsertSocialOAuthConnection,
|
|
18
|
-
claimConnectOperation,
|
|
19
20
|
finishConnectOperation,
|
|
20
21
|
getConnectAttempt,
|
|
21
22
|
type Database,
|
|
@@ -196,6 +197,7 @@ export async function startSocialOAuth(
|
|
|
196
197
|
): Promise<OAuthStartResponse> {
|
|
197
198
|
const { settings } = deps;
|
|
198
199
|
const provider = SOCIAL_OAUTH_PROVIDERS[context.payload.provider];
|
|
200
|
+
await withOrganizationIntegrationAcquisition(deps.db, context, [provider.id], async () => {});
|
|
199
201
|
const client = socialOAuthClientFor(settings, provider.id);
|
|
200
202
|
const redirectUri = socialOAuthRedirectUri(settings, context.requestUrl);
|
|
201
203
|
const returnPath = safeReturnPath(context.payload.returnPath ?? "/integrations");
|
|
@@ -323,6 +325,7 @@ export async function completeSocialOAuthCallback(
|
|
|
323
325
|
}
|
|
324
326
|
try {
|
|
325
327
|
const provider = SOCIAL_OAUTH_PROVIDERS[state.provider];
|
|
328
|
+
await withOrganizationIntegrationAcquisition(db, state, [provider.id], async () => {});
|
|
326
329
|
const client = socialOAuthClientFor(settings, provider.id);
|
|
327
330
|
const key = requireEnvironmentEncryption(settings);
|
|
328
331
|
const verifier = state.encryptedPkceVerifier
|
|
@@ -344,18 +347,31 @@ export async function completeSocialOAuthCallback(
|
|
|
344
347
|
...(token.scope ? { scope: token.scope } : {}),
|
|
345
348
|
};
|
|
346
349
|
const grantedScopes = token.scope ? token.scope.split(/[\s,]+/).filter(Boolean) : state.scopes;
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
provider
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
350
|
+
const acceptedState = state;
|
|
351
|
+
const connection = await withOrganizationIntegrationAcquisition(
|
|
352
|
+
db,
|
|
353
|
+
acceptedState,
|
|
354
|
+
[provider.id],
|
|
355
|
+
async (tx) => {
|
|
356
|
+
await requireConnectOwnerAuthority(
|
|
357
|
+
tx,
|
|
358
|
+
acceptedState,
|
|
359
|
+
acceptedState.ownership === "workspace" ? "workspace:admin" : "workspace:read",
|
|
360
|
+
);
|
|
361
|
+
return upsertSocialOAuthConnection(tx, {
|
|
362
|
+
accountId: acceptedState.accountId,
|
|
363
|
+
workspaceId: acceptedState.workspaceId,
|
|
364
|
+
subjectId: acceptedState.ownership === "personal" ? acceptedState.subjectId : null,
|
|
365
|
+
provider: provider.id,
|
|
366
|
+
accountHandle: identity.handle,
|
|
367
|
+
accountName: identity.name ?? null,
|
|
368
|
+
externalAccountId: identity.externalAccountId,
|
|
369
|
+
scopes: grantedScopes,
|
|
370
|
+
credentialEncrypted: encryptEnvironmentValue(key, JSON.stringify(bundle)),
|
|
371
|
+
tokenMetadata: publicTokenMetadata(bundle),
|
|
372
|
+
});
|
|
373
|
+
},
|
|
374
|
+
);
|
|
359
375
|
return {
|
|
360
376
|
redirectTo: callbackReturnPath(state.returnPath, "success", {
|
|
361
377
|
connectionId: connection.id,
|
|
@@ -407,10 +423,16 @@ async function completeSocialConnect(
|
|
|
407
423
|
authorize,
|
|
408
424
|
};
|
|
409
425
|
try {
|
|
410
|
-
const claim = await
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
426
|
+
const claim = await claimOAuthAcquisition(
|
|
427
|
+
deps.db,
|
|
428
|
+
state,
|
|
429
|
+
{
|
|
430
|
+
...operation,
|
|
431
|
+
expectedRevision: stored.attempt.revision,
|
|
432
|
+
},
|
|
433
|
+
state.provider,
|
|
434
|
+
Boolean(input.code && !input.error),
|
|
435
|
+
);
|
|
414
436
|
if (claim.status === "replayed") return { redirectTo: stored.returnUrl, exactReturn: true };
|
|
415
437
|
if (!personalOwnerStateAccepted(state))
|
|
416
438
|
throw new HTTPException(403, { message: PERSONAL_CONNECTION_PRINCIPAL_MESSAGE });
|
|
@@ -455,46 +477,51 @@ async function completeSocialConnect(
|
|
|
455
477
|
...(token.expiresAt ? { expiresAt: token.expiresAt } : {}),
|
|
456
478
|
...(token.scope ? { scope: token.scope } : {}),
|
|
457
479
|
};
|
|
458
|
-
await
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
480
|
+
await finishOAuthAcquisition(
|
|
481
|
+
deps.db,
|
|
482
|
+
state,
|
|
483
|
+
{
|
|
484
|
+
...operation,
|
|
485
|
+
commit: async (tx, current) => {
|
|
486
|
+
const connection = await upsertSocialOAuthConnection(tx, {
|
|
487
|
+
accountId: state.accountId,
|
|
488
|
+
workspaceId: state.workspaceId,
|
|
489
|
+
...(current.account
|
|
490
|
+
? {
|
|
491
|
+
expectedConnection: {
|
|
492
|
+
id: current.account.id.slice("social:".length),
|
|
493
|
+
version: current.account.version!,
|
|
494
|
+
},
|
|
495
|
+
}
|
|
496
|
+
: {}),
|
|
497
|
+
subjectId: state.ownership === "personal" ? state.subjectId : null,
|
|
498
|
+
provider: provider.id,
|
|
499
|
+
accountHandle: identity.handle,
|
|
500
|
+
accountName: identity.name ?? null,
|
|
501
|
+
externalAccountId: identity.externalAccountId,
|
|
502
|
+
scopes: token.scope ? token.scope.split(/[\s,]+/).filter(Boolean) : state.scopes,
|
|
503
|
+
credentialEncrypted: encryptEnvironmentValue(key, JSON.stringify(bundle)),
|
|
504
|
+
tokenMetadata: publicTokenMetadata(bundle),
|
|
505
|
+
});
|
|
506
|
+
return {
|
|
507
|
+
...current,
|
|
508
|
+
revision: current.revision + 1,
|
|
509
|
+
state: "complete",
|
|
510
|
+
credentialsCommitted: true,
|
|
511
|
+
nextAction: { type: "none" },
|
|
512
|
+
account: {
|
|
513
|
+
id: `social:${connection.id}`,
|
|
514
|
+
providerId: provider.id,
|
|
515
|
+
version: connection.version!,
|
|
516
|
+
label: identity.handle,
|
|
517
|
+
ownership: state.ownership,
|
|
518
|
+
status: "connected",
|
|
519
|
+
},
|
|
520
|
+
};
|
|
521
|
+
},
|
|
496
522
|
},
|
|
497
|
-
|
|
523
|
+
state.provider,
|
|
524
|
+
);
|
|
498
525
|
}
|
|
499
526
|
} catch (error) {
|
|
500
527
|
// An uncertain exchange stays occupied: do not replay a consumed OAuth code.
|
|
@@ -21,6 +21,7 @@ export const FIRST_PARTY_TOOL_AUTHORIZATION = {
|
|
|
21
21
|
goal_pause: { sessionRequired: true, allOf: ["goals:manage"] },
|
|
22
22
|
goal_resume: { sessionRequired: true, allOf: ["goals:manage"] },
|
|
23
23
|
knowledge_search: { sessionRequired: true, allOf: ["documents:search"] },
|
|
24
|
+
knowledge_prepare_save: { sessionRequired: true, allOf: ["documents:search"] },
|
|
24
25
|
knowledge_get: { sessionRequired: true, allOf: ["documents:search"] },
|
|
25
26
|
knowledge_browse: { sessionRequired: true, allOf: ["documents:search"] },
|
|
26
27
|
knowledge_retain_file: { sessionRequired: true, allOf: ["documents:search", "files:read"] },
|
|
@@ -2,12 +2,14 @@ import {
|
|
|
2
2
|
AgentInstructionSaveRequest,
|
|
3
3
|
WorkspaceInstructionPolicyTarget,
|
|
4
4
|
KnowledgeEntryListRequest,
|
|
5
|
+
KnowledgeSavePreparationRequest,
|
|
5
6
|
KnowledgeEntrySaveRequest,
|
|
6
7
|
KnowledgeTaskNotePromotionRequest,
|
|
7
8
|
type AccessGrant,
|
|
8
9
|
} from "@opengeni/contracts";
|
|
9
10
|
import {
|
|
10
11
|
prepareKnowledgeFile,
|
|
12
|
+
prepareKnowledgeSave,
|
|
11
13
|
retainKnowledgeMessage,
|
|
12
14
|
searchKnowledgeEntries,
|
|
13
15
|
requireLiveAgentAttemptAuthorization,
|
|
@@ -131,11 +133,23 @@ export function registerKnowledgeEntryTools(
|
|
|
131
133
|
throw new Error("Pass exactly one existing confirmation target");
|
|
132
134
|
}),
|
|
133
135
|
);
|
|
136
|
+
server.registerTool(
|
|
137
|
+
"knowledge_prepare_save",
|
|
138
|
+
{
|
|
139
|
+
description:
|
|
140
|
+
"Before saving useful lasting Knowledge, find related published entries and unapproved proposals, and fetch the full authorized collection map with descriptions and parent IDs in one read-only call. Supply a concise subject and the proposed fact, decision, requirement or incident. Search spans all authorized collections. Reuse an existing entry when unchanged; read its current content before a correction, preserve evidence, or create only when distinct. Pending matches are unapproved. Choose collections from this map in your writable scope. A collection may have both published and pending versions. If collections.complete is false, continue with collectionCursors=collections.nextCursors; never mistake a partial catalog for the whole map. Long descriptions explicitly report truncation and can be read with knowledge_get. This call saves and approves nothing.",
|
|
141
|
+
inputSchema: KnowledgeSavePreparationRequest.shape,
|
|
142
|
+
},
|
|
143
|
+
(input) =>
|
|
144
|
+
run((context) =>
|
|
145
|
+
prepareKnowledgeSave(deps.db, context, input, () => deps.getDocumentServices().embedder),
|
|
146
|
+
),
|
|
147
|
+
);
|
|
134
148
|
server.registerTool(
|
|
135
149
|
"knowledge_search",
|
|
136
150
|
{
|
|
137
151
|
description:
|
|
138
|
-
"Find Knowledge from retained sources
|
|
152
|
+
"Find Knowledge from useful findings and explicitly retained reference sources. Supporting chat/file evidence is excluded from ordinary published discovery; set includeEvidence=true only when searching that evidence deliberately. Use a concise subject or entity name first (for example Acme); omit scope to search all authorized scopes. If a query returns no entries, retry the key name alone in the same scope or browse groups before concluding the information is absent. Default view=published is accepted knowledge. Also search view=needs_review before creating or updating entries, to reuse pending findings and collections from earlier tasks. Pending revisions are unapproved proposals, not accepted facts or instructions; preserve that status when discussing them. Reading a proposal never approves it. Personal tasks search the verified user's personal and authorized shared Knowledge; shared tasks search shared Knowledge.",
|
|
139
153
|
inputSchema: KnowledgeEntryListRequest.omit({
|
|
140
154
|
view: true,
|
|
141
155
|
sessionId: true,
|
|
@@ -210,16 +224,19 @@ export function registerKnowledgeEntryTools(
|
|
|
210
224
|
"knowledge_retain_file",
|
|
211
225
|
{
|
|
212
226
|
description:
|
|
213
|
-
"Retain
|
|
214
|
-
inputSchema: {
|
|
227
|
+
"Retain source text from an existing uploaded file only when it supports useful lasting Knowledge, or the user wants the file kept as a reusable reference. Ordinary chat attachments are not automatically added to Knowledge. Use purpose=evidence (default) for supporting material, or purpose=reference for a deliberately retained searchable reference. The original stays in Files. Read the image itself before drawing visual conclusions; OCR text is only evidence. Repeated calls reuse the same source and do not change its purpose, duplicate review requests or revive rejected/archived sources. To make previously retained evidence a searchable reference, read its current revision with knowledge_get and use knowledge_save to set source.purpose=reference while preserving content and links. Selected findings are optional separate knowledge_save entries with evidence pointing to the returned revision. A pending receipt never pauses the task.",
|
|
228
|
+
inputSchema: {
|
|
229
|
+
fileId: z.uuid(),
|
|
230
|
+
purpose: z.enum(["evidence", "reference"]).default("evidence"),
|
|
231
|
+
},
|
|
215
232
|
},
|
|
216
|
-
({ fileId }) => run((context) => prepareKnowledgeFile(deps, context, fileId)),
|
|
233
|
+
({ fileId, purpose }) => run((context) => prepareKnowledgeFile(deps, context, fileId, purpose)),
|
|
217
234
|
);
|
|
218
235
|
server.registerTool(
|
|
219
236
|
"knowledge_retain_message",
|
|
220
237
|
{
|
|
221
238
|
description:
|
|
222
|
-
"Retain the exact text of a user message
|
|
239
|
+
"Retain the exact text of a user message as supporting evidence only after identifying useful lasting Knowledge to save. Do not retain routine approvals, acknowledgments, task instructions or status chatter merely because they occurred. The source stays out of ordinary Knowledge discovery and can be read through its evidence link. Use when a user supplies or confirms a durable fact, before saving a finding based on it. Omit messageId for the user message that triggered this turn, or pass an earlier user message event ID from this same conversation. The server copies the actual message, preserves its identity and follows this task's learning policy. Repeated calls reuse the source. Cite the returned entryId/revisionId as evidence in knowledge_save, with location.messageIds=[messageId]. Retaining a message does not prove every statement in it or approve a pending finding.",
|
|
223
240
|
inputSchema: { messageId: z.uuid().optional() },
|
|
224
241
|
},
|
|
225
242
|
({ messageId }) => run((context) => retainKnowledgeMessage(deps.db, context, messageId)),
|
|
@@ -228,7 +245,7 @@ export function registerKnowledgeEntryTools(
|
|
|
228
245
|
"knowledge_save",
|
|
229
246
|
{
|
|
230
247
|
description:
|
|
231
|
-
"Retain useful
|
|
248
|
+
"Retain useful lasting facts, decisions, requirements, incidents or notes, or deliberately selected reference sources. First use knowledge_prepare_save when available (otherwise search published and needs_review Knowledge and browse collections). Skip unchanged duplicates; improve an existing entry when the subject is the same. Do not save routine approvals, acknowledgments, temporary instructions, screenshots or OCR merely because they occurred. State the reusable finding with enough context for a future task, preserve uncertainty and evidence, and place it in the relevant existing collections. Do this autonomously when useful for future work. Choose kind by content: source for retained original text; fact for a specific claim (the label is not verification); decision for a choice made and its reasoning; requirement for a need or constraint; incident for a problem with cause, fix and outcome when known; note for other useful context. Choose the closest kind without asking the user to classify it. A group is a collection about a customer, product, system or subject, not a finding type. Search published and needs_review views for an existing entry and relevant group before creating one, use groupIds to link entries across sources, and reuse an entry in multiple groups instead of copying it. Nest collections by setting a group entry's groupIds to its parent collections; keep the hierarchy shallow and useful, and never create circular membership. Use a new entryId and expectedVersion 0 to create; use an existing ID and its current version to correct or reorganize. Source text and selected facts are independent entries, not mandatory duplicate stages. Evidence pins another entry's exact revision. When a user supplies or confirms a fact, retain the actual message with knowledge_retain_message and cite the returned revision; do not leave its provenance only in prose. Preserve existing evidence and relationships on corrections unless they are specifically no longer applicable; a contradictory original remains useful evidence of what changed. Use the same operationId only for an exact retry. Agent learning decides publication: published is available immediately; pending is saved for review and your task continues without an approval prompt. Do not turn Knowledge into instructions or Skills.",
|
|
232
249
|
inputSchema: KnowledgeEntrySaveRequest.omit({ scope: true }).shape,
|
|
233
250
|
},
|
|
234
251
|
(input) => run((context) => saveKnowledgeEntry(deps.db, context, input)),
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "@opengeni/capabilities";
|
|
8
8
|
import {
|
|
9
9
|
ApiIntegrationPreview,
|
|
10
|
+
OrganizationIntegrationDeniedError,
|
|
10
11
|
ListIntegrationDefinitionsResponse,
|
|
11
12
|
ApiIntegrationUninstallPreview,
|
|
12
13
|
InstallApiIntegrationRequest,
|
|
@@ -17,12 +18,18 @@ import {
|
|
|
17
18
|
UninstallApiIntegrationResult,
|
|
18
19
|
type AccessGrant,
|
|
19
20
|
} from "@opengeni/contracts";
|
|
20
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
requireAccessGrant,
|
|
23
|
+
integrationSourceForOrganizationPolicy,
|
|
24
|
+
type ApiRouteDeps,
|
|
25
|
+
} from "@opengeni/core";
|
|
26
|
+
import { withOrganizationIntegrationPolicyFence } from "@opengeni/db/organization-integration-policy";
|
|
21
27
|
import {
|
|
22
28
|
ApiIntegrationInstallationVersionConflictError,
|
|
23
29
|
buildConnectionTokenResolver,
|
|
24
30
|
getApiIntegrationUninstallPreview,
|
|
25
31
|
getConnectionMetadata,
|
|
32
|
+
getApiIntegrationReconciliationSnapshot,
|
|
26
33
|
installApiIntegration,
|
|
27
34
|
IntegrationFacetBindingOwnershipConflictError,
|
|
28
35
|
IntegrationFacetBindingVersionConflictError,
|
|
@@ -209,6 +216,7 @@ export function registerApiIntegrationRoutes(
|
|
|
209
216
|
const resolved = await resolveForRoute({
|
|
210
217
|
deps,
|
|
211
218
|
transport,
|
|
219
|
+
accountId: grant.accountId,
|
|
212
220
|
workspaceId,
|
|
213
221
|
subjectId: grant.subjectId,
|
|
214
222
|
payload,
|
|
@@ -223,6 +231,7 @@ export function registerApiIntegrationRoutes(
|
|
|
223
231
|
const resolved = await resolveForRoute({
|
|
224
232
|
deps,
|
|
225
233
|
transport,
|
|
234
|
+
accountId: grant.accountId,
|
|
226
235
|
workspaceId,
|
|
227
236
|
subjectId: grant.subjectId,
|
|
228
237
|
payload,
|
|
@@ -308,21 +317,45 @@ export function registerApiIntegrationRoutes(
|
|
|
308
317
|
export async function resolveForRoute(input: {
|
|
309
318
|
deps: ApiRouteDeps;
|
|
310
319
|
transport: ReturnType<typeof createPinnedIntegrationTransport>;
|
|
320
|
+
accountId: string;
|
|
311
321
|
workspaceId: string;
|
|
312
322
|
subjectId: string;
|
|
313
323
|
payload: PreviewApiIntegrationRequest | InstallApiIntegrationRequest;
|
|
314
324
|
}): ReturnType<typeof resolveApiIntegrationPreview> {
|
|
325
|
+
const payload = structuredClone(input.payload);
|
|
326
|
+
const preparation = await withOrganizationIntegrationPolicyFence(
|
|
327
|
+
input.deps.db,
|
|
328
|
+
input,
|
|
329
|
+
async (tx, policy) => {
|
|
330
|
+
try {
|
|
331
|
+
return { source: integrationSourceForOrganizationPolicy(policy, payload.source) };
|
|
332
|
+
} catch (error) {
|
|
333
|
+
if (!(error instanceof OrganizationIntegrationDeniedError)) throw error;
|
|
334
|
+
const install = InstallApiIntegrationRequest.safeParse(payload);
|
|
335
|
+
if (install.success) {
|
|
336
|
+
const resolved = await storedReconciliationPreview(
|
|
337
|
+
{ ...input, deps: { ...input.deps, db: tx } },
|
|
338
|
+
install.data,
|
|
339
|
+
);
|
|
340
|
+
if (resolved) return { resolved };
|
|
341
|
+
}
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
);
|
|
346
|
+
if (preparation.resolved) return preparation.resolved;
|
|
347
|
+
const source = preparation.source!;
|
|
315
348
|
try {
|
|
316
|
-
const connection =
|
|
349
|
+
const connection = payload.connectionId
|
|
317
350
|
? await requireVisibleConnection(
|
|
318
351
|
input.deps,
|
|
319
352
|
input.workspaceId,
|
|
320
353
|
input.subjectId,
|
|
321
|
-
|
|
354
|
+
payload.connectionId,
|
|
322
355
|
)
|
|
323
356
|
: null;
|
|
324
357
|
return await resolveApiIntegrationPreview({
|
|
325
|
-
source
|
|
358
|
+
source,
|
|
326
359
|
connection: connectionDescriptor(connection),
|
|
327
360
|
transport: input.transport,
|
|
328
361
|
authority: {
|
|
@@ -353,6 +386,85 @@ export async function resolveForRoute(input: {
|
|
|
353
386
|
}
|
|
354
387
|
}
|
|
355
388
|
|
|
389
|
+
async function storedReconciliationPreview(
|
|
390
|
+
input: { deps: ApiRouteDeps; accountId: string; workspaceId: string; subjectId: string },
|
|
391
|
+
payload: InstallApiIntegrationRequest,
|
|
392
|
+
): Promise<ResolvedApiIntegrationPreview | null> {
|
|
393
|
+
const snapshot = await getApiIntegrationReconciliationSnapshot(input.deps.db, {
|
|
394
|
+
accountId: input.accountId,
|
|
395
|
+
workspaceId: input.workspaceId,
|
|
396
|
+
subjectId: input.subjectId,
|
|
397
|
+
source: payload.source,
|
|
398
|
+
expectedRevisionId: payload.expectedRevisionId,
|
|
399
|
+
expectedContentSha256: payload.expectedContentSha256,
|
|
400
|
+
...(payload.connectionId !== undefined ? { connectionId: payload.connectionId } : {}),
|
|
401
|
+
...(payload.instanceKey !== undefined ? { instanceKey: payload.instanceKey } : {}),
|
|
402
|
+
...(payload.allowedTools !== undefined ? { allowedTools: payload.allowedTools } : {}),
|
|
403
|
+
});
|
|
404
|
+
if (!snapshot) return null;
|
|
405
|
+
const { runtime, baseServerId, name, provider, requiredScopes } = snapshot;
|
|
406
|
+
const connection = payload.connectionId
|
|
407
|
+
? await requireVisibleConnection(
|
|
408
|
+
input.deps,
|
|
409
|
+
input.workspaceId,
|
|
410
|
+
input.subjectId,
|
|
411
|
+
payload.connectionId,
|
|
412
|
+
)
|
|
413
|
+
: null;
|
|
414
|
+
const scheme = runtime.authScheme;
|
|
415
|
+
const auth = !apiIntegrationRequiresConnection(scheme)
|
|
416
|
+
? { kind: "none" }
|
|
417
|
+
: scheme.kind === "oauth2"
|
|
418
|
+
? { kind: "oauth2", providerDomain: runtime.providerDomain, scopes: requiredScopes }
|
|
419
|
+
: scheme.kind === "api_key"
|
|
420
|
+
? {
|
|
421
|
+
kind: "api_key",
|
|
422
|
+
providerDomain: runtime.providerDomain,
|
|
423
|
+
carrier: scheme.carrier,
|
|
424
|
+
name: scheme.name,
|
|
425
|
+
}
|
|
426
|
+
: scheme.kind === "http"
|
|
427
|
+
? { kind: "http", providerDomain: runtime.providerDomain, scheme: scheme.scheme }
|
|
428
|
+
: null;
|
|
429
|
+
if (!auth) return null;
|
|
430
|
+
return {
|
|
431
|
+
preview: ApiIntegrationPreview.parse({
|
|
432
|
+
source: payload.source,
|
|
433
|
+
definitionId: runtime.definitionId,
|
|
434
|
+
definitionProvenance: runtime.definitionProvenance,
|
|
435
|
+
protocol: runtime.protocol,
|
|
436
|
+
capabilityId: runtime.capabilityId,
|
|
437
|
+
pluginKey: runtime.pluginKey,
|
|
438
|
+
serverId: baseServerId,
|
|
439
|
+
name,
|
|
440
|
+
description: runtime.description,
|
|
441
|
+
provider,
|
|
442
|
+
providerDomain: runtime.providerDomain,
|
|
443
|
+
baseUrl: runtime.baseUrl,
|
|
444
|
+
sourceUrl: runtime.sourceUrl,
|
|
445
|
+
revisionId: runtime.revision.id,
|
|
446
|
+
contentSha256: runtime.revision.contentSha256,
|
|
447
|
+
auth,
|
|
448
|
+
connectionId: connection?.id ?? null,
|
|
449
|
+
connectionOwnership: connection ? (connection.subjectId ? "personal" : "workspace") : null,
|
|
450
|
+
tools: runtime.revision.tools.map((tool) => ({
|
|
451
|
+
id: tool.id,
|
|
452
|
+
operationKey: tool.operationKey,
|
|
453
|
+
name: tool.name,
|
|
454
|
+
description: tool.description,
|
|
455
|
+
safety: tool.safety,
|
|
456
|
+
approvalMode: tool.approvalMode,
|
|
457
|
+
deprecated: tool.deprecated,
|
|
458
|
+
})),
|
|
459
|
+
warnings: [],
|
|
460
|
+
}),
|
|
461
|
+
revision: runtime.revision,
|
|
462
|
+
provider,
|
|
463
|
+
requiredScopes,
|
|
464
|
+
authScheme: runtime.authScheme,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
356
468
|
async function requireVisibleConnection(
|
|
357
469
|
deps: ApiRouteDeps,
|
|
358
470
|
workspaceId: string,
|