@opengeni/core 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.
@@ -0,0 +1,251 @@
1
+ import { expect, test } from "bun:test";
2
+ import type { KnowledgeEntryRecord, KnowledgeEntrySummary } from "@opengeni/contracts";
3
+ import { prepareKnowledgeSave } from "./knowledge-preparation";
4
+
5
+ const context = {
6
+ accountId: "account",
7
+ workspaceId: "workspace",
8
+ actor: { kind: "human", review: true },
9
+ } as never;
10
+ const db = {} as never;
11
+ const embedder = () => {
12
+ throw new Error("not used by fake search");
13
+ };
14
+ function group(title: string, parentIds: string[] = []): KnowledgeEntryRecord {
15
+ const id = crypto.randomUUID();
16
+ return {
17
+ id,
18
+ scope: "workspace",
19
+ version: 1,
20
+ publishedRevisionId: id,
21
+ latestRevisionId: id,
22
+ archived: false,
23
+ createdAt: "2026-09-14T00:00:00Z",
24
+ updatedAt: "2026-09-14T00:00:00Z",
25
+ revision: {
26
+ id,
27
+ entryId: id,
28
+ number: 1,
29
+ change: "upsert",
30
+ previousRevisionId: null,
31
+ restoredFromRevisionId: null,
32
+ createdAt: "2026-09-14T00:00:00Z",
33
+ createdBySessionId: null,
34
+ reviewBatchId: null,
35
+ outcome: "published",
36
+ entry: {
37
+ title,
38
+ kind: "group",
39
+ content: `Description of ${title}`,
40
+ groupIds: parentIds,
41
+ evidence: [],
42
+ relationships: [],
43
+ },
44
+ },
45
+ };
46
+ }
47
+ const summary = (record: KnowledgeEntryRecord) =>
48
+ ({
49
+ ...record,
50
+ revision: {
51
+ ...record.revision,
52
+ title: record.revision.entry.title,
53
+ kind: "group",
54
+ preview: "preview",
55
+ groupIds: record.revision.entry.groupIds,
56
+ sourceKind: null,
57
+ },
58
+ excerpts: [],
59
+ }) as KnowledgeEntrySummary;
60
+
61
+ test("one read returns the complete paginated hierarchy and separately labeled duplicate candidates", async () => {
62
+ const parent = group("Engineering");
63
+ const child = group("Product UI", [parent.id]);
64
+ const pending = group("Proposed collection");
65
+ pending.revision.outcome = "pending";
66
+ const revoked = group("Revoked");
67
+ const calls: unknown[] = [];
68
+ const result = await prepareKnowledgeSave(
69
+ db,
70
+ context,
71
+ { query: "plugin setup consistency" },
72
+ embedder,
73
+ {
74
+ list: async (actualDb, actualContext, request) => {
75
+ expect(actualDb).toBe(db);
76
+ expect(actualContext).toBe(context);
77
+ calls.push(request);
78
+ if (request?.view === "needs_review")
79
+ return { entries: [summary(pending)], nextCursor: null };
80
+ return request?.cursor
81
+ ? { entries: [summary(child), summary(revoked)], nextCursor: null }
82
+ : { entries: [summary(parent)], nextCursor: "page2" };
83
+ },
84
+ get: async (_db, _context, id, options) => {
85
+ expect(options?.revisionId).toBeUndefined();
86
+ return [parent, child, pending].find((record) => record.id === id) ?? null;
87
+ },
88
+ search: async (_db, actualContext, request) => {
89
+ expect(actualContext).toBe(context);
90
+ expect(request.query).toBe("plugin setup consistency");
91
+ expect(request.groupId).toBeUndefined();
92
+ expect(request.scope).toBeUndefined();
93
+ return {
94
+ entries: [summary(request.view === "needs_review" ? pending : child)],
95
+ nextCursor: null,
96
+ searchMode: "hybrid",
97
+ };
98
+ },
99
+ },
100
+ );
101
+ expect(calls).toHaveLength(3);
102
+ expect(result.collections.complete).toBe(true);
103
+ expect(result.collections.entries.map((entry) => entry.title)).toEqual([
104
+ "Engineering",
105
+ "Product UI",
106
+ "Proposed collection",
107
+ ]);
108
+ expect(result.collections.entries[1]?.parentIds).toEqual([parent.id]);
109
+ expect(result.collections.entries[0]?.description).toBe("Description of Engineering");
110
+ expect(result.collections.entries[2]?.view).toBe("needs_review");
111
+ expect(result.matches.published.entries[0]?.id).toBe(child.id);
112
+ expect(result.matches.needs_review.entries[0]?.id).toBe(pending.id);
113
+ });
114
+
115
+ test("large collection maps explicitly continue and long descriptions report loss", async () => {
116
+ const record = group("Large collection");
117
+ record.revision.entry.content = "x".repeat(3000);
118
+ let count = 0;
119
+ const services: NonNullable<Parameters<typeof prepareKnowledgeSave>[4]> = {
120
+ list: async (_db, _context, request) => {
121
+ if (request?.view === "needs_review") return { entries: [], nextCursor: null };
122
+ return { entries: [summary(record)], nextCursor: `page${++count}` };
123
+ },
124
+ get: async () => record,
125
+ search: async () => ({ entries: [], nextCursor: null, searchMode: "keyword" }),
126
+ };
127
+ const result = await prepareKnowledgeSave(db, context, { query: "subject" }, embedder, services);
128
+ expect(result.collections.complete).toBe(false);
129
+ expect(result.collections.nextCursors.published).toBeTruthy();
130
+ expect(result.collections.nextCursors.needs_review).toBeNull();
131
+ expect(result.collections.entries[0]?.descriptionTruncated).toBe(true);
132
+ expect(result.collections.entries[0]?.description).toHaveLength(2000);
133
+ let requestedCursor: string | undefined;
134
+ await prepareKnowledgeSave(
135
+ db,
136
+ context,
137
+ { query: "subject", collectionCursors: result.collections.nextCursors },
138
+ embedder,
139
+ {
140
+ ...services,
141
+ list: async (_db, _context, request) => {
142
+ expect(request?.view).toBe("published");
143
+ requestedCursor = request?.cursor;
144
+ return { entries: [], nextCursor: null };
145
+ },
146
+ },
147
+ );
148
+ expect(requestedCursor).toBe(result.collections.nextCursors.published!);
149
+ });
150
+
151
+ test("a catalog authorization failure is not disguised as an empty complete map", async () => {
152
+ await expect(
153
+ prepareKnowledgeSave(db, context, { query: "subject" }, embedder, {
154
+ list: async () => {
155
+ throw new Error("access revoked");
156
+ },
157
+ get: async () => null,
158
+ search: async () => ({ entries: [], nextCursor: null, searchMode: "keyword" }),
159
+ }),
160
+ ).rejects.toThrow("access revoked");
161
+ });
162
+
163
+ test("read-only principals without proposal access fail before searching or embedding", async () => {
164
+ let touched = false;
165
+ await expect(
166
+ prepareKnowledgeSave(
167
+ db,
168
+ {
169
+ accountId: "account",
170
+ workspaceId: "workspace",
171
+ actor: { kind: "service", review: false },
172
+ } as never,
173
+ { query: "subject" },
174
+ () => {
175
+ touched = true;
176
+ throw new Error("must not embed");
177
+ },
178
+ {
179
+ list: async () => {
180
+ touched = true;
181
+ return { entries: [], nextCursor: null };
182
+ },
183
+ get: async () => {
184
+ touched = true;
185
+ return null;
186
+ },
187
+ search: async () => {
188
+ touched = true;
189
+ return { entries: [], nextCursor: null, searchMode: "keyword" };
190
+ },
191
+ },
192
+ ),
193
+ ).rejects.toMatchObject({ code: "42501" });
194
+ expect(touched).toBe(false);
195
+ });
196
+
197
+ test("published and pending searches reuse one query embedding", async () => {
198
+ let calls = 0;
199
+ await prepareKnowledgeSave(
200
+ db,
201
+ context,
202
+ { query: "plugin dialog" },
203
+ () => ({
204
+ model: "test",
205
+ dimensions: 2,
206
+ embedMany: async () => [],
207
+ embedQuery: async () => {
208
+ calls++;
209
+ return [1, 0];
210
+ },
211
+ }),
212
+ {
213
+ list: async () => ({ entries: [], nextCursor: null }),
214
+ get: async () => null,
215
+ search: async (_db, _context, request, queryEmbedder) => {
216
+ expect(await queryEmbedder().embedQuery(request.query!)).toEqual([1, 0]);
217
+ return { entries: [], nextCursor: null, searchMode: "hybrid" };
218
+ },
219
+ },
220
+ );
221
+ expect(calls).toBe(1);
222
+ });
223
+
224
+ test("catalog rechecks current metadata after a collection changes", async () => {
225
+ const old = group("Old name");
226
+ const current = {
227
+ ...old,
228
+ version: 2,
229
+ revision: {
230
+ ...old.revision,
231
+ id: crypto.randomUUID(),
232
+ entry: { ...old.revision.entry, title: "New name" },
233
+ },
234
+ };
235
+ const result = await prepareKnowledgeSave(db, context, { query: "subject" }, embedder, {
236
+ list: async (_db, _context, request) => ({
237
+ entries: request?.view === "published" ? [summary(old)] : [],
238
+ nextCursor: null,
239
+ }),
240
+ get: async (_db, _context, _id, options) => {
241
+ expect(options?.revisionId).toBeUndefined();
242
+ return current;
243
+ },
244
+ search: async () => ({ entries: [], nextCursor: null, searchMode: "keyword" }),
245
+ });
246
+ expect(result.collections.entries[0]).toMatchObject({
247
+ title: "New name",
248
+ version: 2,
249
+ revisionId: current.revision.id,
250
+ });
251
+ });
@@ -0,0 +1,126 @@
1
+ import {
2
+ KnowledgeSavePreparationRequest,
3
+ type KnowledgeSavePreparationResponse,
4
+ } from "@opengeni/contracts";
5
+ import {
6
+ getKnowledgeEntry,
7
+ listKnowledgeEntries,
8
+ type Database,
9
+ type KnowledgeContext,
10
+ } from "@opengeni/db";
11
+ import type { DocumentEmbedder } from "@opengeni/documents";
12
+ import { searchKnowledgeEntries } from "./knowledge-search";
13
+
14
+ const defaults = {
15
+ get: getKnowledgeEntry,
16
+ list: listKnowledgeEntries,
17
+ search: searchKnowledgeEntries,
18
+ };
19
+ // Normal workspaces return the whole map in one call. Larger maps explicitly
20
+ // continue at a complete page boundary, never silently omit collections.
21
+ const CATALOG_PAGE_SIZE = 25;
22
+ const CATALOG_PAGE_BUDGET = 40;
23
+ const DESCRIPTION_CHARS = 2000;
24
+ const CATALOG_BYTES = 256 * 1024;
25
+
26
+ export async function prepareKnowledgeSave(
27
+ db: Database,
28
+ context: KnowledgeContext,
29
+ input: KnowledgeSavePreparationRequest,
30
+ embedder: () => DocumentEmbedder,
31
+ services = defaults,
32
+ ): Promise<KnowledgeSavePreparationResponse> {
33
+ const request = KnowledgeSavePreparationRequest.parse(input);
34
+ if (context.actor.kind !== "agent" && !(context.actor.kind === "human" && context.actor.review)) {
35
+ throw Object.assign(
36
+ new Error("Preparing Knowledge requires a live agent or human Knowledge reviewer"),
37
+ { code: "42501" },
38
+ );
39
+ }
40
+ let provider: DocumentEmbedder | undefined;
41
+ let queryEmbedding: Promise<number[]> | undefined;
42
+ const sharedEmbedder = (): DocumentEmbedder => {
43
+ provider ??= embedder();
44
+ const current = provider;
45
+ return {
46
+ model: current.model,
47
+ dimensions: current.dimensions,
48
+ embedMany: (texts) => current.embedMany(texts),
49
+ embedQuery: (query) => (queryEmbedding ??= current.embedQuery(query)),
50
+ };
51
+ };
52
+ const collections: KnowledgeSavePreparationResponse["collections"] = {
53
+ entries: [],
54
+ complete: true,
55
+ nextCursors: { published: null, needs_review: null },
56
+ };
57
+ // Search is independent of collection placement and always includes the
58
+ // separate unapproved view. No authoring policy or write is invoked here.
59
+ const matches = {
60
+ published: await services.search(
61
+ db,
62
+ context,
63
+ { query: request.query, limit: request.limit, view: "published" },
64
+ sharedEmbedder,
65
+ ),
66
+ needs_review: await services.search(
67
+ db,
68
+ context,
69
+ { query: request.query, limit: request.limit, view: "needs_review" },
70
+ sharedEmbedder,
71
+ ),
72
+ };
73
+ for (const view of ["published", "needs_review"] as const) {
74
+ if (request.collectionCursors && request.collectionCursors[view] === null) continue;
75
+ let cursor = request.collectionCursors?.[view] ?? undefined;
76
+ let bytes = 0;
77
+ for (let pageNumber = 0; pageNumber < CATALOG_PAGE_BUDGET; pageNumber++) {
78
+ const page = await services.list(db, context, {
79
+ kind: "group",
80
+ view,
81
+ limit: CATALOG_PAGE_SIZE,
82
+ ...(cursor ? { cursor } : {}),
83
+ });
84
+ const records: Array<Awaited<ReturnType<typeof getKnowledgeEntry>>> = [];
85
+ for (let offset = 0; offset < page.entries.length; offset += 4) {
86
+ records.push(
87
+ ...(await Promise.all(
88
+ page.entries
89
+ .slice(offset, offset + 4)
90
+ .map((summary) => services.get(db, context, summary.id, { view })),
91
+ )),
92
+ );
93
+ }
94
+ for (const record of records) {
95
+ // Re-read the current view: an exact historical revision may remain
96
+ // readable after replacement and must not be called the current map.
97
+ if (
98
+ !record ||
99
+ record.archived ||
100
+ record.revision.entry.kind !== "group" ||
101
+ record.revision.outcome !== (view === "published" ? "published" : "pending")
102
+ )
103
+ continue;
104
+ const entry = record.revision.entry;
105
+ const descriptor = {
106
+ id: record.id,
107
+ revisionId: record.revision.id,
108
+ version: record.version,
109
+ scope: record.scope,
110
+ view,
111
+ title: entry.title,
112
+ description: entry.content.slice(0, DESCRIPTION_CHARS),
113
+ descriptionTruncated: entry.content.length > DESCRIPTION_CHARS,
114
+ parentIds: entry.groupIds,
115
+ };
116
+ collections.entries.push(descriptor);
117
+ bytes += Buffer.byteLength(JSON.stringify(descriptor), "utf8");
118
+ }
119
+ cursor = page.nextCursor ?? undefined;
120
+ if (!cursor || bytes >= CATALOG_BYTES) break;
121
+ }
122
+ collections.nextCursors[view] = cursor ?? null;
123
+ if (cursor) collections.complete = false;
124
+ }
125
+ return { collections, matches };
126
+ }
@@ -0,0 +1,83 @@
1
+ import { CORE_INTEGRATION_DEFINITIONS } from "@opengeni/capabilities";
2
+ import {
3
+ assertOrganizationIntegrationAllowed,
4
+ type IntegrationSource,
5
+ type OrganizationIntegrationPolicy,
6
+ } from "@opengeni/contracts";
7
+
8
+ /** Admission for source discovery, before any credential-bearing network work.
9
+ * Auto-detection may try only protocols the organization actually permits.
10
+ */
11
+ export function integrationSourceForOrganizationPolicy(
12
+ policy: OrganizationIntegrationPolicy,
13
+ source: IntegrationSource,
14
+ ): IntegrationSource {
15
+ const snapshot = structuredClone(source);
16
+ if (policy.mode === "unrestricted") return snapshot;
17
+ if (snapshot.kind === "definition") {
18
+ const definition = CORE_INTEGRATION_DEFINITIONS.find(
19
+ (item) => item.id === snapshot.definitionId,
20
+ );
21
+ assertOrganizationIntegrationAllowed(policy, definition?.id ?? null);
22
+ return snapshot;
23
+ }
24
+ if (snapshot.kind !== "auto") {
25
+ assertOrganizationIntegrationAllowed(policy, `custom:${snapshot.kind}`);
26
+ return snapshot;
27
+ }
28
+ const openapi = policy.allowedIntegrationKeys.includes("custom:openapi");
29
+ const graphql = policy.allowedIntegrationKeys.includes("custom:graphql");
30
+ if (openapi && graphql) return snapshot;
31
+ if (openapi) return { ...snapshot, kind: "openapi" };
32
+ if (graphql) return { kind: "graphql", endpoint: snapshot.url };
33
+ assertOrganizationIntegrationAllowed(policy, null);
34
+ return snapshot;
35
+ }
36
+
37
+ const dedicated = [
38
+ { key: "gmail", label: "Gmail" },
39
+ { key: "atlassian", label: "Atlassian (Jira and Confluence)" },
40
+ { key: "slack-personal", label: "Slack personal accounts" },
41
+ { key: "slack-bot", label: "Slack workspace bots" },
42
+ { key: "github-personal", label: "GitHub personal accounts" },
43
+ { key: "github-app", label: "GitHub App installations" },
44
+ { key: "github-lens", label: "GitHub PR review" },
45
+ { key: "fiken", label: "Fiken" },
46
+ { key: "x", label: "X" },
47
+ { key: "reddit", label: "Reddit" },
48
+ ] as const;
49
+
50
+ /** Product identities owned by the server, independent of mutable workspace catalogs. */
51
+ export function organizationIntegrationCatalog(): {
52
+ integrations: Array<{ key: string; label: string; kind: "curated" | "custom" }>;
53
+ } {
54
+ return {
55
+ integrations: [
56
+ ...CORE_INTEGRATION_DEFINITIONS.map((definition) => ({
57
+ key: definition.id,
58
+ label: definition.name,
59
+ kind: "curated" as const,
60
+ })),
61
+ ...dedicated.map((item) => ({ ...item, kind: "curated" as const })),
62
+ { key: "custom:mcp", label: "Custom MCP servers", kind: "custom" },
63
+ { key: "custom:openapi", label: "Custom OpenAPI services", kind: "custom" },
64
+ { key: "custom:graphql", label: "Custom GraphQL services", kind: "custom" },
65
+ ],
66
+ };
67
+ }
68
+
69
+ /** Only use for a validated built-in Connect adapter, never caller-authored catalog metadata. */
70
+ export function integrationKeyForConnectProvider(providerId: string): string | null {
71
+ if (CORE_INTEGRATION_DEFINITIONS.some((definition) => definition.id === providerId))
72
+ return providerId;
73
+ if (dedicated.some((item) => item.key === providerId)) return providerId;
74
+ if (providerId === "fiken-token" || providerId === "fiken-oauth") return "fiken";
75
+ if (providerId === "google-drive-knowledge" || providerId === "google-drive-publish")
76
+ return "google-drive";
77
+ if (["mcp-oauth", "mcp-headers", "mcp-bearer", "mcp-install"].includes(providerId))
78
+ return "custom:mcp";
79
+ if (providerId === "openapi") return "custom:openapi";
80
+ if (providerId === "graphql") return "custom:graphql";
81
+ // Unknown adapter identities do not inherit an allowed provider's authority.
82
+ return null;
83
+ }
@@ -22,7 +22,7 @@ export const OPENGENI_PRODUCT_INTEGRATION_PACK = {
22
22
  "Help an implementation agent add OpenGeni to an external product with adaptive discovery, tenant-safe boundaries, framework-native UI, authorized data tools, and the customer's chosen delivery autonomy. Installation stays inactive until one implementation session selects the Skill.",
23
23
  role: "software-engineering",
24
24
  category: "product-integration",
25
- version: "0.2.0",
25
+ version: "0.2.1",
26
26
  skills: [OPENGENI_PRODUCT_INTEGRATION_SKILL],
27
27
  components: [],
28
28
  tools: [],