@opengeni/api-router 2.6.4 → 2.10.0-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-grant-rls.d.ts +3 -0
- package/dist/app.d.ts +2 -1
- package/dist/app.js +3 -1
- package/dist/auth/organization-user-setup.d.ts +23 -0
- package/dist/{chunk-XTLI3CBH.js → chunk-XXH7DW34.js} +6866 -3554
- package/dist/chunk-XXH7DW34.js.map +1 -0
- package/dist/codemode.d.ts +6 -0
- package/dist/github-access.d.ts +25 -2
- package/dist/http/request-source.d.ts +14 -0
- package/dist/index.js +19 -3
- package/dist/index.js.map +1 -1
- package/dist/integrations/oauth-client.d.ts +2 -11
- package/dist/integrations/personal-github-repositories.d.ts +41 -2
- package/dist/integrations/slack-interactions.d.ts +1 -1
- package/dist/mcp/server.d.ts +158 -1
- package/dist/mcp/session-view.d.ts +56 -2
- package/dist/mcp/session-wait.d.ts +1 -1
- package/dist/mcp-oauth.d.ts +19 -0
- package/dist/model-catalog.d.ts +5 -31
- package/dist/routes/codex.d.ts +27 -2
- package/dist/routes/github.d.ts +2 -0
- package/dist/routes/organization-model-providers.d.ts +3 -0
- package/dist/routes/workspace-artifacts.d.ts +2 -1
- package/dist/work-discovery-observability.d.ts +1 -1
- package/dist/workspace-artifact-content.d.ts +28 -0
- package/dist/workspace-artifact-provenance.d.ts +7 -0
- package/dist/workspace-deletion.d.ts +6 -0
- package/dist/workspace-tool-gateway-observability.d.ts +17 -0
- package/dist/workspace-tool-gateway.d.ts +118 -0
- package/package.json +19 -18
- package/src/access-grant-rls.ts +40 -0
- package/src/app.ts +296 -53
- package/src/auth/organization-user-setup.ts +95 -5
- package/src/codemode.ts +33 -4
- package/src/github-access.ts +117 -1
- package/src/http/auth.ts +11 -0
- package/src/http/request-source.ts +37 -0
- package/src/http/sse.ts +15 -7
- package/src/index.ts +18 -2
- package/src/integrations/oauth-client.ts +168 -203
- package/src/integrations/personal-github-repositories.ts +238 -9
- package/src/integrations/slack-app-home.ts +2 -2
- package/src/integrations/slack-interactions.ts +77 -37
- package/src/mcp/company-brain-governed-writes.ts +2 -2
- package/src/mcp/company-profile-agent-admin.ts +1 -1
- package/src/mcp/remember.ts +1 -1
- package/src/mcp/server.ts +504 -133
- package/src/mcp/session-view.ts +20 -1
- package/src/mcp/session-wait.ts +3 -0
- package/src/mcp-oauth.ts +539 -0
- package/src/model-catalog.ts +45 -337
- package/src/routes/automations.ts +178 -19
- package/src/routes/codex.ts +242 -73
- package/src/routes/connections.ts +271 -16
- package/src/routes/documents.ts +67 -19
- package/src/routes/files.ts +54 -6
- package/src/routes/github.ts +116 -15
- package/src/routes/organization-memberships.ts +59 -16
- package/src/routes/organization-model-providers.ts +231 -0
- package/src/routes/packs.ts +1 -0
- package/src/routes/personal-github.ts +39 -0
- package/src/routes/pr-review.ts +72 -4
- package/src/routes/scheduled-tasks.ts +40 -13
- package/src/routes/sessions.ts +153 -65
- package/src/routes/supergrok.ts +3 -2
- package/src/routes/workspace-artifacts.ts +176 -67
- package/src/routes/workspaces.ts +405 -62
- package/src/sandbox/channel-a.ts +17 -4
- package/src/work-discovery-observability.ts +3 -3
- package/src/workspace-artifact-content.ts +163 -0
- package/src/workspace-artifact-provenance.ts +104 -0
- package/src/workspace-deletion.ts +64 -0
- package/src/workspace-tool-gateway-observability.ts +100 -0
- package/src/workspace-tool-gateway.ts +691 -0
- package/dist/chunk-XTLI3CBH.js.map +0 -1
package/src/routes/codex.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// is added to isAuthExempt. Secrets never leave the server: status/usage read the
|
|
8
8
|
// decrypted token only to call the codex backend; the token is never returned.
|
|
9
9
|
|
|
10
|
-
import { environmentsEncryptionKeyBytes } from "@opengeni/config";
|
|
10
|
+
import { environmentsEncryptionKeyBytes, productLabelForModelId } from "@opengeni/config";
|
|
11
11
|
import {
|
|
12
12
|
accessTokenExpiry,
|
|
13
13
|
buildCodexUsageWindowFromCache,
|
|
@@ -134,6 +134,55 @@ function codexAccountJson(
|
|
|
134
134
|
};
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Derive the two distinct cached worker-readiness meanings exposed by the
|
|
139
|
+
* status route. `poolReady` means at least one effective-pool account passes
|
|
140
|
+
* the worker's cached admission predicate. `workerRoutable` additionally
|
|
141
|
+
* honors rotation-off's active-pointer-only rule; it says nothing about a
|
|
142
|
+
* session-specific manual pin.
|
|
143
|
+
*/
|
|
144
|
+
export function codexWorkerReadiness(input: {
|
|
145
|
+
effectiveSource: "workspace" | "organization" | "disabled";
|
|
146
|
+
rotationEnabled: boolean;
|
|
147
|
+
activeCredentialId: string | null;
|
|
148
|
+
accounts: ReadonlyArray<
|
|
149
|
+
Pick<
|
|
150
|
+
CodexAccountStatus,
|
|
151
|
+
| "id"
|
|
152
|
+
| "status"
|
|
153
|
+
| "allocatorEnabled"
|
|
154
|
+
| "primaryUsedPercent"
|
|
155
|
+
| "primaryResetAt"
|
|
156
|
+
| "secondaryUsedPercent"
|
|
157
|
+
| "secondaryResetAt"
|
|
158
|
+
| "exhaustedUntil"
|
|
159
|
+
>
|
|
160
|
+
>;
|
|
161
|
+
now: Date;
|
|
162
|
+
}): { poolReady: boolean; workerRoutable: boolean } {
|
|
163
|
+
if (input.effectiveSource === "disabled") {
|
|
164
|
+
return { poolReady: false, workerRoutable: false };
|
|
165
|
+
}
|
|
166
|
+
const windowUsed = (used: number | null, resetAt: Date | null): number =>
|
|
167
|
+
resetAt !== null && resetAt.getTime() <= input.now.getTime() ? 0 : (used ?? 0);
|
|
168
|
+
const eligible = (account: (typeof input.accounts)[number]): boolean =>
|
|
169
|
+
account.status === "active" &&
|
|
170
|
+
account.allocatorEnabled &&
|
|
171
|
+
(account.exhaustedUntil === null || account.exhaustedUntil.getTime() <= input.now.getTime()) &&
|
|
172
|
+
Math.max(
|
|
173
|
+
windowUsed(account.primaryUsedPercent, account.primaryResetAt),
|
|
174
|
+
windowUsed(account.secondaryUsedPercent, account.secondaryResetAt),
|
|
175
|
+
) < 100;
|
|
176
|
+
const poolReady = input.accounts.some(eligible);
|
|
177
|
+
const activePointerReady = input.accounts.some(
|
|
178
|
+
(account) => account.id === input.activeCredentialId && eligible(account),
|
|
179
|
+
);
|
|
180
|
+
return {
|
|
181
|
+
poolReady,
|
|
182
|
+
workerRoutable: input.rotationEnabled ? poolReady : activePointerReady,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
137
186
|
// The /codex/usage{,/refresh,/:id} wire wrapper: the rich normalized payload
|
|
138
187
|
// carries its own `status`, surfaced at the top level for back-compat with the
|
|
139
188
|
// existing CodexUsage = { status; usage } shape.
|
|
@@ -144,21 +193,16 @@ function codexUsageJson(payload: CodexUsagePayload): {
|
|
|
144
193
|
return { status: payload.status, usage: payload };
|
|
145
194
|
}
|
|
146
195
|
|
|
147
|
-
export function codexModelsForPicker(
|
|
196
|
+
export function codexModelsForPicker(): Array<{
|
|
148
197
|
id: string;
|
|
149
198
|
label: string;
|
|
150
199
|
provider: string;
|
|
151
200
|
providerLabel: string;
|
|
152
201
|
api: "responses";
|
|
153
202
|
}> {
|
|
154
|
-
const available = new Set(liveSlugs);
|
|
155
|
-
const missing = CODEX_FALLBACK_MODEL_SLUGS.filter((slug) => !available.has(slug));
|
|
156
|
-
if (missing.length > 0) {
|
|
157
|
-
throw new Error(`Codex catalog is missing required models: ${missing.join(", ")}`);
|
|
158
|
-
}
|
|
159
203
|
return CODEX_FALLBACK_MODEL_SLUGS.map((slug) => ({
|
|
160
204
|
id: `${CODEX_MODEL_ID_PREFIX}${slug}`,
|
|
161
|
-
label: slug
|
|
205
|
+
label: productLabelForModelId(slug),
|
|
162
206
|
provider: CODEX_PROVIDER_ID,
|
|
163
207
|
providerLabel: CODEX_PROVIDER_LABEL,
|
|
164
208
|
api: "responses" as const,
|
|
@@ -223,7 +267,7 @@ async function managedCookieHuman(
|
|
|
223
267
|
};
|
|
224
268
|
}
|
|
225
269
|
|
|
226
|
-
async function requireOrganizationCodexHuman(
|
|
270
|
+
export async function requireOrganizationCodexHuman(
|
|
227
271
|
c: Context,
|
|
228
272
|
deps: ApiRouteDeps,
|
|
229
273
|
organizationId: string,
|
|
@@ -239,7 +283,9 @@ async function requireOrganizationCodexHuman(
|
|
|
239
283
|
};
|
|
240
284
|
}
|
|
241
285
|
if (!human) {
|
|
242
|
-
throw new HTTPException(401, {
|
|
286
|
+
throw new HTTPException(401, {
|
|
287
|
+
message: "organization administrator session required",
|
|
288
|
+
});
|
|
243
289
|
}
|
|
244
290
|
try {
|
|
245
291
|
await getOrganizationCodexRotationSettings(deps.db, {
|
|
@@ -249,7 +295,9 @@ async function requireOrganizationCodexHuman(
|
|
|
249
295
|
} catch (error) {
|
|
250
296
|
const state = nestedPostgresSqlState(error);
|
|
251
297
|
if (state === "42501") {
|
|
252
|
-
throw new HTTPException(403, {
|
|
298
|
+
throw new HTTPException(403, {
|
|
299
|
+
message: "organization administration is not authorized",
|
|
300
|
+
});
|
|
253
301
|
}
|
|
254
302
|
if (state === "P0002") {
|
|
255
303
|
throw new HTTPException(404, { message: "organization not found" });
|
|
@@ -270,11 +318,13 @@ async function requireWorkspaceCodexManagementSource(
|
|
|
270
318
|
});
|
|
271
319
|
}
|
|
272
320
|
if (source.effectiveSource === "disabled") {
|
|
273
|
-
throw new HTTPException(409, {
|
|
321
|
+
throw new HTTPException(409, {
|
|
322
|
+
message: "Codex is disabled for this workspace",
|
|
323
|
+
});
|
|
274
324
|
}
|
|
275
325
|
}
|
|
276
326
|
|
|
277
|
-
function requireSameOriginBrowserMutation(c: Context, deps: ApiRouteDeps): void {
|
|
327
|
+
export function requireSameOriginBrowserMutation(c: Context, deps: ApiRouteDeps): void {
|
|
278
328
|
const contentType = c.req.header("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
279
329
|
if (contentType !== "application/json") {
|
|
280
330
|
throw new HTTPException(403, {
|
|
@@ -287,16 +337,28 @@ function requireSameOriginBrowserMutation(c: Context, deps: ApiRouteDeps): void
|
|
|
287
337
|
});
|
|
288
338
|
}
|
|
289
339
|
const origin = c.req.header("origin");
|
|
340
|
+
const localOriginMatches =
|
|
341
|
+
deps.settings.productAccessMode === "local" && localBrowserOriginMatchesRequest(c, origin);
|
|
290
342
|
if (
|
|
291
343
|
deps.settings.productAccessMode === "local"
|
|
292
|
-
? !
|
|
344
|
+
? !localOriginMatches
|
|
293
345
|
: origin !== new URL(deps.settings.publicBaseUrl!).origin
|
|
294
346
|
) {
|
|
295
347
|
throw new HTTPException(403, {
|
|
296
348
|
message: "same-origin browser request required",
|
|
297
349
|
});
|
|
298
350
|
}
|
|
299
|
-
|
|
351
|
+
const fetchSite = c.req.header("sec-fetch-site")?.toLowerCase();
|
|
352
|
+
const localFetchSiteMatches =
|
|
353
|
+
localOriginMatches &&
|
|
354
|
+
(fetchSite === "same-origin" ||
|
|
355
|
+
fetchSite === "same-site" ||
|
|
356
|
+
(fetchSite === "cross-site" && localLoopbackOriginMatchesRequest(c, origin)));
|
|
357
|
+
if (
|
|
358
|
+
deps.settings.productAccessMode === "local"
|
|
359
|
+
? !localFetchSiteMatches
|
|
360
|
+
: fetchSite !== "same-origin"
|
|
361
|
+
) {
|
|
300
362
|
throw new HTTPException(403, {
|
|
301
363
|
message: "same-origin fetch metadata required",
|
|
302
364
|
});
|
|
@@ -333,11 +395,35 @@ function localBrowserOriginMatchesRequest(c: Context, value: string | undefined)
|
|
|
333
395
|
}
|
|
334
396
|
return (
|
|
335
397
|
origin.protocol === request.protocol &&
|
|
336
|
-
origin.hostname === request.hostname
|
|
337
|
-
|
|
398
|
+
(origin.hostname === request.hostname ||
|
|
399
|
+
(isLoopbackHostname(origin.hostname) && isLoopbackHostname(request.hostname)))
|
|
338
400
|
);
|
|
339
401
|
}
|
|
340
402
|
|
|
403
|
+
function localLoopbackOriginMatchesRequest(c: Context, value: string | undefined): boolean {
|
|
404
|
+
if (!value) return false;
|
|
405
|
+
try {
|
|
406
|
+
const origin = new URL(value);
|
|
407
|
+
const forwardedProtocol = c.req.header("x-forwarded-proto")?.trim().toLowerCase();
|
|
408
|
+
const protocol = forwardedProtocol ? `${forwardedProtocol}:` : new URL(c.req.url).protocol;
|
|
409
|
+
const forwardedHost = c.req.header("x-forwarded-host") ?? c.req.header("host");
|
|
410
|
+
if (!forwardedHost || /[\s,/?#@\\]/u.test(forwardedHost)) return false;
|
|
411
|
+
const request = new URL(`${protocol}//${forwardedHost}`);
|
|
412
|
+
return (
|
|
413
|
+
origin.origin === value &&
|
|
414
|
+
origin.protocol === request.protocol &&
|
|
415
|
+
isLoopbackHostname(origin.hostname) &&
|
|
416
|
+
isLoopbackHostname(request.hostname)
|
|
417
|
+
);
|
|
418
|
+
} catch {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function isLoopbackHostname(value: string): boolean {
|
|
424
|
+
return value === "localhost" || value === "[::1]" || /^127(?:\.[0-9]{1,3}){3}$/u.test(value);
|
|
425
|
+
}
|
|
426
|
+
|
|
341
427
|
async function requireRedemptionHuman(
|
|
342
428
|
c: Context,
|
|
343
429
|
deps: ApiRouteDeps,
|
|
@@ -386,11 +472,15 @@ async function requireCodexAppsHuman(
|
|
|
386
472
|
requireSameOriginBrowserMutation(c, deps);
|
|
387
473
|
const human = await managedCookieHuman(c, deps);
|
|
388
474
|
if (!human) {
|
|
389
|
-
throw new HTTPException(401, {
|
|
475
|
+
throw new HTTPException(401, {
|
|
476
|
+
message: "managed browser session required",
|
|
477
|
+
});
|
|
390
478
|
}
|
|
391
479
|
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
392
480
|
if (grant.subjectId !== human.subjectId) {
|
|
393
|
-
throw new HTTPException(403, {
|
|
481
|
+
throw new HTTPException(403, {
|
|
482
|
+
message: "managed browser identity mismatch",
|
|
483
|
+
});
|
|
394
484
|
}
|
|
395
485
|
return { human, accountId: grant.accountId };
|
|
396
486
|
}
|
|
@@ -715,10 +805,14 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
715
805
|
const workspaceId = c.req.param("workspaceId");
|
|
716
806
|
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
717
807
|
const parsed = z
|
|
718
|
-
.object({
|
|
808
|
+
.object({
|
|
809
|
+
mode: z.enum(["automatic", "workspace", "organization", "disabled"]),
|
|
810
|
+
})
|
|
719
811
|
.safeParse(await c.req.json().catch(() => null));
|
|
720
812
|
if (!parsed.success) {
|
|
721
|
-
throw new HTTPException(400, {
|
|
813
|
+
throw new HTTPException(400, {
|
|
814
|
+
message: "a valid Codex source mode is required",
|
|
815
|
+
});
|
|
722
816
|
}
|
|
723
817
|
try {
|
|
724
818
|
return c.json(
|
|
@@ -795,7 +889,9 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
795
889
|
const organizationId = c.req.param("organizationId");
|
|
796
890
|
requireSameOriginBrowserMutation(c, deps);
|
|
797
891
|
const human = await requireOrganizationCodexHuman(c, deps, organizationId);
|
|
798
|
-
const { state } = (await c.req.json().catch(() => null)) as {
|
|
892
|
+
const { state } = (await c.req.json().catch(() => null)) as {
|
|
893
|
+
state?: string;
|
|
894
|
+
};
|
|
799
895
|
const payload = (state
|
|
800
896
|
? readSignedState(state, githubStateSecret)
|
|
801
897
|
: null) as unknown as CodexConnectState | null;
|
|
@@ -806,7 +902,9 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
806
902
|
!payload.deviceAuthId ||
|
|
807
903
|
!payload.userCode
|
|
808
904
|
) {
|
|
809
|
-
throw new HTTPException(400, {
|
|
905
|
+
throw new HTTPException(400, {
|
|
906
|
+
message: "codex connect state is invalid or expired",
|
|
907
|
+
});
|
|
810
908
|
}
|
|
811
909
|
if (
|
|
812
910
|
typeof payload.iat === "number" &&
|
|
@@ -849,26 +947,39 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
849
947
|
organizationId,
|
|
850
948
|
actorSubjectId: human.subjectId,
|
|
851
949
|
});
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
950
|
+
let upserted: Awaited<ReturnType<typeof upsertOrganizationCodexSubscriptionCredential>>;
|
|
951
|
+
try {
|
|
952
|
+
upserted = await upsertOrganizationCodexSubscriptionCredential(db, {
|
|
953
|
+
organizationId,
|
|
954
|
+
actorSubjectId: human.subjectId,
|
|
955
|
+
credentialEncrypted: encryptEnvironmentValue(
|
|
956
|
+
key,
|
|
957
|
+
JSON.stringify({
|
|
958
|
+
access_token: tokens.accessToken,
|
|
959
|
+
refresh_token: tokens.refreshToken,
|
|
960
|
+
id_token: tokens.idToken,
|
|
961
|
+
}),
|
|
962
|
+
),
|
|
963
|
+
chatgptAccountId: id.chatgptAccountId,
|
|
964
|
+
scopes: null,
|
|
965
|
+
planType: id.planType,
|
|
966
|
+
isFedramp: id.isFedramp,
|
|
967
|
+
expiresAt: accessTokenExpiry(tokens.accessToken),
|
|
968
|
+
lastRefreshAt: new Date(),
|
|
969
|
+
accountEmail: id.email ?? null,
|
|
970
|
+
label: id.email ?? id.chatgptAccountId ?? null,
|
|
971
|
+
});
|
|
972
|
+
} catch (error) {
|
|
973
|
+
const cause = (error as { cause?: unknown } | null)?.cause;
|
|
974
|
+
const message =
|
|
975
|
+
cause instanceof Error ? cause.message : error instanceof Error ? error.message : "";
|
|
976
|
+
if (message.includes("active turns are using it")) {
|
|
977
|
+
throw new HTTPException(409, {
|
|
978
|
+
message: "Codex subscription source cannot change while active turns are using it",
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
throw error;
|
|
982
|
+
}
|
|
872
983
|
await signalCodexCapacityTargets(deps, upserted.wakeTargets);
|
|
873
984
|
const rotation = await getOrganizationCodexRotationSettings(db, {
|
|
874
985
|
organizationId,
|
|
@@ -927,7 +1038,9 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
927
1038
|
const organizationId = c.req.param("organizationId");
|
|
928
1039
|
requireSameOriginBrowserMutation(c, deps);
|
|
929
1040
|
const human = await requireOrganizationCodexHuman(c, deps, organizationId);
|
|
930
|
-
const body = (await c.req.json().catch(() => null)) as {
|
|
1041
|
+
const body = (await c.req.json().catch(() => null)) as {
|
|
1042
|
+
label?: unknown;
|
|
1043
|
+
} | null;
|
|
931
1044
|
const renamed = await renameOrganizationCodexAccount(db, {
|
|
932
1045
|
organizationId,
|
|
933
1046
|
actorSubjectId: human.subjectId,
|
|
@@ -967,7 +1080,10 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
967
1080
|
throw error;
|
|
968
1081
|
}
|
|
969
1082
|
await signalCodexCapacityTargets(deps, result.wakeTargets);
|
|
970
|
-
return c.json({
|
|
1083
|
+
return c.json({
|
|
1084
|
+
disconnected: result.removed,
|
|
1085
|
+
newActiveId: result.newActiveCredentialId,
|
|
1086
|
+
});
|
|
971
1087
|
});
|
|
972
1088
|
|
|
973
1089
|
// Begin device-code login: returns the user code + verification URL and a
|
|
@@ -1065,6 +1181,8 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1065
1181
|
upserted: Awaited<ReturnType<typeof upsertCodexSubscriptionCredential>>;
|
|
1066
1182
|
isActive: boolean;
|
|
1067
1183
|
}>(db, { workspaceId, reason: "codex_credential_connected" }, async (tx) => {
|
|
1184
|
+
// The capacity mutation holds the source lock before entering this callback.
|
|
1185
|
+
const sourceBeforeConnect = await getWorkspaceCodexSubscriptionSource(tx, workspaceId);
|
|
1068
1186
|
const upserted = await upsertCodexSubscriptionCredential(tx, {
|
|
1069
1187
|
accountId: grant.accountId,
|
|
1070
1188
|
workspaceId,
|
|
@@ -1092,12 +1210,12 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1092
1210
|
}
|
|
1093
1211
|
await ensureCodexRotationSettings(tx, grant.accountId, workspaceId);
|
|
1094
1212
|
await setInitialActiveCodexCredential(tx, workspaceId, upserted.id);
|
|
1095
|
-
const source = await getWorkspaceCodexSubscriptionSource(tx, workspaceId);
|
|
1096
1213
|
await setWorkspaceCodexSubscriptionModeInTransaction(tx, {
|
|
1097
1214
|
accountId: grant.accountId,
|
|
1098
1215
|
workspaceId,
|
|
1099
1216
|
subjectId: grant.subjectId,
|
|
1100
|
-
mode:
|
|
1217
|
+
mode: sourceBeforeConnect.workspaceKind === "personal" ? "automatic" : "workspace",
|
|
1218
|
+
effectiveSourceBeforeMutation: sourceBeforeConnect.effectiveSource,
|
|
1101
1219
|
});
|
|
1102
1220
|
const rotation = await getCodexRotationSettings(tx, workspaceId);
|
|
1103
1221
|
return {
|
|
@@ -1107,6 +1225,14 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1107
1225
|
},
|
|
1108
1226
|
changed: true,
|
|
1109
1227
|
};
|
|
1228
|
+
}).catch((error: unknown) => {
|
|
1229
|
+
const cause = (error as { cause?: unknown } | null)?.cause;
|
|
1230
|
+
const message =
|
|
1231
|
+
cause instanceof Error ? cause.message : error instanceof Error ? error.message : "";
|
|
1232
|
+
if (message.includes("active turns are using it")) {
|
|
1233
|
+
throw new HTTPException(409, { message });
|
|
1234
|
+
}
|
|
1235
|
+
throw error;
|
|
1110
1236
|
});
|
|
1111
1237
|
const { upserted, isActive } = mutation.result;
|
|
1112
1238
|
if (upserted.kind === "unresolved_redemption") {
|
|
@@ -1116,7 +1242,12 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1116
1242
|
});
|
|
1117
1243
|
}
|
|
1118
1244
|
await signalCodexCapacityTargets(deps, mutation.wakeTargets);
|
|
1119
|
-
return c.json({
|
|
1245
|
+
return c.json({
|
|
1246
|
+
status: "connected",
|
|
1247
|
+
plan: id.planType,
|
|
1248
|
+
accountId: upserted.id,
|
|
1249
|
+
isActive,
|
|
1250
|
+
});
|
|
1120
1251
|
});
|
|
1121
1252
|
|
|
1122
1253
|
// Connection health: the cheapest real call is GET /codex/models (a 200 proves
|
|
@@ -1124,15 +1255,13 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1124
1255
|
app.get("/v1/workspaces/:workspaceId/codex/status", async (c) => {
|
|
1125
1256
|
const workspaceId = c.req.param("workspaceId");
|
|
1126
1257
|
await requireAccessGrant(c, deps, workspaceId, "workspace:read");
|
|
1127
|
-
const status = await
|
|
1128
|
-
|
|
1129
|
-
return c.json({ connected: false });
|
|
1130
|
-
}
|
|
1131
|
-
const [accounts, source] = await Promise.all([
|
|
1258
|
+
const [status, accounts, source, rotation] = await Promise.all([
|
|
1259
|
+
getCodexCredentialStatus(db, workspaceId),
|
|
1132
1260
|
listCodexAccountStatuses(db, workspaceId),
|
|
1133
1261
|
getWorkspaceCodexSubscriptionSource(db, workspaceId),
|
|
1262
|
+
getCodexRotationSettings(db, workspaceId),
|
|
1134
1263
|
]);
|
|
1135
|
-
const activeRow = accounts.find((account) => account.id === status
|
|
1264
|
+
const activeRow = accounts.find((account) => account.id === status?.credentialId) ?? null;
|
|
1136
1265
|
const activeAccount = activeRow
|
|
1137
1266
|
? {
|
|
1138
1267
|
id: activeRow.id,
|
|
@@ -1144,11 +1273,19 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1144
1273
|
chatgptAccountId: activeRow.chatgptAccountId,
|
|
1145
1274
|
}
|
|
1146
1275
|
: null;
|
|
1276
|
+
const activeCredentialId = status?.credentialId ?? rotation?.activeCredentialId ?? null;
|
|
1277
|
+
const readiness = codexWorkerReadiness({
|
|
1278
|
+
effectiveSource: source.effectiveSource,
|
|
1279
|
+
rotationEnabled: rotation?.rotationEnabled ?? false,
|
|
1280
|
+
activeCredentialId,
|
|
1281
|
+
accounts,
|
|
1282
|
+
now: new Date(),
|
|
1283
|
+
});
|
|
1147
1284
|
let valid = false;
|
|
1148
|
-
|
|
1285
|
+
const models = codexModelsForPicker();
|
|
1149
1286
|
let catalogError: string | null = null;
|
|
1150
1287
|
try {
|
|
1151
|
-
const cred = status
|
|
1288
|
+
const cred = status?.credentialId
|
|
1152
1289
|
? await loadCodexCredentialForRun(db, settings, workspaceId, status.credentialId)
|
|
1153
1290
|
: null;
|
|
1154
1291
|
if (cred) {
|
|
@@ -1159,7 +1296,6 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1159
1296
|
clientVersion: CODEX_CLIENT_VERSION,
|
|
1160
1297
|
});
|
|
1161
1298
|
if (live.ok) {
|
|
1162
|
-
models = codexModelsForPicker(live.slugs);
|
|
1163
1299
|
valid = true;
|
|
1164
1300
|
} else {
|
|
1165
1301
|
catalogError = `Codex models request failed with status ${live.status}`;
|
|
@@ -1170,11 +1306,14 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1170
1306
|
catalogError = error instanceof Error ? error.message : String(error);
|
|
1171
1307
|
}
|
|
1172
1308
|
return c.json({
|
|
1173
|
-
connected: status
|
|
1174
|
-
plan: status
|
|
1309
|
+
connected: status?.connected ?? false,
|
|
1310
|
+
plan: status?.planType ?? null,
|
|
1175
1311
|
valid,
|
|
1176
|
-
|
|
1177
|
-
|
|
1312
|
+
activeAccountValid: valid,
|
|
1313
|
+
poolReady: readiness.poolReady,
|
|
1314
|
+
workerRoutable: readiness.workerRoutable,
|
|
1315
|
+
expiresAt: status?.expiresAt ?? null,
|
|
1316
|
+
lastError: catalogError ?? status?.lastError ?? null,
|
|
1178
1317
|
models, // ClientModel[] the picker surfaces under the "no credits" group
|
|
1179
1318
|
activeAccount, // the account a session runs on when unpinned (label for the indicator)
|
|
1180
1319
|
accountCount: accounts.length,
|
|
@@ -1229,7 +1368,9 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1229
1368
|
const workspaceId = c.req.param("workspaceId");
|
|
1230
1369
|
await requireWorkspaceCodexManagementSource(deps, workspaceId);
|
|
1231
1370
|
if (!settings.codexConnectedAppsEnabled) {
|
|
1232
|
-
throw new HTTPException(409, {
|
|
1371
|
+
throw new HTTPException(409, {
|
|
1372
|
+
message: "Codex Apps is disabled for this deployment",
|
|
1373
|
+
});
|
|
1233
1374
|
}
|
|
1234
1375
|
const { human, accountId } = await requireCodexAppsHuman(c, deps, workspaceId);
|
|
1235
1376
|
const parsed = z
|
|
@@ -1239,7 +1380,9 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1239
1380
|
})
|
|
1240
1381
|
.safeParse(await c.req.json().catch(() => null));
|
|
1241
1382
|
if (!parsed.success) {
|
|
1242
|
-
throw new HTTPException(400, {
|
|
1383
|
+
throw new HTTPException(400, {
|
|
1384
|
+
message: "accountId and expectedVersion are required",
|
|
1385
|
+
});
|
|
1243
1386
|
}
|
|
1244
1387
|
const result = await designateCodexAppsCredential(db, {
|
|
1245
1388
|
accountId,
|
|
@@ -1257,10 +1400,14 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1257
1400
|
});
|
|
1258
1401
|
}
|
|
1259
1402
|
if (result.kind === "forbidden") {
|
|
1260
|
-
throw new HTTPException(403, {
|
|
1403
|
+
throw new HTTPException(403, {
|
|
1404
|
+
message: "missing permission: connections:write",
|
|
1405
|
+
});
|
|
1261
1406
|
}
|
|
1262
1407
|
if (result.kind === "unavailable") {
|
|
1263
|
-
throw new HTTPException(409, {
|
|
1408
|
+
throw new HTTPException(409, {
|
|
1409
|
+
message: "codex account requires relogin",
|
|
1410
|
+
});
|
|
1264
1411
|
}
|
|
1265
1412
|
const response = {
|
|
1266
1413
|
credentialId: result.credentialId,
|
|
@@ -1288,7 +1435,9 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1288
1435
|
expectedVersion: parsed.data.expectedVersion,
|
|
1289
1436
|
});
|
|
1290
1437
|
if (result.kind === "forbidden") {
|
|
1291
|
-
throw new HTTPException(403, {
|
|
1438
|
+
throw new HTTPException(403, {
|
|
1439
|
+
message: "missing permission: connections:write",
|
|
1440
|
+
});
|
|
1292
1441
|
}
|
|
1293
1442
|
const response = {
|
|
1294
1443
|
credentialId: result.credentialId,
|
|
@@ -1345,7 +1494,10 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1345
1494
|
if (patch.rotationEnabled === undefined) {
|
|
1346
1495
|
// Strategy-only writes are a deprecated no-op (no db touch): report the
|
|
1347
1496
|
// (only) truth. Callers that also flip rotationEnabled fall through.
|
|
1348
|
-
return c.json({
|
|
1497
|
+
return c.json({
|
|
1498
|
+
rotationStrategy: "sharded",
|
|
1499
|
+
rotationStrategyDeprecated: true,
|
|
1500
|
+
});
|
|
1349
1501
|
}
|
|
1350
1502
|
await ensureCodexRotationSettings(db, grant.accountId, workspaceId);
|
|
1351
1503
|
const mutation = await withCodexCapacityMutation(
|
|
@@ -1454,7 +1606,10 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1454
1606
|
});
|
|
1455
1607
|
}
|
|
1456
1608
|
await signalCodexCapacityTargets(deps, mutation.wakeTargets);
|
|
1457
|
-
return c.json({
|
|
1609
|
+
return c.json({
|
|
1610
|
+
disconnected: result.removed,
|
|
1611
|
+
newActiveId: result.newActiveCredentialId,
|
|
1612
|
+
});
|
|
1458
1613
|
});
|
|
1459
1614
|
|
|
1460
1615
|
// Legacy "disconnect all" (old workspace-wide behavior), deprecated in favor of
|
|
@@ -1752,10 +1907,14 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1752
1907
|
});
|
|
1753
1908
|
}
|
|
1754
1909
|
if (adoption.kind === "not_found") {
|
|
1755
|
-
throw new HTTPException(409, {
|
|
1910
|
+
throw new HTTPException(409, {
|
|
1911
|
+
message: "redemption recovery state changed",
|
|
1912
|
+
});
|
|
1756
1913
|
}
|
|
1757
1914
|
if (adoption.kind === "forbidden") {
|
|
1758
|
-
throw new HTTPException(403, {
|
|
1915
|
+
throw new HTTPException(403, {
|
|
1916
|
+
message: "redemption owner is unavailable",
|
|
1917
|
+
});
|
|
1759
1918
|
}
|
|
1760
1919
|
if (adoption.kind === "conflict") {
|
|
1761
1920
|
throw new HTTPException(409, {
|
|
@@ -1769,7 +1928,9 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1769
1928
|
// and a lost HTTP response must remain replayable after a later health
|
|
1770
1929
|
// transition without another consume call.
|
|
1771
1930
|
if (account.status !== "active" && existing?.status !== "completed") {
|
|
1772
|
-
throw new HTTPException(403, {
|
|
1931
|
+
throw new HTTPException(403, {
|
|
1932
|
+
message: "redemption credential is unavailable",
|
|
1933
|
+
});
|
|
1773
1934
|
}
|
|
1774
1935
|
const secret = settings.betterAuthSecret;
|
|
1775
1936
|
if (!secret) {
|
|
@@ -1968,13 +2129,21 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1968
2129
|
if (fenced.kind !== "ready") {
|
|
1969
2130
|
if (fenced.reason === "confirmation_expired") {
|
|
1970
2131
|
return c.json(
|
|
1971
|
-
{
|
|
2132
|
+
{
|
|
2133
|
+
status: "confirmation_expired",
|
|
2134
|
+
attemptId: attempt.id,
|
|
2135
|
+
retryable: true,
|
|
2136
|
+
},
|
|
1972
2137
|
403,
|
|
1973
2138
|
);
|
|
1974
2139
|
}
|
|
1975
2140
|
if (fenced.reason === "credential_unavailable") {
|
|
1976
2141
|
return c.json(
|
|
1977
|
-
{
|
|
2142
|
+
{
|
|
2143
|
+
status: "provider_unavailable",
|
|
2144
|
+
attemptId: attempt.id,
|
|
2145
|
+
retryable: true,
|
|
2146
|
+
},
|
|
1978
2147
|
503,
|
|
1979
2148
|
);
|
|
1980
2149
|
}
|