@bitkyc08/opencodex 2.10.2 → 2.11.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/README.md +31 -0
- package/bin/ocx.mjs +10 -0
- package/gui/dist/assets/index-Bk-PN-70.css +1 -0
- package/gui/dist/assets/index-BynIEIV-.js +70 -0
- package/gui/dist/index.html +2 -2
- package/package.json +4 -2
- package/src/adapters/cursor/effort-map.ts +11 -0
- package/src/adapters/cursor/live-transport.ts +11 -0
- package/src/adapters/cursor/native-exec-fs.ts +9 -6
- package/src/adapters/cursor/native-exec.ts +4 -2
- package/src/adapters/cursor/protobuf-events.ts +176 -4
- package/src/adapters/cursor/request-builder.ts +15 -4
- package/src/adapters/cursor/tool-definitions.ts +118 -2
- package/src/adapters/google.ts +15 -5
- package/src/adapters/openai-chat.ts +24 -2
- package/src/adapters/openai-responses.ts +2 -1
- package/src/bridge.ts +9 -5
- package/src/chat/outbound.ts +4 -3
- package/src/claude/desktop-3p.ts +222 -2
- package/src/claude/outbound.ts +15 -6
- package/src/cli/account-api.ts +4 -0
- package/src/cli/account-extended.ts +112 -0
- package/src/cli/account.ts +23 -6
- package/src/cli/claude-desktop.ts +26 -3
- package/src/cli/config-command.ts +9 -0
- package/src/cli/help.ts +18 -2
- package/src/cli/index.ts +277 -55
- package/src/cli/models.ts +5 -1
- package/src/cli/provider.ts +8 -2
- package/src/cli/ready.ts +301 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +153 -6
- package/src/clients/config-export.ts +12 -19
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-namespaces.ts +49 -3
- package/src/codex/account-priority.ts +83 -0
- package/src/codex/auth-api.ts +83 -0
- package/src/codex/auth-context.ts +5 -2
- package/src/codex/catalog/provider-fetch.ts +11 -0
- package/src/codex/catalog/sync.ts +23 -1
- package/src/codex/codex-write-lock.ts +16 -4
- package/src/codex/desired-state.ts +37 -4
- package/src/codex/history-job.ts +15 -5
- package/src/codex/history-provider.ts +31 -14
- package/src/codex/history-worker.ts +28 -4
- package/src/codex/inject-coordination.ts +13 -1
- package/src/codex/inject.ts +360 -66
- package/src/codex/internal/history-writer.ts +1 -1
- package/src/codex/native-main-lock-file.ts +5 -1
- package/src/codex/native-main-owner.ts +17 -3
- package/src/codex/native-profile-manager.ts +19 -0
- package/src/codex/native-profile-startup.ts +8 -0
- package/src/codex/native-residue.ts +140 -27
- package/src/codex/pool-rotation.ts +74 -4
- package/src/codex/refresh.ts +7 -0
- package/src/codex/routing.ts +177 -36
- package/src/codex/subagent-model-fallback.ts +34 -4
- package/src/codex/sync.ts +61 -0
- package/src/codex/upstream-host-health.ts +329 -31
- package/src/combos/request.ts +2 -0
- package/src/config.ts +221 -2
- package/src/images/loop.ts +1 -1
- package/src/integrations/native/ownership-preflight.ts +39 -2
- package/src/lib/bun-stream-caps.ts +3 -3
- package/src/lib/sse-decoder.ts +41 -0
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/windows-secret-acl.ts +141 -39
- package/src/lib/windows-user-principal.ts +283 -0
- package/src/lib/winsw.ts +18 -2
- package/src/oauth/key-providers.ts +12 -0
- package/src/providers/derive.ts +54 -2
- package/src/providers/free-directory.ts +6 -5
- package/src/providers/model-discovery.ts +9 -3
- package/src/providers/quota.ts +592 -0
- package/src/providers/registry.ts +316 -13
- package/src/responses/parser.ts +26 -10
- package/src/responses/reasoning-replay-cache.ts +1 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +2 -1
- package/src/server/auth-cors.ts +44 -13
- package/src/server/chat-completions.ts +0 -4
- package/src/server/claude-messages.ts +73 -15
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/index.ts +328 -111
- package/src/server/lifecycle.ts +36 -0
- package/src/server/management/agent-settings-routes.ts +147 -56
- package/src/server/management/config-routes.ts +7 -2
- package/src/server/management/context.ts +4 -0
- package/src/server/management/native-integration-routes.ts +199 -20
- package/src/server/management/provider-routes.ts +41 -0
- package/src/server/management/routing-profile-routes.ts +234 -5
- package/src/server/management/system-restart.ts +12 -10
- package/src/server/management/system-routes.ts +20 -0
- package/src/server/management-auth.ts +51 -3
- package/src/server/ports.ts +41 -1
- package/src/server/proxy-liveness.ts +129 -4
- package/src/server/readiness.ts +99 -0
- package/src/server/relay.ts +113 -97
- package/src/server/request-log.ts +10 -4
- package/src/server/responses/compact.ts +107 -12
- package/src/server/responses/core.ts +220 -39
- package/src/server/responses-item-id-repair.ts +22 -3
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/sse-payload-rewrite.ts +25 -14
- package/src/server/ws-bridge.ts +27 -22
- package/src/service-manager-probe.ts +520 -10
- package/src/service.ts +134 -2
- package/src/storage/worker-lifecycle.ts +14 -14
- package/src/tray/windows-tray.ps1 +74 -9
- package/src/types.ts +68 -2
- package/src/update/index.ts +12 -0
- package/src/update/job.ts +392 -18
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/usage/log.ts +1 -1
- package/src/vision/index.ts +77 -2
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/parse.ts +4 -1
- package/gui/dist/assets/index-BKVqyYqT.js +0 -70
- package/gui/dist/assets/index-Ca_3269W.css +0 -1
|
@@ -17,18 +17,20 @@
|
|
|
17
17
|
* Design of record: devlog/_fin/260803_integrations_toggle_all/030 (routes),
|
|
18
18
|
* 011 (Claude Code), 012 (Grok).
|
|
19
19
|
*/
|
|
20
|
-
import { readRuntimePort, saveConfigPreservingClaudeCode } from "../../config";
|
|
20
|
+
import { loadConfig, readRuntimePort, saveConfigPreservingClaudeCode } from "../../config";
|
|
21
21
|
import { filterCatalogVisibleModels, nativeOpenAiContextWindow, visibleNativeSlugs } from "../../codex/catalog";
|
|
22
|
+
import { inspectDesktop3pConfigLibrary, removeDesktop3pStandardPivot, writeDesktop3pConfig } from "../../claude/desktop-3p";
|
|
22
23
|
import { injectGrokConfig, stripGrokConfig, type GrokInjectModel } from "../../grok/inject";
|
|
23
24
|
import { inspectGrokConfig } from "../../grok/inspect";
|
|
24
25
|
import { grokConfigPath } from "../../grok/status";
|
|
25
26
|
import { assertNativeTeardownOwned } from "../../integrations/native/ownership-preflight";
|
|
27
|
+
import type { CodexNativeRestoreResult } from "../../codex/inject";
|
|
26
28
|
import type { OcxConfig } from "../../types";
|
|
27
29
|
import { jsonResponse } from "../auth-cors";
|
|
28
30
|
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
29
31
|
import type { ManagementContext } from "./context";
|
|
30
32
|
|
|
31
|
-
export type NativeIntegrationClientId = "claude" | "grok" | "codex";
|
|
33
|
+
export type NativeIntegrationClientId = "claude" | "grok" | "codex" | "claude-desktop";
|
|
32
34
|
|
|
33
35
|
/** Every reason this module can decline, in one place (audit r3 #6). */
|
|
34
36
|
export type NativeRefusalReason =
|
|
@@ -36,13 +38,17 @@ export type NativeRefusalReason =
|
|
|
36
38
|
| "orphaned_marker"
|
|
37
39
|
| "home_mismatch"
|
|
38
40
|
| "config_busy"
|
|
39
|
-
| "write_failed"
|
|
41
|
+
| "write_failed"
|
|
42
|
+
| "metadata_unreadable"
|
|
43
|
+
| "cleanup_incomplete"
|
|
44
|
+
| "desired_state_changed";
|
|
40
45
|
|
|
41
46
|
export interface NativeStatus {
|
|
42
47
|
clientId: NativeIntegrationClientId;
|
|
43
48
|
state: "absent" | "current" | "unsafe";
|
|
44
49
|
installed: boolean;
|
|
45
50
|
configPath: string;
|
|
51
|
+
desiredEnabled: boolean;
|
|
46
52
|
/**
|
|
47
53
|
* Set when a disable would be refused right now. ADVISORY: the file can
|
|
48
54
|
* change before the PUT, which re-checks and whose answer is authoritative.
|
|
@@ -61,8 +67,10 @@ export interface NativeToggleEnvelope {
|
|
|
61
67
|
changed: boolean;
|
|
62
68
|
state: NativeStatus["state"];
|
|
63
69
|
message: string;
|
|
70
|
+
desiredEnabled: boolean;
|
|
64
71
|
/** Present when the outcome needs more than success/failure to be honest. */
|
|
65
72
|
reason?: string;
|
|
73
|
+
artifacts?: CodexNativeRestoreResult["artifacts"];
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
export interface NativeRefusalEnvelope {
|
|
@@ -71,21 +79,66 @@ export interface NativeRefusalEnvelope {
|
|
|
71
79
|
clientId: NativeIntegrationClientId;
|
|
72
80
|
reason: NativeRefusalReason;
|
|
73
81
|
message: string;
|
|
82
|
+
/** Present on every post-commit refusal; absent only for pre-commit refusals. */
|
|
83
|
+
desiredEnabled?: boolean;
|
|
84
|
+
residualPaths?: string[];
|
|
74
85
|
}
|
|
75
86
|
|
|
87
|
+
export type NativePostCommitRefusalEnvelope = Omit<NativeRefusalEnvelope, "desiredEnabled"> & {
|
|
88
|
+
desiredEnabled: boolean;
|
|
89
|
+
};
|
|
90
|
+
|
|
76
91
|
function refusal(
|
|
77
92
|
status: number,
|
|
78
93
|
clientId: NativeIntegrationClientId,
|
|
79
94
|
reason: NativeRefusalReason,
|
|
80
95
|
message: string,
|
|
96
|
+
extra: Pick<NativeRefusalEnvelope, "desiredEnabled" | "residualPaths"> = {},
|
|
81
97
|
): Response {
|
|
82
98
|
return jsonResponse({
|
|
83
99
|
error: status >= 500 ? "native integration change failed" : "native integration change refused",
|
|
84
100
|
code: status >= 500 ? "native_integration_failed" : "native_integration_refused",
|
|
85
|
-
clientId, reason, message,
|
|
101
|
+
clientId, reason, message, ...extra,
|
|
86
102
|
} satisfies NativeRefusalEnvelope, status);
|
|
87
103
|
}
|
|
88
104
|
|
|
105
|
+
function postCommitRefusal(
|
|
106
|
+
status: number,
|
|
107
|
+
clientId: NativeIntegrationClientId,
|
|
108
|
+
reason: NativeRefusalReason,
|
|
109
|
+
message: string,
|
|
110
|
+
extra: Pick<NativePostCommitRefusalEnvelope, "desiredEnabled" | "residualPaths">,
|
|
111
|
+
): Response {
|
|
112
|
+
return jsonResponse({
|
|
113
|
+
error: status >= 500 ? "native integration change failed" : "native integration change refused",
|
|
114
|
+
code: status >= 500 ? "native_integration_failed" : "native_integration_refused",
|
|
115
|
+
clientId, reason, message, ...extra,
|
|
116
|
+
} satisfies NativePostCommitRefusalEnvelope, status);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function desktopStatus(config: ManagementContext["config"]): NativeStatus {
|
|
120
|
+
const seen = inspectDesktop3pConfigLibrary({
|
|
121
|
+
appliedFingerprint: config.claudeCode?.desktopProfile?.appliedFingerprint ?? null,
|
|
122
|
+
});
|
|
123
|
+
const state: NativeStatus["state"] = seen.kind === "gateway_ours"
|
|
124
|
+
? "current"
|
|
125
|
+
: seen.kind === "unsafe" || seen.kind === "broken" ? "unsafe" : "absent";
|
|
126
|
+
const disableBlocked = seen.kind === "unsafe" || seen.kind === "broken" || seen.kind === "foreign"
|
|
127
|
+
? {
|
|
128
|
+
reason: seen.kind === "unsafe" && seen.reason === "metadata_unreadable" ? "metadata_unreadable" as const : "write_failed" as const,
|
|
129
|
+
message: "Claude Desktop configuration cannot be changed safely.",
|
|
130
|
+
}
|
|
131
|
+
: null;
|
|
132
|
+
return {
|
|
133
|
+
clientId: "claude-desktop",
|
|
134
|
+
state,
|
|
135
|
+
installed: seen.kind !== "not_installed",
|
|
136
|
+
configPath: seen.libraryPath,
|
|
137
|
+
desiredEnabled: config.clientIntegrations?.["claude-desktop"] !== false,
|
|
138
|
+
disableBlocked,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
89
142
|
/** Absent means ON: the six read sites all treat only an explicit `false` as off. */
|
|
90
143
|
export function claudeCodeEnabled(config: ManagementContext["config"]): boolean {
|
|
91
144
|
return config.claudeCode?.enabled !== false;
|
|
@@ -98,17 +151,30 @@ function claudeStatus(config: ManagementContext["config"], configPath: string):
|
|
|
98
151
|
// The surface exists wherever the proxy does; there is no separate install.
|
|
99
152
|
installed: true,
|
|
100
153
|
configPath,
|
|
154
|
+
desiredEnabled: claudeCodeEnabled(config),
|
|
101
155
|
// Nothing can refuse this disable: no external file, no shared teardown.
|
|
102
156
|
disableBlocked: null,
|
|
103
157
|
};
|
|
104
158
|
}
|
|
105
159
|
|
|
160
|
+
function codexStatus(config: ManagementContext["config"], configPath: string): NativeStatus {
|
|
161
|
+
const desiredEnabled = config.clientIntegrations?.codex !== false;
|
|
162
|
+
return {
|
|
163
|
+
clientId: "codex",
|
|
164
|
+
state: desiredEnabled ? "current" : "absent",
|
|
165
|
+
installed: true,
|
|
166
|
+
configPath,
|
|
167
|
+
desiredEnabled,
|
|
168
|
+
disableBlocked: null,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
106
172
|
/**
|
|
107
173
|
* Grok's GET row (030 §field table). `disableBlocked` is ADVISORY — the file
|
|
108
174
|
* can change before the PUT, which re-checks with the same inspector and whose
|
|
109
175
|
* answer is authoritative.
|
|
110
176
|
*/
|
|
111
|
-
function grokStatus(): NativeStatus {
|
|
177
|
+
function grokStatus(config: ManagementContext["config"]): NativeStatus {
|
|
112
178
|
const seen = inspectGrokConfig();
|
|
113
179
|
let disableBlocked: NativeStatus["disableBlocked"] = null;
|
|
114
180
|
if (seen.kind === "orphaned_marker") {
|
|
@@ -137,6 +203,7 @@ function grokStatus(): NativeStatus {
|
|
|
137
203
|
state,
|
|
138
204
|
installed: seen.kind !== "not_installed",
|
|
139
205
|
configPath: grokConfigPath(),
|
|
206
|
+
desiredEnabled: config.clientIntegrations?.grok !== false,
|
|
140
207
|
disableBlocked,
|
|
141
208
|
};
|
|
142
209
|
}
|
|
@@ -260,9 +327,19 @@ async function handleCodexToggle(ctx: ManagementContext): Promise<Response> {
|
|
|
260
327
|
const port = runtime?.port ?? ctx.config.port;
|
|
261
328
|
const { syncModelsToCodex } = await import("../../codex/sync");
|
|
262
329
|
const applied = await syncModelsToCodex(port);
|
|
330
|
+
if (applied.status === "skipped") {
|
|
331
|
+
return jsonResponse({
|
|
332
|
+
ok: true, clientId: "codex", changed: durable && persisted.status === "committed",
|
|
333
|
+
state: "absent",
|
|
334
|
+
desiredEnabled: enabled,
|
|
335
|
+
message: "Codex integration is OFF; enable did not change Codex.",
|
|
336
|
+
reason: "apply_incomplete",
|
|
337
|
+
} satisfies NativeToggleEnvelope);
|
|
338
|
+
}
|
|
263
339
|
return jsonResponse({
|
|
264
340
|
ok: true, clientId: "codex", changed: durable && persisted.status === "committed",
|
|
265
341
|
state: applied.ok ? "current" : "absent",
|
|
342
|
+
desiredEnabled: enabled,
|
|
266
343
|
message: applied.ok
|
|
267
344
|
? "Codex now routes through opencodex"
|
|
268
345
|
: `Codex intent saved, but applying it did not complete: ${applied.message}`,
|
|
@@ -273,17 +350,28 @@ async function handleCodexToggle(ctx: ManagementContext): Promise<Response> {
|
|
|
273
350
|
}
|
|
274
351
|
|
|
275
352
|
// OFF. Restore the native path; the proxy keeps serving every other client.
|
|
353
|
+
if (durable && persisted.status === "unchanged") {
|
|
354
|
+
const { classifyNativeRoutedResidue } = await import("../../codex/native-residue");
|
|
355
|
+
if (classifyNativeRoutedResidue().kind === "clean") {
|
|
356
|
+
return jsonResponse({
|
|
357
|
+
ok: true, clientId: "codex", changed: false, state: "absent", desiredEnabled: false,
|
|
358
|
+
message: "Codex integration is already OFF and native; no Codex files changed.",
|
|
359
|
+
} satisfies NativeToggleEnvelope);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
276
362
|
const { restoreNativeCodexAsync } = await import("../../codex/inject");
|
|
277
|
-
const restored = await restoreNativeCodexAsync();
|
|
363
|
+
const restored = await restoreNativeCodexAsync({ revalidateDesiredState: true });
|
|
278
364
|
return jsonResponse({
|
|
279
365
|
ok: true, clientId: "codex", changed: durable && persisted.status === "committed",
|
|
280
366
|
state: restored.success ? "absent" : "unsafe",
|
|
367
|
+
desiredEnabled: enabled,
|
|
281
368
|
message: restored.success
|
|
282
369
|
? "Codex restored to its native path; the proxy is still serving other clients"
|
|
283
370
|
: `Codex intent saved, but restoring the native path did not complete: ${restored.message}`,
|
|
284
371
|
...(restored.success
|
|
285
372
|
? (durable ? {} : { reason: "not_durable" })
|
|
286
373
|
: { reason: "restore_incomplete" }),
|
|
374
|
+
artifacts: restored.artifacts,
|
|
287
375
|
} satisfies NativeToggleEnvelope);
|
|
288
376
|
})();
|
|
289
377
|
try {
|
|
@@ -361,6 +449,7 @@ async function handleGrokToggle(ctx: ManagementContext): Promise<Response> {
|
|
|
361
449
|
);
|
|
362
450
|
}
|
|
363
451
|
const durable = persisted.ok;
|
|
452
|
+
const desiredEnabled = durable ? loadConfig().clientIntegrations?.grok !== false : enabled;
|
|
364
453
|
|
|
365
454
|
if (!enabled) {
|
|
366
455
|
/*
|
|
@@ -369,24 +458,25 @@ async function handleGrokToggle(ctx: ManagementContext): Promise<Response> {
|
|
|
369
458
|
* gated: writing our own fence tears nothing down.
|
|
370
459
|
*/
|
|
371
460
|
const owned = assertNativeTeardownOwned();
|
|
372
|
-
if (!owned.ok) return
|
|
461
|
+
if (!owned.ok) return postCommitRefusal(409, "grok", "home_mismatch", owned.message, { desiredEnabled });
|
|
373
462
|
|
|
374
463
|
const result = stripGrokConfig();
|
|
375
464
|
if (result.skippedReason === "no-grok-home") {
|
|
376
|
-
return
|
|
465
|
+
return postCommitRefusal(404, "grok", "not_installed", NOT_INSTALLED_MESSAGE, { desiredEnabled });
|
|
377
466
|
}
|
|
378
467
|
if (result.skippedReason === "orphaned-marker" || !result.ok) {
|
|
379
468
|
// Orphaned can still arrive between the preflight and the strip; the
|
|
380
469
|
// writer refuses it correctly and we map the refusal, never a retry lie.
|
|
381
470
|
return result.skippedReason === "orphaned-marker"
|
|
382
|
-
?
|
|
383
|
-
:
|
|
471
|
+
? postCommitRefusal(409, "grok", "orphaned_marker", ORPHANED_MARKER_MESSAGE, { desiredEnabled })
|
|
472
|
+
: postCommitRefusal(500, "grok", "write_failed", result.message, { desiredEnabled });
|
|
384
473
|
}
|
|
385
474
|
// The writer's own read IS the last read within this synchronous
|
|
386
475
|
// operation (012 Rev 3 N4): strip removed the fence, so absent.
|
|
387
476
|
return jsonResponse({
|
|
388
477
|
ok: true, clientId: "grok", changed: result.changed,
|
|
389
478
|
state: "absent",
|
|
479
|
+
desiredEnabled,
|
|
390
480
|
message: result.changed
|
|
391
481
|
? "Grok integration disabled — the opencodex block was removed. Re-enabling regenerates it from the current model list."
|
|
392
482
|
: "Grok integration is already off",
|
|
@@ -427,13 +517,13 @@ async function handleGrokToggle(ctx: ManagementContext): Promise<Response> {
|
|
|
427
517
|
} catch (error) {
|
|
428
518
|
// A catalog failure must never write an empty fence (syncGrokConfig
|
|
429
519
|
// guards this; the route inherits the rule). Nothing was written.
|
|
430
|
-
return
|
|
431
|
-
`The model catalog is unavailable, so nothing was written (${error instanceof Error ? error.message : String(error)}). Try again once provider discovery recovers
|
|
520
|
+
return postCommitRefusal(500, "grok", "write_failed",
|
|
521
|
+
`The model catalog is unavailable, so nothing was written (${error instanceof Error ? error.message : String(error)}). Try again once provider discovery recovers.`, { desiredEnabled });
|
|
432
522
|
}
|
|
433
523
|
|
|
434
524
|
const recheck = inspectGrokConfig();
|
|
435
|
-
if (recheck.kind === "not_installed") return
|
|
436
|
-
if (recheck.kind === "orphaned_marker") return
|
|
525
|
+
if (recheck.kind === "not_installed") return postCommitRefusal(404, "grok", "not_installed", NOT_INSTALLED_MESSAGE, { desiredEnabled });
|
|
526
|
+
if (recheck.kind === "orphaned_marker") return postCommitRefusal(409, "grok", "orphaned_marker", ORPHANED_MARKER_MESSAGE, { desiredEnabled });
|
|
437
527
|
|
|
438
528
|
const inject = deps.injectGrokConfig ?? injectGrokConfig;
|
|
439
529
|
const result = inject(port, models, {
|
|
@@ -455,7 +545,7 @@ async function handleGrokToggle(ctx: ManagementContext): Promise<Response> {
|
|
|
455
545
|
const after = inspectGrokConfig();
|
|
456
546
|
switch (after.kind) {
|
|
457
547
|
case "orphaned_marker":
|
|
458
|
-
return
|
|
548
|
+
return postCommitRefusal(409, "grok", "orphaned_marker", ORPHANED_MARKER_MESSAGE, { desiredEnabled });
|
|
459
549
|
case "present":
|
|
460
550
|
// A well-formed fence arrived from elsewhere between the strip and
|
|
461
551
|
// this read (`ocx ensure`, another proxy, a hand edit). It is not
|
|
@@ -464,14 +554,16 @@ async function handleGrokToggle(ctx: ManagementContext): Promise<Response> {
|
|
|
464
554
|
return jsonResponse({
|
|
465
555
|
ok: true, clientId: "grok", changed: result.changed,
|
|
466
556
|
state: "current", reason: "non_loopback_superseded",
|
|
557
|
+
desiredEnabled,
|
|
467
558
|
message: "opencodex is bound to a non-loopback address, so this request did not write a block — but a well-formed opencodex block is present in the Grok config, written by something else. The card shows what is on disk.",
|
|
468
559
|
} satisfies NativeToggleEnvelope);
|
|
469
560
|
case "not_installed":
|
|
470
|
-
return
|
|
561
|
+
return postCommitRefusal(404, "grok", "not_installed", NOT_INSTALLED_MESSAGE, { desiredEnabled });
|
|
471
562
|
case "absent":
|
|
472
563
|
return jsonResponse({
|
|
473
564
|
ok: true, clientId: "grok", changed: result.changed,
|
|
474
565
|
state: "absent", reason: "non_loopback_removed",
|
|
566
|
+
desiredEnabled,
|
|
475
567
|
message: "opencodex is bound to a non-loopback address, so Grok cannot be auto-registered. The previously generated block was removed because it pointed at a loopback address that no longer serves.",
|
|
476
568
|
} satisfies NativeToggleEnvelope);
|
|
477
569
|
default: {
|
|
@@ -484,17 +576,18 @@ async function handleGrokToggle(ctx: ManagementContext): Promise<Response> {
|
|
|
484
576
|
}
|
|
485
577
|
|
|
486
578
|
if (result.skippedReason === "no-grok-home") {
|
|
487
|
-
return
|
|
579
|
+
return postCommitRefusal(404, "grok", "not_installed", NOT_INSTALLED_MESSAGE, { desiredEnabled });
|
|
488
580
|
}
|
|
489
581
|
if (result.skippedReason === "orphaned-marker") {
|
|
490
|
-
return
|
|
582
|
+
return postCommitRefusal(409, "grok", "orphaned_marker", ORPHANED_MARKER_MESSAGE, { desiredEnabled });
|
|
491
583
|
}
|
|
492
584
|
if (!result.ok) {
|
|
493
|
-
return
|
|
585
|
+
return postCommitRefusal(500, "grok", "write_failed", result.message, { desiredEnabled });
|
|
494
586
|
}
|
|
495
587
|
return jsonResponse({
|
|
496
588
|
ok: true, clientId: "grok", changed: result.changed,
|
|
497
589
|
state: "current",
|
|
590
|
+
desiredEnabled,
|
|
498
591
|
message: result.changed ? "Grok integration enabled — the opencodex block was regenerated from the current model list." : "Grok integration is already on",
|
|
499
592
|
} satisfies NativeToggleEnvelope);
|
|
500
593
|
})();
|
|
@@ -505,13 +598,93 @@ async function handleGrokToggle(ctx: ManagementContext): Promise<Response> {
|
|
|
505
598
|
}
|
|
506
599
|
}
|
|
507
600
|
|
|
601
|
+
let claudeDesktopToggleFlight: Promise<Response> | null = null;
|
|
602
|
+
|
|
603
|
+
async function handleClaudeDesktopToggle(ctx: ManagementContext): Promise<Response> {
|
|
604
|
+
if (claudeDesktopToggleFlight) {
|
|
605
|
+
return refusal(409, "claude-desktop", "config_busy",
|
|
606
|
+
"Another Claude Desktop change is already in flight. Nothing was written — try again in a moment.");
|
|
607
|
+
}
|
|
608
|
+
claudeDesktopToggleFlight = (async (): Promise<Response> => {
|
|
609
|
+
let body: { enabled?: unknown };
|
|
610
|
+
try {
|
|
611
|
+
body = await readManagementJsonBody(ctx.req);
|
|
612
|
+
} catch (error) {
|
|
613
|
+
rethrowManagementBodyTooLarge(error);
|
|
614
|
+
return jsonResponse({ error: "invalid JSON body" }, 400);
|
|
615
|
+
}
|
|
616
|
+
if (typeof body.enabled !== "boolean") return jsonResponse({ error: "enabled must be a boolean" }, 400);
|
|
617
|
+
|
|
618
|
+
const { setIntegrationEnabled } = await import("../../codex/desired-state");
|
|
619
|
+
const persisted = setIntegrationEnabled("claude-desktop", body.enabled);
|
|
620
|
+
if (!persisted.ok) {
|
|
621
|
+
return refusal(persisted.retryable ? 409 : 500, "claude-desktop", persisted.retryable ? "config_busy" : "write_failed", persisted.message);
|
|
622
|
+
}
|
|
623
|
+
const desiredEnabled = loadConfig().clientIntegrations?.["claude-desktop"] !== false;
|
|
624
|
+
const current = loadConfig();
|
|
625
|
+
const fingerprint = current.claudeCode?.desktopProfile?.appliedFingerprint ?? null;
|
|
626
|
+
|
|
627
|
+
if (!body.enabled) {
|
|
628
|
+
const removed = (ctx.deps.removeDesktop3pStandardPivot ?? removeDesktop3pStandardPivot)({ appliedFingerprint: fingerprint });
|
|
629
|
+
if (removed.kind === "cleanup_incomplete") {
|
|
630
|
+
return postCommitRefusal(500, "claude-desktop", "cleanup_incomplete",
|
|
631
|
+
"Claude Desktop now points at standard mode, but credential cleanup is incomplete.",
|
|
632
|
+
{ desiredEnabled, residualPaths: removed.residualPaths ?? [] });
|
|
633
|
+
}
|
|
634
|
+
if (!removed.ok) {
|
|
635
|
+
return postCommitRefusal(409, "claude-desktop", removed.reason === "metadata_unreadable" ? "metadata_unreadable" : "write_failed",
|
|
636
|
+
"Claude Desktop configuration could not be changed safely.", { desiredEnabled });
|
|
637
|
+
}
|
|
638
|
+
return jsonResponse({
|
|
639
|
+
ok: true, clientId: "claude-desktop", changed: removed.changed, state: "absent", desiredEnabled,
|
|
640
|
+
message: removed.changed ? "Claude Desktop integration disabled." : "Claude Desktop integration is already off.",
|
|
641
|
+
} satisfies NativeToggleEnvelope);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const fetchModels = ctx.deps.fetchAllModels ?? defaultFetchAllModels;
|
|
645
|
+
try {
|
|
646
|
+
const fetched = await fetchModels(current);
|
|
647
|
+
const latest = loadConfig();
|
|
648
|
+
const latestDesiredEnabled = latest.clientIntegrations?.["claude-desktop"] !== false;
|
|
649
|
+
if (!latestDesiredEnabled) {
|
|
650
|
+
return postCommitRefusal(409, "claude-desktop", "desired_state_changed",
|
|
651
|
+
"Claude Desktop enable was cancelled because the desired state changed to off.", { desiredEnabled: latestDesiredEnabled });
|
|
652
|
+
}
|
|
653
|
+
const routed = filterCatalogVisibleModels(fetched, latest).map(model => ({
|
|
654
|
+
provider: model.provider, id: model.id, contextWindow: model.contextWindow,
|
|
655
|
+
}));
|
|
656
|
+
const runtime = (ctx.deps.readRuntimePort ?? readRuntimePort)(process.pid);
|
|
657
|
+
const result = (ctx.deps.writeDesktop3pConfig ?? writeDesktop3pConfig)(
|
|
658
|
+
runtime?.port ?? latest.port,
|
|
659
|
+
[...visibleNativeSlugs(latest)],
|
|
660
|
+
routed,
|
|
661
|
+
latest.apiKeys?.[0]?.key,
|
|
662
|
+
"static",
|
|
663
|
+
latest.claudeCode?.desktopProfile,
|
|
664
|
+
);
|
|
665
|
+
if (!result.written) return postCommitRefusal(500, "claude-desktop", "write_failed", "Claude Desktop apply failed.", { desiredEnabled: latestDesiredEnabled });
|
|
666
|
+
return jsonResponse({
|
|
667
|
+
ok: true, clientId: "claude-desktop", changed: true, state: "current", desiredEnabled: latestDesiredEnabled,
|
|
668
|
+
message: "Claude Desktop integration enabled.",
|
|
669
|
+
} satisfies NativeToggleEnvelope);
|
|
670
|
+
} catch {
|
|
671
|
+
return postCommitRefusal(500, "claude-desktop", "write_failed", "Claude Desktop apply failed.", { desiredEnabled });
|
|
672
|
+
}
|
|
673
|
+
})();
|
|
674
|
+
try {
|
|
675
|
+
return await claudeDesktopToggleFlight;
|
|
676
|
+
} finally {
|
|
677
|
+
claudeDesktopToggleFlight = null;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
508
681
|
export async function handleNativeIntegrationRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
509
682
|
const { req, url, config, deps } = ctx;
|
|
510
683
|
|
|
511
684
|
if (url.pathname === "/api/native-integrations" && req.method === "GET") {
|
|
512
685
|
const { getConfigPath } = await import("../../config");
|
|
513
686
|
return jsonResponse({
|
|
514
|
-
clients: [claudeStatus(config, getConfigPath()), grokStatus()],
|
|
687
|
+
clients: [claudeStatus(config, getConfigPath()), grokStatus(config), codexStatus(config, getConfigPath()), desktopStatus(config)],
|
|
515
688
|
} satisfies NativeStatusListEnvelope);
|
|
516
689
|
}
|
|
517
690
|
|
|
@@ -532,6 +705,7 @@ export async function handleNativeIntegrationRoutes(ctx: ManagementContext): Pro
|
|
|
532
705
|
return jsonResponse({
|
|
533
706
|
ok: true, clientId: "claude", changed: false,
|
|
534
707
|
state: enabled ? "current" : "absent",
|
|
708
|
+
desiredEnabled: enabled,
|
|
535
709
|
message: enabled ? "Claude inbound is already on" : "Claude inbound is already off",
|
|
536
710
|
} satisfies NativeToggleEnvelope);
|
|
537
711
|
}
|
|
@@ -571,6 +745,7 @@ export async function handleNativeIntegrationRoutes(ctx: ManagementContext): Pro
|
|
|
571
745
|
return jsonResponse({
|
|
572
746
|
ok: true, clientId: "claude", changed: true,
|
|
573
747
|
state: enabled ? "current" : "absent",
|
|
748
|
+
desiredEnabled: enabled,
|
|
574
749
|
message: enabled ? "Claude inbound enabled" : "Claude inbound disabled",
|
|
575
750
|
} satisfies NativeToggleEnvelope);
|
|
576
751
|
}
|
|
@@ -583,5 +758,9 @@ export async function handleNativeIntegrationRoutes(ctx: ManagementContext): Pro
|
|
|
583
758
|
return handleCodexToggle(ctx);
|
|
584
759
|
}
|
|
585
760
|
|
|
761
|
+
if (url.pathname === "/api/native-integrations/claude-desktop" && req.method === "PUT") {
|
|
762
|
+
return handleClaudeDesktopToggle(ctx);
|
|
763
|
+
}
|
|
764
|
+
|
|
586
765
|
return null;
|
|
587
766
|
}
|
|
@@ -165,6 +165,45 @@ function applyProviderPatchFields(
|
|
|
165
165
|
next.liveModels = rawBody.liveModels;
|
|
166
166
|
touched = true;
|
|
167
167
|
}
|
|
168
|
+
// The Models page edits the catalog hints in place; keep them on the existing
|
|
169
|
+
// provider mutation path so validation, cache invalidation, and convergence stay unified (#1073).
|
|
170
|
+
if (Object.hasOwn(rawBody, "contextWindow")) {
|
|
171
|
+
const value = rawBody.contextWindow;
|
|
172
|
+
if (value === null) {
|
|
173
|
+
delete next.contextWindow;
|
|
174
|
+
// `Number.isInteger(1e100)` is true, so an integer check alone admits a value that
|
|
175
|
+
// serializes into the catalog as an enormous number and can make Codex reject the whole
|
|
176
|
+
// file. Safe-integer is the real bound for something that ends up in a JSON int field.
|
|
177
|
+
} else if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) {
|
|
178
|
+
next.contextWindow = value;
|
|
179
|
+
} else {
|
|
180
|
+
return { error: "contextWindow must be a positive safe integer or null" };
|
|
181
|
+
}
|
|
182
|
+
touched = true;
|
|
183
|
+
}
|
|
184
|
+
if (Object.hasOwn(rawBody, "modelContextWindows")) {
|
|
185
|
+
const value = rawBody.modelContextWindows;
|
|
186
|
+
if (value === null) {
|
|
187
|
+
delete next.modelContextWindows;
|
|
188
|
+
} else {
|
|
189
|
+
if (!isPlainRecord(value)) return { error: "modelContextWindows must be a plain object or null" };
|
|
190
|
+
const windows: Record<string, number> = { ...(next.modelContextWindows ?? {}) };
|
|
191
|
+
for (const [model, window] of Object.entries(value)) {
|
|
192
|
+
if (!model.trim()) return { error: "modelContextWindows keys must be nonblank model ids" };
|
|
193
|
+
if (window === null) {
|
|
194
|
+
delete windows[model];
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (typeof window !== "number" || !Number.isSafeInteger(window) || window <= 0) {
|
|
198
|
+
return { error: "modelContextWindows values must be positive safe integers or null" };
|
|
199
|
+
}
|
|
200
|
+
windows[model] = window;
|
|
201
|
+
}
|
|
202
|
+
if (Object.keys(windows).length > 0) next.modelContextWindows = windows;
|
|
203
|
+
else delete next.modelContextWindows;
|
|
204
|
+
}
|
|
205
|
+
touched = true;
|
|
206
|
+
}
|
|
168
207
|
|
|
169
208
|
// headers is the one object-valued field in the mask. PATCH semantics merge it
|
|
170
209
|
// shallowly into the existing block so a single fingerprint header can be added
|
|
@@ -249,6 +288,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
249
288
|
allowPrivateNetwork: p.allowPrivateNetwork === true,
|
|
250
289
|
liveModels: p.liveModels !== false,
|
|
251
290
|
models: p.models ?? [],
|
|
291
|
+
contextWindow: p.contextWindow,
|
|
292
|
+
modelContextWindows: p.modelContextWindows,
|
|
252
293
|
authMode: p.authMode,
|
|
253
294
|
apiKeyTransport: p.apiKeyTransport,
|
|
254
295
|
disabled: p.disabled === true,
|