@opengeni/api-router 0.5.7 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.d.ts +21 -3
- package/dist/app.js +3 -1
- package/dist/{chunk-HBEJMWD3.js → chunk-EYYTFA7N.js} +2396 -676
- package/dist/chunk-EYYTFA7N.js.map +1 -0
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/package.json +11 -11
- package/src/app.ts +47 -4
- package/src/github-access.ts +46 -0
- package/src/github-browser-flow.ts +83 -0
- package/src/http/auth.ts +12 -4
- package/src/http/sse.ts +526 -92
- package/src/index.ts +2 -1
- package/src/mcp/server.ts +774 -146
- package/src/mcp/session-view.ts +622 -203
- package/src/mcp/toolspace.ts +110 -25
- package/src/routes/codex.ts +17 -14
- package/src/routes/enrollments.ts +2 -2
- package/src/routes/github.ts +63 -202
- package/src/routes/install.ts +1 -1
- package/src/routes/machines.ts +2 -2
- package/src/routes/sessions.ts +639 -76
- package/src/routes/workspace-capture.ts +56 -38
- package/src/routes/workspaces.ts +30 -11
- package/src/sandbox/access.ts +1 -1
- package/src/sandbox/auth-callout.ts +1 -1
- package/src/sandbox/channel-a.ts +14 -1
- package/src/sandbox/enrollment.ts +4 -4
- package/src/sandbox/machines.ts +1 -1
- package/src/sandbox/metrics-ingestion.ts +1 -1
- package/src/sandbox/viewer.ts +1 -1
- package/dist/chunk-HBEJMWD3.js.map +0 -1
package/src/mcp/toolspace.ts
CHANGED
|
@@ -3,7 +3,12 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
|
|
|
3
3
|
import type { FetchLike, Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
4
4
|
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import { environmentsEncryptionKeyBytes, type McpServerConfig } from "@opengeni/config";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
prefixedMcpToolName,
|
|
8
|
+
type AccessGrant,
|
|
9
|
+
type SessionTurn,
|
|
10
|
+
type ToolRef,
|
|
11
|
+
} from "@opengeni/contracts";
|
|
7
12
|
import {
|
|
8
13
|
hasPermission,
|
|
9
14
|
settingsWithEnabledCapabilityMcpServers,
|
|
@@ -11,6 +16,9 @@ import {
|
|
|
11
16
|
} from "@opengeni/core";
|
|
12
17
|
import {
|
|
13
18
|
buildConnectionTokenResolver,
|
|
19
|
+
buildHostConnectionTokenResolver,
|
|
20
|
+
getSessionRootId,
|
|
21
|
+
getSessionTurn,
|
|
14
22
|
listSessionMcpServerMetadata,
|
|
15
23
|
listSessionMcpServersForRun,
|
|
16
24
|
requireSession,
|
|
@@ -91,6 +99,14 @@ export async function prepareToolspaceMcpSurface(input: {
|
|
|
91
99
|
}
|
|
92
100
|
const sessionId = grant.metadata!.sessionId as string;
|
|
93
101
|
const session = await requireSession(deps.db, grant.workspaceId, sessionId);
|
|
102
|
+
let rootSessionId = sessionId;
|
|
103
|
+
if (deps.connectionCredentials?.mcpCredentials) {
|
|
104
|
+
const resolvedRootSessionId = await getSessionRootId(deps.db, grant.workspaceId, sessionId);
|
|
105
|
+
if (!resolvedRootSessionId) {
|
|
106
|
+
throw new Error(`cannot resolve host MCP credentials for missing session ${sessionId}`);
|
|
107
|
+
}
|
|
108
|
+
rootSessionId = resolvedRootSessionId;
|
|
109
|
+
}
|
|
94
110
|
const selectedIds = selectedMcpServerIds(
|
|
95
111
|
session.tools,
|
|
96
112
|
session.mcpServers.map((server) => server.id),
|
|
@@ -114,12 +130,13 @@ export async function prepareToolspaceMcpSurface(input: {
|
|
|
114
130
|
deps,
|
|
115
131
|
grant,
|
|
116
132
|
sessionId,
|
|
133
|
+
rootSessionId,
|
|
117
134
|
proxyableIds,
|
|
118
135
|
activeTurnId: session.activeTurnId ?? null,
|
|
119
136
|
getRegistry,
|
|
120
137
|
});
|
|
121
138
|
const tools = listing.map((entry) =>
|
|
122
|
-
toolspaceToolFor({ deps, grant, sessionId, entry, getRegistry }),
|
|
139
|
+
toolspaceToolFor({ deps, grant, sessionId, rootSessionId, entry, getRegistry }),
|
|
123
140
|
);
|
|
124
141
|
|
|
125
142
|
return {
|
|
@@ -164,19 +181,32 @@ async function resolveToolListing(input: {
|
|
|
164
181
|
deps: ApiRouteDeps;
|
|
165
182
|
grant: AccessGrant;
|
|
166
183
|
sessionId: string;
|
|
184
|
+
rootSessionId: string;
|
|
167
185
|
proxyableIds: string[];
|
|
168
186
|
activeTurnId: string | null;
|
|
169
187
|
getRegistry: () => Promise<Map<string, McpServerConfig>>;
|
|
170
188
|
}): Promise<ToolListingEntry[]> {
|
|
171
|
-
const { deps, grant, sessionId, proxyableIds, activeTurnId, getRegistry } = input;
|
|
172
|
-
|
|
189
|
+
const { deps, grant, sessionId, rootSessionId, proxyableIds, activeTurnId, getRegistry } = input;
|
|
190
|
+
if (!activeTurnId) {
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
const activeTurn = await getSessionTurn(deps.db, grant.workspaceId, activeTurnId);
|
|
194
|
+
if (!activeTurn || activeTurn.sessionId !== sessionId) {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
// Host credentials can be initiator-specific. A prior turn's tool list must
|
|
198
|
+
// never be reused under a different frozen authority in the same session.
|
|
199
|
+
const cacheKey = await toolListCacheKey(
|
|
200
|
+
deps,
|
|
201
|
+
grant.workspaceId,
|
|
202
|
+
sessionId,
|
|
203
|
+
proxyableIds,
|
|
204
|
+
activeTurn,
|
|
205
|
+
);
|
|
173
206
|
const cached = readToolListCache(cacheKey);
|
|
174
207
|
if (cached) {
|
|
175
208
|
return cached;
|
|
176
209
|
}
|
|
177
|
-
if (!activeTurnId) {
|
|
178
|
-
return [];
|
|
179
|
-
}
|
|
180
210
|
const registry = await getRegistry();
|
|
181
211
|
const entries: ToolListingEntry[] = [];
|
|
182
212
|
for (const serverId of proxyableIds) {
|
|
@@ -184,9 +214,14 @@ async function resolveToolListing(input: {
|
|
|
184
214
|
if (!config || !toolspaceCanProxyServer(config)) {
|
|
185
215
|
continue;
|
|
186
216
|
}
|
|
187
|
-
const connection = await connectToolspaceServer({
|
|
188
|
-
|
|
189
|
-
|
|
217
|
+
const connection = await connectToolspaceServer({
|
|
218
|
+
deps,
|
|
219
|
+
grant,
|
|
220
|
+
config,
|
|
221
|
+
sessionId,
|
|
222
|
+
rootSessionId,
|
|
223
|
+
turn: activeTurn,
|
|
224
|
+
}).catch(() => null);
|
|
190
225
|
if (!connection) {
|
|
191
226
|
continue;
|
|
192
227
|
}
|
|
@@ -213,6 +248,7 @@ async function toolListCacheKey(
|
|
|
213
248
|
workspaceId: string,
|
|
214
249
|
sessionId: string,
|
|
215
250
|
proxyableIds: string[],
|
|
251
|
+
turn: SessionTurn,
|
|
216
252
|
): Promise<string> {
|
|
217
253
|
const metadata = await listSessionMcpServerMetadata(deps.db, workspaceId, sessionId);
|
|
218
254
|
const versions = new Map(metadata.map((server) => [server.id, server.credentialVersion]));
|
|
@@ -221,7 +257,12 @@ async function toolListCacheKey(
|
|
|
221
257
|
.sort()
|
|
222
258
|
.map((id) => `${id}@${versions.get(id) ?? 0}`)
|
|
223
259
|
.join(",");
|
|
224
|
-
|
|
260
|
+
const authority = JSON.stringify({
|
|
261
|
+
turnId: turn.id,
|
|
262
|
+
executionGeneration: turn.executionGeneration,
|
|
263
|
+
initiator: turn.initiator,
|
|
264
|
+
});
|
|
265
|
+
return `${workspaceId}:${sessionId}:${signature}:${authority}`;
|
|
225
266
|
}
|
|
226
267
|
|
|
227
268
|
function readToolListCache(key: string): ToolListingEntry[] | null {
|
|
@@ -263,9 +304,18 @@ async function settingsWithSessionMcpServersForToolspace(
|
|
|
263
304
|
if (metadata.length === 0) {
|
|
264
305
|
return settings;
|
|
265
306
|
}
|
|
266
|
-
|
|
307
|
+
if (metadata.some((server) => server.headerNames.length > 0)) {
|
|
308
|
+
throw new Error(
|
|
309
|
+
"session MCP server credentials require OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY",
|
|
310
|
+
);
|
|
311
|
+
}
|
|
267
312
|
}
|
|
268
|
-
const servers = await listSessionMcpServersForRun(
|
|
313
|
+
const servers = await listSessionMcpServersForRun(
|
|
314
|
+
deps.db,
|
|
315
|
+
workspaceId,
|
|
316
|
+
sessionId,
|
|
317
|
+
encryptionKey ?? null,
|
|
318
|
+
);
|
|
269
319
|
if (servers.length === 0) {
|
|
270
320
|
return settings;
|
|
271
321
|
}
|
|
@@ -284,6 +334,7 @@ async function settingsWithSessionMcpServersForToolspace(
|
|
|
284
334
|
...(server.requireApproval !== undefined
|
|
285
335
|
? { requireApproval: server.requireApproval }
|
|
286
336
|
: {}),
|
|
337
|
+
...(server.connectionRef ? { connectionRef: server.connectionRef } : {}),
|
|
287
338
|
headers: server.headers,
|
|
288
339
|
})),
|
|
289
340
|
],
|
|
@@ -295,6 +346,8 @@ async function connectToolspaceServer(input: {
|
|
|
295
346
|
grant: AccessGrant;
|
|
296
347
|
config: McpServerConfig;
|
|
297
348
|
sessionId: string;
|
|
349
|
+
rootSessionId: string;
|
|
350
|
+
turn: SessionTurn;
|
|
298
351
|
}): Promise<ConnectedToolspaceServer> {
|
|
299
352
|
const baseFetch: FetchLike = input.config.connectionRef
|
|
300
353
|
? connectionBrokerFetch(globalThis.fetch, input)
|
|
@@ -323,10 +376,11 @@ function toolspaceToolFor(input: {
|
|
|
323
376
|
deps: ApiRouteDeps;
|
|
324
377
|
grant: AccessGrant;
|
|
325
378
|
sessionId: string;
|
|
379
|
+
rootSessionId: string;
|
|
326
380
|
entry: ToolListingEntry;
|
|
327
381
|
getRegistry: () => Promise<Map<string, McpServerConfig>>;
|
|
328
382
|
}): ToolspaceRegisteredTool {
|
|
329
|
-
const { deps, grant, sessionId, entry, getRegistry } = input;
|
|
383
|
+
const { deps, grant, sessionId, rootSessionId, entry, getRegistry } = input;
|
|
330
384
|
const { serverId, tool } = entry;
|
|
331
385
|
const name = prefixedMcpToolName(serverId, tool.name);
|
|
332
386
|
const approvalRequired = mcpToolRequiresApproval(entry.requireApproval, tool.name);
|
|
@@ -350,7 +404,7 @@ function toolspaceToolFor(input: {
|
|
|
350
404
|
`toolspace call budget exhausted (${deps.settings.toolspaceMaxCallsPerTurn}/turn)`,
|
|
351
405
|
);
|
|
352
406
|
}
|
|
353
|
-
const turnId = reservation.
|
|
407
|
+
const turnId = reservation.turn.id;
|
|
354
408
|
// Dial only the ONE server this tool belongs to, from the freshly-built
|
|
355
409
|
// registry, and re-check policy against that live config (the listing may
|
|
356
410
|
// have been served from a slightly stale cache entry).
|
|
@@ -362,9 +416,14 @@ function toolspaceToolFor(input: {
|
|
|
362
416
|
if (mcpToolRequiresApproval(config.requireApproval, tool.name)) {
|
|
363
417
|
return mcpError(APPROVAL_REQUIRED_MESSAGE);
|
|
364
418
|
}
|
|
365
|
-
const connection = await connectToolspaceServer({
|
|
366
|
-
|
|
367
|
-
|
|
419
|
+
const connection = await connectToolspaceServer({
|
|
420
|
+
deps,
|
|
421
|
+
grant,
|
|
422
|
+
config,
|
|
423
|
+
sessionId,
|
|
424
|
+
rootSessionId,
|
|
425
|
+
turn: reservation.turn,
|
|
426
|
+
}).catch(() => null);
|
|
368
427
|
if (!connection) {
|
|
369
428
|
return mcpError(`upstream tool failed: ${name}`);
|
|
370
429
|
}
|
|
@@ -443,7 +502,7 @@ async function callRemoteTool(
|
|
|
443
502
|
}
|
|
444
503
|
|
|
445
504
|
type ToolspaceReservation =
|
|
446
|
-
| { status: "ok";
|
|
505
|
+
| { status: "ok"; turn: SessionTurn }
|
|
447
506
|
| { status: "no_active_turn" }
|
|
448
507
|
| { status: "budget_exhausted" };
|
|
449
508
|
|
|
@@ -463,9 +522,13 @@ async function reserveActiveTurnCall(
|
|
|
463
522
|
session.activeTurnId,
|
|
464
523
|
deps.settings.toolspaceMaxCallsPerTurn,
|
|
465
524
|
);
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
525
|
+
if (!reservation.reserved) {
|
|
526
|
+
return { status: "budget_exhausted" };
|
|
527
|
+
}
|
|
528
|
+
const turn = await getSessionTurn(deps.db, workspaceId, session.activeTurnId);
|
|
529
|
+
return turn && turn.sessionId === sessionId
|
|
530
|
+
? { status: "ok", turn }
|
|
531
|
+
: { status: "no_active_turn" };
|
|
469
532
|
}
|
|
470
533
|
|
|
471
534
|
function selectedMcpServerIds(tools: ToolRef[], sessionServerIds: string[]): Set<string> {
|
|
@@ -543,13 +606,28 @@ function connectionBrokerFetch(
|
|
|
543
606
|
grant: AccessGrant;
|
|
544
607
|
config: McpServerConfig;
|
|
545
608
|
sessionId: string;
|
|
609
|
+
rootSessionId: string;
|
|
610
|
+
turn: SessionTurn;
|
|
546
611
|
},
|
|
547
612
|
): FetchLike {
|
|
548
613
|
const connectionRef = input.config.connectionRef;
|
|
549
614
|
if (!connectionRef) {
|
|
550
615
|
return baseFetch;
|
|
551
616
|
}
|
|
552
|
-
const resolveCredential =
|
|
617
|
+
const resolveCredential = input.deps.connectionCredentials?.mcpCredentials
|
|
618
|
+
? buildHostConnectionTokenResolver(input.deps.connectionCredentials.mcpCredentials, {
|
|
619
|
+
accountId: input.grant.accountId,
|
|
620
|
+
workspaceId: input.grant.workspaceId,
|
|
621
|
+
sessionId: input.sessionId,
|
|
622
|
+
rootSessionId: input.rootSessionId,
|
|
623
|
+
turnId: input.turn.id,
|
|
624
|
+
attemptId: input.turn.activeAttemptId,
|
|
625
|
+
executionGeneration: input.turn.executionGeneration,
|
|
626
|
+
initiator: input.turn.initiator,
|
|
627
|
+
initiatorContext: input.turn.initiatorContext,
|
|
628
|
+
surface: "toolspace",
|
|
629
|
+
})
|
|
630
|
+
: buildConnectionTokenResolver(input.deps.db, input.deps.settings);
|
|
553
631
|
return async (requestInput, init) => {
|
|
554
632
|
const request = await mcpRequestInfo(requestInput, init);
|
|
555
633
|
const first = await resolveCredential({
|
|
@@ -557,7 +635,7 @@ function connectionBrokerFetch(
|
|
|
557
635
|
serverId: input.config.id,
|
|
558
636
|
connectionRef,
|
|
559
637
|
forceRefresh: false,
|
|
560
|
-
...(request.toolName ? {
|
|
638
|
+
...(request.toolName ? { toolName: request.toolName } : {}),
|
|
561
639
|
subjectId: input.grant.subjectId,
|
|
562
640
|
});
|
|
563
641
|
if (first.status === "auth_needed") {
|
|
@@ -573,7 +651,7 @@ function connectionBrokerFetch(
|
|
|
573
651
|
serverId: input.config.id,
|
|
574
652
|
connectionRef,
|
|
575
653
|
forceRefresh: true,
|
|
576
|
-
...(request.toolName ? {
|
|
654
|
+
...(request.toolName ? { toolName: request.toolName } : {}),
|
|
577
655
|
subjectId: input.grant.subjectId,
|
|
578
656
|
});
|
|
579
657
|
if (refreshed.status === "auth_needed") {
|
|
@@ -605,9 +683,13 @@ function authNeededFromStatus(
|
|
|
605
683
|
status: "auth_needed",
|
|
606
684
|
reason,
|
|
607
685
|
providerDomain: connectionRef.providerDomain,
|
|
686
|
+
...(connectionRef.provider ? { provider: connectionRef.provider } : {}),
|
|
608
687
|
connectionId: first.connectionId,
|
|
609
688
|
...(connectionRef.scopes ? { scopes: connectionRef.scopes } : {}),
|
|
610
689
|
...(connectionRef.resource ? { resource: connectionRef.resource } : {}),
|
|
690
|
+
...(connectionRef.selectedResources
|
|
691
|
+
? { selectedResources: connectionRef.selectedResources }
|
|
692
|
+
: {}),
|
|
611
693
|
};
|
|
612
694
|
}
|
|
613
695
|
|
|
@@ -617,6 +699,7 @@ async function authNeededFetchResponse(
|
|
|
617
699
|
grant: AccessGrant;
|
|
618
700
|
config: McpServerConfig;
|
|
619
701
|
sessionId: string;
|
|
702
|
+
turn: SessionTurn;
|
|
620
703
|
},
|
|
621
704
|
request: McpRequestInfo,
|
|
622
705
|
auth: Extract<ResolveConnectionCredentialResult, { status: "auth_needed" }>,
|
|
@@ -634,10 +717,12 @@ async function authNeededFetchResponse(
|
|
|
634
717
|
serverId: input.config.id,
|
|
635
718
|
toolName: request.toolName ?? null,
|
|
636
719
|
providerDomain: auth.providerDomain,
|
|
720
|
+
...(auth.provider ? { provider: auth.provider } : {}),
|
|
637
721
|
reason: auth.reason,
|
|
638
722
|
...(auth.connectionId ? { connectionId: auth.connectionId } : {}),
|
|
639
723
|
...(auth.scopes ? { scopes: auth.scopes } : {}),
|
|
640
724
|
...(auth.resource ? { resource: auth.resource } : {}),
|
|
725
|
+
...(auth.selectedResources ? { selectedResources: auth.selectedResources } : {}),
|
|
641
726
|
...(auth.authorizationUrl ? { authorizationUrl: auth.authorizationUrl } : {}),
|
|
642
727
|
subjectId: input.grant.subjectId,
|
|
643
728
|
},
|
package/src/routes/codex.ts
CHANGED
|
@@ -42,10 +42,8 @@ import {
|
|
|
42
42
|
updateCodexRotationSettings,
|
|
43
43
|
upsertCodexSubscriptionCredential,
|
|
44
44
|
withCodexCapacityMutation,
|
|
45
|
-
CODEX_ROTATION_STRATEGIES,
|
|
46
45
|
type CodexAccountStatus,
|
|
47
46
|
type CodexCapacityWakeTarget,
|
|
48
|
-
type CodexRotationStrategy,
|
|
49
47
|
} from "@opengeni/db";
|
|
50
48
|
|
|
51
49
|
// The picker surfaces codex models under their own "no credits" provider group so
|
|
@@ -369,7 +367,9 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
369
367
|
activeAccountId,
|
|
370
368
|
settings: {
|
|
371
369
|
rotationEnabled: rotation?.rotationEnabled ?? false,
|
|
372
|
-
|
|
370
|
+
// sharded-rotation policy: rotation-enabled always behaves as sticky-sharded; report the
|
|
371
|
+
// effective truth, never the stored legacy residue.
|
|
372
|
+
rotationStrategy: "sharded",
|
|
373
373
|
activeCredentialId: activeAccountId,
|
|
374
374
|
},
|
|
375
375
|
});
|
|
@@ -397,8 +397,11 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
397
397
|
return c.json({ activated: true, accountId });
|
|
398
398
|
});
|
|
399
399
|
|
|
400
|
-
//
|
|
401
|
-
//
|
|
400
|
+
// Update rotation settings. admin access. sharded-rotation policy: the strategy picker is GONE —
|
|
401
|
+
// rotation-enabled always behaves as sticky-sharded (worker-side
|
|
402
|
+
// effectiveRotationStrategy normalization). `rotationStrategy` in the body is
|
|
403
|
+
// ACCEPTED-BUT-IGNORED so no existing SDK/UI caller breaks (deprecation), and
|
|
404
|
+
// the stored column is only legacy residue kept for old-binary rollback.
|
|
402
405
|
app.patch("/v1/workspaces/:workspaceId/codex/settings", async (c) => {
|
|
403
406
|
const workspaceId = c.req.param("workspaceId");
|
|
404
407
|
const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
|
|
@@ -406,19 +409,18 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
406
409
|
rotationEnabled?: unknown;
|
|
407
410
|
rotationStrategy?: unknown;
|
|
408
411
|
};
|
|
409
|
-
const patch: { rotationEnabled?: boolean
|
|
412
|
+
const patch: { rotationEnabled?: boolean } = {};
|
|
410
413
|
if (typeof body.rotationEnabled === "boolean") {
|
|
411
414
|
patch.rotationEnabled = body.rotationEnabled;
|
|
412
415
|
}
|
|
413
|
-
if (
|
|
414
|
-
if (!CODEX_ROTATION_STRATEGIES.includes(body.rotationStrategy as CodexRotationStrategy)) {
|
|
415
|
-
throw new HTTPException(400, { message: "invalid rotation strategy" });
|
|
416
|
-
}
|
|
417
|
-
patch.rotationStrategy = body.rotationStrategy as CodexRotationStrategy;
|
|
418
|
-
}
|
|
419
|
-
if (patch.rotationEnabled === undefined && patch.rotationStrategy === undefined) {
|
|
416
|
+
if (patch.rotationEnabled === undefined && body.rotationStrategy === undefined) {
|
|
420
417
|
throw new HTTPException(400, { message: "no settings to update" });
|
|
421
418
|
}
|
|
419
|
+
if (patch.rotationEnabled === undefined) {
|
|
420
|
+
// Strategy-only writes are a deprecated no-op (no db touch): report the
|
|
421
|
+
// (only) truth. Callers that also flip rotationEnabled fall through.
|
|
422
|
+
return c.json({ rotationStrategy: "sharded", rotationStrategyDeprecated: true });
|
|
423
|
+
}
|
|
422
424
|
await ensureCodexRotationSettings(db, grant.accountId, workspaceId);
|
|
423
425
|
const mutation = await withCodexCapacityMutation(
|
|
424
426
|
db,
|
|
@@ -435,7 +437,8 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
435
437
|
await signalCodexCapacityTargets(deps, mutation.wakeTargets);
|
|
436
438
|
return c.json({
|
|
437
439
|
rotationEnabled: updated.rotationEnabled,
|
|
438
|
-
|
|
440
|
+
// sharded-rotation policy: sharded is the only behavior; the stored column is residue.
|
|
441
|
+
rotationStrategy: "sharded",
|
|
439
442
|
activeCredentialId: updated.activeCredentialId,
|
|
440
443
|
});
|
|
441
444
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// apps/api/src/routes/enrollments.ts — the bring-your-own-compute enrollment
|
|
2
|
-
// device-flow routes (M5
|
|
2
|
+
// device-flow routes (M5). Mirrors the other sandbox route
|
|
3
3
|
// modules (registerSessionRoutes / registerApiKeyRoutes): a thin route over a
|
|
4
4
|
// focused service (../sandbox/enrollment.ts), requireAccessGrant BEFORE any Zod
|
|
5
5
|
// parse on the USER-authenticated routes, explicit HTTPException(400) on a parse
|
|
@@ -69,7 +69,7 @@ export function registerEnrollmentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
// A tiny in-process IP token-bucket for the UNAUTHENTICATED agent routes (start/
|
|
72
|
-
// poll). The relay tier owns the heavy stream rate-limiting
|
|
72
|
+
// poll). The relay tier owns the heavy stream rate-limiting; this
|
|
73
73
|
// is the application-tier abuse cap on the device-flow endpoints. Per-IP buckets
|
|
74
74
|
// are pruned lazily. Not a distributed limiter (one replica per bucket) — that is
|
|
75
75
|
// acceptable for a bounded, access-key-gated, short-TTL flow.
|