@bitkyc08/opencodex 2.50.0 → 2.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ocx.mjs +222 -71
- package/gui/dist/assets/{index-C39tnjXO.js → index-D7BdZpZm.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/qoder/adapter.ts +69 -1
- package/src/adapters/qoder/scaffold-guard.ts +233 -0
- package/src/claude/agents-inject.ts +29 -5
- package/src/claude/desktop-3p.ts +31 -3
- package/src/claude/gateway-cache.ts +12 -21
- package/src/cli/capabilities.ts +28 -0
- package/src/cli/claude-agent-startup-sync.ts +26 -1
- package/src/cli/claude.ts +138 -20
- package/src/cli/config-command.ts +67 -1
- package/src/cli/connect.ts +181 -14
- package/src/cli/dispatch.ts +53 -9
- package/src/cli/doctor.ts +9 -2
- package/src/cli/ensure-desired-integrations.ts +10 -0
- package/src/cli/gui-pair-client.ts +1 -12
- package/src/cli/help.ts +4 -1
- package/src/cli/hub.ts +367 -0
- package/src/cli/index.ts +94 -30
- package/src/cli/launcher-context.ts +1 -1
- package/src/cli/registry.ts +43 -3
- package/src/cli/status.ts +325 -5
- package/src/cli/version-skew.ts +4 -1
- package/src/cli.ts +2 -2
- package/src/client/catalog-compatibility.ts +192 -0
- package/src/client/connect.ts +31 -0
- package/src/client/hub-client.ts +52 -0
- package/src/client/hub-state.ts +214 -0
- package/src/codex/account-usability.ts +48 -12
- package/src/codex/auth-api.ts +49 -5
- package/src/codex/catalog/effort.ts +67 -8
- package/src/codex/catalog/sync.ts +85 -0
- package/src/codex/codex-write-lock.ts +11 -2
- package/src/codex/desired-state.ts +47 -1
- package/src/codex/inject-coordination.ts +10 -5
- package/src/codex/inject.ts +26 -10
- package/src/codex/loopback-target.ts +45 -0
- package/src/codex/routing.ts +48 -1
- package/src/codex/runtime.ts +37 -3
- package/src/codex/sync.ts +29 -9
- package/src/codex/warmup.ts +21 -4
- package/src/config/pending-teardown.ts +1 -1
- package/src/config.ts +126 -12
- package/src/generated/compatibility-version.json +136 -76
- package/src/grok/status.ts +9 -1
- package/src/integrations/config-io.ts +54 -1
- package/src/lib/bun-runtime.ts +1 -1
- package/src/lib/gui-pair-capability.ts +27 -0
- package/src/lib/local-destinations.ts +162 -0
- package/src/lib/package-tree-integrity.ts +1 -1
- package/src/lib/process-control.ts +130 -20
- package/src/lib/service-secrets.ts +28 -0
- package/src/lib/test-home-guard.ts +49 -0
- package/src/providers/opencode-go-transport.ts +9 -1
- package/src/providers/quota.ts +5 -1
- package/src/providers/registry.ts +34 -5
- package/src/remote/hub-state.ts +182 -0
- package/src/server/auth-cors.ts +5 -0
- package/src/server/chat-completions.ts +6 -3
- package/src/server/claude-messages.ts +7 -1
- package/src/server/hub-state.ts +98 -0
- package/src/server/index.ts +124 -6
- package/src/server/management/api-access.ts +14 -3
- package/src/server/management/config-routes.ts +2 -2
- package/src/server/management/cursor-integration-routes.ts +13 -4
- package/src/server/proxy-liveness.ts +7 -1
- package/src/server/request-log-conversation.ts +41 -1
- package/src/server/responses/codex-auth-error.ts +18 -1
- package/src/server/responses/codex-ws-exchange.ts +36 -4
- package/src/server/responses/codex-ws-wire.ts +75 -4
- package/src/server/responses/compact.ts +20 -9
- package/src/server/responses/core.ts +57 -10
- package/src/server/responses/policy-fallback.ts +7 -1
- package/src/server/system-env-shell.ts +14 -2
- package/src/server/system-env.ts +106 -14
- package/src/service.ts +906 -94
- package/src/types/config.ts +57 -4
- package/src/update/badge.ts +3 -2
- package/src/update/index.ts +317 -64
- package/src/update/install-detection.d.mts +6 -0
- package/src/update/install-detection.mjs +73 -0
- package/src/update/job.ts +101 -49
- package/src/update/pnpm-global-install.d.mts +144 -0
- package/src/update/pnpm-global-install.mjs +591 -0
- package/src/update/pnpm-invocation.d.mts +43 -0
- package/src/update/pnpm-invocation.mjs +141 -0
- package/src/update/registry-integrity.d.mts +16 -0
- package/src/update/registry-integrity.mjs +37 -0
- package/src/update/transactional-install.d.mts +1 -1
- package/src/update/transactional-install.mjs +101 -7
- package/src/update/tray-update-plan.mjs +1 -1
- package/src/vision/plan.ts +13 -3
- package/src/vision/routed-describe.ts +51 -20
package/src/server/system-env.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { PROXY_MARKER } from "../claude/auth-detect";
|
|
|
7
7
|
import { isProxyAdmissionSecret } from "./auth-cors";
|
|
8
8
|
import type { OcxConfig } from "../types";
|
|
9
9
|
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
10
|
+
import { localAdmissionToken, localInferenceDestination } from "../lib/local-destinations";
|
|
11
|
+
import { probeHostname } from "./proxy-liveness";
|
|
10
12
|
import { providerContextCap } from "../providers/context-cap";
|
|
11
13
|
import { OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
|
|
12
14
|
export { getShellEnvFilePath, installShellHook, uninstallShellHook, claudeCodeCliInstalled, reconcileShellHook } from "./system-env-shell";
|
|
@@ -35,12 +37,35 @@ const MANAGED_SYSTEM_ENV_NAMES = new Set<string>([
|
|
|
35
37
|
|
|
36
38
|
interface SystemEnvTracking {
|
|
37
39
|
pid: number;
|
|
40
|
+
/** The PUBLIC port of the owning proxy: its instance identity and its /healthz port. */
|
|
38
41
|
port: number;
|
|
42
|
+
/**
|
|
43
|
+
* The host `/healthz` answers on — `probeHostname(config.hostname)` at injection time (#4236).
|
|
44
|
+
*
|
|
45
|
+
* Without it the liveness probe went to `127.0.0.1:<public port>`, which does not exist on a
|
|
46
|
+
* tailnet-bound hub: every probe failed, the record was reverted on every start, and the
|
|
47
|
+
* "another instance owns env" guard below could never fire. Absent (⇒ 127.0.0.1) in records
|
|
48
|
+
* written by a build that only ever bound loopback.
|
|
49
|
+
*/
|
|
50
|
+
bindHost?: string;
|
|
51
|
+
/**
|
|
52
|
+
* The exact ANTHROPIC_BASE_URL that was injected, when it is not `http://127.0.0.1:<port>`.
|
|
53
|
+
*
|
|
54
|
+
* The injected destination and the proxy's own `/healthz` address separated the day local
|
|
55
|
+
* clients started honoring the unauthenticated loopback listener, and separated again for a
|
|
56
|
+
* non-loopback bind. Ownership on revert is proven against THIS value — it is what was
|
|
57
|
+
* written — while liveness is probed on `bindHost`/`port`, because the loopback listener
|
|
58
|
+
* serves no `/healthz` and probing the injected port would declare a live proxy stale.
|
|
59
|
+
*/
|
|
60
|
+
clientBaseUrl?: string;
|
|
39
61
|
injectedAt: string;
|
|
40
62
|
/** Keys that were actually set by injection (revert only unsets these). */
|
|
41
63
|
injectedKeys?: string[];
|
|
42
64
|
}
|
|
43
65
|
|
|
66
|
+
/** Bounded host shape: this value is interpolated into a probe URL, so it is validated on read. */
|
|
67
|
+
const BIND_HOST_PATTERN = /^[A-Za-z0-9.\-_]{1,253}$|^\[[0-9A-Fa-f:.]{2,45}\]$/;
|
|
68
|
+
|
|
44
69
|
type SystemEnvResult = { injected: boolean; reason?: string };
|
|
45
70
|
type RevertResult = { reverted: boolean; reason?: string };
|
|
46
71
|
type CleanupResult = { cleaned: boolean; reason?: string };
|
|
@@ -70,7 +95,13 @@ function readTracking(): SystemEnvTracking | undefined {
|
|
|
70
95
|
(name): name is string => typeof name === "string" && MANAGED_SYSTEM_ENV_NAMES.has(name),
|
|
71
96
|
))]
|
|
72
97
|
: undefined;
|
|
73
|
-
|
|
98
|
+
const bindHost = typeof tracking.bindHost === "string" && BIND_HOST_PATTERN.test(tracking.bindHost)
|
|
99
|
+
? tracking.bindHost
|
|
100
|
+
: undefined;
|
|
101
|
+
const clientBaseUrl = typeof tracking.clientBaseUrl === "string" && /^https?:\/\/[^\s/]+$/.test(tracking.clientBaseUrl)
|
|
102
|
+
? tracking.clientBaseUrl
|
|
103
|
+
: undefined;
|
|
104
|
+
return { ...tracking, bindHost, clientBaseUrl, injectedKeys } as SystemEnvTracking;
|
|
74
105
|
} catch {
|
|
75
106
|
return undefined;
|
|
76
107
|
}
|
|
@@ -88,18 +119,40 @@ function ownedBaseUrl(port: number): string {
|
|
|
88
119
|
return `http://127.0.0.1:${port}`;
|
|
89
120
|
}
|
|
90
121
|
|
|
91
|
-
|
|
122
|
+
/** What the tracking record has to remember beyond the public port, or nothing on a pure loopback install. */
|
|
123
|
+
interface TrackedDestination {
|
|
124
|
+
/** `probeHostname(config.hostname)` — where `/healthz` answers. */
|
|
125
|
+
bindHost: string;
|
|
126
|
+
/** The injected ANTHROPIC_BASE_URL. */
|
|
127
|
+
clientBaseUrl: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function writeTracking(port: number, injectedKeys: string[], tracked?: TrackedDestination): void {
|
|
92
131
|
recordOwnedConfigPath(getConfigDir(), getSystemEnvTrackingPath());
|
|
93
132
|
mkdirSync(getConfigDir(), { recursive: true, mode: 0o700 });
|
|
94
133
|
writeFileSync(getSystemEnvTrackingPath(), JSON.stringify({
|
|
95
134
|
pid: process.pid,
|
|
96
135
|
port,
|
|
136
|
+
// Both fields are omitted when they carry no information beyond `port`, so a plain
|
|
137
|
+
// loopback install keeps writing the byte-identical record it always did.
|
|
138
|
+
...(tracked && tracked.bindHost !== "127.0.0.1" ? { bindHost: tracked.bindHost } : {}),
|
|
139
|
+
...(tracked && tracked.clientBaseUrl !== ownedBaseUrl(port) ? { clientBaseUrl: tracked.clientBaseUrl } : {}),
|
|
97
140
|
injectedAt: new Date().toISOString(),
|
|
98
141
|
injectedKeys,
|
|
99
142
|
}), { encoding: "utf8", mode: 0o600 });
|
|
100
143
|
}
|
|
101
144
|
|
|
102
|
-
|
|
145
|
+
/** The base URL that was injected: the loopback listener or the bind address, not always loopback (#4236). */
|
|
146
|
+
function trackedBaseUrl(tracking: Pick<SystemEnvTracking, "port" | "clientBaseUrl">): string {
|
|
147
|
+
return tracking.clientBaseUrl ?? ownedBaseUrl(tracking.port);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Where the owning proxy's `/healthz` answers — the BIND address, never the injected port (#4236). */
|
|
151
|
+
function trackedHealthzOrigin(tracking: Pick<SystemEnvTracking, "port" | "bindHost">): string {
|
|
152
|
+
return `http://${tracking.bindHost ?? "127.0.0.1"}:${tracking.port}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function rollbackInjectedKeys(port: number, injectedKeys: string[], tracked?: TrackedDestination): void {
|
|
103
156
|
const rollbackFailed: string[] = [];
|
|
104
157
|
for (const name of [...injectedKeys].reverse()) {
|
|
105
158
|
try {
|
|
@@ -110,7 +163,7 @@ function rollbackInjectedKeys(port: number, injectedKeys: string[]): void {
|
|
|
110
163
|
}
|
|
111
164
|
|
|
112
165
|
if (rollbackFailed.length > 0) {
|
|
113
|
-
writeTracking(port, rollbackFailed);
|
|
166
|
+
writeTracking(port, rollbackFailed, tracked);
|
|
114
167
|
return;
|
|
115
168
|
}
|
|
116
169
|
|
|
@@ -150,8 +203,39 @@ export async function injectSystemEnv(
|
|
|
150
203
|
|
|
151
204
|
await cleanStaleSystemEnv();
|
|
152
205
|
|
|
206
|
+
// A launchd-started `claude` is a LOCAL client: it dials the unauthenticated loopback
|
|
207
|
+
// listener when one is enabled, because on a tailnet-bound hub nothing answers on
|
|
208
|
+
// 127.0.0.1:<public port> (#4236); with no listener the destination is the BIND address,
|
|
209
|
+
// which answers but demands data-plane admission. `port` stays the instance identity.
|
|
210
|
+
const destination = localInferenceDestination(config, port);
|
|
211
|
+
const tracked: TrackedDestination = {
|
|
212
|
+
bindHost: probeHostname(config.hostname),
|
|
213
|
+
clientBaseUrl: destination.origin,
|
|
214
|
+
};
|
|
215
|
+
const markerMode = systemEnvMarkerMode(config, deps);
|
|
216
|
+
// The data-plane credential this host can present to its own public listener: env token,
|
|
217
|
+
// hardened service token file, then a configured key. Never the admin token.
|
|
218
|
+
const hostAdmissionToken = destination.requiresAdmissionToken ? localAdmissionToken(config) : undefined;
|
|
219
|
+
// The launchd domain is machine-wide, so injecting a destination that will 401 every plain
|
|
220
|
+
// `claude` is worse than not injecting at all. Degrade out loud instead: a subscription
|
|
221
|
+
// launch cannot carry a host token (asserting one logs a claude.ai subscriber out, #253),
|
|
222
|
+
// and with no resolvable credential there is nothing to carry either way.
|
|
223
|
+
if (destination.requiresAdmissionToken && (markerMode !== "proxy" || !hostAdmissionToken)) {
|
|
224
|
+
console.error(
|
|
225
|
+
`⚠ Skipping system-environment injection: ${destination.origin} requires an opencodex data-plane `
|
|
226
|
+
+ "credential that this launch cannot supply. Enable `unauthenticatedLoopbackListener` or bind the "
|
|
227
|
+
+ "proxy to loopback.",
|
|
228
|
+
);
|
|
229
|
+
return { injected: false, reason: "local inference requires a data-plane credential" };
|
|
230
|
+
}
|
|
231
|
+
|
|
153
232
|
const currentBaseUrl = launchctlGetenv("ANTHROPIC_BASE_URL");
|
|
154
|
-
|
|
233
|
+
// Ours is loopback in every topology but one: with the listener off and a non-loopback bind
|
|
234
|
+
// the value WE wrote names the bind address, and rejecting it as "user has custom" would make
|
|
235
|
+
// every subsequent start refuse to refresh its own injection.
|
|
236
|
+
if (currentBaseUrl
|
|
237
|
+
&& !/^http:\/\/127\.0\.0\.1:\d+$/.test(currentBaseUrl)
|
|
238
|
+
&& currentBaseUrl !== destination.origin) {
|
|
155
239
|
return { injected: false, reason: "user has custom ANTHROPIC_BASE_URL" };
|
|
156
240
|
}
|
|
157
241
|
// After stale cleanup, if a tracking file still exists with a DIFFERENT port,
|
|
@@ -167,15 +251,17 @@ export async function injectSystemEnv(
|
|
|
167
251
|
const inject = (name: string, value: string) => {
|
|
168
252
|
setLaunchctlEnv(name, value);
|
|
169
253
|
if (!injectedKeys.includes(name)) injectedKeys.push(name);
|
|
170
|
-
writeTracking(port, injectedKeys);
|
|
254
|
+
writeTracking(port, injectedKeys, tracked);
|
|
171
255
|
};
|
|
172
256
|
|
|
173
257
|
try {
|
|
174
|
-
inject("ANTHROPIC_BASE_URL",
|
|
258
|
+
inject("ANTHROPIC_BASE_URL", destination.origin);
|
|
175
259
|
inject("CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY", "1");
|
|
176
|
-
const markerMode = systemEnvMarkerMode(config, deps);
|
|
177
260
|
if (markerMode === "proxy") {
|
|
178
|
-
if (
|
|
261
|
+
if (hostAdmissionToken) {
|
|
262
|
+
// The guard above proved this exists whenever the destination demands one.
|
|
263
|
+
inject("ANTHROPIC_AUTH_TOKEN", hostAdmissionToken);
|
|
264
|
+
} else if (config.apiKeys?.length) {
|
|
179
265
|
inject("ANTHROPIC_AUTH_TOKEN", config.apiKeys[0].key);
|
|
180
266
|
} else if (launchctlGetenv("ANTHROPIC_AUTH_TOKEN") === undefined) {
|
|
181
267
|
inject("ANTHROPIC_AUTH_TOKEN", PROXY_MARKER);
|
|
@@ -190,7 +276,7 @@ export async function injectSystemEnv(
|
|
|
190
276
|
unsetLaunchctlEnv("ANTHROPIC_AUTH_TOKEN");
|
|
191
277
|
const tokenIdx = injectedKeys.indexOf("ANTHROPIC_AUTH_TOKEN");
|
|
192
278
|
if (tokenIdx >= 0) injectedKeys.splice(tokenIdx, 1);
|
|
193
|
-
writeTracking(port, injectedKeys);
|
|
279
|
+
writeTracking(port, injectedKeys, tracked);
|
|
194
280
|
}
|
|
195
281
|
}
|
|
196
282
|
// Lever keys (devlog 136 B6): user-wins — skip any key the user already set in the
|
|
@@ -242,9 +328,9 @@ export async function injectSystemEnv(
|
|
|
242
328
|
injectClaudeAgentDefs(config, windows);
|
|
243
329
|
} catch { /* best-effort */ }
|
|
244
330
|
|
|
245
|
-
writeTracking(port, injectedKeys);
|
|
331
|
+
writeTracking(port, injectedKeys, tracked);
|
|
246
332
|
} catch (error) {
|
|
247
|
-
rollbackInjectedKeys(port, injectedKeys);
|
|
333
|
+
rollbackInjectedKeys(port, injectedKeys, tracked);
|
|
248
334
|
removeShellEnvFile();
|
|
249
335
|
console.error("Failed to inject system environment; rolled back launchctl changes:", error);
|
|
250
336
|
throw error;
|
|
@@ -266,7 +352,7 @@ export function revertSystemEnv(): RevertResult {
|
|
|
266
352
|
|
|
267
353
|
try {
|
|
268
354
|
const tracksBaseUrl = tracking.injectedKeys?.includes("ANTHROPIC_BASE_URL") ?? true;
|
|
269
|
-
if (tracksBaseUrl && launchctlGetenv("ANTHROPIC_BASE_URL") !==
|
|
355
|
+
if (tracksBaseUrl && launchctlGetenv("ANTHROPIC_BASE_URL") !== trackedBaseUrl(tracking)) {
|
|
270
356
|
return { reverted: false, reason: "ownership mismatch" };
|
|
271
357
|
}
|
|
272
358
|
|
|
@@ -296,7 +382,13 @@ export async function cleanStaleSystemEnv(): Promise<CleanupResult> {
|
|
|
296
382
|
if (!tracking) return { cleaned: false, reason: "no tracking file" };
|
|
297
383
|
|
|
298
384
|
try {
|
|
299
|
-
|
|
385
|
+
// The BIND address and `tracking.port`, never the injected destination: `/healthz` is not
|
|
386
|
+
// on the unauthenticated loopback listener's allowlist, so probing that port would 404 and
|
|
387
|
+
// revert a live proxy's environment (#4236). Nor is it always 127.0.0.1 — a tailnet-bound
|
|
388
|
+
// hub has no loopback socket at all, and probing one there failed every single time, which
|
|
389
|
+
// reverted the record on every start and made the "another instance owns env" guard above
|
|
390
|
+
// unreachable.
|
|
391
|
+
const response = await fetch(`${trackedHealthzOrigin(tracking)}/healthz`, {
|
|
300
392
|
signal: AbortSignal.timeout(1_000),
|
|
301
393
|
});
|
|
302
394
|
if (response.ok) return { cleaned: false, reason: "proxy still alive" };
|