@opengeni/core 2.10.0-canary.0 → 2.10.0-canary.2

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 (50) hide show
  1. package/dist/application/external-workspace-members.d.ts +16 -0
  2. package/dist/application/host-mcp-resolvers.d.ts +13 -0
  3. package/dist/application/new-session-drafts.d.ts +4 -2
  4. package/dist/chunk-5WZYFL45.js +266 -0
  5. package/dist/chunk-5WZYFL45.js.map +1 -0
  6. package/dist/domain/file-owner.d.ts +8 -0
  7. package/dist/domain/knowledge-files.d.ts +20 -0
  8. package/dist/domain/knowledge-messages.d.ts +39 -0
  9. package/dist/domain/knowledge-messages.test.d.ts +1 -0
  10. package/dist/domain/knowledge-search.d.ts +80 -0
  11. package/dist/domain/knowledge-search.test.d.ts +1 -0
  12. package/dist/domain/knowledge.d.ts +9 -0
  13. package/dist/domain/pr-review.d.ts +2 -2
  14. package/dist/domain/resources.d.ts +2 -2
  15. package/dist/domain/scheduled-tasks.d.ts +12 -2
  16. package/dist/domain/sessions.d.ts +6 -2
  17. package/dist/domain/skills.d.ts +16 -0
  18. package/dist/index.d.ts +6 -4
  19. package/dist/index.js +3948 -3939
  20. package/dist/index.js.map +1 -1
  21. package/dist/remote-mcp-credentials.d.ts +16 -1
  22. package/dist/remote-mcp-credentials.js +7 -215
  23. package/dist/remote-mcp-credentials.js.map +1 -1
  24. package/package.json +12 -12
  25. package/src/application/external-workspace-members.ts +89 -0
  26. package/src/application/host-mcp-resolvers.ts +85 -0
  27. package/src/application/new-session-drafts.ts +37 -3
  28. package/src/domain/file-owner.ts +52 -0
  29. package/src/domain/host-mcp-task-admission.ts +27 -17
  30. package/src/domain/knowledge-files.ts +39 -0
  31. package/src/domain/knowledge-messages.test.ts +91 -0
  32. package/src/domain/knowledge-messages.ts +105 -0
  33. package/src/domain/knowledge-search.test.ts +45 -0
  34. package/src/domain/knowledge-search.ts +55 -0
  35. package/src/domain/knowledge.ts +133 -0
  36. package/src/domain/product-integration-skill.gen.ts +7 -7
  37. package/src/domain/resources.ts +36 -25
  38. package/src/domain/scheduled-tasks.ts +362 -139
  39. package/src/domain/sessions.ts +117 -20
  40. package/src/domain/skills.ts +8 -0
  41. package/src/index.ts +9 -4
  42. package/src/remote-mcp-credentials.ts +76 -8
  43. package/dist/domain/company-brain-governed-writes.d.ts +0 -79
  44. package/dist/domain/governed-learning-activation.d.ts +0 -16
  45. package/dist/domain/governed-learning-evaluator.d.ts +0 -6
  46. package/dist/domain/remember.d.ts +0 -72
  47. package/src/domain/company-brain-governed-writes.ts +0 -346
  48. package/src/domain/governed-learning-activation.ts +0 -44
  49. package/src/domain/governed-learning-evaluator.ts +0 -49
  50. package/src/domain/remember.ts +0 -577
@@ -1,8 +1,23 @@
1
1
  import type { Settings } from "@opengeni/config";
2
+ import { type Database, type HostMcpResolverRoute } from "@opengeni/db";
2
3
  import { type ConnectionCredentialsPort } from "@opengeni/contracts";
3
4
  import { pinnedFetch } from "@opengeni/network";
4
5
  /** Optional standalone transport for the existing request-time host port.
5
6
  * No successful credential is cached or persisted. The host must authorize
6
7
  * every immutable request context, including revocation and binding generation.
7
8
  */
