@bitkyc08/opencodex 2.24.1 → 2.25.0-preview.20260818
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/gui/dist/assets/{index-C3FiAveG.js → index-TFd4xi1L.js} +8 -8
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +42 -0
- package/src/adapters/client-fingerprint.ts +9 -5
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
- package/src/adapters/command-code.ts +17 -0
- package/src/adapters/cursor/cursor-errors.ts +49 -0
- package/src/adapters/cursor/live-models.ts +36 -2
- package/src/adapters/cursor/live-transport.ts +55 -4
- package/src/adapters/cursor/native-exec.ts +9 -0
- package/src/adapters/cursor/protobuf-request.ts +160 -9
- package/src/adapters/cursor/request-builder.ts +9 -1
- package/src/adapters/cursor/tool-definitions.ts +7 -2
- package/src/adapters/google-antigravity-wire.ts +1 -1
- package/src/adapters/google.ts +30 -12
- package/src/adapters/openai-responses-url.ts +5 -3
- package/src/adapters/registry.ts +3 -1
- package/src/adapters/tool-catalog-nudge.ts +76 -9
- package/src/bridge.ts +53 -9
- package/src/claude/context-windows.ts +2 -2
- package/src/claude/desktop-3p.ts +6 -6
- package/src/claude/model-info.ts +2 -2
- package/src/cli/claude-desktop.ts +2 -3
- package/src/codex/app-server-processes.ts +69 -35
- package/src/codex/catalog/metadata.ts +29 -10
- package/src/codex/catalog/provider-fetch.ts +21 -11
- package/src/codex/catalog.ts +1 -1
- package/src/codex/injected-marker.ts +9 -3
- package/src/codex/user-identity.ts +88 -6
- package/src/config.ts +1 -0
- package/src/generated/compatibility-version.json +61 -53
- package/src/grok/sync.ts +2 -4
- package/src/lab/projection/rebuild.ts +36 -18
- package/src/lib/windows-elevation.ts +18 -3
- package/src/lib/windows-secret-acl.ts +49 -19
- package/src/oauth/google-antigravity.ts +7 -2
- package/src/providers/antigravity-models.ts +126 -17
- package/src/providers/derive.ts +11 -1
- package/src/responses/parser.ts +4 -0
- package/src/responses/reasoning-replay-cache.ts +16 -1
- package/src/responses/thought-signature-replay.ts +17 -1
- package/src/responses/truncated-stop-reason.ts +60 -0
- package/src/router.ts +2 -10
- package/src/routing/capability.ts +5 -6
- package/src/server/index.ts +3 -4
- package/src/server/management/agent-settings-routes.ts +5 -5
- package/src/server/management/config-routes.ts +2 -2
- package/src/server/management/context.ts +2 -0
- package/src/server/management/native-integration-routes.ts +3 -3
- package/src/server/management/provider-routes.ts +22 -0
- package/src/server/management/shared.ts +4 -4
- package/src/server/management-api.ts +2 -2
- package/src/server/request-log.ts +11 -3
- package/src/server/responses/core.ts +4 -1
- package/src/server/responses/input-admission.ts +13 -10
- package/src/server/system-env.ts +3 -3
- package/src/types.ts +13 -1
|
@@ -122,6 +122,20 @@ export function rebuildLabProjection(configDir?: string): RebuildResult {
|
|
|
122
122
|
|
|
123
123
|
const db = new Database(paths.sqlitePath);
|
|
124
124
|
let transactionOpen = false;
|
|
125
|
+
// Every statement prepared below, so they can be finalized before the close.
|
|
126
|
+
//
|
|
127
|
+
// Bun keeps a prepared statement alive until it is finalized or garbage collected,
|
|
128
|
+
// and on Windows an unfinalized statement holds the database file open: `close()`
|
|
129
|
+
// leaves the handle, and `close(true)` throws "database is locked". A second
|
|
130
|
+
// rebuild then failed to unlink the previous projection with EBUSY, and the retry
|
|
131
|
+
// loop in `wipeSqlite` could only turn that into a slower failure. POSIX allows
|
|
132
|
+
// unlinking an open file, which is why this never surfaced there.
|
|
133
|
+
const prepared: Array<{ finalize(): void }> = [];
|
|
134
|
+
const prepare = (sql: string) => {
|
|
135
|
+
const statement = db.prepare(sql);
|
|
136
|
+
prepared.push(statement);
|
|
137
|
+
return statement;
|
|
138
|
+
};
|
|
125
139
|
try {
|
|
126
140
|
db.exec("PRAGMA journal_mode=DELETE;");
|
|
127
141
|
db.exec("PRAGMA foreign_keys=OFF;");
|
|
@@ -129,50 +143,45 @@ export function rebuildLabProjection(configDir?: string): RebuildResult {
|
|
|
129
143
|
transactionOpen = true;
|
|
130
144
|
resetProjectionSchema(db);
|
|
131
145
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
"INSERT OR REPLACE INTO schema_meta(key, value) VALUES (?, ?)",
|
|
137
|
-
).run("projection_spec_version", LAB_PROJECTION_SPEC_VERSION);
|
|
138
|
-
db.prepare(
|
|
139
|
-
"INSERT OR REPLACE INTO schema_meta(key, value) VALUES (?, ?)",
|
|
140
|
-
).run("built_at_ms", String(Date.now()));
|
|
146
|
+
const insertMeta = prepare("INSERT OR REPLACE INTO schema_meta(key, value) VALUES (?, ?)");
|
|
147
|
+
insertMeta.run("schema_version", String(LAB_SQLITE_SCHEMA_VERSION));
|
|
148
|
+
insertMeta.run("projection_spec_version", LAB_PROJECTION_SPEC_VERSION);
|
|
149
|
+
insertMeta.run("built_at_ms", String(Date.now()));
|
|
141
150
|
|
|
142
|
-
const insertCorruption =
|
|
151
|
+
const insertCorruption = prepare(
|
|
143
152
|
"INSERT INTO corruption(kind, line_number, event_id, detail) VALUES (?, ?, ?, ?)",
|
|
144
153
|
);
|
|
145
154
|
for (const c of corruptions) {
|
|
146
155
|
insertCorruption.run(c.kind, c.lineNumber ?? null, c.eventId ?? null, c.detail);
|
|
147
156
|
}
|
|
148
157
|
|
|
149
|
-
const insertEvent =
|
|
158
|
+
const insertEvent = prepare(
|
|
150
159
|
`INSERT INTO events(event_id, event_kind, recorded_at, producer, producer_version, payload_json, excluded, exclusion_reason)
|
|
151
160
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
152
161
|
);
|
|
153
|
-
const insertSubject =
|
|
162
|
+
const insertSubject = prepare(
|
|
154
163
|
`INSERT OR IGNORE INTO subjects(subject_id, subject_kind, subject_json) VALUES (?, ?, ?)`,
|
|
155
164
|
);
|
|
156
|
-
const insertObs =
|
|
165
|
+
const insertObs = prepare(
|
|
157
166
|
`INSERT INTO observations(
|
|
158
167
|
event_id, subject_id, evidence_layer, suite_id, suite_version, suite_manifest_digest,
|
|
159
168
|
scenario_id, scenario_version, scenario_manifest_digest, outcome, completed_at, execution_mode
|
|
160
169
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
161
170
|
);
|
|
162
|
-
const insertClaim =
|
|
171
|
+
const insertClaim = prepare(
|
|
163
172
|
`INSERT INTO claims(
|
|
164
173
|
event_id, subject_id, capability, polarity, source_manifest_digest,
|
|
165
174
|
effective_at, recorded_at, supersedes_json, current, usable
|
|
166
175
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
167
176
|
);
|
|
168
|
-
const insertInv =
|
|
177
|
+
const insertInv = prepare(
|
|
169
178
|
`INSERT INTO invalidations(event_id, reason, targets_json, recorded_at, applied) VALUES (?, ?, ?, ?, ?)`,
|
|
170
179
|
);
|
|
171
|
-
const insertPurge =
|
|
180
|
+
const insertPurge = prepare(
|
|
172
181
|
`INSERT INTO purges(event_id, target_event_ids_json, target_artifact_digests_json, purge_actions_json, recorded_at)
|
|
173
182
|
VALUES (?, ?, ?, ?, ?)`,
|
|
174
183
|
);
|
|
175
|
-
const insertArtifact =
|
|
184
|
+
const insertArtifact = prepare(
|
|
176
185
|
`INSERT INTO artifacts(digest, artifact_class, media_type, byte_count, status, last_error)
|
|
177
186
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
178
187
|
ON CONFLICT(digest) DO UPDATE SET
|
|
@@ -319,7 +328,7 @@ export function rebuildLabProjection(configDir?: string): RebuildResult {
|
|
|
319
328
|
}
|
|
320
329
|
}
|
|
321
330
|
|
|
322
|
-
const insertVerdict =
|
|
331
|
+
const insertVerdict = prepare(
|
|
323
332
|
`INSERT INTO verdicts(
|
|
324
333
|
projection_key, subject_id, evidence_layer, suite_id, suite_version, suite_manifest_digest,
|
|
325
334
|
projection_spec_version, verdict, as_of, scenario_manifest_digests_json, claim_source_digest,
|
|
@@ -369,6 +378,15 @@ export function rebuildLabProjection(configDir?: string): RebuildResult {
|
|
|
369
378
|
} catch {
|
|
370
379
|
// Closing the disposable DB is still safe if pragma restoration fails.
|
|
371
380
|
}
|
|
381
|
+
// Finalize before closing: an outstanding statement keeps the file open on
|
|
382
|
+
// Windows, and the next rebuild cannot unlink the projection it is replacing.
|
|
383
|
+
for (const statement of prepared) {
|
|
384
|
+
try {
|
|
385
|
+
statement.finalize();
|
|
386
|
+
} catch {
|
|
387
|
+
// A statement already finalized by an error path is not a rebuild failure.
|
|
388
|
+
}
|
|
389
|
+
}
|
|
372
390
|
db.close();
|
|
373
391
|
artifactStore.close();
|
|
374
392
|
}
|
|
@@ -175,7 +175,7 @@ export function assertTrustedSystemExecutableForTests(candidate: string, label:
|
|
|
175
175
|
return assertTrustedSystemExecutable(candidate, label);
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
type ElevationExeOverrides = { powershell?: string; schtasks?: string; taskkill?: string };
|
|
178
|
+
type ElevationExeOverrides = { powershell?: string; schtasks?: string; taskkill?: string; icacls?: string };
|
|
179
179
|
let elevationExeOverridesForTests: ElevationExeOverrides | null = null;
|
|
180
180
|
|
|
181
181
|
/**
|
|
@@ -220,6 +220,15 @@ export function resolveTrustedWindowsTaskkillExe(): string {
|
|
|
220
220
|
return assertTrustedSystemExecutable(candidate, "taskkill.exe");
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
/** Absolute path to System32\\icacls.exe from a trusted system directory. */
|
|
224
|
+
export function resolveTrustedWindowsIcaclsExe(): string {
|
|
225
|
+
if (elevationExeOverridesForTests?.icacls) {
|
|
226
|
+
return elevationExeOverridesForTests.icacls;
|
|
227
|
+
}
|
|
228
|
+
const candidate = join(resolveTrustedWindowsSystemDirectory(), "icacls.exe");
|
|
229
|
+
return assertTrustedSystemExecutable(candidate, "icacls.exe");
|
|
230
|
+
}
|
|
231
|
+
|
|
223
232
|
/** Stable machine-readable marker for a denied `schtasks /create`. Crosses the CLI→proxy boundary. */
|
|
224
233
|
export const WINDOWS_SCHTASKS_CREATE_ACCESS_DENIED_MARKER =
|
|
225
234
|
"OCX_ERROR_CODE=WINDOWS_SCHTASKS_CREATE_ACCESS_DENIED";
|
|
@@ -631,15 +640,21 @@ export function runWindowsElevatedScheduledTaskRegistration(
|
|
|
631
640
|
xml: string,
|
|
632
641
|
): Promise<number> {
|
|
633
642
|
const xmlBase64 = Buffer.from(xml, "utf16le").toString("base64");
|
|
643
|
+
const powerShellPath = windowsPowerShell();
|
|
644
|
+
const powerShellDirectory = powerShellPath.replace(/[\\/][^\\/]+$/, "");
|
|
645
|
+
const scheduledTasksModule = `${powerShellDirectory}\\Modules\\ScheduledTasks\\ScheduledTasks.psd1`;
|
|
634
646
|
const inner = [
|
|
635
647
|
`$taskName = ${psSingleQuote(taskName)}`,
|
|
636
648
|
`$xmlBase64 = ${psSingleQuote(xmlBase64)}`,
|
|
637
649
|
"$xml = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($xmlBase64))",
|
|
638
|
-
|
|
650
|
+
`$module = Microsoft.PowerShell.Core\\Import-Module -Name ${psSingleQuote(scheduledTasksModule)} -PassThru -Force -ErrorAction Stop`,
|
|
651
|
+
"$registerTask = $module.ExportedCommands['Register-ScheduledTask']",
|
|
652
|
+
"if ($null -eq $registerTask) { throw 'Trusted ScheduledTasks module does not export Register-ScheduledTask.' }",
|
|
653
|
+
"& $registerTask -TaskName $taskName -Xml $xml -Force -ErrorAction Stop | Out-Null",
|
|
639
654
|
].join("; ");
|
|
640
655
|
const encodedCommand = Buffer.from(inner, "utf16le").toString("base64");
|
|
641
656
|
const script = [
|
|
642
|
-
`$p = Start-Process -FilePath ${psSingleQuote(
|
|
657
|
+
`$p = Start-Process -FilePath ${psSingleQuote(powerShellPath)}`,
|
|
643
658
|
` -ArgumentList ${psSingleQuote(buildWindowsElevatedArgumentList([
|
|
644
659
|
"-NoProfile",
|
|
645
660
|
"-NonInteractive",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
|
|
32
32
|
import { existsSync, statSync } from "node:fs";
|
|
33
33
|
import { env, platform } from "node:process";
|
|
34
|
+
import { resolveTrustedWindowsIcaclsExe } from "./windows-elevation";
|
|
34
35
|
import {
|
|
35
36
|
resolveCurrentWindowsPrincipal,
|
|
36
37
|
resolveCurrentWindowsPrincipalAsync,
|
|
@@ -273,22 +274,55 @@ export interface IcaclsResult {
|
|
|
273
274
|
type IcaclsRunner = (args: string[], timeoutMs: number) => IcaclsResult;
|
|
274
275
|
type AsyncIcaclsRunner = (args: string[], timeoutMs: number) => Promise<IcaclsResult>;
|
|
275
276
|
|
|
277
|
+
function resolveIcaclsExecutable(): string {
|
|
278
|
+
// Same authority as schtasks/powershell: never take icacls from PATH.
|
|
279
|
+
// A bun-shim or stripped PATH makes `icacls.exe` throw ENOENT, which used to
|
|
280
|
+
// surface as "filesystem may not support per-user NTFS ACLs".
|
|
281
|
+
return resolveTrustedWindowsIcaclsExe();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function spawnFailedResult(): IcaclsResult {
|
|
285
|
+
return { success: false, exitCode: null, timedOut: false, stdout: "" };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Spawn icacls asynchronously, or return null when the executable cannot be
|
|
290
|
+
* launched. The pipe/ignore stdio literals stay inferred here so `stdout` keeps
|
|
291
|
+
* its `ReadableStream` type instead of widening to the generic default.
|
|
292
|
+
*/
|
|
293
|
+
function trySpawnIcacls(args: string[]) {
|
|
294
|
+
try {
|
|
295
|
+
return Bun.spawn([resolveIcaclsExecutable(), ...args], {
|
|
296
|
+
stdin: "ignore",
|
|
297
|
+
stdout: "pipe",
|
|
298
|
+
stderr: "ignore",
|
|
299
|
+
windowsHide: true,
|
|
300
|
+
});
|
|
301
|
+
} catch {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
276
306
|
function defaultIcaclsRunner(args: string[], timeoutMs: number): IcaclsResult {
|
|
277
307
|
// Bun.spawnSync with windowsHide: Node execFileSync has hung under the GUI/proxy even
|
|
278
308
|
// with windowsHide, and console-subsystem tools flash a visible window otherwise.
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
309
|
+
try {
|
|
310
|
+
const result = Bun.spawnSync([resolveIcaclsExecutable(), ...args], {
|
|
311
|
+
stdin: "ignore",
|
|
312
|
+
stdout: "pipe",
|
|
313
|
+
stderr: "ignore",
|
|
314
|
+
timeout: timeoutMs,
|
|
315
|
+
windowsHide: true,
|
|
316
|
+
});
|
|
317
|
+
return {
|
|
318
|
+
success: result.success,
|
|
319
|
+
exitCode: result.exitCode,
|
|
320
|
+
timedOut: result.exitedDueToTimeout ?? false,
|
|
321
|
+
stdout: result.stdout ? result.stdout.toString() : "",
|
|
322
|
+
};
|
|
323
|
+
} catch {
|
|
324
|
+
return spawnFailedResult();
|
|
325
|
+
}
|
|
292
326
|
}
|
|
293
327
|
|
|
294
328
|
/**
|
|
@@ -297,12 +331,8 @@ function defaultIcaclsRunner(args: string[], timeoutMs: number): IcaclsResult {
|
|
|
297
331
|
* we still await process exit before classifying so settlement is confirmed.
|
|
298
332
|
*/
|
|
299
333
|
async function defaultAsyncIcaclsRunner(args: string[], timeoutMs: number): Promise<IcaclsResult> {
|
|
300
|
-
const proc =
|
|
301
|
-
|
|
302
|
-
stdout: "pipe",
|
|
303
|
-
stderr: "ignore",
|
|
304
|
-
windowsHide: true,
|
|
305
|
-
});
|
|
334
|
+
const proc = trySpawnIcacls(args);
|
|
335
|
+
if (!proc) return spawnFailedResult();
|
|
306
336
|
let timedOutByUs = false;
|
|
307
337
|
const timer = setTimeout(() => {
|
|
308
338
|
timedOutByUs = true;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { OAuthCallbackFlow, type OAuthCallbackFlowOptions } from "./callback-server";
|
|
13
13
|
import { generatePKCE } from "./pkce";
|
|
14
14
|
import type { OAuthController, OAuthCredentials } from "./types";
|
|
15
|
-
import { antigravityUserAgent, ANTIGRAVITY_GOOG_API_CLIENT_UA } from "../adapters/client-fingerprint";
|
|
15
|
+
import { antigravityUserAgent, ANTIGRAVITY_GOOG_API_CLIENT_UA, ANTIGRAVITY_IDE_VERSION } from "../adapters/client-fingerprint";
|
|
16
16
|
|
|
17
17
|
const CLIENT_ID = process.env.GOOGLE_ANTIGRAVITY_CLIENT_ID
|
|
18
18
|
|| "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com";
|
|
@@ -111,7 +111,12 @@ async function onboardProject(accessToken: string, signal?: AbortSignal): Promis
|
|
|
111
111
|
const response = await fetch(`${DAILY_API}/${API_VERSION}:onboardUser`, {
|
|
112
112
|
method: "POST",
|
|
113
113
|
headers: { Authorization: `Bearer ${accessToken}`, Accept: "*/*", "Content-Type": "application/json", "User-Agent": antigravityUserAgent(), "x-goog-api-client": ANTIGRAVITY_GOOG_API_CLIENT_UA },
|
|
114
|
-
|
|
114
|
+
// `ide_version` is a version, not a User-Agent. `antigravityUserAgent()` returns the whole
|
|
115
|
+
// header — `antigravity/ide/2.5.5 (aidev_client; os_type=...; arch=...)` — so onboarding was
|
|
116
|
+
// sending a parenthesized UA string in a field the real client fills with `2.5.5`. It is a
|
|
117
|
+
// fingerprint mismatch rather than a crash, which is why nothing failed: the request still
|
|
118
|
+
// succeeds, it just does not look like Antigravity.
|
|
119
|
+
body: JSON.stringify({ tier_id: "free-tier", metadata: { ide_type: "ANTIGRAVITY", ide_name: "antigravity", ide_version: ANTIGRAVITY_IDE_VERSION } }),
|
|
115
120
|
signal: requestSignal(signal),
|
|
116
121
|
});
|
|
117
122
|
if (!response.ok) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isValidModelDiscoveryModelId, MODEL_DISCOVERY_MAX_MODELS } from "./model-discovery-limits";
|
|
2
|
+
import { isModelCacheGenerationCurrent } from "../codex/model-cache";
|
|
2
3
|
|
|
3
4
|
// Google Antigravity (Cloud Code Assist) bundled model list.
|
|
4
5
|
//
|
|
@@ -74,8 +75,12 @@ const ANTIGRAVITY_DISCOVERY_EFFORTS = ["low", "medium", "high"] as const;
|
|
|
74
75
|
|
|
75
76
|
function pickerModelIdForDiscoveredWireId(
|
|
76
77
|
wireId: string,
|
|
78
|
+
info: Record<string, unknown>,
|
|
77
79
|
available: ReadonlyMap<string, Record<string, unknown>>,
|
|
78
80
|
): string {
|
|
81
|
+
const displayModelId = antigravityDisplayModelId(info.displayName, wireId);
|
|
82
|
+
if (displayModelId) return displayModelId;
|
|
83
|
+
|
|
79
84
|
const explicitPickerId = Object.hasOwn(ANTIGRAVITY_PICKER_MODEL_BY_WIRE_ID, wireId)
|
|
80
85
|
? ANTIGRAVITY_PICKER_MODEL_BY_WIRE_ID[wireId]
|
|
81
86
|
: undefined;
|
|
@@ -240,6 +245,8 @@ export const ANTIGRAVITY_MODEL_INPUT_MODALITIES: Record<string, string[]> = {
|
|
|
240
245
|
|
|
241
246
|
export interface AntigravityAvailableModel {
|
|
242
247
|
id: string;
|
|
248
|
+
/** CCA model id used by the agent envelope when `id` comes from display metadata. */
|
|
249
|
+
wireModelId: string;
|
|
243
250
|
contextWindow?: number;
|
|
244
251
|
inputModalities?: string[];
|
|
245
252
|
}
|
|
@@ -254,6 +261,100 @@ function antigravityPositiveInteger(value: unknown): number | undefined {
|
|
|
254
261
|
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
255
262
|
}
|
|
256
263
|
|
|
264
|
+
interface DiscoveredWireModelMapping {
|
|
265
|
+
readonly models: ReadonlyMap<string, string>;
|
|
266
|
+
readonly generation?: { provider: string; cacheGeneration: string };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const discoveredWireModelsByBaseUrl = new Map<string, DiscoveredWireModelMapping>();
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Strip trailing slashes without a backtracking regex.
|
|
273
|
+
*
|
|
274
|
+
* `/\/+$/` is polynomial-ReDoS on attacker-influenceable input (CodeQL js/polynomial-redos):
|
|
275
|
+
* a long run of slashes makes the engine retry every suffix. The base URL comes from provider
|
|
276
|
+
* config, which is not hostile in the ordinary case — but "not hostile today" is a property of
|
|
277
|
+
* the caller, not of this function, and a linear scan costs nothing.
|
|
278
|
+
*/
|
|
279
|
+
function stripTrailingSlashes(value: string): string {
|
|
280
|
+
let end = value.length;
|
|
281
|
+
while (end > 0 && value.charCodeAt(end - 1) === 47) end -= 1;
|
|
282
|
+
return end === value.length ? value : value.slice(0, end);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function antigravityBaseUrlKey(baseUrl: string | undefined): string | undefined {
|
|
286
|
+
if (typeof baseUrl !== "string" || !baseUrl.trim()) return undefined;
|
|
287
|
+
const trimmed = stripTrailingSlashes(baseUrl.trim());
|
|
288
|
+
try {
|
|
289
|
+
const url = new URL(trimmed);
|
|
290
|
+
url.hash = "";
|
|
291
|
+
url.search = "";
|
|
292
|
+
return stripTrailingSlashes(url.toString()).toLowerCase();
|
|
293
|
+
} catch {
|
|
294
|
+
return trimmed.toLowerCase();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Remember the wire ids returned by one live CCA discovery for request routing. */
|
|
299
|
+
export function registerAntigravityDiscoveredWireModels(
|
|
300
|
+
baseUrl: string | undefined,
|
|
301
|
+
models: readonly AntigravityAvailableModel[],
|
|
302
|
+
generation?: { provider: string; cacheGeneration: string },
|
|
303
|
+
): void {
|
|
304
|
+
const key = antigravityBaseUrlKey(baseUrl);
|
|
305
|
+
if (!key) return;
|
|
306
|
+
const wireModels = new Map<string, string>();
|
|
307
|
+
for (const model of models) wireModels.set(model.id, model.wireModelId);
|
|
308
|
+
discoveredWireModelsByBaseUrl.set(key, {
|
|
309
|
+
models: wireModels,
|
|
310
|
+
...(generation ? { generation } : {}),
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function discoveredAntigravityWireModelId(
|
|
315
|
+
modelId: string,
|
|
316
|
+
baseUrl: string | undefined,
|
|
317
|
+
): string | undefined {
|
|
318
|
+
const key = antigravityBaseUrlKey(baseUrl);
|
|
319
|
+
if (!key) return undefined;
|
|
320
|
+
const mapping = discoveredWireModelsByBaseUrl.get(key);
|
|
321
|
+
if (!mapping) return undefined;
|
|
322
|
+
if (mapping.generation
|
|
323
|
+
&& !isModelCacheGenerationCurrent(mapping.generation.provider, mapping.generation.cacheGeneration)) {
|
|
324
|
+
discoveredWireModelsByBaseUrl.delete(key);
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
return mapping.models.get(modelId);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Convert the CCA display label used by `agy` into its public model selector.
|
|
332
|
+
*
|
|
333
|
+
* The wire id is authoritative for requests, while the label is authoritative for the
|
|
334
|
+
* user-facing selector when Google has renamed or re-tiered a model. Keep both instead
|
|
335
|
+
* of maintaining a provider-specific list of known model names.
|
|
336
|
+
*/
|
|
337
|
+
function antigravityDisplayModelId(displayName: unknown, wireId: string): string | undefined {
|
|
338
|
+
if (typeof displayName !== "string") return undefined;
|
|
339
|
+
const label = displayName.trim();
|
|
340
|
+
if (!label || label.length > 512) return undefined;
|
|
341
|
+
const slug = (replaceDots: boolean): string => label
|
|
342
|
+
.normalize("NFKC")
|
|
343
|
+
.toLowerCase()
|
|
344
|
+
.replace(replaceDots ? /\./g : /\s+/g, replaceDots ? "-" : " ")
|
|
345
|
+
.replace(/[^a-z0-9.-]+/g, "-")
|
|
346
|
+
.replace(/-+/g, "-")
|
|
347
|
+
.replace(/^-|-$/g, "");
|
|
348
|
+
const preserved = slug(false);
|
|
349
|
+
const compact = slug(true);
|
|
350
|
+
if (!isValidModelDiscoveryModelId(preserved) && !isValidModelDiscoveryModelId(compact)) return undefined;
|
|
351
|
+
if (preserved === wireId || compact === wireId
|
|
352
|
+
|| preserved === `${wireId}-thinking` || compact === `${wireId}-thinking`) {
|
|
353
|
+
return wireId;
|
|
354
|
+
}
|
|
355
|
+
return isValidModelDiscoveryModelId(preserved) ? preserved : compact;
|
|
356
|
+
}
|
|
357
|
+
|
|
257
358
|
/**
|
|
258
359
|
* Extract the CCA models that are valid for agent requests. The endpoint also returns tab,
|
|
259
360
|
* command, commit-message, transcription, and standalone image-generation models; those are not
|
|
@@ -288,13 +389,6 @@ export function parseAntigravityAvailableModels(
|
|
|
288
389
|
}
|
|
289
390
|
}
|
|
290
391
|
}
|
|
291
|
-
// This model is exposed by Antigravity's agent chat surface even though it is grouped under
|
|
292
|
-
// image generation in the discovery response.
|
|
293
|
-
if (Array.isArray(body.imageGenerationModelIds)
|
|
294
|
-
&& body.imageGenerationModelIds.includes("gemini-3.1-flash-image")) {
|
|
295
|
-
if (ids.length >= limit) return null;
|
|
296
|
-
ids.push("gemini-3.1-flash-image");
|
|
297
|
-
}
|
|
298
392
|
// Newer CCA responses identify tiered Flash models through this index instead of
|
|
299
393
|
// adding their synthetic wire ids to agentModelSorts.
|
|
300
394
|
const tieredModelIds = antigravityRecord(body.tieredModelIds);
|
|
@@ -305,6 +399,12 @@ export function parseAntigravityAvailableModels(
|
|
|
305
399
|
|| !Object.hasOwn(models, id)
|
|
306
400
|
|| !antigravityRecord(models[id])
|
|
307
401
|
|| ids.length >= limit) return null;
|
|
402
|
+
const baseId = id.endsWith("-tiered") ? id.slice(0, -"-tiered".length) : id;
|
|
403
|
+
if (ids.some(agentId =>
|
|
404
|
+
agentId === id
|
|
405
|
+
|| agentId === baseId
|
|
406
|
+
|| ANTIGRAVITY_DISCOVERY_EFFORTS.some(effort => agentId === `${baseId}-${effort}`)
|
|
407
|
+
)) continue;
|
|
308
408
|
ids.push(id);
|
|
309
409
|
}
|
|
310
410
|
}
|
|
@@ -313,23 +413,18 @@ export function parseAntigravityAvailableModels(
|
|
|
313
413
|
for (const wireId of ids) {
|
|
314
414
|
const info = antigravityRecord(models[wireId]);
|
|
315
415
|
if (!info || available.has(wireId)) continue;
|
|
316
|
-
// Legacy compatibility aliases are deliberately routed to newer wire ids for saved
|
|
317
|
-
// selections. They are not safe as independently discovered picker rows.
|
|
318
|
-
const alias = Object.hasOwn(ANTIGRAVITY_MODEL_ALIASES, wireId)
|
|
319
|
-
? ANTIGRAVITY_MODEL_ALIASES[wireId]
|
|
320
|
-
: undefined;
|
|
321
|
-
if (alias && alias !== wireId) continue;
|
|
322
416
|
available.set(wireId, info);
|
|
323
417
|
}
|
|
324
418
|
|
|
325
419
|
const out: AntigravityAvailableModel[] = [];
|
|
326
420
|
const seen = new Set<string>();
|
|
327
421
|
for (const [wireId, info] of available) {
|
|
328
|
-
const id = pickerModelIdForDiscoveredWireId(wireId, available);
|
|
422
|
+
const id = pickerModelIdForDiscoveredWireId(wireId, info, available);
|
|
329
423
|
if (seen.has(id)) continue;
|
|
330
424
|
seen.add(id);
|
|
331
425
|
out.push({
|
|
332
426
|
id,
|
|
427
|
+
wireModelId: wireId,
|
|
333
428
|
...(antigravityPositiveInteger(info.maxTokens) ? { contextWindow: antigravityPositiveInteger(info.maxTokens) } : {}),
|
|
334
429
|
// Tri-state, deliberately not a ternary: `true` asserts image support,
|
|
335
430
|
// `false` asserts against it, and ABSENT is unknown. Collapsing absent into
|
|
@@ -347,7 +442,9 @@ export function parseAntigravityAvailableModels(
|
|
|
347
442
|
return out;
|
|
348
443
|
}
|
|
349
444
|
|
|
350
|
-
export function resolveAntigravityWireModelId(modelId: string): string {
|
|
445
|
+
export function resolveAntigravityWireModelId(modelId: string, baseUrl?: string): string {
|
|
446
|
+
const discovered = discoveredAntigravityWireModelId(modelId, baseUrl);
|
|
447
|
+
if (discovered) return discovered;
|
|
351
448
|
return Object.hasOwn(ANTIGRAVITY_MODEL_ALIASES, modelId)
|
|
352
449
|
? ANTIGRAVITY_MODEL_ALIASES[modelId]
|
|
353
450
|
: modelId;
|
|
@@ -380,7 +477,19 @@ export function retiredAntigravityFlashTier(modelId: string): string | undefined
|
|
|
380
477
|
export function resolveAntigravityEffortWireModel(
|
|
381
478
|
modelId: string,
|
|
382
479
|
effort?: string,
|
|
480
|
+
baseUrl?: string,
|
|
383
481
|
): { wireModelId: string; thinkingLevel?: string } {
|
|
482
|
+
const discoveredWireModelId = discoveredAntigravityWireModelId(modelId, baseUrl);
|
|
483
|
+
if (discoveredWireModelId && (discoveredWireModelId !== modelId || isAntigravitySuffixModelId(modelId))) {
|
|
484
|
+
const defaultLevel = ANTIGRAVITY_THINKING_LEVEL_MODELS[modelId];
|
|
485
|
+
return {
|
|
486
|
+
wireModelId: discoveredWireModelId,
|
|
487
|
+
...(defaultLevel
|
|
488
|
+
? { thinkingLevel: effort ? resolveAntigravityThinkingLevel(effort) ?? defaultLevel : defaultLevel }
|
|
489
|
+
: {}),
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
384
493
|
// Rule 0: retired Flash id — Google has taken the wire id offline, so route to the
|
|
385
494
|
// current generation and carry the tier the retired id encoded. This runs BEFORE the
|
|
386
495
|
// suffix check because those ids are aliases, and rule 1 would drop the tier.
|
|
@@ -394,7 +503,7 @@ export function resolveAntigravityEffortWireModel(
|
|
|
394
503
|
|
|
395
504
|
// Rule 1: suffix/compat alias — suffix IS the effort.
|
|
396
505
|
if (isAntigravitySuffixModelId(modelId)) {
|
|
397
|
-
return { wireModelId: resolveAntigravityWireModelId(modelId) };
|
|
506
|
+
return { wireModelId: resolveAntigravityWireModelId(modelId, baseUrl) };
|
|
398
507
|
}
|
|
399
508
|
|
|
400
509
|
// Rule 1b: single-wire-id Gemini model whose tiers ride on thinkingLevel. Without
|
|
@@ -424,7 +533,7 @@ export function resolveAntigravityEffortWireModel(
|
|
|
424
533
|
}
|
|
425
534
|
|
|
426
535
|
// Rule 5: everything else.
|
|
427
|
-
return { wireModelId: resolveAntigravityWireModelId(modelId) };
|
|
536
|
+
return { wireModelId: resolveAntigravityWireModelId(modelId, baseUrl) };
|
|
428
537
|
}
|
|
429
538
|
|
|
430
539
|
|
package/src/providers/derive.ts
CHANGED
|
@@ -393,6 +393,14 @@ function enrichReasoningSummariesByDestination(prov: OcxProviderConfig): void {
|
|
|
393
393
|
applyReasoningSummaryDefaults(prov, destination?.modelSupportsReasoningSummaries);
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
+
/** Repair the exact low-only ClinePass ladder generated by older key-login presets. */
|
|
397
|
+
export function hasLegacyClinePassReasoningEfforts(name: string, prov: OcxProviderConfig): boolean {
|
|
398
|
+
return name === "cline-pass"
|
|
399
|
+
&& prov.reasoningWireFormat === "gateway-object"
|
|
400
|
+
&& prov.reasoningEfforts?.length === 1
|
|
401
|
+
&& prov.reasoningEfforts[0] === "low";
|
|
402
|
+
}
|
|
403
|
+
|
|
396
404
|
export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig): void {
|
|
397
405
|
const entry = PROVIDER_REGISTRY.find(row => row.id === name);
|
|
398
406
|
if (!entry || !providerMatchesRegistryTransportWithStaticGuards(name, prov)) {
|
|
@@ -426,7 +434,9 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
|
|
|
426
434
|
if (seed.modelInputModalities) prov.modelInputModalities = fillRecordOfArrays(seed.modelInputModalities, prov.modelInputModalities);
|
|
427
435
|
if (prov.defaultMaxOutputTokens === undefined && seed.defaultMaxOutputTokens !== undefined) prov.defaultMaxOutputTokens = seed.defaultMaxOutputTokens;
|
|
428
436
|
if (!prov.modelMaxOutputTokens && seed.modelMaxOutputTokens) prov.modelMaxOutputTokens = { ...seed.modelMaxOutputTokens };
|
|
429
|
-
if (!prov.reasoningEfforts
|
|
437
|
+
if ((!prov.reasoningEfforts || hasLegacyClinePassReasoningEfforts(name, prov)) && seed.reasoningEfforts) {
|
|
438
|
+
prov.reasoningEfforts = [...seed.reasoningEfforts];
|
|
439
|
+
}
|
|
430
440
|
if (!prov.modelReasoningEfforts && seed.modelReasoningEfforts) prov.modelReasoningEfforts = cloneRecordOfArrays(seed.modelReasoningEfforts);
|
|
431
441
|
if (!prov.modelDefaultReasoningEfforts && seed.modelDefaultReasoningEfforts) prov.modelDefaultReasoningEfforts = { ...seed.modelDefaultReasoningEfforts };
|
|
432
442
|
if (!prov.reasoningEffortMap && seed.reasoningEffortMap) prov.reasoningEffortMap = { ...seed.reasoningEffortMap };
|
package/src/responses/parser.ts
CHANGED
|
@@ -171,6 +171,10 @@ function buildTools(tools: unknown[] | undefined): OcxTool[] | undefined {
|
|
|
171
171
|
};
|
|
172
172
|
for (const t of tools) {
|
|
173
173
|
if (!isObj(t)) continue;
|
|
174
|
+
if (t.type === "function" && isObj(t.function) && typeof t.function.name === "string" && t.function.name.length > 0) {
|
|
175
|
+
pushFn(t.function as Record<string, unknown>);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
174
178
|
if (t.type === "function" && typeof t.name === "string") {
|
|
175
179
|
pushFn(t);
|
|
176
180
|
} else if (t.type === "namespace" && Array.isArray(t.tools)) {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* long-lived proxy cannot grow without limit.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import { createHmac, randomBytes } from "node:crypto";
|
|
22
|
+
import { createHash, createHmac, randomBytes } from "node:crypto";
|
|
23
23
|
import type {
|
|
24
24
|
OcxProviderConfig,
|
|
25
25
|
OcxReasoningReplayIdentity,
|
|
@@ -108,6 +108,21 @@ export function reasoningReplayDestinationIdentity(baseUrl: string | undefined):
|
|
|
108
108
|
return `destination:${processLocalIdentity("destination", canonical)}`;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* The same destination identity, but stable across restarts.
|
|
113
|
+
*
|
|
114
|
+
* The process-local form above is keyed by `randomBytes(32)` minted at module load, which
|
|
115
|
+
* is correct for an in-memory cache and fatal for a durable one: every key would change on
|
|
116
|
+
* restart and the store would silently stop matching anything. A plain digest of the same
|
|
117
|
+
* canonical URL is equally non-reversible for this purpose — the input is a configured
|
|
118
|
+
* endpoint, not a secret — and needs no persisted salt or new on-disk state.
|
|
119
|
+
*/
|
|
120
|
+
export function durableReplayDestinationIdentity(baseUrl: string | undefined): string | undefined {
|
|
121
|
+
if (!nonEmpty(baseUrl)) return undefined;
|
|
122
|
+
const canonical = baseUrl.trim().replace(/\/+$/, "");
|
|
123
|
+
return `destination:${createHash("sha256").update("destination\0").update(canonical).digest("hex")}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
111
126
|
/** Produce a non-reversible process-local identity for credential material. */
|
|
112
127
|
export function reasoningReplayCredentialIdentity(
|
|
113
128
|
kind: "key" | "oauth" | "codex",
|
|
@@ -27,6 +27,11 @@ import type { OcxProviderOpaqueToolCallMetadata, OcxReasoningReplayScopeRef } fr
|
|
|
27
27
|
import { isCarryableSignature, responsesExtraContentFromProviderMetadata } from "./provider-opaque-metadata";
|
|
28
28
|
|
|
29
29
|
const STORE_FILE_NAME = "thought-signature-replay.json";
|
|
30
|
+
/**
|
|
31
|
+
* Bumped whenever `keyFor` changes shape. v3 added the durable destination identity, so a
|
|
32
|
+
* v2 file's keys can never match and are dropped on load instead of aging out invisibly.
|
|
33
|
+
*/
|
|
34
|
+
const STORE_VERSION = 3;
|
|
30
35
|
|
|
31
36
|
/** Bound on remembered entries; real signatures are a few hundred bytes, so this stays small. */
|
|
32
37
|
const MAX_ENTRIES = 16_384;
|
|
@@ -83,6 +88,11 @@ function keyFor(callId: string, scope: OcxReasoningReplayScopeRef | undefined):
|
|
|
83
88
|
return JSON.stringify([
|
|
84
89
|
scope.clientThreadId,
|
|
85
90
|
identity.providerName,
|
|
91
|
+
// Destination, unlike the credential identity, has a restart-stable form: it is a
|
|
92
|
+
// configured endpoint rather than a secret, so a plain digest works where the
|
|
93
|
+
// reasoning cache's randomBytes-keyed HMAC cannot. Without it, one provider NAME
|
|
94
|
+
// serving two endpoints shares signatures across both.
|
|
95
|
+
identity.providerDestinationDurableIdentity ?? "destination:unknown",
|
|
86
96
|
identity.adapterName,
|
|
87
97
|
identity.modelId,
|
|
88
98
|
callId,
|
|
@@ -103,6 +113,12 @@ function load(): void {
|
|
|
103
113
|
if (typeof parsed !== "object" || parsed === null || !Array.isArray((parsed as { entries?: unknown }).entries)) {
|
|
104
114
|
return;
|
|
105
115
|
}
|
|
116
|
+
// The version was written but never read, so a key-shape change could not be
|
|
117
|
+
// announced — old entries simply went dead and aged out on TTL, which is silent and
|
|
118
|
+
// indistinguishable from a store that is not working. Reading it makes a shape change
|
|
119
|
+
// an explicit drop: entries keyed by an older scheme are discarded on load rather than
|
|
120
|
+
// lingering as permanent misses.
|
|
121
|
+
if ((parsed as { version?: unknown }).version !== STORE_VERSION) return;
|
|
106
122
|
const nowMs = Date.now();
|
|
107
123
|
for (const entry of (parsed as { entries: unknown[] }).entries) {
|
|
108
124
|
if (typeof entry !== "object" || entry === null) continue;
|
|
@@ -141,7 +157,7 @@ function persist(): Promise<void> {
|
|
|
141
157
|
persistChain = persistChain
|
|
142
158
|
.then(async () => {
|
|
143
159
|
const snapshot = JSON.stringify({
|
|
144
|
-
version:
|
|
160
|
+
version: STORE_VERSION,
|
|
145
161
|
entries: [...entries].map(([key, entry]) => ({ key, sig: entry.sig, savedAt: entry.savedAt })),
|
|
146
162
|
});
|
|
147
163
|
await atomicWriteFileAsync(storePath(), snapshot);
|