@bitkyc08/opencodex 2.68.0 → 2.69.0-preview.20260928
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 +25 -17
- package/gui/dist/assets/App-cDPyCb_V.js +51 -0
- package/gui/dist/assets/{Tray-Bn8WhKBH.js → Tray-H1IAB8mN.js} +1 -1
- package/gui/dist/assets/index-BF9QnKO_.css +1 -0
- package/gui/dist/assets/index-CI6YTp8z.js +86 -0
- package/gui/dist/assets/{quota-summary-DL-bNuNL.js → quota-summary-CutOlPu2.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/factory-droid.svg +8 -0
- package/package.json +1 -1
- package/src/adapters/anthropic-output-schema.ts +109 -11
- package/src/adapters/coding-agent/protocol.ts +106 -11
- package/src/adapters/coding-agent/turn.ts +23 -18
- package/src/adapters/google-errors.ts +16 -1
- package/src/adapters/google-http.ts +14 -0
- package/src/adapters/openai-chat/passthrough.ts +2 -0
- package/src/adapters/openai-chat/summary-budget.ts +72 -0
- package/src/adapters/openai-chat/tool-call-id-remint.ts +11 -2
- package/src/adapters/openai-chat.ts +6 -9
- package/src/bridge/response-json.ts +3 -1
- package/src/bridge/sse.ts +3 -1
- package/src/claude/desktop-picker.ts +6 -2
- package/src/claude/desktop-system-proxy.ts +155 -0
- package/src/claude/inbound-model-options.ts +10 -2
- package/src/claude/inbound.ts +1 -1
- package/src/claude/intercept/picker-ca-cleanup.ts +31 -0
- package/src/claude/intercept/picker-ca.ts +283 -83
- package/src/claude/intercept/picker-models.ts +4 -1
- package/src/claude/intercept/picker-trust.ts +15 -2
- package/src/claude/intercept/runtime.ts +99 -32
- package/src/cli/account-api.ts +5 -2
- package/src/cli/account-extended.ts +47 -11
- package/src/cli/account-target.ts +12 -7
- package/src/cli/account.ts +47 -3
- package/src/cli/capabilities.ts +33 -7
- package/src/cli/claude-agent-startup-sync.ts +2 -0
- package/src/cli/claude-desktop.ts +37 -13
- package/src/cli/cross-home-owner.ts +138 -0
- package/src/cli/dispatch.ts +1 -1
- package/src/cli/doctor.ts +18 -8
- package/src/cli/ensure-desired-integrations.ts +5 -0
- package/src/cli/export-command.ts +6 -6
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +15 -18
- package/src/cli/integrations.ts +8 -1
- package/src/cli/models-runtime-subcommands.ts +1 -0
- package/src/cli/models-runtime.ts +106 -4
- package/src/cli/models.ts +6 -1
- package/src/cli/opencode.ts +2 -83
- package/src/cli/registry.ts +3 -2
- package/src/cli/status.ts +86 -7
- package/src/client/connect.ts +24 -10
- package/src/client/link-ingress.ts +3 -1
- package/src/client/link-join.ts +76 -9
- package/src/client/link-relay-transport.ts +184 -0
- package/src/client/link-relay.ts +16 -4
- package/src/client/machine-listener.ts +2 -0
- package/src/clients/config-export/constants.ts +8 -0
- package/src/clients/config-export/contracts.ts +13 -1
- package/src/clients/config-export/droid.ts +71 -0
- package/src/clients/config-export/kilo.ts +116 -0
- package/src/clients/config-export.ts +33 -2
- package/src/codex/auth-api/routes.ts +1 -1
- package/src/codex/catalog/access-programs.ts +14 -2
- package/src/codex/catalog/provider-models.ts +1 -1
- package/src/codex/catalog-auto-refresh.ts +122 -0
- package/src/codex/config-drift-heal.ts +80 -0
- package/src/codex/desktop-switches.ts +7 -5
- package/src/codex/inject/bounded-config-reader.ts +69 -0
- package/src/codex/inject/config-toml.ts +31 -3
- package/src/codex/inject/routing-target.ts +6 -1
- package/src/codex/inject.ts +4 -0
- package/src/codex/model-entitlements.ts +32 -5
- package/src/codex/native-profile-startup.ts +17 -5
- package/src/codex/project-config-warnings.ts +72 -21
- package/src/codex/quota-auto-refresh-state.ts +8 -2
- package/src/codex/quota-auto-refresh.ts +80 -9
- package/src/codex/sync.ts +10 -0
- package/src/combos/failover.ts +4 -1
- package/src/combos/jev.ts +15 -4
- package/src/combos/types.ts +16 -0
- package/src/config/diagnostics.ts +19 -1
- package/src/config/load-degrade.ts +29 -0
- package/src/config/macos-system-proxy.ts +114 -0
- package/src/config/proxy-env.ts +67 -20
- package/src/config/schema/config-schema.ts +15 -0
- package/src/config/schema/leaf-validators.ts +23 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +220 -144
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/config-io.ts +12 -3
- package/src/integrations/droid-settings.ts +65 -0
- package/src/integrations/kilo-candidates.ts +30 -0
- package/src/integrations/mutation-plan.ts +62 -7
- package/src/integrations/native/ownership-preflight.ts +46 -12
- package/src/integrations/registry.ts +136 -7
- package/src/integrations/state.ts +52 -4
- package/src/integrations/target.ts +3 -0
- package/src/integrations/writer.ts +55 -14
- package/src/lib/account-selection-events.ts +14 -0
- package/src/lib/jsonc.ts +110 -0
- package/src/lib/standalone.ts +9 -1
- package/src/lib/windows-system-proxy.ts +101 -1
- package/src/link/relay-auth.ts +29 -0
- package/src/link/ssh-argv.ts +6 -3
- package/src/link/ssh-runner.ts +5 -5
- package/src/oauth/generic-account-failover.ts +52 -8
- package/src/oauth/index.ts +37 -14
- package/src/oauth/store.ts +96 -11
- package/src/oauth/token-guardian.ts +7 -3
- package/src/oauth/types.ts +2 -0
- package/src/plugins/loader.ts +5 -3
- package/src/providers/fastwire.ts +3 -0
- package/src/providers/key-failover.ts +13 -5
- package/src/providers/kiro-model-catalog.ts +4 -3
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/openai-tiers-destination.ts +9 -0
- package/src/providers/quota/account-cache.ts +17 -3
- package/src/providers/quota/vendor-probes-oauth.ts +2 -0
- package/src/providers/quota.ts +2 -2
- package/src/responses/code-mode-helper-compat.ts +14 -1
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/server/auth-cors.ts +1 -0
- package/src/server/images.ts +70 -20
- package/src/server/index/link-listener.ts +20 -3
- package/src/server/index/link-relay-sessions.ts +138 -0
- package/src/server/index/optional-listeners.ts +11 -1
- package/src/server/index/serve-options.ts +12 -0
- package/src/server/inference/client-encoder-delivery.ts +1 -0
- package/src/server/local-management-read-client.ts +21 -2
- package/src/server/management/agent-settings-routes.ts +19 -5
- package/src/server/management/combo-routes.ts +5 -3
- package/src/server/management/config-routes.ts +32 -6
- package/src/server/management/model-routes.ts +232 -7
- package/src/server/management/model-rows.ts +199 -1
- package/src/server/management/oauth-account-routes.ts +63 -3
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/port-reclaim.ts +170 -32
- package/src/server/responses/adapter-delivery.ts +6 -4
- package/src/server/responses/adapter-dispatch.ts +72 -2
- package/src/server/responses/core-combo.ts +6 -1
- package/src/server/responses/core-normalize.ts +19 -4
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/memory-models.ts +169 -0
- package/src/server/responses/passthrough-delivery.ts +4 -0
- package/src/server/responses/passthrough-dispatch.ts +10 -0
- package/src/server/responses/request-prepare.ts +86 -4
- package/src/server/responses/request-transport.ts +4 -0
- package/src/server/responses/run-turn-execution.ts +5 -2
- package/src/server/responses/shadow-target-availability.ts +17 -4
- package/src/server/responses/sidecar-execution.ts +18 -2
- package/src/server/responses/skills-snapshot.ts +239 -0
- package/src/server/responses-undeclared-tool-guard.ts +43 -6
- package/src/server/search.ts +1 -1
- package/src/service/guards.ts +4 -5
- package/src/service/state.ts +39 -2
- package/src/service.ts +2 -2
- package/src/types/config.ts +46 -4
- package/src/types/provider.ts +9 -0
- package/src/types/request.ts +6 -0
- package/src/types/tools.ts +38 -8
- package/src/types.ts +3 -0
- package/src/update/async-check.ts +5 -1
- package/src/update/index.ts +30 -12
- package/src/update/job.ts +3 -1
- package/src/update/pnpm-read-policy.d.mts +4 -0
- package/src/update/pnpm-read-policy.mjs +44 -0
- package/src/update/worker-launch.ts +152 -11
- package/src/usage/cost.ts +6 -3
- package/src/usage/log.ts +4 -0
- package/src/web-search/backends.ts +2 -2
- package/src/web-search/devin-executor.ts +1 -1
- package/src/web-search/sidecar-providers.ts +5 -5
- package/gui/dist/assets/App-mpsibsFj.js +0 -51
- package/gui/dist/assets/index-BDUBS8PW.css +0 -1
- package/gui/dist/assets/index-kUE8tuAK.js +0 -86
|
@@ -7,6 +7,8 @@ import { classifyInterceptClient, interceptRouteFor } from "./client-class";
|
|
|
7
7
|
import { CLAUDE_INTERCEPT_HOSTS, isBrowserConnect, startConnectProxy, type ConnectProxyHandle } from "./connect-proxy";
|
|
8
8
|
import { startClaudeInterceptListener } from "./listener";
|
|
9
9
|
import { claudeInterceptCaCertPath, ensureLocalInterceptCaForStartup, issueLocalInterceptLeaf } from "./local-ca";
|
|
10
|
+
import { discardPickerCaKey, ensurePickerCa } from "./picker-ca";
|
|
11
|
+
import { drainPendingPickerCaUntrust } from "./picker-ca-cleanup";
|
|
10
12
|
import type { PickerRouteInput } from "./picker-models";
|
|
11
13
|
import { createPickerRuntime, type CreatePickerRuntimeOptions, type PickerRuntime } from "./picker-runtime";
|
|
12
14
|
import type { SecurityRunner } from "./picker-trust";
|
|
@@ -117,6 +119,8 @@ export interface StartClaudeInterceptOptions<T> {
|
|
|
117
119
|
loadPickerRoutes?: () => Promise<PickerRouteInput>;
|
|
118
120
|
/** Test seam: builds the picker runtime. */
|
|
119
121
|
createPicker?: (options: CreatePickerRuntimeOptions) => PickerRuntime;
|
|
122
|
+
/** Test seam: bind real CONNECT handlers on kernel-assigned ports without probe-and-release races. */
|
|
123
|
+
startProxy?: typeof startConnectProxy;
|
|
120
124
|
/** Test seams: the macOS `security` runner and platform for the picker runtime and controller. */
|
|
121
125
|
pickerSecurity?: SecurityRunner;
|
|
122
126
|
pickerPlatform?: NodeJS.Platform;
|
|
@@ -127,11 +131,16 @@ export interface StartClaudeInterceptOptions<T> {
|
|
|
127
131
|
* rejecting; callers treat it as a degraded optional integration, never as a startup failure.
|
|
128
132
|
*/
|
|
129
133
|
export async function startClaudeIntercept<T>(options: StartClaudeInterceptOptions<T>): Promise<ClaudeInterceptHandle<T> | null> {
|
|
134
|
+
// An older release may have left the picker's exportable signing key on disk. Remove it on every
|
|
135
|
+
// start, even when the intercept or the picker is off, so an upgrade with the picker disabled
|
|
136
|
+
// does not keep the key of a CA that may still be trusted. Failure here must not block startup.
|
|
137
|
+
try { discardPickerCaKey(options.configDir ?? getConfigDir()); } catch { /* retried on the next start */ }
|
|
130
138
|
if (!claudeInterceptEnabled(options.config)) return null;
|
|
131
139
|
const explicitPort = typeof options.config.claudeCode?.intercept?.port === "number";
|
|
132
140
|
if (options.requestedPort === 0 && !explicitPort) return null;
|
|
133
141
|
const configDir = options.configDir ?? getConfigDir();
|
|
134
142
|
const ca = await ensureLocalInterceptCaForStartup(configDir);
|
|
143
|
+
const startProxy = options.startProxy ?? startConnectProxy;
|
|
135
144
|
const authToken = ensureClaudeInterceptProxyToken(configDir);
|
|
136
145
|
const leaf = issueLocalInterceptLeaf(ca, CLAUDE_INTERCEPT_HOSTS);
|
|
137
146
|
// Refresh an env we already own (e.g. a pre-auth proxy URL left by an upgrade) before the
|
|
@@ -156,7 +165,7 @@ export async function startClaudeIntercept<T>(options: StartClaudeInterceptOptio
|
|
|
156
165
|
});
|
|
157
166
|
let proxy: ConnectProxyHandle;
|
|
158
167
|
try {
|
|
159
|
-
proxy = await
|
|
168
|
+
proxy = await startProxy(claudeInterceptProxyPort(options.config, options.publicPort), {
|
|
160
169
|
interceptPort: listener.port!,
|
|
161
170
|
// A real apply may recreate a missing token while this listener remains live.
|
|
162
171
|
// Read current validated authority per CONNECT; absent/invalid means deny, not mint.
|
|
@@ -173,7 +182,52 @@ export async function startClaudeIntercept<T>(options: StartClaudeInterceptOptio
|
|
|
173
182
|
let pickerProxyLive = false;
|
|
174
183
|
try {
|
|
175
184
|
if (options.loadPickerRoutes) {
|
|
176
|
-
picker
|
|
185
|
+
// A picker authority is process-scoped, and older releases persisted an exportable ca.key
|
|
186
|
+
// next to the published certificate. Drop that key before anything else: a cleanup failure
|
|
187
|
+
// below must never leave a signing key on disk that outlives this process.
|
|
188
|
+
discardPickerCaKey(configDir);
|
|
189
|
+
const { inspectDesktopPickerProfile } = await import("../desktop-picker-profile");
|
|
190
|
+
// The applied profile row is durable evidence of the user's picker selection. Rotation keeps
|
|
191
|
+
// it in place — row id and its recorded previous selection included — so the picker proxy
|
|
192
|
+
// can fail to bind without losing it, and the restore enable below only updates the proxy
|
|
193
|
+
// URL inside the same row rather than recreating a selection around a placeholder pivot.
|
|
194
|
+
const pickerProfile = inspectDesktopPickerProfile({
|
|
195
|
+
configDir,
|
|
196
|
+
...(options.pickerPlatform ? { platform: options.pickerPlatform } : {}),
|
|
197
|
+
});
|
|
198
|
+
const pickerProfileApplied = pickerProfile.kind === "applied";
|
|
199
|
+
let pickerBlocked = false;
|
|
200
|
+
try {
|
|
201
|
+
// A prior process may have died after publishing the journal but before cleanup. Finish
|
|
202
|
+
// that entry before rotation can replace it, then drain the newly queued predecessor.
|
|
203
|
+
const drain = () => drainPendingPickerCaUntrust(configDir, options.pickerSecurity, options.pickerPlatform);
|
|
204
|
+
if (!await drain()) pickerBlocked = true;
|
|
205
|
+
if (!pickerBlocked) {
|
|
206
|
+
ensurePickerCa(configDir, { rotation: "startup" });
|
|
207
|
+
if (!await drain()) pickerBlocked = true;
|
|
208
|
+
}
|
|
209
|
+
} catch (error) {
|
|
210
|
+
pickerBlocked = true;
|
|
211
|
+
console.warn(`⚠ Claude Desktop picker CA cleanup deferred: ${error instanceof Error ? error.message : String(error)}`);
|
|
212
|
+
}
|
|
213
|
+
if (pickerBlocked) {
|
|
214
|
+
console.warn("⚠ Claude Desktop picker disabled: the previous certificate could not be untrusted");
|
|
215
|
+
if (pickerProfile.kind === "applied") {
|
|
216
|
+
// Keep Desktop's actual pinned egress alive without ever constructing a TLS terminator.
|
|
217
|
+
const port = Number(new URL(pickerProfile.proxyUrl).port);
|
|
218
|
+
try {
|
|
219
|
+
pickerProxy = await startProxy(port, {
|
|
220
|
+
interceptPort: listener.port!,
|
|
221
|
+
interceptHosts: [],
|
|
222
|
+
selectTunnel: () => ({ kind: "blind" }),
|
|
223
|
+
});
|
|
224
|
+
pickerProxyLive = true;
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.warn(`⚠ Claude Desktop blind egress relay could not start: ${error instanceof Error ? error.message : String(error)}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
picker = pickerBlocked ? null : (options.createPicker ?? createPickerRuntime)({
|
|
177
231
|
config: options.config,
|
|
178
232
|
configDir,
|
|
179
233
|
loadRoutes: options.loadPickerRoutes,
|
|
@@ -184,35 +238,39 @@ export async function startClaudeIntercept<T>(options: StartClaudeInterceptOptio
|
|
|
184
238
|
...(options.pickerSecurity ? { security: options.pickerSecurity } : {}),
|
|
185
239
|
...(options.pickerPlatform ? { platform: options.pickerPlatform } : {}),
|
|
186
240
|
});
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
241
|
+
if (picker) {
|
|
242
|
+
const runtime = picker;
|
|
243
|
+
const interceptPort = listener.port!;
|
|
244
|
+
try {
|
|
245
|
+
pickerProxy = await startProxy(claudePickerProxyPort(options.config, options.publicPort), {
|
|
246
|
+
interceptPort,
|
|
247
|
+
// No authToken: Desktop's egressProxyUrl cannot present proxy credentials, so this
|
|
248
|
+
// listener stays an unauthenticated loopback relay until the profile format can carry
|
|
249
|
+
// one. The intercept proxy above is the credential-bearing hop.
|
|
250
|
+
// No host list here: the choice below depends on which client opened the tunnel.
|
|
251
|
+
interceptHosts: [],
|
|
252
|
+
selectTunnel: (host, port, request) => {
|
|
253
|
+
// Desktop hands its pinned egress proxy to the Claude Code processes it spawns, so their
|
|
254
|
+
// api.anthropic.com traffic arrives here too and gets the same intercept as on the Claude
|
|
255
|
+
// Code proxy. Those processes trust only the intercept CA, so the picker never terminates
|
|
256
|
+
// their claude.ai tunnels; only the app's own (browser) CONNECTs reach the picker.
|
|
257
|
+
if (!isBrowserConnect(request)) {
|
|
258
|
+
return port === 443 && (CLAUDE_INTERCEPT_HOSTS as readonly string[]).includes(host.toLowerCase())
|
|
259
|
+
? { kind: "intercept", port: interceptPort }
|
|
260
|
+
: { kind: "blind" };
|
|
261
|
+
}
|
|
262
|
+
return runtime.selectTunnel(host, port);
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
pickerProxyLive = true;
|
|
266
|
+
} catch (error) {
|
|
267
|
+
// Picker mode is optional: a busy port leaves the intercept pair running without it.
|
|
268
|
+
// The selected profile row survives, so the next startup retries the restore once the
|
|
269
|
+
// port is free again.
|
|
270
|
+
console.warn(`⚠ Claude Desktop picker proxy could not start: ${error instanceof Error ? error.message : String(error)}`);
|
|
271
|
+
await runtime.stop();
|
|
272
|
+
picker = null;
|
|
273
|
+
}
|
|
216
274
|
}
|
|
217
275
|
if (picker) {
|
|
218
276
|
// Dynamic: the controller reaches desktop-first-party, which imports this module.
|
|
@@ -232,6 +290,15 @@ export async function startClaudeIntercept<T>(options: StartClaudeInterceptOptio
|
|
|
232
290
|
...(options.pickerPlatform ? { platform: options.pickerPlatform } : {}),
|
|
233
291
|
});
|
|
234
292
|
await picker.start();
|
|
293
|
+
// The rotated authority still needs the user's consent in the login keychain — a decline
|
|
294
|
+
// reports trust_pending, and the picker stays a blind tunnel until trust is granted. When
|
|
295
|
+
// Desktop was pinned to the picker before the restart, the regular enable flow re-trusts
|
|
296
|
+
// the new authority and rewrites the profile row in place, so a restart does not silently
|
|
297
|
+
// turn the picker off.
|
|
298
|
+
if (pickerProfileApplied && controller) {
|
|
299
|
+
void controller.enable({ persist: false, context: "server" })
|
|
300
|
+
.catch(error => console.warn(`⚠ Claude Desktop picker restore failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
301
|
+
}
|
|
235
302
|
}
|
|
236
303
|
}
|
|
237
304
|
} catch (error) {
|
|
@@ -248,7 +315,7 @@ export async function startClaudeIntercept<T>(options: StartClaudeInterceptOptio
|
|
|
248
315
|
const state: ClaudeInterceptState = {
|
|
249
316
|
proxyPort: proxy.port,
|
|
250
317
|
caCertPath: claudeInterceptCaCertPath(configDir),
|
|
251
|
-
pickerProxyPort:
|
|
318
|
+
pickerProxyPort: pickerProxy?.port ?? null,
|
|
252
319
|
};
|
|
253
320
|
activeState = state;
|
|
254
321
|
activePicker = picker;
|
package/src/cli/account-api.ts
CHANGED
|
@@ -28,7 +28,7 @@ export interface AccountRow {
|
|
|
28
28
|
active: boolean;
|
|
29
29
|
needsReauth?: boolean;
|
|
30
30
|
autoSelectable?: boolean;
|
|
31
|
-
skipReason?: "needs_reauth" | "suspended" | "cooldown" | "quota_exhausted";
|
|
31
|
+
skipReason?: "paused" | "needs_reauth" | "suspended" | "cooldown" | "quota_exhausted";
|
|
32
32
|
selectionExcludedReason?: "plan_excluded";
|
|
33
33
|
selectionExcludedPlan?: string;
|
|
34
34
|
/** Registered credential that is still excluded from routing until validation completes. */
|
|
@@ -345,6 +345,8 @@ interface OAuthAccountDto {
|
|
|
345
345
|
email?: string;
|
|
346
346
|
active?: boolean;
|
|
347
347
|
needsReauth?: boolean;
|
|
348
|
+
/** Present only for providers that support operator pause (generic OAuth pools). */
|
|
349
|
+
paused?: boolean;
|
|
348
350
|
autoSelectable?: boolean;
|
|
349
351
|
skipReason?: unknown;
|
|
350
352
|
/** Always sent by the management route; explicitly `null` when the tier is unknown. */
|
|
@@ -355,7 +357,7 @@ interface OAuthAccountDto {
|
|
|
355
357
|
}
|
|
356
358
|
|
|
357
359
|
function isKiroSkipReason(value: unknown): value is NonNullable<AccountRow["skipReason"]> {
|
|
358
|
-
return value === "needs_reauth" || value === "suspended"
|
|
360
|
+
return value === "paused" || value === "needs_reauth" || value === "suspended"
|
|
359
361
|
|| value === "cooldown" || value === "quota_exhausted";
|
|
360
362
|
}
|
|
361
363
|
|
|
@@ -385,6 +387,7 @@ async function fetchOAuthRows(
|
|
|
385
387
|
email: a.email,
|
|
386
388
|
active: a.active ?? a.id === activeId,
|
|
387
389
|
needsReauth: a.needsReauth,
|
|
390
|
+
...(a.paused === true ? { paused: true } : {}),
|
|
388
391
|
...(name === "kiro" && typeof a.autoSelectable === "boolean"
|
|
389
392
|
? { autoSelectable: a.autoSelectable } : {}),
|
|
390
393
|
...(name === "kiro" && a.autoSelectable === false && isKiroSkipReason(a.skipReason)
|
|
@@ -772,14 +772,21 @@ export async function cmdPriority(args: string[], deps: AccountDeps): Promise<nu
|
|
|
772
772
|
return 0;
|
|
773
773
|
}
|
|
774
774
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
775
|
+
function resolveGenericOAuthPauseTarget(accounts: unknown[], requested: string): { id: string } | { error: string } {
|
|
776
|
+
const rows = accounts.filter((value): value is { id: string; alias?: unknown } =>
|
|
777
|
+
typeof value === "object" && value !== null && typeof (value as { id?: unknown }).id === "string",
|
|
778
|
+
);
|
|
779
|
+
if (rows.some(account => account.id === requested)) return { id: requested };
|
|
780
|
+
const exact = rows.filter(account => account.alias === requested);
|
|
781
|
+
const matches = exact.length > 0
|
|
782
|
+
? exact
|
|
783
|
+
: rows.filter(account => typeof account.alias === "string" && account.alias.toLowerCase() === requested.toLowerCase());
|
|
784
|
+
if (matches.length === 1) return { id: matches[0]!.id };
|
|
785
|
+
if (matches.length > 1) return { error: `alias "${requested}" names ${matches.length} accounts; use the account id` };
|
|
786
|
+
return { error: `Account not found: no OAuth account has the id or alias "${requested}"` };
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/** Pause or resume a Codex account or a generic OAuth provider account. */
|
|
783
790
|
export async function cmdPause(args: string[], deps: AccountDeps, paused: boolean): Promise<number> {
|
|
784
791
|
const wantsJson = flag(args, "--json");
|
|
785
792
|
const name = args.shift();
|
|
@@ -788,11 +795,40 @@ export async function cmdPause(args: string[], deps: AccountDeps, paused: boolea
|
|
|
788
795
|
if (!name || !requestedId || args.length) return usage();
|
|
789
796
|
const classified = configAndType(deps, name);
|
|
790
797
|
if ("error" in classified) return usage(`Error: ${classified.error}`);
|
|
791
|
-
if (classified.type !== "codex") {
|
|
792
|
-
return usage(`Error: ${verb} applies to the openai Codex account pool`);
|
|
793
|
-
}
|
|
794
798
|
const baseUrl = await resolveBaseUrl(deps);
|
|
795
799
|
if (!baseUrl) return proxyUnreachable();
|
|
800
|
+
|
|
801
|
+
if (classified.type === "oauth") {
|
|
802
|
+
if (name === "anthropic") return usage(`Error: ${verb} is not supported for the Anthropic OAuth pool`);
|
|
803
|
+
const list = await apiJson(deps, baseUrl, "GET", `/api/oauth/accounts?provider=${encodeURIComponent(name)}`);
|
|
804
|
+
if (list.status === 0) return proxyUnreachable(list.transportError);
|
|
805
|
+
if (list.status !== 200) return apiError(list.json, `failed to list ${name} OAuth accounts`, list.status);
|
|
806
|
+
const target = resolveGenericOAuthPauseTarget(Array.isArray(list.json.accounts) ? list.json.accounts : [], requestedId);
|
|
807
|
+
if ("error" in target) return usage(`Error: ${target.error}`);
|
|
808
|
+
|
|
809
|
+
const response = await apiJson(deps, baseUrl, "PUT", "/api/oauth/accounts/pause", {
|
|
810
|
+
provider: name,
|
|
811
|
+
accountId: target.id,
|
|
812
|
+
paused,
|
|
813
|
+
});
|
|
814
|
+
if (response.status === 0) return proxyUnreachable(response.transportError);
|
|
815
|
+
if (response.status !== 200) return apiError(response.json, `failed to ${verb} ${requestedId}`, response.status);
|
|
816
|
+
|
|
817
|
+
if (wantsJson) {
|
|
818
|
+
console.log(JSON.stringify({ ok: true, provider: name, id: target.id, paused,
|
|
819
|
+
activeAccountId: response.json.activeAccountId }, null, 2));
|
|
820
|
+
} else {
|
|
821
|
+
console.log(`${name}: ${requestedId} ${paused ? "paused" : "resumed"}`);
|
|
822
|
+
if (response.json.activeAccountChanged === true) {
|
|
823
|
+
console.error(`Active account changed to ${String(response.json.activeAccountId)}.`);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return 0;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
if (classified.type !== "codex") {
|
|
830
|
+
return usage(`Error: ${verb} applies to the openai Codex account pool or a generic OAuth provider`);
|
|
831
|
+
}
|
|
796
832
|
const target = await resolveCodexAccountTarget(deps, baseUrl, requestedId);
|
|
797
833
|
if ("networkDown" in target) return proxyUnreachable(target.transportError);
|
|
798
834
|
if ("error" in target) return reportCodexAccountTargetError(target);
|
|
@@ -15,7 +15,7 @@ const MAIN_ALIAS = "main";
|
|
|
15
15
|
|
|
16
16
|
export type CodexAccountTarget =
|
|
17
17
|
| { id: string }
|
|
18
|
-
| { error: string; kind: "not_found" | "ambiguous" | "reserved" }
|
|
18
|
+
| { error: string; kind: "not_found" | "ambiguous" | "reserved" | "unavailable" }
|
|
19
19
|
| { networkDown: true; transportError?: string };
|
|
20
20
|
|
|
21
21
|
/** Built-in selectors cannot be reused as Codex account aliases. */
|
|
@@ -35,18 +35,21 @@ export async function resolveCodexAccountTarget(
|
|
|
35
35
|
requested: string,
|
|
36
36
|
): Promise<CodexAccountTarget> {
|
|
37
37
|
if (requested === MAIN_ALIAS || requested === MAIN_CODEX_ACCOUNT_ID) return { id: MAIN_CODEX_ACCOUNT_ID };
|
|
38
|
-
if (isReservedCodexAccountWord(requested)) {
|
|
39
|
-
return { error: `"${requested}" is reserved; it clears the selection with \`ocx account use\` and names no account`, kind: "reserved" };
|
|
40
|
-
}
|
|
41
38
|
const res = await apiJson(deps, baseUrl, "GET", "/api/codex-auth/accounts");
|
|
42
39
|
if (res.status === 0) return { networkDown: true, transportError: res.transportError };
|
|
43
40
|
// The list is only needed to turn an alias into an id. If the proxy cannot produce it, send
|
|
44
41
|
// the argument as the id it may already be and let the route answer, as the CLI always did.
|
|
45
|
-
if (res.status !== 200)
|
|
42
|
+
if (res.status !== 200) {
|
|
43
|
+
if (!isReservedCodexAccountWord(requested)) return { id: requested };
|
|
44
|
+
return { error: `Cannot safely resolve reserved account selector "${requested}" while the account list is unavailable`, kind: "unavailable" };
|
|
45
|
+
}
|
|
46
46
|
const accounts = (Array.isArray(res.json.accounts) ? res.json.accounts : [])
|
|
47
47
|
.filter((entry): entry is { id: string; alias?: unknown } =>
|
|
48
48
|
typeof entry === "object" && entry !== null && typeof (entry as { id?: unknown }).id === "string");
|
|
49
49
|
if (accounts.some(account => account.id === requested)) return { id: requested };
|
|
50
|
+
if (isReservedCodexAccountWord(requested)) {
|
|
51
|
+
return { error: `"${requested}" is reserved; it clears the selection with \`ocx account use\` and names no account`, kind: "reserved" };
|
|
52
|
+
}
|
|
50
53
|
const exact = accounts.filter(account => account.alias === requested);
|
|
51
54
|
const matches = exact.length > 0
|
|
52
55
|
? exact
|
|
@@ -69,10 +72,12 @@ export async function resolveCodexUseTarget(
|
|
|
69
72
|
baseUrl: string,
|
|
70
73
|
requested: string,
|
|
71
74
|
): Promise<CodexUseTarget> {
|
|
72
|
-
if (requested === AUTO_ACCOUNT_ARGUMENT) return { accountId: null };
|
|
73
75
|
const target = await resolveCodexAccountTarget(deps, baseUrl, requested);
|
|
74
76
|
if ("networkDown" in target) return target;
|
|
75
|
-
if ("error" in target)
|
|
77
|
+
if ("error" in target) {
|
|
78
|
+
if (requested === AUTO_ACCOUNT_ARGUMENT && target.kind === "reserved") return { accountId: null };
|
|
79
|
+
return target;
|
|
80
|
+
}
|
|
76
81
|
return { accountId: target.id };
|
|
77
82
|
}
|
|
78
83
|
|
package/src/cli/account.ts
CHANGED
|
@@ -45,6 +45,7 @@ const ACCOUNT_USAGE = `Usage:
|
|
|
45
45
|
ocx account history openai <pool-account-id> [--limit <1-200>] [--json]
|
|
46
46
|
ocx account current <provider> [--json]
|
|
47
47
|
ocx account use <provider> <account-or-key-id|alias|main|auto> [--json]
|
|
48
|
+
ocx account clear <provider> [--json]
|
|
48
49
|
ocx account refresh <provider> [--json]
|
|
49
50
|
ocx account auto-switch <provider> <on|off|status|threshold <0-100>> [--json]
|
|
50
51
|
ocx account alias <provider> <account-or-key-id|alias> <display-name|-> [--json]
|
|
@@ -68,8 +69,10 @@ const ACCOUNT_USAGE = `Usage:
|
|
|
68
69
|
|
|
69
70
|
List and switch provider accounts and API-key pools (masked output only).
|
|
70
71
|
'main' selects the Codex App login for the openai account pool; 'auto' clears the
|
|
71
|
-
selection so the pool places work by its own strategy
|
|
72
|
-
|
|
72
|
+
selection so the pool places work by its own strategy — unless an account actually
|
|
73
|
+
carries that id, which wins, so 'ocx account clear' is the spelling that always
|
|
74
|
+
clears. A Codex account can be named by the alias set with 'ocx account alias'
|
|
75
|
+
wherever an id is accepted.`;
|
|
73
76
|
|
|
74
77
|
function consumeFlag(args: string[], flag: string): boolean {
|
|
75
78
|
const idx = args.indexOf(flag);
|
|
@@ -105,7 +108,8 @@ function statusText(row: AccountRow): string {
|
|
|
105
108
|
if (row.paused) parts.push("paused");
|
|
106
109
|
if (row.active) parts.push(row.type === "codex" ? "selected" : "active");
|
|
107
110
|
if (row.needsReauth && !(row.provider === "kiro" && row.skipReason === "needs_reauth")) parts.push("needs-reauth");
|
|
108
|
-
|
|
111
|
+
// A paused Kiro row already says "paused"; repeating it as a skip reason adds nothing.
|
|
112
|
+
if (row.provider === "kiro" && row.autoSelectable === false && !(row.paused && row.skipReason === "paused"))
|
|
109
113
|
parts.push(row.skipReason ? `not-auto-selected(${row.skipReason})` : "not-auto-selected");
|
|
110
114
|
if (row.validationPending) parts.push("validation-pending");
|
|
111
115
|
if (row.selectionExcludedReason === "plan_excluded") {
|
|
@@ -352,6 +356,45 @@ async function cmdUse(rest: string[], deps: AccountDeps): Promise<number> {
|
|
|
352
356
|
return 0;
|
|
353
357
|
}
|
|
354
358
|
|
|
359
|
+
/** `ocx account clear` never resolves its argument as an account id, so an account literally
|
|
360
|
+
* named `auto` cannot shadow the verb that returns the pool to automatic selection. */
|
|
361
|
+
async function cmdClear(rest: string[], deps: AccountDeps): Promise<number> {
|
|
362
|
+
const wantsJson = consumeFlag(rest, "--json");
|
|
363
|
+
const name = rest.shift();
|
|
364
|
+
const leftover = leftoverArgsError(rest);
|
|
365
|
+
if (!name || leftover) {
|
|
366
|
+
if (leftover) console.error(leftover);
|
|
367
|
+
console.error(ACCOUNT_USAGE);
|
|
368
|
+
return 1;
|
|
369
|
+
}
|
|
370
|
+
const config = deps.loadConfigImpl?.() ?? loadConfig();
|
|
371
|
+
const c = classifyAccount(config, name);
|
|
372
|
+
if ("error" in c) {
|
|
373
|
+
console.error(`Error: ${c.error}. Known candidates: ${candidateNames(config)}`);
|
|
374
|
+
return 1;
|
|
375
|
+
}
|
|
376
|
+
if (c.type !== "codex") {
|
|
377
|
+
console.error(`Error: ${name} has no automatic-selection pin to clear; clear applies to Codex account pools`);
|
|
378
|
+
return 1;
|
|
379
|
+
}
|
|
380
|
+
const baseUrl = await resolveBaseUrl(deps);
|
|
381
|
+
if (!baseUrl) return proxyUnreachable();
|
|
382
|
+
const res = await apiJson(deps, baseUrl, "PUT", "/api/codex-auth/active", { accountId: null });
|
|
383
|
+
if (res.status === 0) return proxyUnreachable(res.transportError);
|
|
384
|
+
if (res.status !== 200) return apiError(res.json, `failed to clear ${name}`, res.status);
|
|
385
|
+
const pinDrainReason = typeof res.json.pinDrainReason === "string" ? res.json.pinDrainReason : undefined;
|
|
386
|
+
if (wantsJson) {
|
|
387
|
+
console.log(JSON.stringify({
|
|
388
|
+
ok: true, provider: name, type: c.type, activeId: null,
|
|
389
|
+
...(pinDrainReason !== undefined ? { pinDrained: true, pinDrainReason } : {}),
|
|
390
|
+
}, null, 2));
|
|
391
|
+
} else {
|
|
392
|
+
console.log(`${name}: automatic account selection (pin cleared)`);
|
|
393
|
+
}
|
|
394
|
+
await explainCodexUseOutcome(deps, baseUrl, name, null, pinDrainReason);
|
|
395
|
+
return 0;
|
|
396
|
+
}
|
|
397
|
+
|
|
355
398
|
export async function cmdAccount(args: string[], deps: AccountDeps = {}): Promise<number> {
|
|
356
399
|
const [sub, ...rest] = args;
|
|
357
400
|
try {
|
|
@@ -362,6 +405,7 @@ export async function cmdAccount(args: string[], deps: AccountDeps = {}): Promis
|
|
|
362
405
|
}
|
|
363
406
|
if (sub === "current") return await cmdCurrent(rest, deps);
|
|
364
407
|
if (sub === "use") return await cmdUse(rest, deps);
|
|
408
|
+
if (sub === "clear") return await cmdClear(rest, deps);
|
|
365
409
|
if (sub === "refresh") return await cmdRefresh(rest, deps);
|
|
366
410
|
if (sub === "auto-switch") return await cmdAutoSwitch(rest, deps);
|
|
367
411
|
if (sub === "alias" || sub === "rename") return await cmdAlias(rest, deps);
|
package/src/cli/capabilities.ts
CHANGED
|
@@ -240,6 +240,25 @@ export const CAPABILITIES: readonly Capability[] = [
|
|
|
240
240
|
json: "payload",
|
|
241
241
|
details: ["Uses the exact upstream model ID after the first slash. Omitted cache rates default to zero; sibling model prices are preserved."],
|
|
242
242
|
},
|
|
243
|
+
{
|
|
244
|
+
command: ["models", "set"],
|
|
245
|
+
summary: "Save per-model overrides for a routed model, or clear them back to the computed values.",
|
|
246
|
+
routes: [{ method: "PUT", path: "/api/model-settings" }],
|
|
247
|
+
flags: [
|
|
248
|
+
{ name: "--context-window", value: "string", summary: "Context window in tokens; 0 or - clears the override." },
|
|
249
|
+
{ name: "--modalities", value: "string", summary: "Comma-separated text,image,audio; - clears the override." },
|
|
250
|
+
{ name: "--reasoning-efforts", value: "string", summary: "Comma-separated ladder; \"\" for no reasoning, - to inherit." },
|
|
251
|
+
{ name: "--default-reasoning-effort", value: "string", summary: "Ladder member a request inherits when it omits one; - to inherit." },
|
|
252
|
+
{ name: "--reset", value: "boolean", summary: "Clear every override on this model; cannot be combined with the options above." },
|
|
253
|
+
{ name: "--json", value: "boolean", summary: "Emit the saved state as JSON." },
|
|
254
|
+
],
|
|
255
|
+
mutates: true,
|
|
256
|
+
json: "envelope",
|
|
257
|
+
details: [
|
|
258
|
+
"Addresses a routed model as provider/model. The native openai lane and combos have no per-model overrides.",
|
|
259
|
+
"Unlike ocx models edit, which changes a custom model's own definition, this edits a row that already exists.",
|
|
260
|
+
],
|
|
261
|
+
},
|
|
243
262
|
{
|
|
244
263
|
command: ["status"],
|
|
245
264
|
summary: "Proxy status, injection state, and version skew between this CLI and the running proxy.",
|
|
@@ -522,21 +541,28 @@ export const CAPABILITIES: readonly Capability[] = [
|
|
|
522
541
|
},
|
|
523
542
|
{
|
|
524
543
|
command: ["account", "pause"],
|
|
525
|
-
summary: "
|
|
526
|
-
//
|
|
527
|
-
routes: [
|
|
544
|
+
summary: "Exclude one account in a Codex or supported generic OAuth pool from automatic selection.",
|
|
545
|
+
// Resume uses the same endpoints with `paused: false`.
|
|
546
|
+
routes: [
|
|
547
|
+
{ method: "PUT", path: "/api/codex-auth/accounts/pause" },
|
|
548
|
+
{ method: "GET", path: "/api/oauth/accounts" },
|
|
549
|
+
{ method: "PUT", path: "/api/oauth/accounts/pause" },
|
|
550
|
+
],
|
|
528
551
|
flags: [{ name: "--json", value: "boolean", summary: "Emit the pause result as JSON." }],
|
|
529
552
|
mutates: true,
|
|
530
553
|
json: "envelope",
|
|
531
554
|
details: [
|
|
532
|
-
"
|
|
533
|
-
"The issue that requested this reported the route as POST; it is PUT.",
|
|
555
|
+
"Codex pause unbinds pinned threads and selects a fallback when possible; with no fallback, a paused-but-selected Codex account still receives requests. Generic OAuth pause never dispatches to that account: it is excluded from new requests, failover and refresh, and an all-paused pool answers 403. Anthropic is unsupported.",
|
|
534
556
|
],
|
|
535
557
|
},
|
|
536
558
|
{
|
|
537
559
|
command: ["account", "resume"],
|
|
538
|
-
summary: "Return a paused account to
|
|
539
|
-
routes: [
|
|
560
|
+
summary: "Return a paused account to a Codex or supported generic OAuth pool.",
|
|
561
|
+
routes: [
|
|
562
|
+
{ method: "PUT", path: "/api/codex-auth/accounts/pause" },
|
|
563
|
+
{ method: "GET", path: "/api/oauth/accounts" },
|
|
564
|
+
{ method: "PUT", path: "/api/oauth/accounts/pause" },
|
|
565
|
+
],
|
|
540
566
|
flags: [{ name: "--json", value: "boolean", summary: "Emit the resume result as JSON." }],
|
|
541
567
|
mutates: true,
|
|
542
568
|
json: "envelope",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OcxConfig } from "../types";
|
|
2
|
+
import { siblingOfLivePort } from "../codex/sibling-start";
|
|
2
3
|
import { injectClaudeAgentDefs } from "../claude/agents-inject";
|
|
3
4
|
import { readCachedHubState } from "../client/hub-state";
|
|
4
5
|
import { fetchClaudeContextWindows } from "./claude";
|
|
@@ -77,6 +78,7 @@ export async function syncClaudeAgentDefsAtProxyStartup(
|
|
|
77
78
|
port: number,
|
|
78
79
|
deps: ClaudeAgentStartupSyncDeps = {},
|
|
79
80
|
): Promise<string[] | null> {
|
|
81
|
+
if (siblingOfLivePort() !== null) return null;
|
|
80
82
|
const inject = deps.injectAgentDefs ?? injectClaudeAgentDefs;
|
|
81
83
|
const warn = deps.warn ?? (message => console.warn(message));
|
|
82
84
|
|
|
@@ -136,23 +136,36 @@ function persistPickerPreference(value: boolean): boolean {
|
|
|
136
136
|
return outcome.status !== "unavailable";
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
function pickerTrustPaths(deps: ApplyProfileDeps, configDir = getConfigDir()): { caPath: string; leafPath: string; sha1: string } {
|
|
140
|
-
const
|
|
139
|
+
function pickerTrustPaths(deps: ApplyProfileDeps, configDir = getConfigDir()): { caPath: string; leafPath: string; sha1: string; certPem: string } {
|
|
140
|
+
const caPath = pickerCaCertPath(configDir);
|
|
141
|
+
const certPem = deps.ensurePickerCaImpl
|
|
142
|
+
? deps.ensurePickerCaImpl(configDir).certPem
|
|
143
|
+
: readFileSync(caPath, "utf8");
|
|
141
144
|
return {
|
|
142
|
-
caPath
|
|
145
|
+
caPath,
|
|
143
146
|
leafPath: pickerLeafCertPath(configDir),
|
|
144
|
-
sha1: pickerCaFingerprints(
|
|
147
|
+
sha1: pickerCaFingerprints(certPem).sha1,
|
|
148
|
+
certPem,
|
|
145
149
|
};
|
|
146
150
|
}
|
|
147
151
|
|
|
148
|
-
async function trustPickerLocally(
|
|
152
|
+
async function trustPickerLocally(
|
|
153
|
+
deps: ApplyProfileDeps,
|
|
154
|
+
expectedCaSha256?: string | null,
|
|
155
|
+
): Promise<{ ok: true; callerAddedTrust: boolean; caPath: string; sha1: string } | { ok: false; reason: string }> {
|
|
149
156
|
try {
|
|
150
157
|
const configDir = getConfigDir();
|
|
151
158
|
const paths = pickerTrustPaths(deps, configDir);
|
|
159
|
+
if (!deps.ensurePickerCaImpl) {
|
|
160
|
+
// The bytes in ca.pem are only safe to trust when they match the authority the running
|
|
161
|
+
// server actually owns — a replaced file must never land in the login keychain.
|
|
162
|
+
const live = pickerCaFingerprints(paths.certPem).sha256;
|
|
163
|
+
if (expectedCaSha256 == null || expectedCaSha256 !== live) return { ok: false, reason: "ca_unverified" };
|
|
164
|
+
}
|
|
152
165
|
const inspect = deps.inspectPickerTrustImpl ?? inspectPickerTrust;
|
|
153
166
|
const before = await inspect(paths.leafPath, paths.sha1, deps.security, deps.platform);
|
|
154
167
|
if (before === "trusted") return { ok: true, callerAddedTrust: false, caPath: paths.caPath, sha1: paths.sha1 };
|
|
155
|
-
const trust = await (deps.trustPickerCaImpl ?? trustPickerCa)(paths.caPath, deps.security, deps.platform);
|
|
168
|
+
const trust = await (deps.trustPickerCaImpl ?? trustPickerCa)(paths.caPath, deps.security, deps.platform, { pem: paths.certPem });
|
|
156
169
|
if (!trust.ok) return { ok: false, reason: trust.reason ?? "trust_declined" };
|
|
157
170
|
return { ok: true, callerAddedTrust: true, caPath: paths.caPath, sha1: paths.sha1 };
|
|
158
171
|
} catch (error) {
|
|
@@ -597,17 +610,28 @@ async function handleClaudeDesktopPickerCommand(
|
|
|
597
610
|
});
|
|
598
611
|
|
|
599
612
|
if (action === "trust") {
|
|
600
|
-
|
|
613
|
+
if (!(await liveDesktopProxy(deps))) {
|
|
614
|
+
console.error("proxy_unavailable");
|
|
615
|
+
return 1;
|
|
616
|
+
}
|
|
617
|
+
// Verify the published CA file against the authority the live server reports — the status
|
|
618
|
+
// response carries caSha256, and a first enable materializes one when none exists yet.
|
|
619
|
+
let caSha256: string | null;
|
|
620
|
+
try {
|
|
621
|
+
caSha256 = (await pickerRuntimeRequest<{ ok?: boolean; picker?: DesktopPickerStatus }>(
|
|
622
|
+
"/api/claude-desktop/picker", {}, deps,
|
|
623
|
+
)).picker?.caSha256 ?? null;
|
|
624
|
+
if (caSha256 === null) caSha256 = (await sendEnable(false)).picker?.caSha256 ?? null;
|
|
625
|
+
} catch (error) {
|
|
626
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
627
|
+
return 1;
|
|
628
|
+
}
|
|
629
|
+
const trusted = await trustPickerLocally(deps, caSha256);
|
|
601
630
|
if (!trusted.ok) {
|
|
602
631
|
console.error(trusted.reason);
|
|
603
632
|
return 1;
|
|
604
633
|
}
|
|
605
634
|
localTrust = trusted;
|
|
606
|
-
if (!(await liveDesktopProxy(deps))) {
|
|
607
|
-
await compensateLocalPickerTrust(localTrust, deps);
|
|
608
|
-
console.error("proxy_unavailable");
|
|
609
|
-
return 1;
|
|
610
|
-
}
|
|
611
635
|
}
|
|
612
636
|
|
|
613
637
|
let response: PickerRouteResponse;
|
|
@@ -619,7 +643,7 @@ async function handleClaudeDesktopPickerCommand(
|
|
|
619
643
|
return 1;
|
|
620
644
|
}
|
|
621
645
|
if (action === "on" && response.picker?.reason === "trust_pending") {
|
|
622
|
-
const trusted = await trustPickerLocally(deps);
|
|
646
|
+
const trusted = await trustPickerLocally(deps, response.picker.caSha256);
|
|
623
647
|
if (!trusted.ok) {
|
|
624
648
|
console.error(trusted.reason);
|
|
625
649
|
return 1;
|