8
- export declare function createRemoteMcpCredentialsPort(settings: Pick<Settings, "hostMcpCredentialResolversJson" | "environment" | "integrationsAllowPrivateNetworkTargets">, transport?: typeof pinnedFetch): ConnectionCredentialsPort;
9
+ export declare function createRemoteMcpCredentialsPort(settings: Pick<Settings, "hostMcpCredentialResolversJson" | "environment" | "integrationsAllowPrivateNetworkTargets">, transport?: typeof pinnedFetch, routes?: {
10
+ resolve: (input: {
11
+ accountId: string;
12
+ workspaceId: string;
13
+ }) => Promise<HostMcpResolverRoute>;
14
+ isCurrent: (input: {
15
+ accountId: string;
16
+ workspaceId: string;
17
+ }, route: HostMcpResolverRoute) => Promise<boolean>;
18
+ }): ConnectionCredentialsPort;
19
+ /** Native standalone transport; the database remains authoritative on every
20
+ * resolution/use. Embedded in-process ports retain their existing precedence. */
21
+ export declare function createNativeRemoteMcpCredentialsPort(settings: Settings, db: Database, transport?: typeof pinnedFetch): ConnectionCredentialsPort;
22
+ /** Parses only server configuration; never return parser errors containing secrets. */
23
+ export declare function hasLegacyHostMcpResolver(settings: Pick<Settings, "hostMcpCredentialResolversJson">, accountId: string): boolean;
@@ -1,219 +1,11 @@
1
- // src/remote-mcp-credentials.ts
2
1
  import {
3
- McpConnectionResourceScope
4
- } from "@opengeni/contracts";
5
- import { pinnedFetch, readResponseJsonBounded, validateHttpUrl } from "@opengeni/network";
6
- import { z } from "zod";
7
- var configuration = z.array(
8
- z.object({
9
- accountId: z.string().uuid(),
10
- url: z.string().max(2048),
11
- bearerToken: z.string().min(1).max(8192).regex(/^[^\r\n]+$/),
12
- timeoutMs: z.number().int().min(100).max(3e4).default(1e4)
13
- }).strict()
14
- ).max(128);
15
- var scope = {
16
- accountId: z.string(),
17
- workspaceId: z.string(),
18
- sessionId: z.string(),
19
- providerDomain: z.string(),
20
- provider: z.string().optional(),
21
- scopes: z.array(z.string()).max(256).optional(),
22
- resource: z.string().optional(),
23
- selectedResources: z.array(McpConnectionResourceScope).max(256).optional()
24
- };
25
- var resolution = z.discriminatedUnion("status", [
26
- z.object({
27
- ...scope,
28
- status: z.literal("ok"),
29
- connectionId: z.string().min(1),
30
- headers: z.record(z.string(), z.string()),
31
- placements: z.array(
32
- z.object({
33
- carrier: z.enum(["header", "query", "cookie"]),
34
- name: z.string(),
35
- value: z.string(),
36
- prefix: z.string().optional()
37
- }).strict()
38
- ).max(64).optional(),
39
- expiresAt: z.string().datetime({ offset: true })
40
- }).strict(),
41
- z.object({
42
- ...scope,
43
- status: z.literal("auth_needed"),
44
- connectionId: z.string().optional(),
45
- reason: z.enum([
46
- "missing_connection",
47
- "expired",
48
- "insufficient_scope",
49
- "refresh_failed",
50
- "personal_authority_unavailable",
51
- "unsupported_auth",
52
- "resource_scope_unavailable"
53
- ]),
54
- authorizationUrl: z.string().optional()
55
- }).strict()
56
- ]);
57
- var gatewayResolution = z.discriminatedUnion("status", [
58
- resolution.options[0].omit({ sessionId: true }).extend({ requestId: z.string().uuid() }),
59
- resolution.options[1].omit({ sessionId: true }).extend({ requestId: z.string().uuid() })
60
- ]);
61
- var responseEnvelope = z.object({
62
- version: z.literal(1),
63
- requestId: z.string().uuid(),
64
- destinationUrl: z.string(),
65
- resolution: z.union([resolution, gatewayResolution])
66
- }).strict();
67
- function normalizeResolution(value) {
68
- const common = {
69
- accountId: value.accountId,
70
- workspaceId: value.workspaceId,
71
- ..."sessionId" in value ? { sessionId: value.sessionId } : { requestId: value.requestId },
72
- providerDomain: value.providerDomain,
73
- ...value.provider !== void 0 ? { provider: value.provider } : {},
74
- ...value.scopes !== void 0 ? { scopes: value.scopes } : {},
75
- ...value.resource !== void 0 ? { resource: value.resource } : {},
76
- ...value.selectedResources !== void 0 ? { selectedResources: value.selectedResources } : {}
77
- };
78
- if (value.status === "auth_needed")
79
- return {
80
- ...common,
81
- status: "auth_needed",
82
- reason: value.reason,
83
- ...value.connectionId !== void 0 ? { connectionId: value.connectionId } : {},
84
- ...value.authorizationUrl !== void 0 ? { authorizationUrl: value.authorizationUrl } : {}
85
- };
86
- return {
87
- ...common,
88
- status: "ok",
89
- connectionId: value.connectionId,
90
- headers: value.headers,
91
- expiresAt: value.expiresAt,
92
- ...value.placements !== void 0 ? {
93
- placements: value.placements.map((placement) => ({
94
- carrier: placement.carrier,
95
- name: placement.name,
96
- value: placement.value,
97
- ...placement.prefix !== void 0 ? { prefix: placement.prefix } : {}
98
- }))
99
- } : {}
100
- };
101
- }
102
- function createRemoteMcpCredentialsPort(settings, transport = pinnedFetch) {
103
- if (!settings.hostMcpCredentialResolversJson) return {};
104
- let entries;
105
- try {
106
- entries = configuration.parse(JSON.parse(settings.hostMcpCredentialResolversJson));
107
- for (const entry of entries) validateHttpUrl(entry.url);
108
- if (new Set(entries.map((entry) => entry.accountId)).size !== entries.length) throw new Error();
109
- } catch {
110
- throw new Error("Invalid host MCP credential resolver configuration");
111
- }
112
- const byAccount = new Map(entries.map((entry) => [entry.accountId, entry]));
113
- const active = /* @__PURE__ */ new Map();
114
- let physicalRequests = 0;
115
- const resolveRemote = async (input) => {
116
- const serialized = JSON.stringify(input);
117
- const request = JSON.parse(serialized);
118
- const unavailable = (reason) => ({
119
- status: "auth_needed",
120
- accountId: request.accountId,
121
- workspaceId: request.workspaceId,
122
- ..."sessionId" in request ? { sessionId: request.sessionId } : { requestId: request.requestId },
123
- providerDomain: request.connectionRef.providerDomain,
124
- ...request.connectionRef.provider !== void 0 ? { provider: request.connectionRef.provider } : {},
125
- ...request.connectionRef.connectionId !== void 0 ? { connectionId: request.connectionRef.connectionId } : {},
126
- ...request.connectionRef.scopes !== void 0 ? { scopes: request.connectionRef.scopes } : {},
127
- ...request.connectionRef.resource !== void 0 ? { resource: request.connectionRef.resource } : {},
128
- ...request.connectionRef.selectedResources !== void 0 ? { selectedResources: request.connectionRef.selectedResources } : {},
129
- reason
130
- });
131
- const config = byAccount.get(request.accountId);
132
- if (!config || request.connectionRef.authoritySource !== "host")
133
- return unavailable("unsupported_auth");
134
- if (new TextEncoder().encode(serialized).byteLength > 65536)
135
- return unavailable("refresh_failed");
136
- const pending = active.get(serialized);
137
- if (pending) return structuredClone(await pending);
138
- if (physicalRequests >= 128) return unavailable("refresh_failed");
139
- const run = async () => {
140
- const requestId = crypto.randomUUID();
141
- const controller = new AbortController();
142
- let timer;
143
- const deadline = new Promise((_, reject) => {
144
- timer = setTimeout(() => {
145
- controller.abort();
146
- reject(new Error("Host resolver deadline"));
147
- }, config.timeoutMs);
148
- });
149
- try {
150
- physicalRequests++;
151
- return await Promise.race([
152
- deadline,
153
- (async () => {
154
- const response = await transport(
155
- config.url,
156
- {
157
- method: "POST",
158
- redirect: "manual",
159
- signal: controller.signal,
160
- headers: {
161
- Authorization: `Bearer ${config.bearerToken}`,
162
- "Content-Type": "application/json"
163
- },
164
- body: JSON.stringify({ version: 1, requestId, request })
165
- },
166
- settings,
167
- { requireHttpsOutsideLocalTest: true, label: "Host MCP credential resolver" }
168
- );
169
- if (!response.ok) {
170
- await response.body?.cancel();
171
- return unavailable("refresh_failed");
172
- }
173
- const parsed = responseEnvelope.parse(
174
- await readResponseJsonBounded(response, 65536, "Host MCP credential resolver", {
175
- signal: controller.signal
176
- })
177
- );
178
- if (parsed.requestId !== requestId || parsed.destinationUrl !== request.destinationUrl || parsed.resolution.accountId !== request.accountId || parsed.resolution.workspaceId !== request.workspaceId || ("sessionId" in request ? !("sessionId" in parsed.resolution) || parsed.resolution.sessionId !== request.sessionId : !("requestId" in parsed.resolution) || parsed.resolution.requestId !== request.requestId))
179
- return unavailable("refresh_failed");
180
- if (parsed.resolution.status === "ok" && (Date.parse(parsed.resolution.expiresAt) <= Date.now() + 5e3 || Date.parse(parsed.resolution.expiresAt) > Date.now() + 9e5)) {
181
- return unavailable("refresh_failed");
182
- }
183
- return normalizeResolution(parsed.resolution);
184
- })().finally(() => {
185
- physicalRequests--;
186
- })
187
- ]);
188
- } catch {
189
- return unavailable("refresh_failed");
190
- } finally {
191
- clearTimeout(timer);
192
- }
193
- };
194
- const task = run();
195
- active.set(serialized, task);
196
- try {
197
- return structuredClone(await task);
198
- } finally {
199
- if (active.get(serialized) === task) active.delete(serialized);
200
- }
201
- };
202
- return {
203
- mcpAuthoritySource: "host",
204
- async mcpCredentials(input) {
205
- const result = await resolveRemote(input);
206
- if (!("sessionId" in result)) throw new Error("Invalid host turn credential scope");
207
- return result;
208
- },
209
- async mcpGatewayCredentials(input) {
210
- const result = await resolveRemote(input);
211
- if (!("requestId" in result)) throw new Error("Invalid host gateway credential scope");
212
- return result;
213
- }
214
- };
215
- }
2
+ createNativeRemoteMcpCredentialsPort,
3
+ createRemoteMcpCredentialsPort,
4
+ hasLegacyHostMcpResolver
5
+ } from "./chunk-5WZYFL45.js";
216
6
  export {
217
- createRemoteMcpCredentialsPort
7
+ createNativeRemoteMcpCredentialsPort,
8
+ createRemoteMcpCredentialsPort,
9
+ hasLegacyHostMcpResolver
218
10
  };
