@opengeni/api-router 0.5.3 → 0.5.4
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-3HIA43CC.js → chunk-DO2G3JSB.js} +5184 -2195
- package/dist/chunk-DO2G3JSB.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +297 -54
- package/dist/index.js.map +1 -1
- package/package.json +20 -20
- package/src/app.ts +415 -147
- package/src/auth/managed-auth.ts +32 -16
- package/src/http/auth.ts +8 -1
- package/src/http/common.ts +6 -2
- package/src/http/sse.ts +27 -6
- package/src/index.ts +196 -74
- package/src/integrations/oauth-client.ts +403 -120
- package/src/integrations/provider-domain.ts +4 -1
- package/src/mcp/documents.ts +173 -94
- package/src/mcp/server.ts +1517 -692
- package/src/mcp/session-view.ts +8 -2
- package/src/mcp/toolspace.ts +175 -84
- package/src/observability.ts +7 -1
- package/src/routes/api-keys.ts +39 -23
- package/src/routes/billing.ts +180 -65
- package/src/routes/capabilities.ts +17 -8
- package/src/routes/catalog-assets.ts +5 -2
- package/src/routes/codex.ts +244 -63
- package/src/routes/connections.ts +71 -33
- package/src/routes/documents.ts +242 -92
- package/src/routes/enrollments.ts +100 -70
- package/src/routes/environments.ts +205 -136
- package/src/routes/files.ts +164 -39
- package/src/routes/github.ts +123 -50
- package/src/routes/install.ts +9 -2
- package/src/routes/machines.ts +9 -8
- package/src/routes/packs.ts +141 -89
- package/src/routes/rigs.ts +189 -0
- package/src/routes/scheduled-tasks.ts +51 -9
- package/src/routes/sessions.ts +839 -329
- package/src/routes/social.ts +50 -38
- package/src/routes/workspace-capture.ts +238 -0
- package/src/routes/workspaces.ts +159 -13
- package/src/sandbox/access.ts +11 -3
- package/src/sandbox/auth-callout.ts +5 -1
- package/src/sandbox/channel-a.ts +104 -27
- package/src/sandbox/enrollment.ts +13 -3
- package/src/sandbox/machines.ts +68 -59
- package/src/sandbox/metrics-ingestion.ts +238 -17
- package/src/sandbox/viewer.ts +172 -46
- package/dist/chunk-3HIA43CC.js.map +0 -1
package/src/mcp/session-view.ts
CHANGED
|
@@ -208,7 +208,10 @@ function buildTruncationEvent(
|
|
|
208
208
|
* head and a tail of events and drop the middle behind a marker. `events` is
|
|
209
209
|
* assumed oldest-first (as `listSessionEvents` returns).
|
|
210
210
|
*/
|
|
211
|
-
export function capEventPage(
|
|
211
|
+
export function capEventPage(
|
|
212
|
+
events: SessionEvent[],
|
|
213
|
+
config: EventCapConfig = DEFAULT_EVENT_CAP,
|
|
214
|
+
): CappedEventPage {
|
|
212
215
|
const realLast = events[events.length - 1];
|
|
213
216
|
const nextAfter = realLast ? realLast.sequence : null;
|
|
214
217
|
|
|
@@ -274,7 +277,10 @@ export function capSessionDetail<T extends { metadata?: unknown; initialMessage?
|
|
|
274
277
|
}
|
|
275
278
|
}
|
|
276
279
|
if (typeof session.initialMessage === "string" && session.initialMessage.length > perFieldChars) {
|
|
277
|
-
(out as { initialMessage?: unknown }).initialMessage = clampString(
|
|
280
|
+
(out as { initialMessage?: unknown }).initialMessage = clampString(
|
|
281
|
+
session.initialMessage,
|
|
282
|
+
perFieldChars,
|
|
283
|
+
);
|
|
278
284
|
changed = true;
|
|
279
285
|
}
|
|
280
286
|
return changed ? out : session;
|
package/src/mcp/toolspace.ts
CHANGED
|
@@ -4,7 +4,11 @@ import type { FetchLike, Transport } from "@modelcontextprotocol/sdk/shared/tran
|
|
|
4
4
|
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import { environmentsEncryptionKeyBytes, type McpServerConfig } from "@opengeni/config";
|
|
6
6
|
import { prefixedMcpToolName, type AccessGrant, type ToolRef } from "@opengeni/contracts";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
hasPermission,
|
|
9
|
+
settingsWithEnabledCapabilityMcpServers,
|
|
10
|
+
type ApiRouteDeps,
|
|
11
|
+
} from "@opengeni/core";
|
|
8
12
|
import {
|
|
9
13
|
buildConnectionTokenResolver,
|
|
10
14
|
listSessionMcpServerMetadata,
|
|
@@ -45,8 +49,10 @@ type McpTool = {
|
|
|
45
49
|
|
|
46
50
|
const APPROVAL_REQUIRED_MESSAGE = "requires approval - invoke via the agent";
|
|
47
51
|
const TOOLSPACE_AUTH_NEEDED_ERROR_CODE = -32001;
|
|
48
|
-
const TOOLSPACE_AUTH_NEEDED_MESSAGE =
|
|
49
|
-
|
|
52
|
+
const TOOLSPACE_AUTH_NEEDED_MESSAGE =
|
|
53
|
+
"Authentication required - a connection link was posted to the session.";
|
|
54
|
+
const TOOLSPACE_NO_ACTIVE_TURN_MESSAGE =
|
|
55
|
+
"no active turn - toolspace calls require an in-flight turn";
|
|
50
56
|
// First-party OpenGeni MCP proxies (files/docs) route back through the same
|
|
51
57
|
// /mcp mount. They are excluded from the toolspace surface by construction so a
|
|
52
58
|
// toolspace principal can never re-enter /mcp as a first-party caller, even if
|
|
@@ -68,9 +74,11 @@ type ToolListingEntry = {
|
|
|
68
74
|
};
|
|
69
75
|
|
|
70
76
|
export function isToolspaceGrant(settings: ApiRouteDeps["settings"], grant: AccessGrant): boolean {
|
|
71
|
-
return
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
return (
|
|
78
|
+
settings.toolspaceEnabled &&
|
|
79
|
+
hasPermission(grant.permissions, "toolspace:call") &&
|
|
80
|
+
typeof grant.metadata?.sessionId === "string"
|
|
81
|
+
);
|
|
74
82
|
}
|
|
75
83
|
|
|
76
84
|
export async function prepareToolspaceMcpSurface(input: {
|
|
@@ -83,7 +91,10 @@ export async function prepareToolspaceMcpSurface(input: {
|
|
|
83
91
|
}
|
|
84
92
|
const sessionId = grant.metadata!.sessionId as string;
|
|
85
93
|
const session = await requireSession(deps.db, grant.workspaceId, sessionId);
|
|
86
|
-
const selectedIds = selectedMcpServerIds(
|
|
94
|
+
const selectedIds = selectedMcpServerIds(
|
|
95
|
+
session.tools,
|
|
96
|
+
session.mcpServers.map((server) => server.id),
|
|
97
|
+
);
|
|
87
98
|
// Proxyable ids: everything selected except the first-party OpenGeni tool
|
|
88
99
|
// server and the first-party MCP proxies, both of which would re-enter /mcp.
|
|
89
100
|
const proxyableIds = [...selectedIds].filter((id) => toolspaceCanProxyServerId(id));
|
|
@@ -96,7 +107,8 @@ export async function prepareToolspaceMcpSurface(input: {
|
|
|
96
107
|
// request, and only when we actually need it (a cache-miss listing or a real
|
|
97
108
|
// tools/call), so a cache-hit request does no registry work.
|
|
98
109
|
let registryPromise: Promise<Map<string, McpServerConfig>> | null = null;
|
|
99
|
-
const getRegistry = () =>
|
|
110
|
+
const getRegistry = () =>
|
|
111
|
+
(registryPromise ??= buildToolspaceRegistry(deps, grant.workspaceId, sessionId));
|
|
100
112
|
|
|
101
113
|
const listing = await resolveToolListing({
|
|
102
114
|
deps,
|
|
@@ -106,7 +118,9 @@ export async function prepareToolspaceMcpSurface(input: {
|
|
|
106
118
|
activeTurnId: session.activeTurnId ?? null,
|
|
107
119
|
getRegistry,
|
|
108
120
|
});
|
|
109
|
-
const tools = listing.map((entry) =>
|
|
121
|
+
const tools = listing.map((entry) =>
|
|
122
|
+
toolspaceToolFor({ deps, grant, sessionId, entry, getRegistry }),
|
|
123
|
+
);
|
|
110
124
|
|
|
111
125
|
return {
|
|
112
126
|
sessionId,
|
|
@@ -127,8 +141,17 @@ async function buildToolspaceRegistry(
|
|
|
127
141
|
workspaceId: string,
|
|
128
142
|
sessionId: string,
|
|
129
143
|
): Promise<Map<string, McpServerConfig>> {
|
|
130
|
-
const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
131
|
-
|
|
144
|
+
const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
145
|
+
deps.db,
|
|
146
|
+
workspaceId,
|
|
147
|
+
deps.settings,
|
|
148
|
+
);
|
|
149
|
+
const withSessionServers = await settingsWithSessionMcpServersForToolspace(
|
|
150
|
+
deps,
|
|
151
|
+
workspaceId,
|
|
152
|
+
sessionId,
|
|
153
|
+
runtimeSettings,
|
|
154
|
+
);
|
|
132
155
|
return new Map(withSessionServers.mcpServers.map((server) => [server.id, server]));
|
|
133
156
|
}
|
|
134
157
|
|
|
@@ -161,12 +184,16 @@ async function resolveToolListing(input: {
|
|
|
161
184
|
if (!config || !toolspaceCanProxyServer(config)) {
|
|
162
185
|
continue;
|
|
163
186
|
}
|
|
164
|
-
const connection = await connectToolspaceServer({ deps, grant, config, sessionId }).catch(
|
|
187
|
+
const connection = await connectToolspaceServer({ deps, grant, config, sessionId }).catch(
|
|
188
|
+
() => null,
|
|
189
|
+
);
|
|
165
190
|
if (!connection) {
|
|
166
191
|
continue;
|
|
167
192
|
}
|
|
168
193
|
try {
|
|
169
|
-
const listed = await connection.client
|
|
194
|
+
const listed = await connection.client
|
|
195
|
+
.listTools(undefined, toolspaceRequestOptions(config))
|
|
196
|
+
.catch(() => ({ tools: [] }));
|
|
170
197
|
for (const tool of listed.tools as McpTool[]) {
|
|
171
198
|
if (!tool?.name || !allowedByConfig(config, tool.name)) {
|
|
172
199
|
continue;
|
|
@@ -254,7 +281,9 @@ async function settingsWithSessionMcpServersForToolspace(
|
|
|
254
281
|
...(server.allowedTools ? { allowedTools: server.allowedTools } : {}),
|
|
255
282
|
...(server.timeoutMs ? { timeoutMs: server.timeoutMs } : {}),
|
|
256
283
|
cacheToolsList: server.cacheToolsList ?? false,
|
|
257
|
-
...(server.requireApproval !== undefined
|
|
284
|
+
...(server.requireApproval !== undefined
|
|
285
|
+
? { requireApproval: server.requireApproval }
|
|
286
|
+
: {}),
|
|
258
287
|
headers: server.headers,
|
|
259
288
|
})),
|
|
260
289
|
],
|
|
@@ -270,7 +299,10 @@ async function connectToolspaceServer(input: {
|
|
|
270
299
|
const baseFetch: FetchLike = input.config.connectionRef
|
|
271
300
|
? connectionBrokerFetch(globalThis.fetch, input)
|
|
272
301
|
: globalThis.fetch;
|
|
273
|
-
const client = new Client(
|
|
302
|
+
const client = new Client(
|
|
303
|
+
{ name: `opengeni-toolspace-${input.config.id}`, version: "1.0.0" },
|
|
304
|
+
{ capabilities: {} },
|
|
305
|
+
);
|
|
274
306
|
const transport = new StreamableHTTPClientTransport(new URL(input.config.url), {
|
|
275
307
|
...(baseFetch !== globalThis.fetch ? { fetch: baseFetch } : {}),
|
|
276
308
|
requestInit: {
|
|
@@ -314,7 +346,9 @@ function toolspaceToolFor(input: {
|
|
|
314
346
|
return mcpError(TOOLSPACE_NO_ACTIVE_TURN_MESSAGE);
|
|
315
347
|
}
|
|
316
348
|
if (reservation.status === "budget_exhausted") {
|
|
317
|
-
return mcpError(
|
|
349
|
+
return mcpError(
|
|
350
|
+
`toolspace call budget exhausted (${deps.settings.toolspaceMaxCallsPerTurn}/turn)`,
|
|
351
|
+
);
|
|
318
352
|
}
|
|
319
353
|
const turnId = reservation.turnId;
|
|
320
354
|
// Dial only the ONE server this tool belongs to, from the freshly-built
|
|
@@ -328,41 +362,47 @@ function toolspaceToolFor(input: {
|
|
|
328
362
|
if (mcpToolRequiresApproval(config.requireApproval, tool.name)) {
|
|
329
363
|
return mcpError(APPROVAL_REQUIRED_MESSAGE);
|
|
330
364
|
}
|
|
331
|
-
const connection = await connectToolspaceServer({ deps, grant, config, sessionId }).catch(
|
|
365
|
+
const connection = await connectToolspaceServer({ deps, grant, config, sessionId }).catch(
|
|
366
|
+
() => null,
|
|
367
|
+
);
|
|
332
368
|
if (!connection) {
|
|
333
369
|
return mcpError(`upstream tool failed: ${name}`);
|
|
334
370
|
}
|
|
335
371
|
try {
|
|
336
372
|
const callId = crypto.randomUUID();
|
|
337
|
-
await appendAndPublishEvents(deps.db, deps.bus, grant.workspaceId, sessionId, [
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
373
|
+
await appendAndPublishEvents(deps.db, deps.bus, grant.workspaceId, sessionId, [
|
|
374
|
+
{
|
|
375
|
+
type: "agent.toolCall.created",
|
|
376
|
+
turnId,
|
|
377
|
+
producerId: grant.subjectId,
|
|
378
|
+
payload: {
|
|
379
|
+
id: callId,
|
|
380
|
+
name,
|
|
381
|
+
arguments: args,
|
|
382
|
+
origin: "toolspace",
|
|
383
|
+
subjectId: grant.subjectId,
|
|
384
|
+
raw: {
|
|
385
|
+
type: "toolspace_call",
|
|
386
|
+
serverId,
|
|
387
|
+
toolName: tool.name,
|
|
388
|
+
},
|
|
351
389
|
},
|
|
352
390
|
},
|
|
353
|
-
|
|
391
|
+
]);
|
|
354
392
|
const output = await callRemoteTool(deps, connection, tool.name, args);
|
|
355
|
-
await appendAndPublishEvents(deps.db, deps.bus, grant.workspaceId, sessionId, [
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
393
|
+
await appendAndPublishEvents(deps.db, deps.bus, grant.workspaceId, sessionId, [
|
|
394
|
+
{
|
|
395
|
+
type: "agent.toolCall.output",
|
|
396
|
+
turnId,
|
|
397
|
+
producerId: grant.subjectId,
|
|
398
|
+
payload: {
|
|
399
|
+
id: callId,
|
|
400
|
+
output,
|
|
401
|
+
origin: "toolspace",
|
|
402
|
+
subjectId: grant.subjectId,
|
|
403
|
+
},
|
|
364
404
|
},
|
|
365
|
-
|
|
405
|
+
]);
|
|
366
406
|
return output;
|
|
367
407
|
} finally {
|
|
368
408
|
await connection.close();
|
|
@@ -378,10 +418,14 @@ async function callRemoteTool(
|
|
|
378
418
|
args: Record<string, unknown>,
|
|
379
419
|
): Promise<ToolspaceCallResult> {
|
|
380
420
|
try {
|
|
381
|
-
return await server.client.callTool(
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
421
|
+
return (await server.client.callTool(
|
|
422
|
+
{
|
|
423
|
+
name: toolName,
|
|
424
|
+
arguments: args,
|
|
425
|
+
},
|
|
426
|
+
undefined,
|
|
427
|
+
toolspaceRequestOptions(server.config),
|
|
428
|
+
)) as ToolspaceCallResult;
|
|
385
429
|
} catch (error) {
|
|
386
430
|
if (isToolspaceAuthNeededError(error)) {
|
|
387
431
|
return mcpError(TOOLSPACE_AUTH_NEEDED_MESSAGE);
|
|
@@ -403,7 +447,11 @@ type ToolspaceReservation =
|
|
|
403
447
|
| { status: "no_active_turn" }
|
|
404
448
|
| { status: "budget_exhausted" };
|
|
405
449
|
|
|
406
|
-
async function reserveActiveTurnCall(
|
|
450
|
+
async function reserveActiveTurnCall(
|
|
451
|
+
deps: ApiRouteDeps,
|
|
452
|
+
workspaceId: string,
|
|
453
|
+
sessionId: string,
|
|
454
|
+
): Promise<ToolspaceReservation> {
|
|
407
455
|
const session = await requireSession(deps.db, workspaceId, sessionId);
|
|
408
456
|
if (!session.activeTurnId) {
|
|
409
457
|
return { status: "no_active_turn" };
|
|
@@ -458,7 +506,10 @@ function allowedByConfig(config: McpServerConfig, toolName: string): boolean {
|
|
|
458
506
|
return !config.allowedTools || config.allowedTools.includes(toolName);
|
|
459
507
|
}
|
|
460
508
|
|
|
461
|
-
function mcpToolRequiresApproval(
|
|
509
|
+
function mcpToolRequiresApproval(
|
|
510
|
+
policy: McpServerConfig["requireApproval"],
|
|
511
|
+
unprefixedName: string,
|
|
512
|
+
): boolean {
|
|
462
513
|
if (policy === true) {
|
|
463
514
|
return true;
|
|
464
515
|
}
|
|
@@ -472,7 +523,10 @@ function mcpError(message: string): ToolspaceCallResult {
|
|
|
472
523
|
};
|
|
473
524
|
}
|
|
474
525
|
|
|
475
|
-
function toolspaceRequestOptions(config: McpServerConfig): {
|
|
526
|
+
function toolspaceRequestOptions(config: McpServerConfig): {
|
|
527
|
+
timeout?: number;
|
|
528
|
+
maxTotalTimeout?: number;
|
|
529
|
+
} {
|
|
476
530
|
return config.timeoutMs ? { timeout: config.timeoutMs, maxTotalTimeout: config.timeoutMs } : {};
|
|
477
531
|
}
|
|
478
532
|
|
|
@@ -509,7 +563,10 @@ function connectionBrokerFetch(
|
|
|
509
563
|
if (first.status === "auth_needed") {
|
|
510
564
|
return await authNeededFetchResponse(input, request, first);
|
|
511
565
|
}
|
|
512
|
-
const response = await baseFetch(
|
|
566
|
+
const response = await baseFetch(
|
|
567
|
+
fetchInputForAttempt(requestInput),
|
|
568
|
+
withConnectionHeaders(requestInput, init, first.headers),
|
|
569
|
+
);
|
|
513
570
|
if (response.status === 401) {
|
|
514
571
|
const refreshed = await resolveCredential({
|
|
515
572
|
workspaceId: input.grant.workspaceId,
|
|
@@ -522,10 +579,17 @@ function connectionBrokerFetch(
|
|
|
522
579
|
if (refreshed.status === "auth_needed") {
|
|
523
580
|
return await authNeededFetchResponse(input, request, refreshed);
|
|
524
581
|
}
|
|
525
|
-
return await baseFetch(
|
|
582
|
+
return await baseFetch(
|
|
583
|
+
fetchInputForAttempt(requestInput),
|
|
584
|
+
withConnectionHeaders(requestInput, init, refreshed.headers),
|
|
585
|
+
);
|
|
526
586
|
}
|
|
527
587
|
if (response.status === 403) {
|
|
528
|
-
return await authNeededFetchResponse(
|
|
588
|
+
return await authNeededFetchResponse(
|
|
589
|
+
input,
|
|
590
|
+
request,
|
|
591
|
+
authNeededFromStatus(input.config, first, "insufficient_scope"),
|
|
592
|
+
);
|
|
529
593
|
}
|
|
530
594
|
return response;
|
|
531
595
|
};
|
|
@@ -557,33 +621,44 @@ async function authNeededFetchResponse(
|
|
|
557
621
|
request: McpRequestInfo,
|
|
558
622
|
auth: Extract<ResolveConnectionCredentialResult, { status: "auth_needed" }>,
|
|
559
623
|
): Promise<Response> {
|
|
560
|
-
await appendAndPublishEvents(
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
624
|
+
await appendAndPublishEvents(
|
|
625
|
+
input.deps.db,
|
|
626
|
+
input.deps.bus,
|
|
627
|
+
input.grant.workspaceId,
|
|
628
|
+
input.sessionId,
|
|
629
|
+
[
|
|
630
|
+
{
|
|
631
|
+
type: "tool.auth_needed",
|
|
632
|
+
producerId: input.grant.subjectId,
|
|
633
|
+
payload: {
|
|
634
|
+
serverId: input.config.id,
|
|
635
|
+
toolName: request.toolName ?? null,
|
|
636
|
+
providerDomain: auth.providerDomain,
|
|
637
|
+
reason: auth.reason,
|
|
638
|
+
...(auth.connectionId ? { connectionId: auth.connectionId } : {}),
|
|
639
|
+
...(auth.scopes ? { scopes: auth.scopes } : {}),
|
|
640
|
+
...(auth.resource ? { resource: auth.resource } : {}),
|
|
641
|
+
...(auth.authorizationUrl ? { authorizationUrl: auth.authorizationUrl } : {}),
|
|
642
|
+
subjectId: input.grant.subjectId,
|
|
643
|
+
},
|
|
644
|
+
},
|
|
645
|
+
],
|
|
646
|
+
).catch(() => undefined);
|
|
575
647
|
if (request.method === "tools/call") {
|
|
576
|
-
return new Response(
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
648
|
+
return new Response(
|
|
649
|
+
JSON.stringify({
|
|
650
|
+
jsonrpc: "2.0",
|
|
651
|
+
id: request.id ?? null,
|
|
652
|
+
error: {
|
|
653
|
+
code: TOOLSPACE_AUTH_NEEDED_ERROR_CODE,
|
|
654
|
+
message: TOOLSPACE_AUTH_NEEDED_MESSAGE,
|
|
655
|
+
},
|
|
656
|
+
}),
|
|
657
|
+
{
|
|
658
|
+
status: 200,
|
|
659
|
+
headers: { "content-type": "application/json" },
|
|
582
660
|
},
|
|
583
|
-
|
|
584
|
-
status: 200,
|
|
585
|
-
headers: { "content-type": "application/json" },
|
|
586
|
-
});
|
|
661
|
+
);
|
|
587
662
|
}
|
|
588
663
|
return new Response("Authentication required for MCP server connection", { status: 401 });
|
|
589
664
|
}
|
|
@@ -594,10 +669,20 @@ async function mcpRequestInfo(_input: string | URL, init?: RequestInit): Promise
|
|
|
594
669
|
return {};
|
|
595
670
|
}
|
|
596
671
|
try {
|
|
597
|
-
const parsed = JSON.parse(body) as {
|
|
672
|
+
const parsed = JSON.parse(body) as {
|
|
673
|
+
id?: unknown;
|
|
674
|
+
method?: unknown;
|
|
675
|
+
params?: { name?: unknown };
|
|
676
|
+
};
|
|
598
677
|
const method = typeof parsed.method === "string" ? parsed.method : undefined;
|
|
599
|
-
const id =
|
|
600
|
-
|
|
678
|
+
const id =
|
|
679
|
+
typeof parsed.id === "string" || typeof parsed.id === "number" || parsed.id === null
|
|
680
|
+
? parsed.id
|
|
681
|
+
: undefined;
|
|
682
|
+
const toolName =
|
|
683
|
+
method === "tools/call" && typeof parsed.params?.name === "string"
|
|
684
|
+
? parsed.params.name
|
|
685
|
+
: undefined;
|
|
601
686
|
return {
|
|
602
687
|
...(method ? { method } : {}),
|
|
603
688
|
...(id !== undefined ? { id } : {}),
|
|
@@ -608,7 +693,11 @@ async function mcpRequestInfo(_input: string | URL, init?: RequestInit): Promise
|
|
|
608
693
|
}
|
|
609
694
|
}
|
|
610
695
|
|
|
611
|
-
function withConnectionHeaders(
|
|
696
|
+
function withConnectionHeaders(
|
|
697
|
+
_input: string | URL,
|
|
698
|
+
init: RequestInit | undefined,
|
|
699
|
+
authHeaders: Record<string, string>,
|
|
700
|
+
): RequestInit {
|
|
612
701
|
const headers = new Headers(init?.headers);
|
|
613
702
|
for (const [name, value] of Object.entries(authHeaders)) {
|
|
614
703
|
headers.set(name, value);
|
|
@@ -621,7 +710,9 @@ function fetchInputForAttempt(input: string | URL): string | URL {
|
|
|
621
710
|
}
|
|
622
711
|
|
|
623
712
|
function isToolspaceAuthNeededError(error: unknown): boolean {
|
|
624
|
-
return
|
|
625
|
-
|
|
626
|
-
|
|
713
|
+
return (
|
|
714
|
+
error instanceof Error &&
|
|
715
|
+
((error as { code?: unknown }).code === TOOLSPACE_AUTH_NEEDED_ERROR_CODE ||
|
|
716
|
+
error.message.includes(TOOLSPACE_AUTH_NEEDED_MESSAGE))
|
|
717
|
+
);
|
|
627
718
|
}
|
package/src/observability.ts
CHANGED
|
@@ -20,7 +20,13 @@ function eventAttributes(attributes: Record<string, unknown> | undefined): Attri
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function eventAttributeValue(value: unknown): AttributeValue {
|
|
23
|
-
if (
|
|
23
|
+
if (
|
|
24
|
+
value === null ||
|
|
25
|
+
value === undefined ||
|
|
26
|
+
typeof value === "string" ||
|
|
27
|
+
typeof value === "number" ||
|
|
28
|
+
typeof value === "boolean"
|
|
29
|
+
) {
|
|
24
30
|
return value;
|
|
25
31
|
}
|
|
26
32
|
try {
|
package/src/routes/api-keys.ts
CHANGED
|
@@ -14,26 +14,36 @@ export function registerApiKeyRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
14
14
|
return c.json({ apiKeys: await listApiKeys(deps.db, workspaceId) });
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
-
app.post(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
17
|
+
app.post(
|
|
18
|
+
"/v1/workspaces/:workspaceId/api-keys",
|
|
19
|
+
zValidator("json", CreateApiKeyRequest.omit({ workspaceId: true })),
|
|
20
|
+
async (c) => {
|
|
21
|
+
const workspaceId = c.req.param("workspaceId");
|
|
22
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "api_keys:manage");
|
|
23
|
+
const body = c.req.valid("json");
|
|
24
|
+
const permissions: Permission[] =
|
|
25
|
+
body.permissions.length > 0 ? (body.permissions as Permission[]) : ["workspace:read"];
|
|
26
|
+
ensureDelegablePermissions(grant.permissions, permissions);
|
|
27
|
+
await requireLimit(deps, {
|
|
28
|
+
accountId: grant.accountId,
|
|
29
|
+
workspaceId,
|
|
30
|
+
action: "api_key:create",
|
|
31
|
+
quantity: 1,
|
|
32
|
+
});
|
|
33
|
+
const token = generateApiKeyToken();
|
|
34
|
+
const prefix = token.slice(0, 14);
|
|
35
|
+
const apiKey = await createApiKey(deps.db, {
|
|
36
|
+
accountId: grant.accountId,
|
|
37
|
+
workspaceId: grant.workspaceId,
|
|
38
|
+
name: body.name,
|
|
39
|
+
prefix,
|
|
40
|
+
keyHash: await sha256Hex(token),
|
|
41
|
+
permissions,
|
|
42
|
+
expiresAt: body.expiresAt ? new Date(body.expiresAt) : null,
|
|
43
|
+
});
|
|
44
|
+
return c.json(CreateApiKeyResponse.parse({ apiKey, token }), 201);
|
|
45
|
+
},
|
|
46
|
+
);
|
|
37
47
|
|
|
38
48
|
app.delete("/v1/workspaces/:workspaceId/api-keys/:apiKeyId", async (c) => {
|
|
39
49
|
const workspaceId = c.req.param("workspaceId");
|
|
@@ -48,18 +58,24 @@ function ensureDelegablePermissions(grantPermissions: Permission[], requested: P
|
|
|
48
58
|
}
|
|
49
59
|
const missing = requested.filter((permission) => !grantPermissions.includes(permission));
|
|
50
60
|
if (missing.length > 0) {
|
|
51
|
-
throw new HTTPException(403, {
|
|
61
|
+
throw new HTTPException(403, {
|
|
62
|
+
message: `cannot delegate missing permissions: ${missing.join(", ")}`,
|
|
63
|
+
});
|
|
52
64
|
}
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
function generateApiKeyToken(): string {
|
|
56
68
|
const bytes = new Uint8Array(32);
|
|
57
69
|
crypto.getRandomValues(bytes);
|
|
58
|
-
const secret = Array.from(bytes)
|
|
70
|
+
const secret = Array.from(bytes)
|
|
71
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
72
|
+
.join("");
|
|
59
73
|
return `ogk_${secret}`;
|
|
60
74
|
}
|
|
61
75
|
|
|
62
76
|
async function sha256Hex(value: string): Promise<string> {
|
|
63
77
|
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
64
|
-
return Array.from(new Uint8Array(digest))
|
|
78
|
+
return Array.from(new Uint8Array(digest))
|
|
79
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
80
|
+
.join("");
|
|
65
81
|
}
|