@bitkyc08/opencodex 2.10.1 → 2.10.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.
- package/bin/ocx.mjs +18 -9
- package/gui/dist/assets/index-BKVqyYqT.js +70 -0
- package/gui/dist/assets/{index-Cd6_PBKn.css → index-Ca_3269W.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/commandcode-color.svg +1 -0
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/package.json +1 -1
- package/src/adapters/command-code.ts +453 -0
- package/src/adapters/google.ts +3 -0
- package/src/cli/claude.ts +37 -22
- package/src/cli/index.ts +16 -3
- package/src/cli/launcher-context.ts +77 -0
- package/src/codex/admission.ts +1 -1
- package/src/codex/app-server-processes.ts +44 -1
- package/src/codex/catalog/sync.ts +22 -3
- package/src/codex/catalog-write-serialization.ts +1 -1
- package/src/codex/codex-write-lock.ts +1 -1
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/desired-state.ts +27 -7
- package/src/codex/history-job.ts +1 -1
- package/src/codex/history-lock.ts +1 -1
- package/src/codex/history-worker.ts +1 -1
- package/src/codex/internal/history-writer.ts +1 -1
- package/src/codex/transition-state.ts +1 -1
- package/src/codex/user-identity.ts +1 -1
- package/src/config.ts +6 -1
- package/src/integrations/config-io.ts +1 -1
- package/src/integrations/journal.ts +1 -1
- package/src/integrations/merge.ts +1 -1
- package/src/integrations/ownership.ts +1 -1
- package/src/integrations/registry.ts +1 -1
- package/src/integrations/serialize.ts +1 -1
- package/src/integrations/state.ts +1 -1
- package/src/integrations/store.ts +1 -1
- package/src/integrations/writer.ts +1 -1
- package/src/lib/bounded-body.ts +3 -1
- package/src/lib/bun-runtime.ts +21 -17
- package/src/lib/bun-stream-caps.ts +1 -1
- package/src/lib/local-management-attestation.ts +51 -0
- package/src/lib/shadow-call.ts +4 -4
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/health.ts +46 -2
- package/src/oauth/index.ts +38 -3
- package/src/providers/command-code-efforts.ts +85 -0
- package/src/providers/google-vertex-location.ts +14 -0
- package/src/providers/registry.ts +138 -26
- package/src/routing/capability.ts +1 -0
- package/src/server/adapter-resolve.ts +3 -0
- package/src/server/auth-cors.ts +6 -1
- package/src/server/index.ts +44 -2
- package/src/server/management/integration-routes.ts +1 -1
- package/src/server/management/native-integration-routes.ts +2 -2
- package/src/server/responses/core.ts +13 -9
- package/src/storage/scanner.ts +1 -1
- package/src/types.ts +6 -0
- package/src/usage/log.ts +1 -1
- package/gui/dist/assets/index-ChZQsmBY.js +0 -70
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import type { OAuthController, OAuthCredentials } from "./types";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { isAddrInUse } from "../server/ports";
|
|
5
|
+
import { parseCallbackInput } from "./callback-server";
|
|
6
|
+
|
|
7
|
+
const COMMAND_CODE_STUDIO_URL = "https://commandcode.ai";
|
|
8
|
+
const COMMAND_CODE_CALLBACK_PORT = 5959;
|
|
9
|
+
const LOGIN_TIMEOUT_MS = 120_000;
|
|
10
|
+
const CALLBACK_PATH = "/callback";
|
|
11
|
+
|
|
12
|
+
interface CommandCodeCallback {
|
|
13
|
+
apiKey: string;
|
|
14
|
+
state: string;
|
|
15
|
+
userId: string;
|
|
16
|
+
userName: string;
|
|
17
|
+
keyName: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface CommandCodeLocalAuth {
|
|
21
|
+
apiKey?: unknown;
|
|
22
|
+
userId?: unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CommandCodeLoginOptions {
|
|
26
|
+
/** Add-account and reauthentication flows must select a fresh browser identity. */
|
|
27
|
+
importLocal?: "fallback" | "off";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function shouldImportLocalCommandCodeAuth(options: CommandCodeLoginOptions = {}): boolean {
|
|
31
|
+
return options.importLocal !== "off";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function importLocalCommandCodeAuth(signal?: AbortSignal): Promise<OAuthCredentials | undefined> {
|
|
35
|
+
if (signal?.aborted) {
|
|
36
|
+
throw signal.reason ?? new DOMException("Command Code login aborted", "AbortError");
|
|
37
|
+
}
|
|
38
|
+
let parsed: CommandCodeLocalAuth;
|
|
39
|
+
try {
|
|
40
|
+
parsed = JSON.parse(await Bun.file(join(homedir(), ".commandcode", "auth.json")).text()) as CommandCodeLocalAuth;
|
|
41
|
+
} catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
if (typeof parsed.apiKey !== "string" || parsed.apiKey.length === 0) return undefined;
|
|
45
|
+
let accountId: string | undefined;
|
|
46
|
+
try {
|
|
47
|
+
const response = await fetch("https://api.commandcode.ai/alpha/whoami", {
|
|
48
|
+
headers: { Authorization: `Bearer ${parsed.apiKey}`, Accept: "application/json" },
|
|
49
|
+
signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(10_000)]) : AbortSignal.timeout(10_000),
|
|
50
|
+
});
|
|
51
|
+
if (!response.ok) return undefined;
|
|
52
|
+
// Carry the validated whoami identity so an imported credential keeps multi-account
|
|
53
|
+
// semantics even when the local auth.json omits userId.
|
|
54
|
+
const body = (await response.json()) as { user?: { id?: unknown } };
|
|
55
|
+
if (typeof body.user?.id === "string" && body.user.id.length > 0) accountId = body.user.id;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (signal?.aborted) throw signal.reason ?? new DOMException("Command Code login aborted", "AbortError");
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
if (!accountId && typeof parsed.userId === "string" && parsed.userId.length > 0) accountId = parsed.userId;
|
|
61
|
+
return {
|
|
62
|
+
access: parsed.apiKey,
|
|
63
|
+
refresh: parsed.apiKey,
|
|
64
|
+
expires: Number.MAX_SAFE_INTEGER,
|
|
65
|
+
...(accountId ? { accountId } : {}),
|
|
66
|
+
source: "local-cli",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function randomState(): string {
|
|
71
|
+
const bytes = new Uint8Array(32);
|
|
72
|
+
crypto.getRandomValues(bytes);
|
|
73
|
+
return Buffer.from(bytes).toString("base64url");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function parseCommandCodeCallback(value: unknown, expectedState: string): CommandCodeCallback {
|
|
77
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
78
|
+
throw new Error("Command Code callback must be an object");
|
|
79
|
+
}
|
|
80
|
+
const body = value as Record<string, unknown>;
|
|
81
|
+
if (body.state !== expectedState) throw new Error("Command Code OAuth state mismatch");
|
|
82
|
+
for (const field of ["apiKey", "userId", "userName", "keyName"] as const) {
|
|
83
|
+
if (typeof body[field] !== "string" || body[field].length === 0) {
|
|
84
|
+
throw new Error(`Command Code callback missing ${field}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return body as unknown as CommandCodeCallback;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function createCallbackServer(state: string): {
|
|
91
|
+
servers: Array<ReturnType<typeof Bun.serve>>;
|
|
92
|
+
callback: Promise<CommandCodeCallback>;
|
|
93
|
+
} {
|
|
94
|
+
let resolve!: (value: CommandCodeCallback) => void;
|
|
95
|
+
const callback = new Promise<CommandCodeCallback>((res) => { resolve = res; });
|
|
96
|
+
const fetch = async (request: Request): Promise<Response> => {
|
|
97
|
+
const url = new URL(request.url);
|
|
98
|
+
const origin = request.headers.get("origin");
|
|
99
|
+
const headers = new Headers({
|
|
100
|
+
"Content-Type": "application/json",
|
|
101
|
+
"Access-Control-Allow-Origin": origin === COMMAND_CODE_STUDIO_URL ? origin : COMMAND_CODE_STUDIO_URL,
|
|
102
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
103
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
104
|
+
});
|
|
105
|
+
if (request.method === "OPTIONS") return new Response(null, { status: 204, headers });
|
|
106
|
+
if (url.pathname !== CALLBACK_PATH) return Response.json({ success: false, error: "Not found" }, { status: 404, headers });
|
|
107
|
+
if (request.method !== "POST") return Response.json({ success: false, error: "Method not allowed" }, { status: 405, headers });
|
|
108
|
+
try {
|
|
109
|
+
const body = await request.json();
|
|
110
|
+
const parsed = parseCommandCodeCallback(body, state);
|
|
111
|
+
queueMicrotask(() => resolve(parsed));
|
|
112
|
+
return Response.json({ success: true }, { headers });
|
|
113
|
+
} catch (error) {
|
|
114
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
115
|
+
return Response.json({ success: false, error: message }, { status: 400, headers });
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const create = (port: number): Array<ReturnType<typeof Bun.serve>> => {
|
|
119
|
+
// The advertised callback host is `127.0.0.1`; on Windows `localhost` commonly resolves to `::1`
|
|
120
|
+
// first, so also bind the IPv6 loopback best-effort (mirrors the shared OAuthCallbackFlow).
|
|
121
|
+
const servers = [Bun.serve({ hostname: "127.0.0.1", port, fetch })];
|
|
122
|
+
try {
|
|
123
|
+
servers.push(Bun.serve({ hostname: "::1", port: servers[0].port, fetch }));
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (isAddrInUse(error)) {
|
|
126
|
+
for (const server of servers) server.stop(true);
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
// IPv6 unsupported (EAFNOSUPPORT etc.) degrades to the IPv4-only listener.
|
|
130
|
+
}
|
|
131
|
+
return servers;
|
|
132
|
+
};
|
|
133
|
+
try {
|
|
134
|
+
return { servers: create(COMMAND_CODE_CALLBACK_PORT), callback };
|
|
135
|
+
} catch {
|
|
136
|
+
return { servers: create(0), callback };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Validate a raw pasted Command Code API key and return the validated identity. */
|
|
141
|
+
async function validatePastedApiKey(apiKey: string): Promise<{ userId: string; userName: string } | undefined> {
|
|
142
|
+
try {
|
|
143
|
+
const response = await fetch("https://api.commandcode.ai/alpha/whoami", {
|
|
144
|
+
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
|
|
145
|
+
signal: AbortSignal.timeout(10_000),
|
|
146
|
+
});
|
|
147
|
+
if (!response.ok) return undefined;
|
|
148
|
+
const body = (await response.json()) as { user?: { id?: unknown; userName?: unknown } };
|
|
149
|
+
const userId = body.user?.id;
|
|
150
|
+
const userName = body.user?.userName;
|
|
151
|
+
if (typeof userId !== "string" || typeof userName !== "string") return undefined;
|
|
152
|
+
if (!userId.trim() || !userName.trim()) return undefined;
|
|
153
|
+
return { userId, userName };
|
|
154
|
+
} catch {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Manual-paste fallback: a raw API key, or a pasted callback JSON/URL that carries `apiKey`. */
|
|
160
|
+
function parsePastedCommandCodeInput(input: string, expectedState: string): CommandCodeCallback | undefined {
|
|
161
|
+
const trimmed = input.trim();
|
|
162
|
+
if (!trimmed) return undefined;
|
|
163
|
+
if (trimmed.startsWith("{")) {
|
|
164
|
+
try {
|
|
165
|
+
return parseCommandCodeCallback(JSON.parse(trimmed) as unknown, expectedState);
|
|
166
|
+
} catch {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const parsed = parseCallbackInput(trimmed);
|
|
171
|
+
const apiKey = parsed.code?.trim();
|
|
172
|
+
if (!apiKey) return undefined;
|
|
173
|
+
// A URL/query-shaped paste is an authorization response and must carry a matching state,
|
|
174
|
+
// mirroring the shared OAuth callback flow; a stale or attacker-supplied URL from another
|
|
175
|
+
// session must not be accepted. Raw in-session keys are exempt (no state to compare).
|
|
176
|
+
if (parsed.kind !== "raw" && parsed.state !== expectedState) return undefined;
|
|
177
|
+
return { apiKey, state: expectedState, userId: "", userName: "", keyName: "manual" };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function loginCommandCode(ctrl: OAuthController, options: CommandCodeLoginOptions = {}): Promise<OAuthCredentials> {
|
|
181
|
+
if (ctrl.signal?.aborted) {
|
|
182
|
+
throw ctrl.signal.reason ?? new DOMException("Command Code login aborted", "AbortError");
|
|
183
|
+
}
|
|
184
|
+
if (shouldImportLocalCommandCodeAuth(options)) {
|
|
185
|
+
const local = await importLocalCommandCodeAuth(ctrl.signal);
|
|
186
|
+
if (local) {
|
|
187
|
+
ctrl.onProgress?.("Imported existing Command Code CLI authentication.");
|
|
188
|
+
return local;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const state = randomState();
|
|
192
|
+
let servers: Array<ReturnType<typeof Bun.serve>> = [];
|
|
193
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
194
|
+
try {
|
|
195
|
+
const created = createCallbackServer(state);
|
|
196
|
+
servers = created.servers;
|
|
197
|
+
const callbackUrl = `http://127.0.0.1:${servers[0].port}${CALLBACK_PATH}`;
|
|
198
|
+
const authUrl = `${COMMAND_CODE_STUDIO_URL}/studio/auth/cli?callback=${encodeURIComponent(callbackUrl)}&state=${encodeURIComponent(state)}`;
|
|
199
|
+
ctrl.onAuth?.({ url: authUrl, instructions: "Sign in with Command Code in the browser." });
|
|
200
|
+
ctrl.onProgress?.("Waiting for Command Code authentication...");
|
|
201
|
+
const timeout = new Promise<never>((_, reject) => {
|
|
202
|
+
timeoutId = setTimeout(() => reject(new Error("Command Code OAuth callback timed out")), LOGIN_TIMEOUT_MS);
|
|
203
|
+
ctrl.signal?.addEventListener("abort", () => { if (timeoutId) clearTimeout(timeoutId); reject(ctrl.signal?.reason); }, { once: true });
|
|
204
|
+
});
|
|
205
|
+
const manual = ctrl.onManualCodeInput
|
|
206
|
+
? (async (): Promise<CommandCodeCallback> => {
|
|
207
|
+
while (true) {
|
|
208
|
+
// The loop keeps waiting until a valid paste arrives; invalid pastes re-prompt.
|
|
209
|
+
// Yield between iterations so an abort signal can interrupt a fast re-prompt loop.
|
|
210
|
+
if (ctrl.signal?.aborted) throw ctrl.signal.reason ?? new DOMException("Command Code login aborted", "AbortError");
|
|
211
|
+
const input = await ctrl.onManualCodeInput?.(state);
|
|
212
|
+
if (input === undefined) continue;
|
|
213
|
+
const pasted = parsePastedCommandCodeInput(input, state);
|
|
214
|
+
if (!pasted) continue;
|
|
215
|
+
const identity = await validatePastedApiKey(pasted.apiKey);
|
|
216
|
+
if (identity) return { ...pasted, ...identity };
|
|
217
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
218
|
+
}
|
|
219
|
+
})()
|
|
220
|
+
: undefined;
|
|
221
|
+
const result = await Promise.race([created.callback, timeout, ...(manual ? [manual] : [])]);
|
|
222
|
+
if (result === undefined) throw new Error("Command Code OAuth callback cancelled");
|
|
223
|
+
return {
|
|
224
|
+
access: result.apiKey,
|
|
225
|
+
refresh: result.apiKey,
|
|
226
|
+
expires: Number.MAX_SAFE_INTEGER,
|
|
227
|
+
accountId: result.userId,
|
|
228
|
+
source: "oauth",
|
|
229
|
+
};
|
|
230
|
+
} finally {
|
|
231
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
232
|
+
for (const server of servers) server.stop(true);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export async function refreshCommandCodeToken(apiKey: string): Promise<OAuthCredentials> {
|
|
237
|
+
if (!apiKey) throw new Error("Command Code API key missing; run ocx login command-code");
|
|
238
|
+
return { access: apiKey, refresh: apiKey, expires: Number.MAX_SAFE_INTEGER, source: "oauth" };
|
|
239
|
+
}
|
package/src/oauth/health.ts
CHANGED
|
@@ -4,6 +4,13 @@ import { isAccountNeedsReauth } from "../codex/account-runtime-state";
|
|
|
4
4
|
import { getCodexAccountCredential, listCodexAccountIds } from "../codex/account-store";
|
|
5
5
|
import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
|
|
6
6
|
import { configuredAdminToken } from "../lib/admin-secrets";
|
|
7
|
+
import { readRuntimePort } from "../config";
|
|
8
|
+
import {
|
|
9
|
+
LOCAL_ATTESTATION_CHALLENGE_HEADER,
|
|
10
|
+
LOCAL_ATTESTATION_PROOF_HEADER,
|
|
11
|
+
createLocalAttestationChallenge,
|
|
12
|
+
verifyLocalAttestationProof,
|
|
13
|
+
} from "../lib/local-management-attestation";
|
|
7
14
|
import { maskAccountId } from "../lib/privacy";
|
|
8
15
|
import { findLiveProxy, probeHostname } from "../server/proxy-liveness";
|
|
9
16
|
import { loadAuthStore, peekAuthStore, peekOAuthRefreshIntent, readOAuthRefreshIntent } from "./store";
|
|
@@ -328,6 +335,7 @@ type LiveProxyCodexHealthResult = {
|
|
|
328
335
|
async function fetchCodexHealthFromLiveProxy(
|
|
329
336
|
fetchImpl: typeof fetch = fetch,
|
|
330
337
|
findLiveProxyImpl: typeof findLiveProxy = findLiveProxy,
|
|
338
|
+
readRuntimePortImpl: typeof readRuntimePort = readRuntimePort,
|
|
331
339
|
): Promise<LiveProxyCodexHealthResult> {
|
|
332
340
|
const live = await findLiveProxyImpl();
|
|
333
341
|
if (!live) return { source: "unavailable", entries: null };
|
|
@@ -335,8 +343,39 @@ async function fetchCodexHealthFromLiveProxy(
|
|
|
335
343
|
// interchangeable with the admin credential even on loopback.
|
|
336
344
|
const token = configuredAdminToken();
|
|
337
345
|
const headers: Record<string, string> = {};
|
|
338
|
-
if (token) headers.Authorization = `Bearer ${token}`;
|
|
339
346
|
try {
|
|
347
|
+
if (token) {
|
|
348
|
+
// Public /healthz identity is intentionally forgeable enough for liveness, not
|
|
349
|
+
// strong enough to receive a bearer. Prove the listener knows the per-process
|
|
350
|
+
// secret stored in the protected runtime record before attaching the admin token.
|
|
351
|
+
if (live.source !== "runtime" || live.pid === null) {
|
|
352
|
+
return { source: "management-api-unavailable", entries: null };
|
|
353
|
+
}
|
|
354
|
+
const attestedPid = live.pid;
|
|
355
|
+
const runtime = readRuntimePortImpl(attestedPid);
|
|
356
|
+
if (!runtime?.attestationSecret || runtime.port !== live.port) {
|
|
357
|
+
return { source: "management-api-unavailable", entries: null };
|
|
358
|
+
}
|
|
359
|
+
const challenge = createLocalAttestationChallenge();
|
|
360
|
+
const proofResponse = await fetchImpl(
|
|
361
|
+
`http://${probeHostname(live.hostname)}:${live.port}/healthz`,
|
|
362
|
+
{
|
|
363
|
+
headers: { [LOCAL_ATTESTATION_CHALLENGE_HEADER]: challenge },
|
|
364
|
+
signal: AbortSignal.timeout(4000),
|
|
365
|
+
},
|
|
366
|
+
);
|
|
367
|
+
const proof = proofResponse.headers.get(LOCAL_ATTESTATION_PROOF_HEADER);
|
|
368
|
+
if (!proofResponse.ok || !verifyLocalAttestationProof(
|
|
369
|
+
runtime.attestationSecret,
|
|
370
|
+
challenge,
|
|
371
|
+
attestedPid,
|
|
372
|
+
live.port,
|
|
373
|
+
proof,
|
|
374
|
+
)) {
|
|
375
|
+
return { source: "management-api-unavailable", entries: null };
|
|
376
|
+
}
|
|
377
|
+
headers.Authorization = `Bearer ${token}`;
|
|
378
|
+
}
|
|
340
379
|
const res = await fetchImpl(
|
|
341
380
|
`http://${probeHostname(live.hostname)}:${live.port}/api/codex-auth/accounts`,
|
|
342
381
|
{ headers, signal: AbortSignal.timeout(4000) },
|
|
@@ -387,10 +426,15 @@ export async function collectOAuthHealthEntriesForCli(
|
|
|
387
426
|
deps: {
|
|
388
427
|
fetchImpl?: typeof fetch;
|
|
389
428
|
findLiveProxyImpl?: typeof findLiveProxy;
|
|
429
|
+
readRuntimePortImpl?: typeof readRuntimePort;
|
|
390
430
|
} = {},
|
|
391
431
|
): Promise<OAuthCliHealthReport> {
|
|
392
432
|
const entries = collectOAuthHealthEntries(now, { observeOnly: true, includeLocalCodex: false });
|
|
393
|
-
const remote = await fetchCodexHealthFromLiveProxy(
|
|
433
|
+
const remote = await fetchCodexHealthFromLiveProxy(
|
|
434
|
+
deps.fetchImpl,
|
|
435
|
+
deps.findLiveProxyImpl,
|
|
436
|
+
deps.readRuntimePortImpl,
|
|
437
|
+
);
|
|
394
438
|
if (remote.entries) {
|
|
395
439
|
for (const entry of remote.entries) entries.push(entry);
|
|
396
440
|
return { entries, codexHealthSource: "management-api" };
|
package/src/oauth/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { loginChatGPT, refreshChatGPTToken } from "./chatgpt";
|
|
|
12
12
|
import { loginAntigravity, refreshAntigravityToken } from "./google-antigravity";
|
|
13
13
|
import { loginCursor, refreshCursorToken } from "./cursor";
|
|
14
14
|
import { loginGithubCopilot, refreshGithubCopilotToken, validateCopilotApiBaseUrl } from "./github-copilot";
|
|
15
|
+
import { loginCommandCode, refreshCommandCodeToken } from "./command-code";
|
|
15
16
|
import { deriveOAuthDefaultModel, deriveOAuthProviderConfig } from "../providers/derive";
|
|
16
17
|
import { apiKeyPoolEntryId, sanitizeApiKeyValue } from "../providers/api-keys";
|
|
17
18
|
import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryTransport } from "../providers/registry";
|
|
@@ -160,6 +161,14 @@ function oauthDefaultModel(id: string): string {
|
|
|
160
161
|
}
|
|
161
162
|
|
|
162
163
|
export const OAUTH_PROVIDERS: Record<string, OAuthProviderDef> = {
|
|
164
|
+
"command-code": {
|
|
165
|
+
// Add-account/reauth must not reimport the current local CLI credential.
|
|
166
|
+
login: (ctrl, opts) => loginCommandCode(ctrl, { importLocal: opts?.forceLogin ? "off" : "fallback" }),
|
|
167
|
+
refresh: refreshCommandCodeToken,
|
|
168
|
+
providerConfig: oauthConfig("command-code"),
|
|
169
|
+
defaultModel: oauthDefaultModel("command-code"),
|
|
170
|
+
defaultRefreshPolicy: "disabled",
|
|
171
|
+
},
|
|
163
172
|
xai: {
|
|
164
173
|
// forceLogin skips the local grok-cli import so a SECOND account can be chosen in the browser.
|
|
165
174
|
login: (ctrl, opts) => loginXai(ctrl, { importLocal: opts?.forceLogin ? "off" : "fallback" }),
|
|
@@ -722,12 +731,25 @@ const OAUTH_RECONCILE_FIELDS: (keyof OcxProviderConfig)[] = [
|
|
|
722
731
|
const GOOGLE_ANTIGRAVITY_PROVIDER = "google-antigravity";
|
|
723
732
|
const GOOGLE_ANTIGRAVITY_STATIC_CATALOG_VERSION = 1 as const;
|
|
724
733
|
|
|
734
|
+
/** Only migrate the three-model experimental seed; an operator's later `liveModels: false` wins. */
|
|
735
|
+
function isLegacyCommandCodeStaticCatalog(provider: OcxProviderConfig): boolean {
|
|
736
|
+
return provider.liveModels === false
|
|
737
|
+
&& provider.defaultModel === "deepseek-v4-flash"
|
|
738
|
+
&& JSON.stringify(provider.models) === JSON.stringify(["deepseek-v4-flash", "kimi-k3", "glm-5.2"]);
|
|
739
|
+
}
|
|
740
|
+
|
|
725
741
|
export function reconcileOAuthProviders(config: OcxConfig): boolean {
|
|
726
742
|
let changed = false;
|
|
727
743
|
const migrateAntigravityStaticCatalog =
|
|
728
744
|
config.googleAntigravityStaticCatalogVersion !== GOOGLE_ANTIGRAVITY_STATIC_CATALOG_VERSION;
|
|
729
745
|
for (const [name, prov] of Object.entries(config.providers)) {
|
|
730
746
|
const def = OAUTH_PROVIDERS[name];
|
|
747
|
+
if (name === "command-code" && isLegacyCommandCodeStaticCatalog(prov)) {
|
|
748
|
+
// The former experimental preset was the exact three-model seed above. It was not a user
|
|
749
|
+
// choice to disable discovery, so promote only that shape to the account live catalog.
|
|
750
|
+
prov.liveModels = true;
|
|
751
|
+
changed = true;
|
|
752
|
+
}
|
|
731
753
|
// Normalize the canonical row before the OAuth-only reconciliation guard. The old GUI and a
|
|
732
754
|
// manual edit both persist the same bare `true`, with no source metadata, so every ambiguous
|
|
733
755
|
// pre-marker value is reset once. A deliberate live-discovery choice can be re-enabled after
|
|
@@ -761,7 +783,10 @@ export function reconcileOAuthProviders(config: OcxConfig): boolean {
|
|
|
761
783
|
changed = true;
|
|
762
784
|
}
|
|
763
785
|
// Heal a defaultModel that no longer exists in the refreshed list (e.g. a deprecated snapshot).
|
|
764
|
-
|
|
786
|
+
// Skip providers without a static preset `models` list: for live-discovery providers
|
|
787
|
+
// (e.g. command-code OAuth) the account-scoped catalog is not enumerable here, so any
|
|
788
|
+
// persisted defaultModel is a user selection and must not be overwritten by the seed.
|
|
789
|
+
if (prov.defaultModel && preset.defaultModel && preset.models && preset.models.length > 0 && !(prov.models ?? []).includes(prov.defaultModel)) {
|
|
765
790
|
prov.defaultModel = preset.defaultModel;
|
|
766
791
|
changed = true;
|
|
767
792
|
}
|
|
@@ -836,9 +861,15 @@ export function upsertOAuthProvider(config: OcxConfig, provider: string): void {
|
|
|
836
861
|
// reset once; users who deliberately forced discovery can re-enable it after migration.
|
|
837
862
|
const preserveExistingLiveModels = provider !== GOOGLE_ANTIGRAVITY_PROVIDER
|
|
838
863
|
|| config.googleAntigravityStaticCatalogVersion === GOOGLE_ANTIGRAVITY_STATIC_CATALOG_VERSION;
|
|
839
|
-
if (preserveExistingLiveModels && typeof existing?.liveModels === "boolean") {
|
|
864
|
+
if (preserveExistingLiveModels && typeof existing?.liveModels === "boolean" && !isLegacyCommandCodeStaticCatalog(existing)) {
|
|
840
865
|
next.liveModels = existing.liveModels;
|
|
841
866
|
}
|
|
867
|
+
// The Command Code protocol-version pin is an operator compatibility control. A re-login,
|
|
868
|
+
// add-account, or reauth rebuilds the row from the preset, which has no version; carry the
|
|
869
|
+
// existing pin so authentication changes do not silently revert the documented control.
|
|
870
|
+
if (existing?.commandCodeVersion !== undefined) {
|
|
871
|
+
next.commandCodeVersion = existing.commandCodeVersion;
|
|
872
|
+
}
|
|
842
873
|
if (existing && getProviderRegistryEntry(provider)?.allowKeyAuthOverride === true) {
|
|
843
874
|
// Shared sanitizeApiKeyValue trim / no-CRLF checks from api-key pool writes.
|
|
844
875
|
let storedApiKey = sanitizeApiKeyValue(existing.apiKey);
|
|
@@ -1095,7 +1126,11 @@ export function submitManualLoginCode(provider: string, input: string): { ok: tr
|
|
|
1095
1126
|
// protected. Early posts (flow not yet waiting, no expectedState) are stashed and
|
|
1096
1127
|
// re-validated by the callback loop.
|
|
1097
1128
|
const parsed = parseCallbackInput(trimmed);
|
|
1098
|
-
|
|
1129
|
+
// Command Code's manual fallback accepts a pasted JSON callback payload
|
|
1130
|
+
// (`{ apiKey, state, ... }`) which has no `code` param. Let it through the
|
|
1131
|
+
// shared gate so the provider-specific parser can validate it.
|
|
1132
|
+
const isCommandCodeJson = provider === "command-code" && trimmed.startsWith("{") && !parsed.code;
|
|
1133
|
+
if (!parsed.code && !isCommandCodeJson) return { ok: false, error: "no authorization code found in input" };
|
|
1099
1134
|
if (parsed.kind !== "raw" && slot.expectedState !== undefined) {
|
|
1100
1135
|
if (parsed.state === undefined) return { ok: false, error: "redirect URL is missing the state parameter" };
|
|
1101
1136
|
if (parsed.state !== slot.expectedState) return { ok: false, error: "state mismatch — paste the redirect URL from THIS login attempt" };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
2
|
+
|
|
3
|
+
const COMMAND_CODE_MODEL_EFFORTS = {
|
|
4
|
+
"deepseek/deepseek-v4-pro": {
|
|
5
|
+
efforts: ["high", "max"],
|
|
6
|
+
profileUrl: "https://commandcode.ai/models/deepseek-v4-pro",
|
|
7
|
+
},
|
|
8
|
+
"deepseek/deepseek-v4-flash": {
|
|
9
|
+
efforts: ["high", "max"],
|
|
10
|
+
profileUrl: "https://commandcode.ai/models/deepseek-v4-flash",
|
|
11
|
+
},
|
|
12
|
+
"zai-org/glm-5.2": {
|
|
13
|
+
efforts: ["high", "max"],
|
|
14
|
+
profileUrl: "https://commandcode.ai/models/glm-5-2",
|
|
15
|
+
},
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Official Command Code model-profile facts, not a model catalog. Models remain
|
|
20
|
+
* account-scoped and come exclusively from the authenticated /provider/v1/models endpoint.
|
|
21
|
+
*/
|
|
22
|
+
export const COMMAND_CODE_MODEL_REASONING_EFFORTS: Record<string, string[]> = Object.fromEntries(
|
|
23
|
+
Object.entries(COMMAND_CODE_MODEL_EFFORTS).map(([id, row]) => [id, [...row.efforts]]),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const refreshedEfforts = new Map<string, string[]>();
|
|
27
|
+
|
|
28
|
+
function keyFor(modelId: string): string {
|
|
29
|
+
return modelId.trim().toLowerCase();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function commandCodeReasoningEfforts(modelId: string): readonly string[] | undefined {
|
|
33
|
+
const key = keyFor(modelId);
|
|
34
|
+
return refreshedEfforts.get(key) ?? COMMAND_CODE_MODEL_REASONING_EFFORTS[key];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parsedProfileEfforts(page: string): string[] | undefined {
|
|
38
|
+
const match = page.match(/Reasoning efforts\s+([^.;]+?)\s+are supported;\s*([^.]*)/i);
|
|
39
|
+
if (!match) return undefined;
|
|
40
|
+
const listed = match[1]!.toLowerCase().match(/\b(?:low|medium|high|xhigh|max)\b/g) ?? [];
|
|
41
|
+
const mapped = match[2]!.toLowerCase().match(/\b(?:low|medium|high|xhigh|max)\s+maps to\s+(?:low|medium|high|xhigh|max)\b/g) ?? [];
|
|
42
|
+
const normalized = new Set(listed);
|
|
43
|
+
for (const mapping of mapped) {
|
|
44
|
+
const [, source, target] = mapping.match(/(low|medium|high|xhigh|max)\s+maps to\s+(low|medium|high|xhigh|max)/) ?? [];
|
|
45
|
+
if (source && target) {
|
|
46
|
+
normalized.delete(source);
|
|
47
|
+
normalized.add(target);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return normalized.size > 0 ? [...normalized] : [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Refresh one stale effort record only after the upstream rejects an effort request.
|
|
55
|
+
* A failed or unparseable public profile deliberately leaves the known table unchanged.
|
|
56
|
+
*/
|
|
57
|
+
export async function refreshCommandCodeReasoningEfforts(
|
|
58
|
+
modelId: string,
|
|
59
|
+
fetchFn: typeof globalThis.fetch = globalThis.fetch,
|
|
60
|
+
): Promise<readonly string[] | undefined> {
|
|
61
|
+
const key = keyFor(modelId);
|
|
62
|
+
const profile = COMMAND_CODE_MODEL_EFFORTS[key as keyof typeof COMMAND_CODE_MODEL_EFFORTS];
|
|
63
|
+
if (!profile) return undefined;
|
|
64
|
+
try {
|
|
65
|
+
const response = await fetchFn(profile.profileUrl, {
|
|
66
|
+
headers: { Accept: "text/html" },
|
|
67
|
+
signal: AbortSignal.timeout(10_000),
|
|
68
|
+
});
|
|
69
|
+
if (!response.ok) return undefined;
|
|
70
|
+
// Bound the profile page before parsing: a large or malformed page must not
|
|
71
|
+
// allocate unbounded memory on the request path.
|
|
72
|
+
const observed = await readBoundedResponseBody(response, { maxBytes: 256 * 1024 });
|
|
73
|
+
if (!observed.displaySafe) return undefined;
|
|
74
|
+
const efforts = parsedProfileEfforts(observed.text);
|
|
75
|
+
if (efforts === undefined) return undefined;
|
|
76
|
+
refreshedEfforts.set(key, efforts);
|
|
77
|
+
return efforts;
|
|
78
|
+
} catch {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function resetCommandCodeReasoningEffortsForTest(): void {
|
|
84
|
+
refreshedEfforts.clear();
|
|
85
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vertex regional hosts are formed as `<location>-aiplatform.googleapis.com`.
|
|
3
|
+
* Restricting the location to one lowercase DNS label keeps user configuration
|
|
4
|
+
* from changing the request authority while remaining forward-compatible with
|
|
5
|
+
* new Google regions and multi-regions.
|
|
6
|
+
*/
|
|
7
|
+
const GOOGLE_VERTEX_LOCATION_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
8
|
+
|
|
9
|
+
export function googleVertexLocationConfigError(location: unknown): string | null {
|
|
10
|
+
if (typeof location !== "string" || !GOOGLE_VERTEX_LOCATION_LABEL.test(location)) {
|
|
11
|
+
return "Vertex AI location must be a single lowercase Google Cloud location label (for example, us-central1 or global)";
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
}
|