219
11
  //# sourceMappingURL=remote-mcp-credentials.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/remote-mcp-credentials.ts"],"sourcesContent":["import type { Settings } from \"@opengeni/config\";\nimport {\n McpConnectionResourceScope,\n type ConnectionCredentialsPort,\n type McpCredentialsRequest,\n type McpCredentialResolution,\n type McpGatewayCredentialsRequest,\n type McpGatewayCredentialResolution,\n} from \"@opengeni/contracts\";\nimport { pinnedFetch, readResponseJsonBounded, validateHttpUrl } from \"@opengeni/network\";\nimport { z } from \"zod\";\n\nconst configuration = z\n .array(\n z\n .object({\n accountId: z.string().uuid(),\n url: z.string().max(2048),\n bearerToken: z\n .string()\n .min(1)\n .max(8192)\n .regex(/^[^\\r\\n]+$/),\n timeoutMs: z.number().int().min(100).max(30_000).default(10_000),\n })\n .strict(),\n )\n .max(128);\n\nconst scope = {\n accountId: z.string(),\n workspaceId: z.string(),\n sessionId: z.string(),\n providerDomain: z.string(),\n provider: z.string().optional(),\n scopes: z.array(z.string()).max(256).optional(),\n resource: z.string().optional(),\n selectedResources: z.array(McpConnectionResourceScope).max(256).optional(),\n};\nconst resolution = z.discriminatedUnion(\"status\", [\n z\n .object({\n ...scope,\n status: z.literal(\"ok\"),\n connectionId: z.string().min(1),\n headers: z.record(z.string(), z.string()),\n placements: z\n .array(\n z\n .object({\n carrier: z.enum([\"header\", \"query\", \"cookie\"]),\n name: z.string(),\n value: z.string(),\n prefix: z.string().optional(),\n })\n .strict(),\n )\n .max(64)\n .optional(),\n expiresAt: z.string().datetime({ offset: true }),\n })\n .strict(),\n z\n .object({\n ...scope,\n status: z.literal(\"auth_needed\"),\n connectionId: z.string().optional(),\n reason: z.enum([\n \"missing_connection\",\n \"expired\",\n \"insufficient_scope\",\n \"refresh_failed\",\n \"personal_authority_unavailable\",\n \"unsupported_auth\",\n \"resource_scope_unavailable\",\n ]),\n authorizationUrl: z.string().optional(),\n })\n .strict(),\n]);\nconst gatewayResolution = z.discriminatedUnion(\"status\", [\n resolution.options[0].omit({ sessionId: true }).extend({ requestId: z.string().uuid() }),\n resolution.options[1].omit({ sessionId: true }).extend({ requestId: z.string().uuid() }),\n]);\nconst responseEnvelope = z\n .object({\n version: z.literal(1),\n requestId: z.string().uuid(),\n destinationUrl: z.string(),\n resolution: z.union([resolution, gatewayResolution]),\n })\n .strict();\n\ntype RemoteResolution = McpCredentialResolution | McpGatewayCredentialResolution;\nfunction normalizeResolution(\n value: z.infer<typeof resolution> | z.infer<typeof gatewayResolution>,\n): RemoteResolution {\n const common = {\n accountId: value.accountId,\n workspaceId: value.workspaceId,\n ...(\"sessionId\" in value ? { sessionId: value.sessionId } : { requestId: value.requestId }),\n providerDomain: value.providerDomain,\n ...(value.provider !== undefined ? { provider: value.provider } : {}),\n ...(value.scopes !== undefined ? { scopes: value.scopes } : {}),\n ...(value.resource !== undefined ? { resource: value.resource } : {}),\n ...(value.selectedResources !== undefined\n ? { selectedResources: value.selectedResources }\n : {}),\n };\n if (value.status === \"auth_needed\")\n return {\n ...common,\n status: \"auth_needed\",\n reason: value.reason,\n ...(value.connectionId !== undefined ? { connectionId: value.connectionId } : {}),\n ...(value.authorizationUrl !== undefined ? { authorizationUrl: value.authorizationUrl } : {}),\n };\n return {\n ...common,\n status: \"ok\",\n connectionId: value.connectionId,\n headers: value.headers,\n expiresAt: value.expiresAt,\n ...(value.placements !== undefined\n ? {\n placements: value.placements.map((placement) => ({\n carrier: placement.carrier,\n name: placement.name,\n value: placement.value,\n ...(placement.prefix !== undefined ? { prefix: placement.prefix } : {}),\n })),\n }\n : {}),\n };\n}\n\n/** Optional standalone transport for the existing request-time host port.\n * No successful credential is cached or persisted. The host must authorize\n * every immutable request context, including revocation and binding generation.\n */\nexport function createRemoteMcpCredentialsPort(\n settings: Pick<\n Settings,\n \"hostMcpCredentialResolversJson\" | \"environment\" | \"integrationsAllowPrivateNetworkTargets\"\n >,\n transport: typeof pinnedFetch = pinnedFetch,\n): ConnectionCredentialsPort {\n if (!settings.hostMcpCredentialResolversJson) return {};\n let entries: z.infer<typeof configuration>;\n try {\n entries = configuration.parse(JSON.parse(settings.hostMcpCredentialResolversJson));\n for (const entry of entries) validateHttpUrl(entry.url);\n if (new Set(entries.map((entry) => entry.accountId)).size !== entries.length) throw new Error();\n } catch {\n // Configuration includes credentials: never return parser input or causes.\n throw new Error(\"Invalid host MCP credential resolver configuration\");\n }\n const byAccount = new Map(entries.map((entry) => [entry.accountId, entry]));\n const active = new Map<string, Promise<RemoteResolution>>();\n let physicalRequests = 0;\n const resolveRemote = async (\n input: McpCredentialsRequest | McpGatewayCredentialsRequest,\n ): Promise<RemoteResolution> => {\n const serialized = JSON.stringify(input);\n const request: McpCredentialsRequest | McpGatewayCredentialsRequest = JSON.parse(serialized);\n const unavailable = (reason: \"unsupported_auth\" | \"refresh_failed\"): RemoteResolution => ({\n status: \"auth_needed\",\n accountId: request.accountId,\n workspaceId: request.workspaceId,\n ...(\"sessionId\" in request\n ? { sessionId: request.sessionId }\n : { requestId: request.requestId }),\n providerDomain: request.connectionRef.providerDomain,\n ...(request.connectionRef.provider !== undefined\n ? { provider: request.connectionRef.provider }\n : {}),\n ...(request.connectionRef.connectionId !== undefined\n ? { connectionId: request.connectionRef.connectionId }\n : {}),\n ...(request.connectionRef.scopes !== undefined\n ? { scopes: request.connectionRef.scopes }\n : {}),\n ...(request.connectionRef.resource !== undefined\n ? { resource: request.connectionRef.resource }\n : {}),\n ...(request.connectionRef.selectedResources !== undefined\n ? { selectedResources: request.connectionRef.selectedResources }\n : {}),\n reason,\n });\n const config = byAccount.get(request.accountId);\n if (!config || request.connectionRef.authoritySource !== \"host\")\n return unavailable(\"unsupported_auth\");\n // Snapshot before any asynchronous operation; never retain mutable caller\n // objects as the authority for a delayed response.\n if (new TextEncoder().encode(serialized).byteLength > 65_536)\n return unavailable(\"refresh_failed\");\n const pending = active.get(serialized);\n if (pending) return structuredClone(await pending);\n if (physicalRequests >= 128) return unavailable(\"refresh_failed\");\n const run = async (): Promise<RemoteResolution> => {\n const requestId = crypto.randomUUID();\n const controller = new AbortController();\n let timer: ReturnType<typeof setTimeout> | undefined;\n const deadline = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n controller.abort();\n reject(new Error(\"Host resolver deadline\"));\n }, config.timeoutMs);\n });\n try {\n physicalRequests++;\n return await Promise.race([\n deadline,\n (async () => {\n const response = await transport(\n config.url,\n {\n method: \"POST\",\n redirect: \"manual\",\n signal: controller.signal,\n headers: {\n Authorization: `Bearer ${config.bearerToken}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({ version: 1, requestId, request }),\n },\n settings,\n { requireHttpsOutsideLocalTest: true, label: \"Host MCP credential resolver\" },\n );\n if (!response.ok) {\n await response.body?.cancel();\n return unavailable(\"refresh_failed\");\n }\n const parsed = responseEnvelope.parse(\n await readResponseJsonBounded(response, 65_536, \"Host MCP credential resolver\", {\n signal: controller.signal,\n }),\n );\n if (\n parsed.requestId !== requestId ||\n parsed.destinationUrl !== request.destinationUrl ||\n parsed.resolution.accountId !== request.accountId ||\n parsed.resolution.workspaceId !== request.workspaceId ||\n (\"sessionId\" in request\n ? !(\"sessionId\" in parsed.resolution) ||\n parsed.resolution.sessionId !== request.sessionId\n : !(\"requestId\" in parsed.resolution) ||\n parsed.resolution.requestId !== request.requestId)\n )\n return unavailable(\"refresh_failed\");\n if (\n parsed.resolution.status === \"ok\" &&\n (Date.parse(parsed.resolution.expiresAt) <= Date.now() + 5_000 ||\n Date.parse(parsed.resolution.expiresAt) > Date.now() + 900_000)\n ) {\n return unavailable(\"refresh_failed\");\n }\n // Existing buildHostConnectionTokenResolver validates the precise\n // connection, provider, scopes, resources and credential placements.\n return normalizeResolution(parsed.resolution);\n })().finally(() => {\n physicalRequests--;\n }),\n ]);\n } catch {\n return unavailable(\"refresh_failed\");\n } finally {\n clearTimeout(timer);\n }\n };\n const task = run();\n active.set(serialized, task);\n try {\n return structuredClone(await task);\n } finally {\n if (active.get(serialized) === task) active.delete(serialized);\n }\n };\n return {\n mcpAuthoritySource: \"host\",\n async mcpCredentials(input) {\n const result = await resolveRemote(input);\n if (!(\"sessionId\" in result)) throw new Error(\"Invalid host turn credential scope\");\n return result;\n },\n async mcpGatewayCredentials(input) {\n const result = await resolveRemote(input);\n if (!(\"requestId\" in result)) throw new Error(\"Invalid host gateway credential scope\");\n return result;\n },\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/core",
