@yansigit/opencodex 2.36.1-dev.20260829.47 → 2.36.1-dev.20260829.50
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/{ApiKeys-DAuYQsGJ.js → ApiKeys-BLPOvK4z.js} +1 -1
- package/gui/dist/assets/{Claude-bN_80RT8.js → Claude-BbHv3EU7.js} +1 -1
- package/gui/dist/assets/{CodexSet-D3dWX_ay.js → CodexSet-hgu5hES-.js} +1 -1
- package/gui/dist/assets/{FileIntegrationPage-BGyzkPs2.js → FileIntegrationPage-RCmDvvsx.js} +1 -1
- package/gui/dist/assets/{Grok-CuaKd1TT.js → Grok-DH-ggRqA.js} +1 -1
- package/gui/dist/assets/{Integrations-C7NGQoxR.js → Integrations-D6m5E2-T.js} +2 -2
- package/gui/dist/assets/{IntegrationsOverview-DjVorYO5.js → IntegrationsOverview-C1rbnXV7.js} +1 -1
- package/gui/dist/assets/{Logs-B21a-9xe.js → Logs-Bqb0vYRk.js} +1 -1
- package/gui/dist/assets/{Models-CN7Q3kuK.js → Models-gMnzHf0c.js} +1 -1
- package/gui/dist/assets/{NumberStepper-BWg90tEs.js → NumberStepper-awaoZNlj.js} +1 -1
- package/gui/dist/assets/{Providers-CP_i-7LS.js → Providers-Bo6wuQMa.js} +1 -1
- package/gui/dist/assets/{RestoreDialog-3M0NqCiD.js → RestoreDialog-DX7SGWIk.js} +1 -1
- package/gui/dist/assets/{Startup-BVPFoP2s.js → Startup-B1htiLU9.js} +1 -1
- package/gui/dist/assets/{Storage-BdsG9ypz.js → Storage-B3MfosmS.js} +1 -1
- package/gui/dist/assets/{Subagents-CguR_bGQ.js → Subagents-L6Q2V9PZ.js} +1 -1
- package/gui/dist/assets/{Usage-C0kKi9_H.js → Usage-xbZnkDoj.js} +1 -1
- package/gui/dist/assets/{codex-stale-banner-D4gvSWnO.js → codex-stale-banner-W4FhikN7.js} +1 -1
- package/gui/dist/assets/{data-surface-BtO3lwam.js → data-surface-B8PX8rjm.js} +1 -1
- package/gui/dist/assets/{data-surface-BCycbObp.js → data-surface-BbBMp0Wj.js} +1 -1
- package/gui/dist/assets/{index-CU1jE0st.js → index-JEC_6CUW.js} +3 -3
- package/gui/dist/assets/{model-display-D2elXkuE.js → model-display-BvLWy9D5.js} +1 -1
- package/gui/dist/assets/{provider-payload-DfkXWE7v.js → provider-payload-BRfSsonS.js} +1 -1
- package/gui/dist/assets/{section-tabs-BP7gEPK6.js → section-tabs-B8sLMiz6.js} +1 -1
- package/gui/dist/assets/shared-DI18-uF7.js +69 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -1
- package/src/adapters/base.ts +26 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +34 -41
- package/src/adapters/cursor/envelope-echo.ts +128 -0
- package/src/adapters/cursor/request-builder.ts +19 -12
- package/src/adapters/cursor/tool-definitions.ts +2 -1
- package/src/adapters/cursor/tool-result-normalize.ts +23 -31
- package/src/adapters/cursor.ts +21 -2
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +71 -2
- package/src/adapters/google.ts +19 -4
- package/src/adapters/kiro-constants.ts +12 -0
- package/src/adapters/kiro.ts +128 -11
- package/src/adapters/openai-chat.ts +16 -2
- package/src/adapters/openai-responses.ts +15 -2
- package/src/adapters/tool-catalog-nudge.ts +2 -1
- package/src/adapters/xai-web-search.ts +10 -14
- package/src/claude/outbound.ts +14 -3
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +93 -16
- package/src/cli/account-extended.ts +262 -36
- package/src/cli/account-main.ts +12 -12
- package/src/cli/account.ts +40 -10
- package/src/cli/agent.ts +8 -1
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +535 -0
- package/src/cli/claude-desktop.ts +31 -11
- package/src/cli/config-command.ts +7 -3
- package/src/cli/dispatch.ts +195 -27
- package/src/cli/doctor.ts +100 -1
- package/src/cli/help.ts +11 -2
- package/src/cli/index.ts +19 -3
- package/src/cli/init.ts +2 -2
- package/src/cli/inspect.ts +230 -0
- package/src/cli/observe.ts +11 -3
- package/src/cli/provider.ts +58 -74
- package/src/cli/registry.ts +34 -2
- package/src/cli/runtime-api.ts +51 -7
- package/src/cli/status.ts +16 -0
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/usage-report.ts +52 -2
- package/src/cli/version-skew.ts +46 -0
- package/src/codex/account-label.ts +21 -0
- package/src/codex/catalog/provider-fetch.ts +4 -0
- package/src/codex/convergence.ts +110 -15
- package/src/codex/transition-state.ts +12 -3
- package/src/compatibility/openai-responses.ts +9 -1
- package/src/config.ts +209 -11
- package/src/generated/compatibility-version.json +115 -75
- package/src/integrations/ownership-policy.ts +24 -5
- package/src/integrations/ownership.ts +36 -2
- package/src/integrations/state.ts +40 -7
- package/src/integrations/writer.ts +21 -3
- package/src/lib/admin-secrets.ts +24 -0
- package/src/lib/errors.ts +25 -1
- package/src/lib/service-secrets.ts +15 -0
- package/src/oauth/index.ts +143 -82
- package/src/oauth/login-cli.ts +53 -11
- package/src/oauth/store.ts +16 -6
- package/src/providers/alibaba-region-startup.ts +31 -11
- package/src/providers/api-keys.ts +58 -34
- package/src/providers/key-failover.ts +59 -46
- package/src/providers/label.ts +34 -1
- package/src/providers/model-rename-startup.ts +25 -7
- package/src/providers/openai-tier-startup.ts +38 -14
- package/src/responses/turn-termination.ts +107 -0
- package/src/server/index.ts +10 -0
- package/src/server/management/context.ts +10 -3
- package/src/server/management/logs-usage-routes.ts +24 -43
- package/src/server/management/model-routes.ts +284 -165
- package/src/server/management/oauth-account-routes.ts +93 -15
- package/src/server/management/provider-routes.ts +200 -87
- package/src/server/management/route-registry.ts +317 -0
- package/src/server/proxy-liveness.ts +27 -4
- package/src/server/request-log.ts +37 -17
- package/src/server/responses/core.ts +80 -4
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/service.ts +34 -0
- package/src/storage/policy-input.ts +166 -0
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +115 -221
- package/src/usage/log.ts +32 -6
- package/src/usage/summary.ts +22 -34
- package/gui/dist/assets/shared-BdYGV-yJ.js +0 -69
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
type ManagedContribution,
|
|
13
13
|
type ManagedFragment,
|
|
14
14
|
} from "../clients/config-export";
|
|
15
|
-
import { canonicalContribution, fingerprint } from "./ownership";
|
|
15
|
+
import { canonicalContribution, fingerprint, semanticContribution } from "./ownership";
|
|
16
16
|
|
|
17
17
|
type JsonObject = Record<string, unknown>;
|
|
18
18
|
|
|
@@ -124,11 +124,10 @@ export function validRefreshablePaths(
|
|
|
124
124
|
});
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
|
|
128
|
-
export function protectedContributionFingerprint(
|
|
127
|
+
function contributionWithoutRefreshablePaths(
|
|
129
128
|
contribution: ManagedContribution,
|
|
130
129
|
refreshablePaths: readonly (readonly string[])[],
|
|
131
|
-
):
|
|
130
|
+
): ManagedContribution {
|
|
132
131
|
const fragments = contribution.fragments.map(cloneFragment);
|
|
133
132
|
for (const refreshablePath of refreshablePaths) {
|
|
134
133
|
for (const fragment of fragments) {
|
|
@@ -137,5 +136,25 @@ export function protectedContributionFingerprint(
|
|
|
137
136
|
break;
|
|
138
137
|
}
|
|
139
138
|
}
|
|
140
|
-
return
|
|
139
|
+
return { ...contribution, fragments };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Fingerprint a contribution after removing only its explicitly refreshable paths. */
|
|
143
|
+
export function protectedContributionFingerprint(
|
|
144
|
+
contribution: ManagedContribution,
|
|
145
|
+
refreshablePaths: readonly (readonly string[])[],
|
|
146
|
+
): string {
|
|
147
|
+
return fingerprint(canonicalContribution(
|
|
148
|
+
contributionWithoutRefreshablePaths(contribution, refreshablePaths),
|
|
149
|
+
));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Semantic protected fingerprint that ignores JSON object-key order only. */
|
|
153
|
+
export function semanticProtectedContributionFingerprint(
|
|
154
|
+
contribution: ManagedContribution,
|
|
155
|
+
refreshablePaths: readonly (readonly string[])[],
|
|
156
|
+
): string {
|
|
157
|
+
return fingerprint(semanticContribution(
|
|
158
|
+
contributionWithoutRefreshablePaths(contribution, refreshablePaths),
|
|
159
|
+
));
|
|
141
160
|
}
|
|
@@ -22,8 +22,38 @@ export function fingerprint(text: string): string {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* Canonicalize JSON object members recursively for semantic comparisons.
|
|
26
|
+
* Arrays stay ordered because their position can carry configuration meaning.
|
|
27
|
+
*/
|
|
28
|
+
function semanticJsonValue(value: unknown): unknown {
|
|
29
|
+
if (Array.isArray(value)) return value.map(semanticJsonValue);
|
|
30
|
+
if (value === null || typeof value !== "object") return value;
|
|
31
|
+
|
|
32
|
+
const record = value as Record<string, unknown>;
|
|
33
|
+
return Object.fromEntries(
|
|
34
|
+
Object.keys(record)
|
|
35
|
+
.sort()
|
|
36
|
+
.map(key => [key, semanticJsonValue(record[key])]),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Stable semantic bytes of a contribution. Third-party clients may
|
|
42
|
+
* re-serialize JSON object members in a different order; that formatting-only
|
|
43
|
+
* rewrite must not look like a protected-value edit.
|
|
44
|
+
*/
|
|
45
|
+
export function semanticContribution(contribution: ManagedContribution): string {
|
|
46
|
+
const sorted = [...contribution.fragments].sort((a, b) => {
|
|
47
|
+
const left = a.path.join("\u0000");
|
|
48
|
+
const right = b.path.join("\u0000");
|
|
49
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
50
|
+
});
|
|
51
|
+
return JSON.stringify(sorted.map(fragment => [fragment.path, semanticJsonValue(fragment.value)]));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Legacy-compatible bytes used by existing persisted fingerprints. Fragment
|
|
56
|
+
* paths are stable, while nested object insertion order remains exact.
|
|
27
57
|
*/
|
|
28
58
|
export function canonicalContribution(contribution: ManagedContribution): string {
|
|
29
59
|
const sorted = [...contribution.fragments].sort((a, b) => {
|
|
@@ -41,11 +71,15 @@ export interface OwnershipRecord {
|
|
|
41
71
|
fileFingerprint: string;
|
|
42
72
|
/** Hash of our contribution — detects catalog/port drift. */
|
|
43
73
|
blockFingerprint: string;
|
|
74
|
+
/** Key-order-independent companion for JSON clients that normalize objects. */
|
|
75
|
+
semanticBlockFingerprint?: string;
|
|
44
76
|
/**
|
|
45
77
|
* Hash of the fields the client must not rewrite. Present only when a
|
|
46
78
|
* client has explicitly declared runtime-derived paths below.
|
|
47
79
|
*/
|
|
48
80
|
protectedBlockFingerprint?: string;
|
|
81
|
+
/** Key-order-independent companion to `protectedBlockFingerprint`. */
|
|
82
|
+
semanticProtectedBlockFingerprint?: string;
|
|
49
83
|
/**
|
|
50
84
|
* Exact document paths a client may derive after apply. These are recorded
|
|
51
85
|
* per operation so later catalog changes cannot widen an older grant.
|
|
@@ -12,10 +12,11 @@ import { ClientPathError, EXPORT_CLIENTS, opencodeProxyBaseUrl, type ExportModel
|
|
|
12
12
|
import type { OcxConfig } from "../types";
|
|
13
13
|
import { PARSE_FAILED, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
|
|
14
14
|
import { SNAPSHOT_RETENTION } from "./journal";
|
|
15
|
-
import { canonicalContribution, fingerprint, type OwnershipRecord } from "./ownership";
|
|
15
|
+
import { canonicalContribution, fingerprint, semanticContribution, type OwnershipRecord } from "./ownership";
|
|
16
16
|
import {
|
|
17
17
|
protectedContributionFingerprint,
|
|
18
18
|
refreshablePathsOf,
|
|
19
|
+
semanticProtectedContributionFingerprint,
|
|
19
20
|
validRefreshablePaths,
|
|
20
21
|
} from "./ownership-policy";
|
|
21
22
|
import { INTEGRATION_CLIENTS, type IntegrationClientId } from "./registry";
|
|
@@ -153,21 +154,49 @@ function recordedBlockIsOwned(
|
|
|
153
154
|
if (!observed) return false;
|
|
154
155
|
if (fingerprint(canonicalContribution(observed)) === record.blockFingerprint) return true;
|
|
155
156
|
|
|
157
|
+
const observedSemanticFingerprint = fingerprint(semanticContribution(observed));
|
|
158
|
+
if (
|
|
159
|
+
typeof record.semanticBlockFingerprint === "string"
|
|
160
|
+
&& observedSemanticFingerprint === record.semanticBlockFingerprint
|
|
161
|
+
) return true;
|
|
162
|
+
|
|
163
|
+
const desiredFingerprint = fingerprint(canonicalContribution(desired));
|
|
164
|
+
if (
|
|
165
|
+
desiredFingerprint === record.blockFingerprint
|
|
166
|
+
&& observedSemanticFingerprint === fingerprint(semanticContribution(desired))
|
|
167
|
+
) return true;
|
|
168
|
+
|
|
156
169
|
if (
|
|
157
170
|
typeof record.protectedBlockFingerprint === "string"
|
|
158
171
|
&& validRefreshablePaths(observed, record.refreshablePaths)
|
|
159
172
|
&& record.refreshablePaths.length > 0
|
|
160
173
|
) {
|
|
161
|
-
|
|
162
|
-
|
|
174
|
+
const observedProtectedFingerprint = protectedContributionFingerprint(
|
|
175
|
+
observed,
|
|
176
|
+
record.refreshablePaths,
|
|
177
|
+
);
|
|
178
|
+
if (observedProtectedFingerprint === record.protectedBlockFingerprint) return true;
|
|
179
|
+
|
|
180
|
+
const observedSemanticProtectedFingerprint = semanticProtectedContributionFingerprint(
|
|
181
|
+
observed,
|
|
182
|
+
record.refreshablePaths,
|
|
183
|
+
);
|
|
184
|
+
if (
|
|
185
|
+
typeof record.semanticProtectedBlockFingerprint === "string"
|
|
186
|
+
&& observedSemanticProtectedFingerprint === record.semanticProtectedBlockFingerprint
|
|
187
|
+
) return true;
|
|
188
|
+
|
|
189
|
+
return protectedContributionFingerprint(desired, record.refreshablePaths)
|
|
190
|
+
=== record.protectedBlockFingerprint
|
|
191
|
+
&& observedSemanticProtectedFingerprint
|
|
192
|
+
=== semanticProtectedContributionFingerprint(desired, record.refreshablePaths);
|
|
163
193
|
}
|
|
164
194
|
|
|
165
|
-
const desiredFingerprint = fingerprint(canonicalContribution(desired));
|
|
166
195
|
if (desiredFingerprint !== record.blockFingerprint) return false;
|
|
167
196
|
const legacyPaths = refreshablePathsOf(desired);
|
|
168
197
|
return legacyPaths.length > 0
|
|
169
|
-
&&
|
|
170
|
-
===
|
|
198
|
+
&& semanticProtectedContributionFingerprint(observed, legacyPaths)
|
|
199
|
+
=== semanticProtectedContributionFingerprint(desired, legacyPaths);
|
|
171
200
|
}
|
|
172
201
|
|
|
173
202
|
/**
|
|
@@ -257,7 +286,11 @@ export function classifyIntegration(input: {
|
|
|
257
286
|
}
|
|
258
287
|
return { state: "stale" };
|
|
259
288
|
}
|
|
260
|
-
|
|
289
|
+
const desiredFingerprint = typeof input.record.semanticBlockFingerprint === "string"
|
|
290
|
+
? fingerprint(semanticContribution(input.contribution))
|
|
291
|
+
: fingerprint(canonicalContribution(input.contribution));
|
|
292
|
+
const recordedFingerprint = input.record.semanticBlockFingerprint ?? input.record.blockFingerprint;
|
|
293
|
+
return recordedFingerprint === desiredFingerprint
|
|
261
294
|
? { state: "current" }
|
|
262
295
|
: { state: "stale" };
|
|
263
296
|
}
|
|
@@ -15,8 +15,18 @@ import { EXPORT_CLIENTS, type ExportModel, type ManagedContribution } from "../c
|
|
|
15
15
|
import { isLoopbackHostname } from "../codex/inject";
|
|
16
16
|
import type { OcxConfig } from "../types";
|
|
17
17
|
import { PARSE_FAILED, defaultIntegrationIO, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
|
|
18
|
-
import {
|
|
19
|
-
|
|
18
|
+
import {
|
|
19
|
+
fingerprint,
|
|
20
|
+
canonicalContribution,
|
|
21
|
+
fragmentPathsOf,
|
|
22
|
+
semanticContribution,
|
|
23
|
+
type OwnershipRecord,
|
|
24
|
+
} from "./ownership";
|
|
25
|
+
import {
|
|
26
|
+
protectedContributionFingerprint,
|
|
27
|
+
refreshablePathsOf,
|
|
28
|
+
semanticProtectedContributionFingerprint,
|
|
29
|
+
} from "./ownership-policy";
|
|
20
30
|
import { createdContainerPaths, mergeContribution, removeFragments } from "./merge";
|
|
21
31
|
import { INTEGRATION_CLIENTS, isLoopbackOnly, type IntegrationClientId } from "./registry";
|
|
22
32
|
import { classifyIntegration, exportContextOf } from "./state";
|
|
@@ -361,8 +371,13 @@ function applyOrRefreshIntegration(input: IntegrationWriteInput, allowAbsent: bo
|
|
|
361
371
|
record: {
|
|
362
372
|
clientId, configPath, fileFingerprint: fingerprint(text),
|
|
363
373
|
blockFingerprint: fingerprint(canonicalContribution(contribution)),
|
|
374
|
+
semanticBlockFingerprint: fingerprint(semanticContribution(contribution)),
|
|
364
375
|
...(refreshablePaths.length > 0 ? {
|
|
365
376
|
protectedBlockFingerprint: protectedContributionFingerprint(contribution, refreshablePaths),
|
|
377
|
+
semanticProtectedBlockFingerprint: semanticProtectedContributionFingerprint(
|
|
378
|
+
contribution,
|
|
379
|
+
refreshablePaths,
|
|
380
|
+
),
|
|
366
381
|
refreshablePaths,
|
|
367
382
|
} : {}),
|
|
368
383
|
fragmentPaths: fragmentPathsOf(contribution), createdContainers: created,
|
|
@@ -556,7 +571,10 @@ export function restoreIntegration(input: IntegrationRestoreInput): WriteOutcome
|
|
|
556
571
|
? (restoredText === null ? "absent" : "conflict")
|
|
557
572
|
: !recordDescribesBytes
|
|
558
573
|
? "conflict"
|
|
559
|
-
:
|
|
574
|
+
: (
|
|
575
|
+
restoredRecord.semanticBlockFingerprint === fingerprint(semanticContribution(fresh))
|
|
576
|
+
|| restoredRecord.blockFingerprint === fingerprint(canonicalContribution(fresh))
|
|
577
|
+
)
|
|
560
578
|
? "current"
|
|
561
579
|
: "stale";
|
|
562
580
|
|
package/src/lib/admin-secrets.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { timingSafeEqual } from "node:crypto";
|
|
1
2
|
import { lstatSync, readFileSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { getConfigDir } from "../config";
|
|
@@ -23,3 +24,26 @@ export function loadAdminTokenFromFile(configDir = getConfigDir()): string | nul
|
|
|
23
24
|
export function configuredAdminToken(configDir = getConfigDir(), env: NodeJS.ProcessEnv = process.env): string | null {
|
|
24
25
|
return env.OPENCODEX_ADMIN_AUTH_TOKEN?.trim() || loadAdminTokenFromFile(configDir);
|
|
25
26
|
}
|
|
27
|
+
|
|
28
|
+
export const ADMIN_TOKEN_PREFIX = "ocx_admin_";
|
|
29
|
+
|
|
30
|
+
function secretTextEquals(left: string, right: string): boolean {
|
|
31
|
+
const a = Buffer.from(left);
|
|
32
|
+
const b = Buffer.from(right);
|
|
33
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* True when `token` is a management credential: minted `ocx_admin_…` shape, or
|
|
38
|
+
* byte-equal to the configured admin token (env or admin-api-token file).
|
|
39
|
+
* Used by the service write/start chokepoint and by doctor so the two cannot drift.
|
|
40
|
+
*/
|
|
41
|
+
export function tokenCollidesWithAdmin(
|
|
42
|
+
token: string,
|
|
43
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
44
|
+
configDir = getConfigDir(),
|
|
45
|
+
): boolean {
|
|
46
|
+
if (token.startsWith(ADMIN_TOKEN_PREFIX)) return true;
|
|
47
|
+
const admin = configuredAdminToken(configDir, env);
|
|
48
|
+
return admin !== null && secretTextEquals(token, admin);
|
|
49
|
+
}
|
package/src/lib/errors.ts
CHANGED
|
@@ -349,6 +349,12 @@ export function inferHttpStatusFromAdapterMessage(message: string): number {
|
|
|
349
349
|
// subscription/permission wording.
|
|
350
350
|
if (isAuthenticationMessage(lower)) return 401;
|
|
351
351
|
if (isSubscriptionGateMessage(lower) || isPermissionMessage(lower)) return 403;
|
|
352
|
+
// Same precedence rule as classifyCursorError: an explicit gRPC FAILED_PRECONDITION is a
|
|
353
|
+
// structured, deterministic rejection, so it outranks the overload keywords that routinely
|
|
354
|
+
// appear beside it ("failed_precondition: model unavailable for this plan"). Without this,
|
|
355
|
+
// the message matched "unavailable" and returned a retryable 503, so clients kept retrying
|
|
356
|
+
// a rejection that can never succeed.
|
|
357
|
+
if (lower.includes("failed_precondition") || lower.includes("failed precondition")) return 400;
|
|
352
358
|
if (
|
|
353
359
|
lower.includes("unavailable") ||
|
|
354
360
|
lower.includes("overloaded") ||
|
|
@@ -424,6 +430,24 @@ export function httpStatusFromTerminalError(error: {
|
|
|
424
430
|
if (message && isClientClosedMessage(message)) return 499;
|
|
425
431
|
if (error.type === "invalid_request_error") return 400;
|
|
426
432
|
if (error.type === "proxy_error") return 500;
|
|
427
|
-
|
|
433
|
+
// A structured server class must not be downgraded to a CLIENT error by message wording.
|
|
434
|
+
// classifyError assigns `server_error` + `upstream_server_error` to every 5xx it sees, so
|
|
435
|
+
// the class is authoritative about blame: the upstream failed, the caller did not send a
|
|
436
|
+
// bad request. What it is NOT authoritative about is which server status fits — a stall is
|
|
437
|
+
// genuinely 504 and an overload genuinely 503, and flattening those to 502 discards
|
|
438
|
+
// information both the log surface and the retry policy read. So message inference still
|
|
439
|
+
// chooses the specific status, and only a client-error verdict is overridden.
|
|
440
|
+
//
|
|
441
|
+
// The override is deliberately narrowed to 400 alone. 429, 499, 401 and 403 are all
|
|
442
|
+
// actionable signals the caller routes on — retry-after, client cancellation, re-auth,
|
|
443
|
+
// entitlement — and overriding them would trade one kind of misreport for another. 400 is
|
|
444
|
+
// the single verdict that both blames the caller and stops the retry, which is the failure
|
|
445
|
+
// being fixed: an upstream 500 whose text happens to contain "malformed" or "invalid
|
|
446
|
+
// request" used to return 400, so Claude Code stopped retrying a retryable failure.
|
|
447
|
+
const structuredServerClass = error.type === "server_error" || error.code === "upstream_server_error";
|
|
448
|
+
if (message) {
|
|
449
|
+
const inferred = inferHttpStatusFromAdapterMessage(message);
|
|
450
|
+
return structuredServerClass && inferred === 400 ? 502 : inferred;
|
|
451
|
+
}
|
|
428
452
|
return 502;
|
|
429
453
|
}
|
|
@@ -23,3 +23,18 @@ export function loadServiceTokenFromFile(env: Record<string, string | undefined>
|
|
|
23
23
|
return null;
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Contents of the installed service token file. The launch wrapper always re-exports
|
|
29
|
+
* this file as OPENCODEX_API_AUTH_TOKEN, so doctor and start must inspect it even
|
|
30
|
+
* when the calling shell has no data-plane env var.
|
|
31
|
+
* Returns the token or null — never throws, never logs the value.
|
|
32
|
+
*/
|
|
33
|
+
export function readInstalledServiceToken(): string | null {
|
|
34
|
+
try {
|
|
35
|
+
const token = readFileSync(serviceApiTokenFilePath(), "utf8").trim();
|
|
36
|
+
return token || null;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/oauth/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { KiroOAuthMetadata, OAuthController, OAuthCredentials } from "./types";
|
|
2
2
|
import { parseCallbackInput } from "./callback-server";
|
|
3
3
|
import type { OcxConfig, OcxProviderConfig, RefreshPolicy } from "../types";
|
|
4
|
-
import { loadConfig,
|
|
4
|
+
import { initializePersistedConfigIfMissing, loadConfig, mutatePersistedConfig, resolveEnvValue } from "../config";
|
|
5
5
|
import { maskEmail } from "../lib/privacy";
|
|
6
6
|
import { KiroTokenRefreshError, environmentKiroRoutingMetadata, loginKiro, refreshKiroToken, settleKiroLoginTransaction } from "./kiro";
|
|
7
7
|
import { getAccountCredential, getAccountSet, removeAccount, saveAccountCredential, saveCredential, setActiveAccount, getCredential, credentialGeneration, createOAuthRefreshIntentLock, mergeAccountCredential, markAccountNeedsReauthIfGeneration, readOAuthRefreshIntent, writeOAuthRefreshIntent, markOAuthRefreshIntentStaleOwner, clearOAuthRefreshIntent, normalizeAuthStoreBuffer, OAuthMutationBusyError } from "./store";
|
|
@@ -1023,42 +1023,107 @@ function migrateLegacyAntigravityStaticCatalog(config: OcxConfig): boolean {
|
|
|
1023
1023
|
return true;
|
|
1024
1024
|
}
|
|
1025
1025
|
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1026
|
+
interface OAuthReconcileProjection {
|
|
1027
|
+
config: OcxConfig;
|
|
1028
|
+
changed: boolean;
|
|
1029
|
+
touchedProviders: string[];
|
|
1030
|
+
touchedAntigravityVersion: boolean;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function projectOAuthProviderReconciliation(config: OcxConfig): OAuthReconcileProjection {
|
|
1034
|
+
const projected = structuredClone(config);
|
|
1035
|
+
const touchedProviders = new Set<string>();
|
|
1036
|
+
const beforeAntigravity = JSON.stringify(projected.providers[GOOGLE_ANTIGRAVITY_PROVIDER]);
|
|
1037
|
+
const beforeAntigravityVersion = projected.googleAntigravityStaticCatalogVersion;
|
|
1038
|
+
let changed = migrateLegacyAntigravityStaticCatalog(projected);
|
|
1039
|
+
if (JSON.stringify(projected.providers[GOOGLE_ANTIGRAVITY_PROVIDER]) !== beforeAntigravity) {
|
|
1040
|
+
touchedProviders.add(GOOGLE_ANTIGRAVITY_PROVIDER);
|
|
1041
|
+
}
|
|
1042
|
+
const touchedAntigravityVersion = projected.googleAntigravityStaticCatalogVersion !== beforeAntigravityVersion;
|
|
1043
|
+
|
|
1044
|
+
for (const [name, prov] of Object.entries(projected.providers)) {
|
|
1045
|
+
const beforeProvider = JSON.stringify(prov);
|
|
1029
1046
|
const def = OAUTH_PROVIDERS[name];
|
|
1030
1047
|
if (name === "command-code" && isLegacyCommandCodeStaticCatalog(prov)) {
|
|
1031
1048
|
// The former experimental preset was the exact three-model seed above. It was not a user
|
|
1032
1049
|
// choice to disable discovery, so promote only that shape to the account live catalog.
|
|
1033
1050
|
prov.liveModels = true;
|
|
1034
|
-
changed = true;
|
|
1035
1051
|
}
|
|
1036
|
-
if (
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1052
|
+
if (def && prov.authMode === "oauth") {
|
|
1053
|
+
const preset = def.providerConfig;
|
|
1054
|
+
for (const field of OAUTH_RECONCILE_FIELDS) {
|
|
1055
|
+
if (JSON.stringify(prov[field]) === JSON.stringify(preset[field])) continue;
|
|
1056
|
+
if (preset[field] !== undefined) {
|
|
1057
|
+
prov[field] = cloneProviderField(preset[field]) as never;
|
|
1058
|
+
} else {
|
|
1059
|
+
delete prov[field];
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
if (prov.liveModels === undefined && preset.liveModels !== undefined) {
|
|
1063
|
+
prov.liveModels = preset.liveModels;
|
|
1064
|
+
}
|
|
1065
|
+
// Heal a defaultModel that no longer exists in the refreshed list (e.g. a deprecated snapshot).
|
|
1066
|
+
// Skip providers without a static preset `models` list: for live-discovery providers
|
|
1067
|
+
// (e.g. command-code OAuth) the account-scoped catalog is not enumerable here, so any
|
|
1068
|
+
// persisted defaultModel is a user selection and must not be overwritten by the seed.
|
|
1069
|
+
if (prov.defaultModel && preset.defaultModel && preset.models && preset.models.length > 0 && !(prov.models ?? []).includes(prov.defaultModel)) {
|
|
1070
|
+
prov.defaultModel = preset.defaultModel;
|
|
1044
1071
|
}
|
|
1045
|
-
changed = true;
|
|
1046
1072
|
}
|
|
1047
|
-
if (prov
|
|
1048
|
-
prov.liveModels = preset.liveModels;
|
|
1073
|
+
if (JSON.stringify(prov) !== beforeProvider) {
|
|
1049
1074
|
changed = true;
|
|
1075
|
+
touchedProviders.add(name);
|
|
1050
1076
|
}
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
return {
|
|
1080
|
+
config: projected,
|
|
1081
|
+
changed,
|
|
1082
|
+
touchedProviders: [...touchedProviders],
|
|
1083
|
+
touchedAntigravityVersion,
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function adoptOAuthReconciliation(config: OcxConfig, projection: OAuthReconcileProjection): void {
|
|
1088
|
+
for (const name of projection.touchedProviders) {
|
|
1089
|
+
const provider = projection.config.providers[name];
|
|
1090
|
+
if (provider) config.providers[name] = structuredClone(provider);
|
|
1091
|
+
else delete config.providers[name];
|
|
1092
|
+
}
|
|
1093
|
+
if (projection.touchedAntigravityVersion) {
|
|
1094
|
+
config.googleAntigravityStaticCatalogVersion = projection.config.googleAntigravityStaticCatalogVersion;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function withOAuthReconciliationTouchedKeys(
|
|
1099
|
+
projection: OAuthReconcileProjection,
|
|
1100
|
+
required: OAuthReconcileProjection,
|
|
1101
|
+
): OAuthReconcileProjection {
|
|
1102
|
+
return {
|
|
1103
|
+
...projection,
|
|
1104
|
+
touchedProviders: [...new Set([...projection.touchedProviders, ...required.touchedProviders])],
|
|
1105
|
+
touchedAntigravityVersion: projection.touchedAntigravityVersion || required.touchedAntigravityVersion,
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
export function reconcileOAuthProviders(config: OcxConfig, persist = true): boolean {
|
|
1110
|
+
const projection = projectOAuthProviderReconciliation(config);
|
|
1111
|
+
if (!projection.changed) return false;
|
|
1112
|
+
if (!persist) {
|
|
1113
|
+
adoptOAuthReconciliation(config, projection);
|
|
1114
|
+
return true;
|
|
1115
|
+
}
|
|
1116
|
+
if (persist) {
|
|
1117
|
+
const outcome = mutatePersistedConfig(fresh => {
|
|
1118
|
+
const next = projectOAuthProviderReconciliation(fresh);
|
|
1119
|
+
if (next.changed) adoptOAuthReconciliation(fresh, next);
|
|
1120
|
+
return { changed: next.changed, value: next };
|
|
1121
|
+
});
|
|
1122
|
+
if (outcome.status !== "unavailable") {
|
|
1123
|
+
adoptOAuthReconciliation(config, withOAuthReconciliationTouchedKeys(outcome.value, projection));
|
|
1058
1124
|
}
|
|
1059
1125
|
}
|
|
1060
|
-
|
|
1061
|
-
return changed;
|
|
1126
|
+
return true;
|
|
1062
1127
|
}
|
|
1063
1128
|
|
|
1064
1129
|
/** Runtime guards: provider config is intentionally passthrough, so persisted fields may be malformed. */
|
|
@@ -1091,24 +1156,19 @@ function preservableApiKeyPool(value: unknown): NonNullable<OcxProviderConfig["a
|
|
|
1091
1156
|
return pool.length > 0 ? pool : undefined;
|
|
1092
1157
|
}
|
|
1093
1158
|
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
* same content-derived id as the API-key manager when the active key was missing from the
|
|
1108
|
-
* pool). Key mode reflects stored user intent (explicit `"key"` or omitted mode with safe
|
|
1109
|
-
* key material) — never whether the login CLI process can resolve an env reference. Env-backed
|
|
1110
|
-
* availability is decided at proxy routing time in `router.ts`.
|
|
1111
|
-
*/
|
|
1159
|
+
const OAUTH_LOGIN_OWNED_PROVIDER_FIELDS = [
|
|
1160
|
+
"adapter",
|
|
1161
|
+
"baseUrl",
|
|
1162
|
+
"authMode",
|
|
1163
|
+
"headers",
|
|
1164
|
+
"apiKeyTransport",
|
|
1165
|
+
"responsesPath",
|
|
1166
|
+
"tlsProfile",
|
|
1167
|
+
"googleMode",
|
|
1168
|
+
"keyOptional",
|
|
1169
|
+
] as const satisfies readonly (keyof OcxProviderConfig)[];
|
|
1170
|
+
|
|
1171
|
+
/** Add/refresh only an OAuth provider's login-owned config fields (does not persist). */
|
|
1112
1172
|
export function upsertOAuthProvider(config: OcxConfig, provider: string): void {
|
|
1113
1173
|
if (provider === "chatgpt") return;
|
|
1114
1174
|
const def = OAUTH_PROVIDERS[provider];
|
|
@@ -1117,35 +1177,16 @@ export function upsertOAuthProvider(config: OcxConfig, provider: string): void {
|
|
|
1117
1177
|
const namespaceCollision = codexAccountNamespaceProviderCollisionError(config.codexAccountNamespaces, provider);
|
|
1118
1178
|
if (namespaceCollision) throw new Error(namespaceCollision);
|
|
1119
1179
|
const existing = config.providers[provider];
|
|
1120
|
-
const next: OcxProviderConfig =
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
next
|
|
1125
|
-
}
|
|
1126
|
-
// The Command Code protocol-version pin is an operator compatibility control. A re-login,
|
|
1127
|
-
// add-account, or reauth rebuilds the row from the preset, which has no version; carry the
|
|
1128
|
-
// existing pin so authentication changes do not silently revert the documented control.
|
|
1129
|
-
if (existing?.commandCodeVersion !== undefined) {
|
|
1130
|
-
next.commandCodeVersion = existing.commandCodeVersion;
|
|
1131
|
-
}
|
|
1132
|
-
if (existing?.projectContext !== undefined) {
|
|
1133
|
-
next.projectContext = existing.projectContext;
|
|
1134
|
-
}
|
|
1135
|
-
// User-configured price overlays are operator data, not preset state; a
|
|
1136
|
-
// re-login, add-account, or reauth must not silently drop them from the
|
|
1137
|
-
// Logs/Usage estimates.
|
|
1138
|
-
if (existing?.modelCosts !== undefined) {
|
|
1139
|
-
next.modelCosts = existing.modelCosts;
|
|
1140
|
-
}
|
|
1141
|
-
// The per-provider account-failover opt-out is operator intent about SPENDING, and the login
|
|
1142
|
-
// path is exactly where losing it does damage: adding a second account both rebuilds this row
|
|
1143
|
-
// from the preset and creates the 2-account quorum that turns presence-driven rotation on
|
|
1144
|
-
// (#2568d). Dropping the opt-out here would enable the thing the operator switched off, at the
|
|
1145
|
-
// moment they were doing something unrelated.
|
|
1146
|
-
if (existing?.oauthAccountFailover !== undefined) {
|
|
1147
|
-
next.oauthAccountFailover = existing.oauthAccountFailover;
|
|
1180
|
+
const next: OcxProviderConfig = structuredClone(existing ?? def.providerConfig);
|
|
1181
|
+
for (const field of OAUTH_LOGIN_OWNED_PROVIDER_FIELDS) {
|
|
1182
|
+
const value = def.providerConfig[field];
|
|
1183
|
+
if (value === undefined) delete next[field];
|
|
1184
|
+
else next[field] = structuredClone(value) as never;
|
|
1148
1185
|
}
|
|
1186
|
+
// OAuth-only providers must never retain credentials for a different auth mechanism.
|
|
1187
|
+
delete next.apiKey;
|
|
1188
|
+
delete next.apiKeyPool;
|
|
1189
|
+
delete next.azureCredential;
|
|
1149
1190
|
if (existing && getProviderRegistryEntry(provider)?.allowKeyAuthOverride === true) {
|
|
1150
1191
|
// Shared sanitizeApiKeyValue trim / no-CRLF checks from api-key pool writes.
|
|
1151
1192
|
let storedApiKey = sanitizeApiKeyValue(existing.apiKey);
|
|
@@ -1160,7 +1201,9 @@ export function upsertOAuthProvider(config: OcxConfig, provider: string): void {
|
|
|
1160
1201
|
// Keep routing and listProviderApiKeys in sync: never leave a hidden active key that
|
|
1161
1202
|
// is absent from the pool (listing would fall back to pool[0] as "active").
|
|
1162
1203
|
if (!pool.some(entry => entry.key === storedApiKey)) {
|
|
1163
|
-
|
|
1204
|
+
const id = apiKeyPoolEntryId(storedApiKey);
|
|
1205
|
+
if (pool.some(entry => entry.id === id)) throw new Error("API-key pool ID collision");
|
|
1206
|
+
pool.push({ id, key: storedApiKey });
|
|
1164
1207
|
}
|
|
1165
1208
|
next.apiKey = storedApiKey;
|
|
1166
1209
|
next.apiKeyPool = pool;
|
|
@@ -1175,7 +1218,8 @@ interface RunLoginDeps {
|
|
|
1175
1218
|
saveCredential?: typeof saveCredential;
|
|
1176
1219
|
saveAccountCredential?: typeof saveAccountCredential;
|
|
1177
1220
|
loadConfig?: typeof loadConfig;
|
|
1178
|
-
|
|
1221
|
+
mutatePersistedConfig?: typeof mutatePersistedConfig;
|
|
1222
|
+
initializePersistedConfigIfMissing?: typeof initializePersistedConfigIfMissing;
|
|
1179
1223
|
settleKiroLoginTransaction?: typeof settleKiroLoginTransaction;
|
|
1180
1224
|
removeAccount?: typeof removeAccount;
|
|
1181
1225
|
setActiveAccount?: typeof setActiveAccount;
|
|
@@ -1210,7 +1254,8 @@ export async function runLogin(
|
|
|
1210
1254
|
const def = OAUTH_PROVIDERS[provider];
|
|
1211
1255
|
if (!def) throw new UnsupportedOAuthProviderError(provider);
|
|
1212
1256
|
const loadLatestConfig = deps.loadConfig ?? loadConfig;
|
|
1213
|
-
const
|
|
1257
|
+
const mutateLatestConfig = deps.mutatePersistedConfig ?? mutatePersistedConfig;
|
|
1258
|
+
const initializeLatestConfig = deps.initializePersistedConfigIfMissing ?? initializePersistedConfigIfMissing;
|
|
1214
1259
|
if (provider !== "chatgpt") {
|
|
1215
1260
|
const preflightConfig = loadLatestConfig();
|
|
1216
1261
|
const namespaceCollision = codexAccountNamespaceProviderCollisionError(
|
|
@@ -1265,16 +1310,32 @@ export async function runLogin(
|
|
|
1265
1310
|
if (provider !== "chatgpt") {
|
|
1266
1311
|
// Re-run against post-credential state so same-provider API-key additions, removals,
|
|
1267
1312
|
// and active-key switches survive. A late namespace claim wins over provider creation.
|
|
1268
|
-
const
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1313
|
+
const publish = () => mutateLatestConfig<{ error: string } | { config: OcxConfig }>(fresh => {
|
|
1314
|
+
const lateCollision = codexAccountNamespaceProviderCollisionError(
|
|
1315
|
+
fresh.codexAccountNamespaces,
|
|
1316
|
+
provider,
|
|
1317
|
+
);
|
|
1318
|
+
if (lateCollision) return { changed: false, value: { error: lateCollision } };
|
|
1319
|
+
upsertOAuthProvider(fresh, provider);
|
|
1320
|
+
return { changed: true, value: { config: structuredClone(fresh) } };
|
|
1321
|
+
});
|
|
1322
|
+
let outcome = publish();
|
|
1323
|
+
let published = false;
|
|
1324
|
+
if (outcome.status === "unavailable" && outcome.reason === "missing") {
|
|
1325
|
+
const initial = loadLatestConfig();
|
|
1326
|
+
const lateCollision = codexAccountNamespaceProviderCollisionError(
|
|
1327
|
+
initial.codexAccountNamespaces,
|
|
1328
|
+
provider,
|
|
1329
|
+
);
|
|
1330
|
+
if (lateCollision) throw new OAuthProviderPublicationError();
|
|
1331
|
+
upsertOAuthProvider(initial, provider);
|
|
1332
|
+
const initialized = initializeLatestConfig(initial);
|
|
1333
|
+
published = initialized === "created";
|
|
1334
|
+
if (initialized === "exists") outcome = publish();
|
|
1335
|
+
}
|
|
1336
|
+
if (!published && (outcome.status === "unavailable" || "error" in outcome.value)) {
|
|
1274
1337
|
throw new OAuthProviderPublicationError();
|
|
1275
1338
|
}
|
|
1276
|
-
upsertOAuthProvider(latestConfig, provider);
|
|
1277
|
-
saveLatestConfig(latestConfig);
|
|
1278
1339
|
}
|
|
1279
1340
|
} catch (error) {
|
|
1280
1341
|
const errors: unknown[] = [error];
|