@bitkyc08/opencodex 2.25.0 → 2.26.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/bin/ocx.mjs +59 -7
- package/gui/dist/assets/index-RL6b1bTV.js +102 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/base.ts +18 -0
- package/src/adapters/client-fingerprint.ts +0 -2
- package/src/adapters/cursor/http1-bidi.ts +361 -0
- package/src/adapters/cursor/live-models.ts +117 -30
- package/src/adapters/cursor/live-transport.ts +137 -56
- package/src/adapters/cursor/native-exec-fs.ts +1 -1
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +0 -1
- package/src/adapters/cursor/protobuf-request.ts +89 -21
- package/src/adapters/cursor/tool-definitions.ts +18 -7
- package/src/adapters/cursor/tool-result-normalize.ts +92 -0
- package/src/adapters/cursor/transport.ts +7 -0
- package/src/adapters/cursor.ts +2 -0
- package/src/adapters/google-http.ts +38 -10
- package/src/adapters/google.ts +23 -3
- package/src/adapters/openai-chat.ts +38 -13
- package/src/adapters/openai-responses.ts +129 -9
- package/src/adapters/registry.ts +30 -1
- package/src/bridge.ts +50 -12
- package/src/chat/inbound.ts +1 -0
- package/src/claude/agents-inject.ts +3 -2
- package/src/cli/dispatch.ts +19 -6
- package/src/cli/help.ts +2 -1
- package/src/cli/integrations.ts +35 -0
- package/src/cli/minimax.ts +8 -2
- package/src/cli/registry.ts +13 -2
- package/src/clients/config-export.ts +120 -1
- package/src/codex/account-store.ts +17 -1
- package/src/codex/auth-api.ts +51 -17
- package/src/codex/catalog/effort.ts +8 -5
- package/src/codex/catalog/provider-fetch.ts +39 -27
- package/src/codex/catalog/sync.ts +30 -8
- package/src/codex/inject.ts +39 -13
- package/src/codex/main-account.ts +29 -1
- package/src/codex/plan-from-token.ts +140 -0
- package/src/codex/plan.ts +25 -0
- package/src/codex/prompt-journal.ts +6 -2
- package/src/codex/quota-rejection.ts +21 -7
- package/src/codex/refresh.ts +4 -2
- package/src/codex/sync.ts +106 -3
- package/src/codex/warmup.ts +187 -81
- package/src/config.ts +85 -46
- package/src/generated/compatibility-version.json +127 -83
- package/src/grok/inject.ts +1 -1
- package/src/images/loop.ts +1 -0
- package/src/integrations/registry.ts +7 -0
- package/src/lab/automation/config-persistence.ts +2 -2
- package/src/lab/automation/persistence.ts +2 -2
- package/src/lab/ledger/purge.ts +2 -2
- package/src/lab/subject/behavior-fingerprint.ts +2 -2
- package/src/lib/config-ownership.ts +5 -2
- package/src/lib/destination-policy.ts +18 -1
- package/src/lib/provider-outbound.ts +7 -0
- package/src/lib/redact.ts +11 -0
- package/src/lib/tool-argument-integers.ts +50 -6
- package/src/lib/upstream-http-version.ts +57 -0
- package/src/lib/upstream-retry.ts +2 -1
- package/src/lib/windows-atomic-replace.ts +155 -0
- package/src/lib/windows-service-wrappers.ts +72 -0
- package/src/oauth/google-antigravity.ts +2 -2
- package/src/oauth/index.ts +60 -8
- package/src/providers/antigravity-models.ts +58 -4
- package/src/providers/command-code-efforts.ts +36 -9
- package/src/providers/context-cap.ts +9 -0
- package/src/providers/derive.ts +4 -0
- package/src/providers/fastwire.ts +453 -0
- package/src/providers/registry.ts +21 -1
- package/src/providers/service-tier.ts +173 -89
- package/src/responses/parser.ts +25 -16
- package/src/responses/reasoning-replay-cache.ts +30 -0
- package/src/responses/thought-signature-replay.ts +74 -4
- package/src/router.ts +6 -0
- package/src/routing/compatibility/behavior.ts +27 -16
- package/src/server/index.ts +10 -1
- package/src/server/management/oauth-account-routes.ts +10 -2
- package/src/server/management/shared.ts +1 -1
- package/src/server/management/system-routes.ts +15 -0
- package/src/server/request-log.ts +57 -3
- package/src/server/responses/agent-task-recovery.ts +6 -1
- package/src/server/responses/core.ts +209 -40
- package/src/server/responses/fetch-helpers.ts +15 -36
- package/src/server/responses/responses-field-backfill.ts +173 -0
- package/src/server/responses-reasoning-summary-rewrite.ts +171 -0
- package/src/service.ts +38 -36
- package/src/storage/cleanup.ts +2 -2
- package/src/tray/windows.ts +3 -2
- package/src/types.ts +101 -14
- package/src/update/job.ts +9 -18
- package/src/update/transactional-install.d.mts +22 -0
- package/src/update/transactional-install.mjs +259 -0
- package/src/usage/cost.ts +34 -5
- package/src/usage/log.ts +74 -4
- package/src/vision/anthropic-describe.ts +10 -6
- package/src/web-search/anthropic-executor.ts +8 -6
- package/gui/dist/assets/index-DxJ7kXj9.js +0 -102
package/src/grok/inject.ts
CHANGED
|
@@ -324,7 +324,7 @@ export function buildGrokManagedBlock(
|
|
|
324
324
|
`[model.${alias}]`,
|
|
325
325
|
`model = ${tomlString(model.id)}`,
|
|
326
326
|
`base_url = ${tomlString(baseUrl)}`,
|
|
327
|
-
'api_backend = "
|
|
327
|
+
'api_backend = "responses"',
|
|
328
328
|
'api_key = "opencodex-loopback"',
|
|
329
329
|
`name = ${tomlString(model.name ?? `OCX ${model.id}`)}`,
|
|
330
330
|
// Best-effort attribution tag for the usage dashboard. Upstream Grok sends
|
package/src/images/loop.ts
CHANGED
|
@@ -27,6 +27,8 @@ import {
|
|
|
27
27
|
opencodeGlobalConfigPath,
|
|
28
28
|
openclawConfigPath,
|
|
29
29
|
openclawHomeDir,
|
|
30
|
+
zcodeConfigPath,
|
|
31
|
+
zcodeHomeDir,
|
|
30
32
|
type ExportClientId,
|
|
31
33
|
} from "../clients/config-export";
|
|
32
34
|
|
|
@@ -119,6 +121,11 @@ export const INTEGRATION_CLIENTS: Record<IntegrationClientId, IntegrationClientS
|
|
|
119
121
|
configPath: (env = process.env, home = homedir()) => mcodeConfigPath(env, home),
|
|
120
122
|
detectDir: (env = process.env, home = homedir()) => mcodeHomeDir(env, home),
|
|
121
123
|
},
|
|
124
|
+
zcode: {
|
|
125
|
+
id: "zcode",
|
|
126
|
+
configPath: (env = process.env, home = homedir()) => zcodeConfigPath(env, home),
|
|
127
|
+
detectDir: (env = process.env, home = homedir()) => zcodeHomeDir(env, home),
|
|
128
|
+
},
|
|
122
129
|
};
|
|
123
130
|
|
|
124
131
|
export const INTEGRATION_CLIENT_IDS: readonly IntegrationClientId[] =
|
|
@@ -6,11 +6,11 @@ import {
|
|
|
6
6
|
linkSync,
|
|
7
7
|
openSync,
|
|
8
8
|
readFileSync,
|
|
9
|
-
renameSync,
|
|
10
9
|
unlinkSync,
|
|
11
10
|
writeFileSync,
|
|
12
11
|
} from "node:fs";
|
|
13
12
|
import { dirname, join } from "node:path";
|
|
13
|
+
import { renameAtomicFile } from "../../lib/windows-atomic-replace";
|
|
14
14
|
import {
|
|
15
15
|
ensureLabDirs,
|
|
16
16
|
labAutomationPolicyPath,
|
|
@@ -217,7 +217,7 @@ function writeConfigUnlocked(
|
|
|
217
217
|
if (configCommitFaultForTests === "before_publish") {
|
|
218
218
|
throw new LabAutomationError("synthetic automation config commit failure", "invalid_state");
|
|
219
219
|
}
|
|
220
|
-
|
|
220
|
+
renameAtomicFile(tmp, path, undefined, "lab-automation");
|
|
221
221
|
return normalized;
|
|
222
222
|
} finally {
|
|
223
223
|
if (fd !== null) closeSync(fd);
|
|
@@ -6,11 +6,11 @@ import {
|
|
|
6
6
|
linkSync,
|
|
7
7
|
openSync,
|
|
8
8
|
readFileSync,
|
|
9
|
-
renameSync,
|
|
10
9
|
unlinkSync,
|
|
11
10
|
writeFileSync,
|
|
12
11
|
} from "node:fs";
|
|
13
12
|
import { dirname, join } from "node:path";
|
|
13
|
+
import { renameAtomicFile } from "../../lib/windows-atomic-replace";
|
|
14
14
|
import {
|
|
15
15
|
ensureLabDirs,
|
|
16
16
|
labAutomationPolicyPath,
|
|
@@ -125,7 +125,7 @@ function atomicWriteJson(path: string, payload: unknown): void {
|
|
|
125
125
|
const tmp = join(dir, `.${basename(path)}.${process.pid}.${Date.now()}.tmp`);
|
|
126
126
|
const text = JSON.stringify(payload);
|
|
127
127
|
writeFileSync(tmp, text, { encoding: "utf8", mode: 0o600 });
|
|
128
|
-
|
|
128
|
+
renameAtomicFile(tmp, path, undefined, "lab-automation");
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
function basename(path: string): string {
|
package/src/lab/ledger/purge.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type TrustedArtifactDir,
|
|
6
6
|
} from "../artifacts/secure-fs";
|
|
7
7
|
import { ArtifactFsError } from "../artifacts/secure-fs";
|
|
8
|
+
import { renameAtomicFile } from "../../lib/windows-atomic-replace";
|
|
8
9
|
import {
|
|
9
10
|
LAB_EVENT_SCHEMA_VERSION,
|
|
10
11
|
LAB_PRODUCER,
|
|
@@ -29,7 +30,6 @@ import {
|
|
|
29
30
|
fsyncSync,
|
|
30
31
|
openSync,
|
|
31
32
|
readdirSync,
|
|
32
|
-
renameSync,
|
|
33
33
|
rmSync,
|
|
34
34
|
unlinkSync,
|
|
35
35
|
writeSync,
|
|
@@ -79,7 +79,7 @@ function atomicRewriteLedger(ledgerPath: string, events: LabEvent[]): void {
|
|
|
79
79
|
} finally {
|
|
80
80
|
closeSync(fd);
|
|
81
81
|
}
|
|
82
|
-
|
|
82
|
+
renameAtomicFile(tmpPath, ledgerPath, undefined, "lab-ledger");
|
|
83
83
|
renamed = true;
|
|
84
84
|
} catch (err) {
|
|
85
85
|
if (!renamed) {
|
|
@@ -5,7 +5,7 @@ import type { LabBehaviorSource, LabBehaviorValues } from "../live/types";
|
|
|
5
5
|
const CLOSED_KEYS = new Set([
|
|
6
6
|
"wire.adapter", "wire.upstreamProtocol", "wire.responsesPath", "wire.commandCodeVersion", "wire.modelSuffixMode",
|
|
7
7
|
"auth.mode", "auth.transport",
|
|
8
|
-
"responses.stateful", "responses.upstreamStreaming", "responses.serviceTier", "responses.snapshotRepair", "responses.itemIdRepair",
|
|
8
|
+
"responses.stateful", "responses.upstreamStreaming", "responses.serviceTier", "responses.fastWireKind", "responses.fastWireValue", "responses.snapshotRepair", "responses.itemIdRepair",
|
|
9
9
|
"limits.contextWindow", "limits.maxInputTokens", "limits.maxOutputTokens",
|
|
10
10
|
"modalities.input",
|
|
11
11
|
"sampling.omitTemperature", "sampling.omitTopP", "sampling.omitPenalties",
|
|
@@ -72,6 +72,6 @@ export function normalizeBehaviorValues(values: LabBehaviorValues): LabBehaviorV
|
|
|
72
72
|
|
|
73
73
|
/** Hash the authoritative production resolver output; Lab performs validation/canonicalization only. */
|
|
74
74
|
export function buildBehaviorFingerprintV1(values: LabBehaviorValues): string {
|
|
75
|
-
const payload = { schemaVersion: 1, resolverVersion:
|
|
75
|
+
const payload = { schemaVersion: 1, resolverVersion: 2, values: normalizeBehaviorValues(values) };
|
|
76
76
|
return createHash("sha256").update(jcsStringify(payload)).digest("hex");
|
|
77
77
|
}
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
readFileSync,
|
|
6
6
|
readdirSync,
|
|
7
7
|
realpathSync,
|
|
8
|
-
renameSync,
|
|
9
8
|
rmdirSync,
|
|
10
9
|
unlinkSync,
|
|
11
10
|
writeFileSync,
|
|
@@ -13,6 +12,7 @@ import {
|
|
|
13
12
|
import { randomUUID } from "node:crypto";
|
|
14
13
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
15
14
|
import type { GenerationContext } from "./state-store-sweeper";
|
|
15
|
+
import { renameAtomicFile } from "./windows-atomic-replace";
|
|
16
16
|
|
|
17
17
|
export const CONFIG_OWNER_FILE = ".opencodex-owner.json";
|
|
18
18
|
export const CONFIG_UNINSTALL_MANIFEST = ".opencodex-uninstall.json";
|
|
@@ -232,7 +232,10 @@ function writeManifest(configDir: string, manifest: ConfigUninstallManifest): vo
|
|
|
232
232
|
const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
233
233
|
writeFileSync(temp, `${JSON.stringify(manifest, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
234
234
|
try {
|
|
235
|
-
|
|
235
|
+
// Same Windows sharing-violation tolerance the config writer has: a
|
|
236
|
+
// scanner holding the manifest must not turn uninstall bookkeeping into a
|
|
237
|
+
// hard failure.
|
|
238
|
+
renameAtomicFile(temp, path, undefined, "config-ownership");
|
|
236
239
|
} catch (error) {
|
|
237
240
|
try { unlinkSync(temp); } catch { /* best effort */ }
|
|
238
241
|
throw error;
|
|
@@ -241,10 +241,18 @@ export function assessUrlDestination(url: string): UrlDestinationAssessment | nu
|
|
|
241
241
|
* Returns the validated addresses so direct callers can pin the connect peer and
|
|
242
242
|
* avoid a second, rebindable resolution. DNS failures remain fail-closed here;
|
|
243
243
|
* the provider proxy wrapper alone may recognize that typed failure and degrade.
|
|
244
|
+
*
|
|
245
|
+
* `allowBenchmarkAddresses` is an explicit outbound-only opt-in for Clash/Surge/
|
|
246
|
+
* Mihomo fake-IP DNS (IANA benchmark space 198.18.0.0/15, credit #1748): a hostname
|
|
247
|
+
* answer in that range is accepted without marking the destination private, so the
|
|
248
|
+
* caller can keep the hostname on its configured HTTP(S) proxy path. It applies to
|
|
249
|
+
* resolved answers only — a literal 198.18.x URL still rejects — and mixed answers
|
|
250
|
+
* that include any other non-public address still fail. Callers that do not pass it
|
|
251
|
+
* (image and Lab fetch) keep rejecting benchmark space.
|
|
244
252
|
*/
|
|
245
253
|
export async function resolvePublicAddresses(
|
|
246
254
|
url: string,
|
|
247
|
-
options?: string | { context?: string; allowPrivateNetwork?: boolean },
|
|
255
|
+
options?: string | { context?: string; allowPrivateNetwork?: boolean; allowBenchmarkAddresses?: boolean },
|
|
248
256
|
): Promise<{
|
|
249
257
|
hostname: string;
|
|
250
258
|
addresses: { address: string; family: number }[];
|
|
@@ -254,6 +262,7 @@ export async function resolvePublicAddresses(
|
|
|
254
262
|
? `${options.trim() || "image"} URL`
|
|
255
263
|
: options?.context?.trim() || "image URL";
|
|
256
264
|
const privateNetworkAllowed = typeof options === "object" && options?.allowPrivateNetwork === true;
|
|
265
|
+
const benchmarkAllowed = typeof options === "object" && options?.allowBenchmarkAddresses === true;
|
|
257
266
|
let hostname: string;
|
|
258
267
|
try {
|
|
259
268
|
hostname = normalizeHostname(new URL(url.trim()).hostname);
|
|
@@ -294,6 +303,14 @@ export async function resolvePublicAddresses(
|
|
|
294
303
|
const ipKind = isIP(address) || (family === 4 || family === 6 ? family : 0);
|
|
295
304
|
const assessment = ipKind === 4 ? classifyIpv4(address) : ipKind === 6 ? classifyIpv6(normalizeHostname(address)) : null;
|
|
296
305
|
if (!assessment || assessment.kind !== "public") {
|
|
306
|
+
// Hostname → 198.18.0.0/15 under the explicit opt-in is Clash/Surge/Mihomo
|
|
307
|
+
// fake-IP DNS, not a LAN provider. Accept it without allowPrivateNetwork and
|
|
308
|
+
// do not mark the destination private, so the caller's HTTP(S)_PROXY path
|
|
309
|
+
// still applies (credit #1748).
|
|
310
|
+
if (benchmarkAllowed && assessment?.kind === "private" && assessment.detail === "benchmark address") {
|
|
311
|
+
validatedAddresses.push({ address, family: ipKind === 4 || ipKind === 6 ? ipKind : (family || 4) });
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
297
314
|
const allowedPrivateAddress = privateNetworkAllowed
|
|
298
315
|
&& assessment
|
|
299
316
|
&& (assessment.kind === "loopback" || assessment.kind === "private");
|
|
@@ -148,6 +148,13 @@ async function providerOutboundRequest(
|
|
|
148
148
|
resolved = await resolveAddresses(url, {
|
|
149
149
|
context: "provider URL",
|
|
150
150
|
allowPrivateNetwork: allowPrivate,
|
|
151
|
+
// Clash/Surge/Mihomo fake-IP DNS (198.18.0.0/15) answers are admitted only
|
|
152
|
+
// when this exact host will use the configured outbound proxy: the hostname
|
|
153
|
+
// then rides the proxy as an ordinary CONNECT instead of failing as a private
|
|
154
|
+
// destination or being pin-connected to the fake-IP (credit #1748). A NO_PROXY
|
|
155
|
+
// match is a direct route, so it keeps the benchmark answer rejected. Image/Lab
|
|
156
|
+
// fetch never passes this flag.
|
|
157
|
+
allowBenchmarkAddresses: proxyConfigured && !noProxyMatches(parsed),
|
|
151
158
|
});
|
|
152
159
|
} catch (error) {
|
|
153
160
|
const dnsResolutionFailed = error instanceof DestinationDnsResolutionError
|
package/src/lib/redact.ts
CHANGED
|
@@ -443,6 +443,17 @@ export function redactSecretString(value: string): string {
|
|
|
443
443
|
return redacted;
|
|
444
444
|
}
|
|
445
445
|
|
|
446
|
+
/** Shared bounded representation for caller-controlled scalar metadata stored in logs. */
|
|
447
|
+
export function sanitizeLogMetadataString(value: unknown, maxLength = 64): string | undefined {
|
|
448
|
+
if (typeof value !== "string" || !Number.isInteger(maxLength) || maxLength < 1) return undefined;
|
|
449
|
+
// Remove every control/line-separator code point that common terminals and log viewers
|
|
450
|
+
// can render as a record boundary before the value reaches a single-line log field.
|
|
451
|
+
const filtered = value.trim().replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, "");
|
|
452
|
+
if (!filtered) return undefined;
|
|
453
|
+
const redacted = redactSecretString(filtered).trim();
|
|
454
|
+
return redacted ? redacted.slice(0, maxLength) : undefined;
|
|
455
|
+
}
|
|
456
|
+
|
|
446
457
|
export function redactSecrets(value: unknown): unknown {
|
|
447
458
|
if (typeof value === "string") return redactSecretString(value);
|
|
448
459
|
if (Array.isArray(value)) return value.map(item => redactSecrets(item));
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
// Schema-aware repair for
|
|
1
|
+
// Schema-aware repair for tool arguments whose serialized representation disagrees
|
|
2
|
+
// with the declared schema in a way that has exactly one faithful reading.
|
|
3
|
+
//
|
|
4
|
+
// Case 1 — integer fields arriving as integral floats (issue #1611).
|
|
2
5
|
//
|
|
3
6
|
// Grok serializes integer tool-call arguments through a float representation, so
|
|
4
7
|
// `yield_time_ms: 120000` leaves the provider as `120000.0`. Codex declares those
|
|
@@ -15,7 +18,23 @@
|
|
|
15
18
|
// `120000.0` has exactly one integer reading, so it is repaired;
|
|
16
19
|
// - `1.5` in an integer field is a genuine disagreement with the schema and is left
|
|
17
20
|
// alone so it still fails, rather than being truncated into a plausible lie.
|
|
18
|
-
//
|
|
21
|
+
//
|
|
22
|
+
// Case 2 — string fields arriving as bare integers (issue #1938).
|
|
23
|
+
//
|
|
24
|
+
// Cursor-served models emit `{"cell_id": 4}` where the schema declares
|
|
25
|
+
// `{"type": "string"}`. Codex rejects the call before the tool runs (invalid type:
|
|
26
|
+
// integer `4`, expected a string), and the model never self-corrects, so every such
|
|
27
|
+
// call is a hard failure loop. A safely-integral JSON number in a string-declared
|
|
28
|
+
// field has exactly one faithful string reading (`4` -> `"4"`), so it is repaired
|
|
29
|
+
// under the same intent boundary:
|
|
30
|
+
// - only when the field declares `string` and no numeric type (a
|
|
31
|
+
// `["integer","string"]` union accepts the number as-is and is left alone);
|
|
32
|
+
// - a non-integral number (`4.5`) in a string field is a genuine disagreement and
|
|
33
|
+
// is left alone so it still fails;
|
|
34
|
+
// - beyond 2^53-1 the parsed value may differ from the serialized text, so the
|
|
35
|
+
// original bytes stay.
|
|
36
|
+
//
|
|
37
|
+
// Anything without a declared `integer`/`string` type is never touched.
|
|
19
38
|
|
|
20
39
|
/** JSON Schema subset we need; provider tool schemas are untrusted input. */
|
|
21
40
|
type SchemaNode = Record<string, unknown>;
|
|
@@ -33,6 +52,21 @@ function declaresInteger(schema: SchemaNode): boolean {
|
|
|
33
52
|
return Array.isArray(type) && type.includes("integer");
|
|
34
53
|
}
|
|
35
54
|
|
|
55
|
+
/** True when the node declares `string`, including `["string","null"]` unions. */
|
|
56
|
+
function declaresString(schema: SchemaNode): boolean {
|
|
57
|
+
const type = schema.type;
|
|
58
|
+
if (type === "string") return true;
|
|
59
|
+
return Array.isArray(type) && type.includes("string");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** True when the node accepts a JSON number (`integer` or `number`), so a numeric
|
|
63
|
+
* value is already schema-valid and must not be rewritten into a string. */
|
|
64
|
+
function declaresNumeric(schema: SchemaNode): boolean {
|
|
65
|
+
const type = schema.type;
|
|
66
|
+
if (type === "integer" || type === "number") return true;
|
|
67
|
+
return Array.isArray(type) && (type.includes("integer") || type.includes("number"));
|
|
68
|
+
}
|
|
69
|
+
|
|
36
70
|
/**
|
|
37
71
|
* Resolve a local `$ref` (`#/$defs/Foo`, `#/definitions/Foo`).
|
|
38
72
|
*
|
|
@@ -91,8 +125,17 @@ function coerceValue(value: unknown, schema: SchemaNode | undefined, root: Schem
|
|
|
91
125
|
|
|
92
126
|
if (typeof value === "number") {
|
|
93
127
|
if (!resolved) return { value, changed: false };
|
|
94
|
-
const
|
|
95
|
-
|
|
128
|
+
const branches = compositionBranches(resolved);
|
|
129
|
+
const integerDeclared = declaresInteger(resolved) || branches.some(declaresInteger);
|
|
130
|
+
if (!integerDeclared && safelyIntegral(value)) {
|
|
131
|
+
// Issue #1938: a bare integer in a string-only field has exactly one faithful
|
|
132
|
+
// string reading. A field that also accepts a numeric type keeps the number.
|
|
133
|
+
const stringDeclared = declaresString(resolved) || branches.some(declaresString);
|
|
134
|
+
const numericDeclared = declaresNumeric(resolved) || branches.some(declaresNumeric);
|
|
135
|
+
if (stringDeclared && !numericDeclared) {
|
|
136
|
+
return { value: String(value), changed: true };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
96
139
|
// Not an integer field, already an integer, non-integral, or unrepresentable:
|
|
97
140
|
// in every one of those cases the received value is the right thing to keep.
|
|
98
141
|
if (!integerDeclared || !safelyIntegral(value)) return { value, changed: false };
|
|
@@ -141,8 +184,9 @@ export function coerceIntegerToolArguments(
|
|
|
141
184
|
parameters: Record<string, unknown> | undefined,
|
|
142
185
|
): string {
|
|
143
186
|
if (!parameters || !args) return args;
|
|
144
|
-
// Cheap reject: a payload with no
|
|
145
|
-
|
|
187
|
+
// Cheap reject: a payload with no digit cannot need either repair (integral-float
|
|
188
|
+
// -> integer, or bare-integer -> string).
|
|
189
|
+
if (!/\d/.test(args)) return args;
|
|
146
190
|
let parsed: unknown;
|
|
147
191
|
try {
|
|
148
192
|
parsed = JSON.parse(args);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { OcxProviderConfig, UpstreamHttpVersion } from "../types";
|
|
2
|
+
|
|
3
|
+
/** True when a provider explicitly selected the HTTP/1.1 compatibility path. */
|
|
4
|
+
export function isPinnedHttp1(version: UpstreamHttpVersion | undefined): boolean {
|
|
5
|
+
return version === "http1.1" || version === "h1";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Bun's fetch accepts a non-standard `protocol` init to pin the HTTP version
|
|
10
|
+
* (BunFetchRequestInit.protocol). The DOM lib types do not include it, so the
|
|
11
|
+
* value is represented through a RequestInit cast at this shared boundary.
|
|
12
|
+
*/
|
|
13
|
+
const UPSTREAM_HTTP_VERSION_PROTOCOL: Record<Exclude<UpstreamHttpVersion, "auto">, string> = {
|
|
14
|
+
"http1.1": "http1.1",
|
|
15
|
+
h1: "h1",
|
|
16
|
+
http2: "http2",
|
|
17
|
+
h2: "h2",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Raised before I/O when a fixed Bun HTTP-version pin cannot apply to the target URL. */
|
|
21
|
+
export class UpstreamHttpVersionTargetError extends Error {
|
|
22
|
+
readonly code = "upstream_http_version_target";
|
|
23
|
+
|
|
24
|
+
constructor(readonly observedProtocol: string) {
|
|
25
|
+
super(`upstream HTTP version pin requires an HTTPS target (received ${observedProtocol})`);
|
|
26
|
+
this.name = "UpstreamHttpVersionTargetError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Attach Bun's `protocol` pin for one explicit version value. */
|
|
31
|
+
export function withUpstreamHttpVersionValue(
|
|
32
|
+
input: Parameters<typeof globalThis.fetch>[0],
|
|
33
|
+
init: RequestInit | undefined,
|
|
34
|
+
version: UpstreamHttpVersion | undefined,
|
|
35
|
+
): RequestInit | undefined {
|
|
36
|
+
if (!version || version === "auto") return init;
|
|
37
|
+
// Bun's protocol pin requires an https: target. An explicit fixed-version request must fail
|
|
38
|
+
// locally when the pin cannot be honored instead of silently negotiating another transport.
|
|
39
|
+
const target = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
40
|
+
let targetUrl: URL;
|
|
41
|
+
try {
|
|
42
|
+
targetUrl = new URL(target);
|
|
43
|
+
} catch {
|
|
44
|
+
throw new UpstreamHttpVersionTargetError("unparseable");
|
|
45
|
+
}
|
|
46
|
+
if (targetUrl.protocol !== "https:") throw new UpstreamHttpVersionTargetError(targetUrl.protocol);
|
|
47
|
+
return { ...(init ?? {}), protocol: UPSTREAM_HTTP_VERSION_PROTOCOL[version] } as RequestInit;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Attach Bun's `protocol` pin when the provider opted into a fixed HTTP version. */
|
|
51
|
+
export function withUpstreamHttpVersion(
|
|
52
|
+
input: Parameters<typeof globalThis.fetch>[0],
|
|
53
|
+
init: RequestInit | undefined,
|
|
54
|
+
provider: Pick<OcxProviderConfig, "upstreamHttpVersion">,
|
|
55
|
+
): RequestInit | undefined {
|
|
56
|
+
return withUpstreamHttpVersionValue(input, init, provider.upstreamHttpVersion);
|
|
57
|
+
}
|
|
@@ -215,6 +215,7 @@ export async function fetchWithAttemptDeadline(
|
|
|
215
215
|
timeoutMs: number,
|
|
216
216
|
abortSignal?: AbortSignal,
|
|
217
217
|
preferIdentityEncoding = false,
|
|
218
|
+
executor: typeof globalThis.fetch = globalThis.fetch,
|
|
218
219
|
): Promise<Response> {
|
|
219
220
|
const attemptTimeout = clearableDeadline(timeoutMs, abortSignal);
|
|
220
221
|
const headers = new Headers(init.headers);
|
|
@@ -222,7 +223,7 @@ export async function fetchWithAttemptDeadline(
|
|
|
222
223
|
headers.set("accept-encoding", "identity");
|
|
223
224
|
}
|
|
224
225
|
try {
|
|
225
|
-
return await
|
|
226
|
+
return await executor(url, {
|
|
226
227
|
...init,
|
|
227
228
|
headers,
|
|
228
229
|
signal: attemptTimeout.signal,
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows-tolerant atomic replace.
|
|
3
|
+
*
|
|
4
|
+
* POSIX `rename()` replaces the destination entry unconditionally. Windows can
|
|
5
|
+
* refuse the same call with EBUSY, EPERM or EACCES while another process holds
|
|
6
|
+
* the target open — a real-time scanner that just indexed the file, a sync
|
|
7
|
+
* client, a backup agent. The hold is usually momentary, so a bounded retry
|
|
8
|
+
* turns an operational failure back into a successful publish.
|
|
9
|
+
*
|
|
10
|
+
* The envelope is deliberately small: two retries, 25ms then 50ms, about 75ms
|
|
11
|
+
* total. It is sized for a scanner blinking, not for a file someone actually
|
|
12
|
+
* has open. Widening it without evidence would trade a rare failure for a
|
|
13
|
+
* routine stall.
|
|
14
|
+
*
|
|
15
|
+
* This lives in its own module rather than in config.ts because
|
|
16
|
+
* config-ownership.ts is one of its callers and config.ts already imports
|
|
17
|
+
* config-ownership.ts — exporting it from there would close an import cycle.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { renameSync } from "node:fs";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Which durable publisher retried. A closed union on purpose: the value is a
|
|
24
|
+
* label in a diagnostic surface, and a path-derived string could carry a
|
|
25
|
+
* username. The type is the enforcement — a path cannot be passed here without
|
|
26
|
+
* failing typecheck. (`privacy:scan` reads file text and cannot see a runtime
|
|
27
|
+
* value, so it is a backstop for the response body, not the guard.)
|
|
28
|
+
*/
|
|
29
|
+
export type ReplacePublisher =
|
|
30
|
+
| "config"
|
|
31
|
+
| "prompt-journal"
|
|
32
|
+
| "config-ownership"
|
|
33
|
+
| "claude-agents"
|
|
34
|
+
| "lab-automation"
|
|
35
|
+
| "lab-ledger"
|
|
36
|
+
| "storage-cleanup"
|
|
37
|
+
| "tray";
|
|
38
|
+
|
|
39
|
+
/** The Windows error codes this module treats as a momentary hold. */
|
|
40
|
+
export type ReplaceRetryCode = "EBUSY" | "EPERM" | "EACCES";
|
|
41
|
+
|
|
42
|
+
export interface ReplaceRetryCounts {
|
|
43
|
+
/** Replaces that hit this code and were retried. */
|
|
44
|
+
retried: number;
|
|
45
|
+
/** Replaces that exhausted every retry and threw. */
|
|
46
|
+
exhausted: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Keyed by publisher AND code. The code is the diagnostic half: EBUSY from a
|
|
51
|
+
* scanner, EACCES from a permissions problem and EPERM from a lock are three
|
|
52
|
+
* different stories, and collapsing them would leave the counters unable to
|
|
53
|
+
* answer the question they exist for.
|
|
54
|
+
*/
|
|
55
|
+
const counters = new Map<string, ReplaceRetryCounts>();
|
|
56
|
+
|
|
57
|
+
function counterKey(publisher: ReplacePublisher, code: ReplaceRetryCode): string {
|
|
58
|
+
return `${publisher}:${code}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function bump(
|
|
62
|
+
publisher: ReplacePublisher,
|
|
63
|
+
code: ReplaceRetryCode,
|
|
64
|
+
field: keyof ReplaceRetryCounts,
|
|
65
|
+
): void {
|
|
66
|
+
const key = counterKey(publisher, code);
|
|
67
|
+
const current = counters.get(key) ?? { retried: 0, exhausted: 0 };
|
|
68
|
+
current[field] += 1;
|
|
69
|
+
counters.set(key, current);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Process-lifetime snapshot, keyed `publisher:CODE`. These reset on restart,
|
|
74
|
+
* which is fine: the question they answer is "does this ever fire at all", not
|
|
75
|
+
* "how often per hour".
|
|
76
|
+
*/
|
|
77
|
+
export function readWindowsReplaceRetryCounters(): Record<string, ReplaceRetryCounts> {
|
|
78
|
+
const out: Record<string, ReplaceRetryCounts> = {};
|
|
79
|
+
for (const [key, counts] of counters) out[key] = { ...counts };
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Test-only: the counters are module state and cases must not leak into each other. */
|
|
84
|
+
export function resetWindowsReplaceRetryCountersForTests(): void {
|
|
85
|
+
counters.clear();
|
|
86
|
+
}
|
|
87
|
+
export interface AtomicRenameIO {
|
|
88
|
+
platform: NodeJS.Platform;
|
|
89
|
+
rename: (source: string, destination: string) => void;
|
|
90
|
+
sleep: (milliseconds: number) => void;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const MAX_RETRIES = 2;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Windows sharing violations only, returning the code so the caller can record
|
|
97
|
+
* which one. Any other error is the caller's to see, immediately.
|
|
98
|
+
*/
|
|
99
|
+
function transientWindowsReplaceCode(
|
|
100
|
+
platform: NodeJS.Platform,
|
|
101
|
+
error: unknown,
|
|
102
|
+
): ReplaceRetryCode | null {
|
|
103
|
+
if (platform !== "win32") return null;
|
|
104
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
105
|
+
return code === "EBUSY" || code === "EPERM" || code === "EACCES" ? code : null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function renameAtomicFile(
|
|
109
|
+
source: string,
|
|
110
|
+
destination: string,
|
|
111
|
+
io: AtomicRenameIO = {
|
|
112
|
+
platform: process.platform,
|
|
113
|
+
rename: renameSync,
|
|
114
|
+
sleep: Bun.sleepSync,
|
|
115
|
+
},
|
|
116
|
+
publisher: ReplacePublisher = "config",
|
|
117
|
+
): void {
|
|
118
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
119
|
+
try {
|
|
120
|
+
io.rename(source, destination);
|
|
121
|
+
return;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
const code = transientWindowsReplaceCode(io.platform, error);
|
|
124
|
+
if (!code) throw error;
|
|
125
|
+
if (attempt >= MAX_RETRIES) {
|
|
126
|
+
bump(publisher, code, "exhausted");
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
bump(publisher, code, "retried");
|
|
130
|
+
io.sleep(25 * (attempt + 1));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function renameAtomicFileAsync(
|
|
136
|
+
source: string,
|
|
137
|
+
destination: string,
|
|
138
|
+
publisher: ReplacePublisher = "config",
|
|
139
|
+
): Promise<void> {
|
|
140
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
141
|
+
try {
|
|
142
|
+
renameSync(source, destination);
|
|
143
|
+
return;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
const code = transientWindowsReplaceCode(process.platform, error);
|
|
146
|
+
if (!code) throw error;
|
|
147
|
+
if (attempt >= MAX_RETRIES) {
|
|
148
|
+
bump(publisher, code, "exhausted");
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
bump(publisher, code, "retried");
|
|
152
|
+
await Bun.sleep(25 * (attempt + 1));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared termination of surviving Windows scheduler launcher/wrapper processes.
|
|
3
|
+
*
|
|
4
|
+
* `schtasks /end` ends the task instance but often leaves wscript/cmd running the
|
|
5
|
+
* `:loop` batch, which brings the proxy back during a stop, a restart, or
|
|
6
|
+
* post-update reclaim. Both the service teardown path and the update job need
|
|
7
|
+
* that guarantee, and they used to carry a copy each.
|
|
8
|
+
*
|
|
9
|
+
* The copies drifted in both directions, which is why this module exists rather
|
|
10
|
+
* than a second careful implementation: the updater received the #1589 argv
|
|
11
|
+
* cleanup that service.ts missed, and service.ts received the canonical-path
|
|
12
|
+
* scoping the updater missed. One of those gaps could force-terminate a wrapper
|
|
13
|
+
* belonging to a DIFFERENT OpenCodex home.
|
|
14
|
+
*
|
|
15
|
+
* Matching is scoped to the CANONICAL paths of one installation, never a bare
|
|
16
|
+
* filename, and the path must appear as a COMPLETE command-line token
|
|
17
|
+
* (wscript.exe spawns the .vbs as an argument; cmd.exe /c runs the .cmd). A
|
|
18
|
+
* substring match is excluded: an unrelated process whose command line merely
|
|
19
|
+
* contains the filename, and a wrapper under another home whose path ends with
|
|
20
|
+
* the same name, must both survive.
|
|
21
|
+
*/
|
|
22
|
+
import { spawnSync } from "node:child_process";
|
|
23
|
+
|
|
24
|
+
import { resolveTrustedWindowsPowerShellExe } from "./windows-elevation";
|
|
25
|
+
|
|
26
|
+
/** Quote for PowerShell: single-quote the value and double any embedded quote. */
|
|
27
|
+
function quoteForPowerShell(value: string): string {
|
|
28
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The PowerShell that finds and stops the wrappers for exactly these paths.
|
|
33
|
+
* Exported for tests: the matching rule is the whole point of this module, and
|
|
34
|
+
* the spawn itself is best-effort and unobservable.
|
|
35
|
+
*/
|
|
36
|
+
export function windowsWrapperKillScript(paths: readonly string[]): string {
|
|
37
|
+
return [
|
|
38
|
+
`$pats = @(${paths.map(quoteForPowerShell).join(", ")});`,
|
|
39
|
+
"Get-CimInstance Win32_Process | Where-Object {",
|
|
40
|
+
" if ($_.ProcessId -eq $PID) { return $false };",
|
|
41
|
+
" $c = $_.CommandLine; if (-not $c) { return $false };",
|
|
42
|
+
" foreach ($p in $pats) {",
|
|
43
|
+
" $i = $c.IndexOf($p, [System.StringComparison]::OrdinalIgnoreCase);",
|
|
44
|
+
" if ($i -lt 0) { continue };",
|
|
45
|
+
" $before = if ($i -gt 0) { $c.Substring($i - 1, 1) } else { ' ' };",
|
|
46
|
+
" $end = $i + $p.Length;",
|
|
47
|
+
" $after = if ($end -lt $c.Length) { $c.Substring($end, 1) } else { ' ' };",
|
|
48
|
+
" if ($before -match '[\\s\"'']' -and $after -match '[\\s\"'']') { return $true };",
|
|
49
|
+
" };",
|
|
50
|
+
" $false",
|
|
51
|
+
"} | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }",
|
|
52
|
+
].join(" ");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Best-effort: never throws, never reports. A wrapper that survives is handled
|
|
57
|
+
* by the caller's own stop verification.
|
|
58
|
+
*/
|
|
59
|
+
export function killWindowsSchedulerWrappers(paths: {
|
|
60
|
+
scriptPath: string;
|
|
61
|
+
launcherPath: string;
|
|
62
|
+
}): void {
|
|
63
|
+
if (process.platform !== "win32") return;
|
|
64
|
+
try {
|
|
65
|
+
spawnSync(resolveTrustedWindowsPowerShellExe(), [
|
|
66
|
+
// No `-WindowStyle Hidden` here: Bun 1.3.14 can fail that direct CLI pair
|
|
67
|
+
// before the command runs (#1589). `windowsHide` below is sufficient.
|
|
68
|
+
"-NoProfile", "-NoLogo", "-NonInteractive",
|
|
69
|
+
"-Command", windowsWrapperKillScript([paths.scriptPath, paths.launcherPath]),
|
|
70
|
+
], { stdio: "ignore", timeout: 5000, windowsHide: true });
|
|
71
|
+
} catch { /* best-effort */ }
|
|
72
|
+
}
|
|
@@ -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,
|
|
15
|
+
import { antigravityUserAgent, 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";
|
|
@@ -110,7 +110,7 @@ async function onboardProject(accessToken: string, signal?: AbortSignal): Promis
|
|
|
110
110
|
if (signal?.aborted) throw signal.reason ?? new Error("Antigravity onboarding aborted");
|
|
111
111
|
const response = await fetch(`${DAILY_API}/${API_VERSION}:onboardUser`, {
|
|
112
112
|
method: "POST",
|
|
113
|
-
headers: { Authorization: `Bearer ${accessToken}`, Accept: "*/*", "Content-Type": "application/json", "User-Agent": antigravityUserAgent()
|
|
113
|
+
headers: { Authorization: `Bearer ${accessToken}`, Accept: "*/*", "Content-Type": "application/json", "User-Agent": antigravityUserAgent() },
|
|
114
114
|
// `ide_version` is a version, not a User-Agent. `antigravityUserAgent()` returns the whole
|
|
115
115
|
// header — `antigravity/ide/2.5.5 (aidev_client; os_type=...; arch=...)` — so onboarding was
|
|
116
116
|
// sending a parenthesized UA string in a field the real client fills with `2.5.5`. It is a
|