3
- "version": "2.10.0-canary.0",
3
+ "version": "2.10.0-canary.2",
4
4
  "description": "OpenGeni framework-agnostic core: the domain, access, and billing layers (neutral access, off-HTTP V2 surface). Behavior-preserving extraction from apps/api — keeps Hono's HTTPException for error throwing (typed-errors cleanup deferred).",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -58,17 +58,17 @@
58
58
  },
59
59
  "dependencies": {
60
60
  "@modelcontextprotocol/sdk": "^1.29.0",
61
- "@opengeni/capabilities": "^0.3.3-canary.12",
62
- "@opengeni/codex": "^0.2.22-canary.12",
63
- "@opengeni/config": "^1.2.0-canary.0",
64
- "@opengeni/contracts": "^3.1.0-canary.0",
65
- "@opengeni/db": "^4.4.0-canary.0",
66
- "@opengeni/documents": "^0.8.28-canary.0",
67
- "@opengeni/events": "^0.4.26-canary.0",
68
- "@opengeni/network": "^0.3.1-canary.12",
69
- "@opengeni/observability": "^0.8.27-canary.0",
70
- "@opengeni/runtime": "^2.6.0-canary.0",
71
- "@opengeni/storage": "^0.2.128-canary.0",
61
+ "@opengeni/capabilities": "^0.3.3-canary.14",
62
+ "@opengeni/codex": "^0.2.22-canary.14",
63
+ "@opengeni/config": "^1.2.0-canary.2",
64
+ "@opengeni/contracts": "^3.1.0-canary.2",
65
+ "@opengeni/db": "^4.4.0-canary.2",
66
+ "@opengeni/documents": "^0.8.28-canary.2",
67
+ "@opengeni/events": "^0.4.26-canary.2",
68
+ "@opengeni/network": "^0.3.1-canary.14",
69
+ "@opengeni/observability": "^0.8.27-canary.2",
70
+ "@opengeni/runtime": "^2.6.0-canary.2",
71
+ "@opengeni/storage": "^0.2.128-canary.2",
72
72
  "hono": "^4.12.18",
73
73
  "zod": "^4.2.1"
74
74
  },
@@ -2,6 +2,8 @@ import type { Context } from "hono";
2
2
  import { HTTPException } from "hono/http-exception";
3
3
  import {
4
4
  AddExternalWorkspaceMemberRequest,
5
+ ExternalIdentityReference,
6
+ CancelExternalWorkspaceMemberGrantRequest,
5
7
  type ExternalIdentity,
6
8
  } from "@opengeni/contracts/external-identities";
7
9
  import {
@@ -12,7 +14,12 @@ import {
12
14
  requireWorkspace,
13
15
  setRlsContext,
14
16
  withWorkspaceSubjectRls,
17
+ addExternalWorkspaceMemberOperation,
18
+ cancelExternalWorkspaceMemberGrant,
19
+ lookupExternalIdentity,
20
+ nestedPostgresSqlState,
15
21
  } from "@opengeni/db";
22
+ import { organizationMembershipHttpStatus } from "../domain/organization-membership-lifecycle";
16
23
  import {
17
24
  accountScopedApiKeyWorkspaceAuthority,
18
25
  hasPermission,
@@ -43,6 +50,21 @@ export async function addExternalWorkspaceMemberForRequest(
43
50
  ) {
44
51
  throw new HTTPException(403, { message: "membership exceeds key authority" });
45
52
  }
53
+ if (payload.operationId) {
54
+ try {
55
+ return await addExternalWorkspaceMemberOperation(
56
+ deps.db,
57
+ {
58
+ organizationId: authority.accountId,
59
+ workspaceId,
60
+ actorSubjectId: grant.subjectId,
61
+ },
62
+ { ...payload, operationId: payload.operationId },
63
+ );
64
+ } catch (error) {
65
+ rethrowExternalWorkspaceOperation(error);
66
+ }
67
+ }
46
68
  return withWorkspaceSubjectRls(deps.db, workspaceId, grant.subjectId, async (tx) => {
47
69
  await lockExternalWorkspaceMembershipLifecycle(tx, grant.accountId);
48
70
  const live = await requireFreshAccessGrant(
@@ -93,3 +115,70 @@ export async function addExternalWorkspaceMemberForRequest(
93
115
  return identity;
94
116
  });
95
117
  }
118
+
119
+ function rethrowExternalWorkspaceOperation(error: unknown): never {
120
+ const status = organizationMembershipHttpStatus(nestedPostgresSqlState(error));
121
+ if (status)
122
+ throw new HTTPException(status, {
123
+ message:
124
+ status === 409
125
+ ? "External membership operation changed or was cancelled"
126
+ : "External membership operation is not available",
127
+ });
128
+ throw error;
129
+ }
130
+
131
+ async function externalService(c: Context, deps: AccessDeps, organizationId: string) {
132
+ const context = await requireAccessContext(c, deps);
133
+ const authority = accountScopedApiKeyWorkspaceAuthority(context);
134
+ if (
135
+ !authority ||
136
+ authority.accountId !== organizationId ||
137
+ !hasPermission(authority.permissions, "members:manage")
138
+ ) {
139
+ throw new HTTPException(403, {
140
+ message: "External membership operation requires an organization service key",
141
+ });
142
+ }
143
+ return { organizationId, actorSubjectId: context.subjectId };
144
+ }
145
+
146
+ export async function lookupExternalIdentityForRequest(
147
+ c: Context,
148
+ deps: AccessDeps,
149
+ organizationId: string,
150
+ input: unknown,
151
+ ) {
152
+ const service = await externalService(c, deps, organizationId);
153
+ const identity = ExternalIdentityReference.safeParse(input);
154
+ if (!identity.success)
155
+ throw new HTTPException(422, { message: "Invalid external identity reference" });
156
+ try {
157
+ return await lookupExternalIdentity(deps.db, service, identity.data);
158
+ } catch (error) {
159
+ rethrowExternalWorkspaceOperation(error);
160
+ }
161
+ }
162
+
163
+ export async function cancelExternalWorkspaceMemberGrantForRequest(
164
+ c: Context,
165
+ deps: AccessDeps,
166
+ organizationId: string,
167
+ workspaceId: string,
168
+ membershipId: string,
169
+ input: unknown,
170
+ ) {
171
+ const service = await externalService(c, deps, organizationId);
172
+ const request = CancelExternalWorkspaceMemberGrantRequest.safeParse(input);
173
+ if (!request.success)
174
+ throw new HTTPException(422, { message: "Invalid external grant cancellation" });
175
+ try {
176
+ return await cancelExternalWorkspaceMemberGrant(
177
+ deps.db,
178
+ { ...service, workspaceId, membershipId },
179
+ request.data,
180
+ );
181
+ } catch (error) {
182
+ rethrowExternalWorkspaceOperation(error);
183
+ }
184
+ }
@@ -0,0 +1,85 @@
1
+ import type { Context } from "hono";
2
+ import { HTTPException } from "hono/http-exception";
3
+ import { z } from "zod";
4
+ import {
5
+ HostMcpResolverSource,
6
+ PutHostMcpResolverRequest,
7
+ RevokeHostMcpResolverRequest,
8
+ } from "@opengeni/contracts/host-mcp-resolvers";
9
+ import { getHostMcpResolver, mutateHostMcpResolver, HostMcpResolverError } from "@opengeni/db";
10
+ import { isLocalTestEnvironment, validateHttpUrl } from "@opengeni/network";
11
+ import {
12
+ accountScopedApiKeyWorkspaceAuthority,
13
+ requireAccessContext,
14
+ type AccessDeps,
15
+ } from "../access";
16
+ import { requireVariableSetEncryption } from "../domain/environments";
17
+ import { hasLegacyHostMcpResolver } from "../remote-mcp-credentials";
18
+
19
+ export async function hostMcpResolverForRequest(
20
+ context: Context,
21
+ deps: AccessDeps,
22
+ organizationId: string,
23
+ source: string,
24
+ method: "get" | "put" | "revoke",
25
+ raw?: unknown,
26
+ ) {
27
+ const id = z.string().uuid().safeParse(organizationId);
28
+ const externalSource = HostMcpResolverSource.safeParse(source);
29
+ if (!id.success || !externalSource.success)
30
+ throw new HTTPException(422, { message: "invalid resolver identity" });
31
+ const access = await requireAccessContext(context, deps);
32
+ const service = accountScopedApiKeyWorkspaceAuthority(access);
33
+ const account = access.accountGrants.find(
34
+ (grant) => grant.accountId === id.data && grant.subjectId === access.subjectId,
35
+ );
36
+ if (!service || service.accountId !== id.data || !account?.permissions.includes("account:admin"))
37
+ throw new HTTPException(403, {
38
+ message: "resolver administration requires an organization service key",
39
+ });
40
+ const actor = { accountId: id.data, subjectId: access.subjectId };
41
+ try {
42
+ if (method === "get") return await getHostMcpResolver(deps.db, actor, externalSource.data);
43
+ const common = {
44
+ externalSource: externalSource.data,
45
+ encryptionKey: requireVariableSetEncryption(deps.settings),
46
+ legacyConfigured: hasLegacyHostMcpResolver(deps.settings, id.data),
47
+ };
48
+ if (method === "put") {
49
+ const parsed = PutHostMcpResolverRequest.safeParse(raw);
50
+ if (!parsed.success) throw new HTTPException(422, { message: "invalid resolver operation" });
51
+ let url: string;
52
+ try {
53
+ url = validateHttpUrl(parsed.data.url, {
54
+ allowLoopbackHttp: isLocalTestEnvironment(deps.settings.environment),
55
+ });
56
+ if (
57
+ new URL(url).protocol !== "https:" &&
58
+ !isLocalTestEnvironment(deps.settings.environment)
59
+ )
60
+ throw new Error();
61
+ } catch {
62
+ throw new HTTPException(422, { message: "invalid resolver URL" });
63
+ }
64
+ return await mutateHostMcpResolver(deps.db, actor, {
65
+ ...common,
66
+ kind: "put",
67
+ request: { ...parsed.data, url },
68
+ });
69
+ }
70
+ const parsed = RevokeHostMcpResolverRequest.safeParse(raw);
71
+ if (!parsed.success) throw new HTTPException(422, { message: "invalid resolver operation" });
72
+ return await mutateHostMcpResolver(deps.db, actor, {
73
+ ...common,
74
+ kind: "revoke",
75
+ request: parsed.data,
76
+ });
77
+ } catch (error) {
78
+ if (error instanceof HostMcpResolverError)
79
+ throw new HTTPException(error.status, { message: error.message });
80
+ if (error instanceof HTTPException) throw error;
81
+ // Database/encryption errors can carry SQL parameters. Never expose or log
82
+ // the underlying credential-bearing cause at the public route boundary.
83
+ throw new HTTPException(503, { message: "resolver administration unavailable" });
84
+ }
85
+ }
@@ -1,3 +1,5 @@
1
+ import { fileOwnerContextForAccess } from "../domain/file-owner";
2
+ import { withSessionRlsActorContext } from "@opengeni/db";
1
3
  import {
2
4
  NewSessionDraft,
3
5
  SaveNewSessionDraftRequest,
@@ -184,7 +186,7 @@ async function hydrateNewSessionDraft(
184
186
  }
185
187
 
186
188
  /** Read the authenticated actor's server-authoritative pre-session composer state. */
187
- export async function getActorNewSessionDraft(
189
+ async function getActorNewSessionDraftInFileScope(
188
190
  deps: Pick<NewSessionDraftDependencies, "settings" | "db">,
189
191
  grant: AccessGrant,
190
192
  workspaceId: string,
@@ -219,7 +221,7 @@ export async function getActorNewSessionDraft(
219
221
  * draft may represent incomplete options, while no invalid option can become a
220
222
  * session without passing that single canonical create boundary.
221
223
  */
222
- export async function saveActorNewSessionDraft(
224
+ async function saveActorNewSessionDraftInFileScope(
223
225
  deps: NewSessionDraftDependencies,
224
226
  grant: AccessGrant,
225
227
  workspaceId: string,
@@ -253,7 +255,16 @@ export async function saveActorNewSessionDraft(
253
255
  message: "object storage is not configured",
254
256
  });
255
257
  }
256
- await validateFileResources(deps.db, grant.accountId, workspaceId, grant.subjectId, resources);
258
+ await validateFileResources(
259
+ deps.db,
260
+ grant.accountId,
261
+ workspaceId,
262
+ grant.subjectId,
263
+ resources,
264
+ externalAuthorization
265
+ ? await fileOwnerContextForAccess(deps, externalAuthorization, "sessions:create")
266
+ : undefined,
267
+ );
257
268
  assertConfiguredModel(deps.settings, input.model);
258
269
  await assertWorkspaceModelPolicyAllows(deps.db, deps.settings, workspaceId, input.model);
259
270
 
@@ -299,3 +310,26 @@ export async function saveActorNewSessionDraft(
299
310
  throw error;
300
311
  }
301
312
  }
313
+
314
+ export async function getActorNewSessionDraft(
315
+ deps: Parameters<typeof getActorNewSessionDraftInFileScope>[0],
316
+ grant: AccessGrant,
317
+ workspaceId: string,
318
+ authorization?: AccessGrantAuthorization,
319
+ ): Promise<NewSessionDraftValue> {
320
+ const actor = authorization
321
+ ? await fileOwnerContextForAccess(deps, authorization, "sessions:read")
322
+ : { subjectId: grant.subjectId, privateFileOwnerSubjectId: null };
323
+ return withSessionRlsActorContext(actor, () =>
324
+ getActorNewSessionDraftInFileScope(deps, grant, workspaceId),
325
+ );
326
+ }
327
+ export async function saveActorNewSessionDraft(
328
+ ...args: Parameters<typeof saveActorNewSessionDraftInFileScope>
329
+ ): Promise<NewSessionDraftValue> {
330
+ const [deps, grant, , , , authorization] = args;
331
+ const actor = authorization
332
+ ? await fileOwnerContextForAccess(deps, authorization, "sessions:create")
333
+ : { subjectId: grant.subjectId, privateFileOwnerSubjectId: null };
334
+ return withSessionRlsActorContext(actor, () => saveActorNewSessionDraftInFileScope(...args));
335
+ }
@@ -0,0 +1,52 @@
1
+ import { requireLiveAgentAttemptAuthorization } from "../session-authorization";
2
+ import { freezeAgentLearningPolicy, type SessionRlsActorContext } from "@opengeni/db";
3
+ import type { AccessGrant, Permission } from "@opengeni/contracts";
4
+ import { requirePermission, type AccessGrantAuthorization } from "../access";
5
+ import type { ApiRouteDeps } from "../dependencies";
6
+ import { knowledgeContextForAccess } from "./knowledge";
7
+
8
+ /** Personal original-file access follows verified ownership, never a subject-shaped bearer. */
9
+ export async function fileOwnerContextForAccess(
10
+ deps: Pick<ApiRouteDeps, "db">,
11
+ access: AccessGrantAuthorization,
12
+ permission: Permission,
13
+ ): Promise<SessionRlsActorContext> {
14
+ if (access.grant.principalKind === "agent_attempt")
15
+ return fileOwnerContextForAgent(deps, access.grant, permission);
16
+ const context = await knowledgeContextForAccess(deps, access, permission);
17
+ if (context.actor.kind === "human")
18
+ return {
19
+ subjectId: context.actor.subjectId,
20
+ privateFileOwnerSubjectId: context.actor.subjectId,
21
+ };
22
+ return { subjectId: access.grant.subjectId, privateFileOwnerSubjectId: null };
23
+ }
24
+
25
+ /** Internal MCP/core callers can prove an agent attempt without inventing a human authorization. */
26
+ export async function fileOwnerContextForAgent(
27
+ deps: Pick<ApiRouteDeps, "db">,
28
+ grant: AccessGrant,
29
+ permission: Permission,
30
+ ): Promise<SessionRlsActorContext> {
31
+ requirePermission(grant, permission);
32
+ const sessionId = grant.metadata?.sessionId;
33
+ if (grant.principalKind !== "agent_attempt" || typeof sessionId !== "string")
34
+ throw new Error("File access requires an exact agent attempt");
35
+ const attempt = await requireLiveAgentAttemptAuthorization(deps.db, grant, sessionId);
36
+ const snapshot = await freezeAgentLearningPolicy(deps.db, {
37
+ accountId: grant.accountId,
38
+ workspaceId: grant.workspaceId,
39
+ actor: {
40
+ kind: "agent",
41
+ sessionId,
42
+ turnId: attempt.turnId,
43
+ attemptId: attempt.attemptId,
44
+ executionGeneration: attempt.executionGeneration,
45
+ },
46
+ });
47
+ return {
48
+ subjectId: grant.subjectId,
49
+ initiatingHumanSubjectId: attempt.initiatingHumanSubjectId,
50
+ privateFileOwnerSubjectId: snapshot.defaultScope === "personal" ? snapshot.subjectId : null,
51
+ };
52
+ }