@bitkyc08/opencodex 2.8.2-preview.20260731 → 2.9.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 +24 -0
- package/bin/ocx.mjs +32 -4
- package/gui/dist/assets/index-CHwf3tTD.css +1 -0
- package/gui/dist/assets/index-u5eFOv2y.js +67 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-normalize.ts +114 -20
- package/src/adapters/anthropic.ts +86 -5
- package/src/adapters/azure.ts +3 -3
- package/src/adapters/base.ts +7 -3
- package/src/adapters/cursor/discovery.ts +14 -4
- package/src/adapters/cursor/effort-map.ts +18 -6
- package/src/adapters/cursor/framing.ts +102 -27
- package/src/adapters/cursor/kv-store.ts +30 -3
- package/src/adapters/cursor/live-models.ts +22 -2
- package/src/adapters/cursor/live-transport.ts +245 -49
- package/src/adapters/cursor/mcp-manager.ts +105 -8
- package/src/adapters/cursor/native-exec-mcp.ts +5 -3
- package/src/adapters/cursor/native-exec-shell.ts +296 -14
- package/src/adapters/cursor/native-exec.ts +381 -33
- package/src/adapters/cursor/protobuf-events.ts +28 -1
- package/src/adapters/cursor/protobuf-request.ts +71 -39
- package/src/adapters/cursor/request-builder.ts +2 -2
- package/src/adapters/cursor/transport.ts +2 -0
- package/src/adapters/cursor.ts +13 -2
- package/src/adapters/google-antigravity-replay.ts +184 -17
- package/src/adapters/google.ts +58 -8
- package/src/adapters/kiro-thinking.ts +23 -9
- package/src/adapters/kiro-tools.ts +49 -18
- package/src/adapters/kiro.ts +377 -133
- package/src/adapters/mimo-free.ts +36 -4
- package/src/adapters/openai-chat.ts +89 -14
- package/src/adapters/openai-responses.ts +130 -14
- package/src/adapters/run-turn-queue.ts +7 -1
- package/src/bridge.ts +466 -69
- package/src/chat/outbound.ts +144 -38
- package/src/claude/inbound-debug.ts +53 -8
- package/src/claude/outbound.ts +224 -38
- package/src/cli/agent-driven.ts +34 -1
- package/src/cli/catalog-prewarm.ts +5 -2
- package/src/cli/doctor.ts +12 -0
- package/src/cli/export-command.ts +187 -0
- package/src/cli/help.ts +11 -0
- package/src/cli/index.ts +13 -3
- package/src/cli/opencode.ts +36 -151
- package/src/cli/star-prompt.ts +13 -4
- package/src/cli/status-oauth.ts +12 -2
- package/src/clients/config-export.ts +377 -0
- package/src/codex/account-runtime-state.ts +19 -1
- package/src/codex/account-store.ts +162 -82
- package/src/codex/auth-api.ts +467 -159
- package/src/codex/auth-context.ts +15 -2
- package/src/codex/catalog/aggregation.ts +15 -0
- package/src/codex/catalog/effort.ts +16 -6
- package/src/codex/catalog/parsing.ts +3 -1
- package/src/codex/catalog/provider-fetch.ts +29 -0
- package/src/codex/catalog/sync.ts +64 -7
- package/src/codex/inject.ts +5 -5
- package/src/codex/main-account-cache.ts +8 -1
- package/src/codex/model-cache.ts +81 -2
- package/src/codex/pool-rotation.ts +39 -0
- package/src/codex/project-config-warnings.ts +12 -1
- package/src/codex/quota.ts +35 -3
- package/src/codex/routing.ts +46 -1
- package/src/codex/shim.ts +10 -4
- package/src/codex/subagent-model-fallback.ts +12 -0
- package/src/codex/websocket-registry.ts +27 -0
- package/src/combos/failover.ts +31 -1
- package/src/combos/request.ts +9 -0
- package/src/combos/resolve.ts +60 -4
- package/src/combos/types.ts +12 -0
- package/src/config.ts +510 -55
- package/src/github/star-state.ts +13 -1
- package/src/images/fulfill.ts +39 -1
- package/src/images/loop.ts +52 -12
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +173 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bun-stream-caps.ts +31 -7
- package/src/lib/config-ownership.ts +33 -0
- package/src/lib/crash-guard.ts +65 -5
- package/src/lib/debug-log-buffer.ts +47 -6
- package/src/lib/errors.ts +3 -0
- package/src/lib/gcp-adc.ts +40 -2
- package/src/lib/injection-debug-log.ts +26 -2
- package/src/lib/sidecar-tracker.ts +5 -2
- package/src/lib/sse-decoder.ts +257 -37
- package/src/lib/state-store-registrations.ts +109 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/translator-budget.ts +356 -0
- package/src/lib/windows-secret-acl.ts +33 -12
- package/src/lib/winsw.ts +8 -1
- package/src/oauth/anthropic-routing.ts +31 -7
- package/src/oauth/google-antigravity.ts +2 -1
- package/src/oauth/health.ts +30 -12
- package/src/oauth/index.ts +127 -23
- package/src/oauth/kiro.ts +10 -2
- package/src/oauth/store.ts +165 -18
- package/src/oauth/token-guardian.ts +43 -4
- package/src/oauth/types.ts +2 -1
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/derive.ts +12 -0
- package/src/providers/key-failover.ts +12 -0
- package/src/providers/openai-sidecar.ts +4 -1
- package/src/providers/quota.ts +68 -7
- package/src/providers/registry.ts +279 -3
- package/src/responses/parser.ts +5 -1
- package/src/responses/spill-store.ts +394 -0
- package/src/responses/state.ts +520 -102
- package/src/router.ts +18 -1
- package/src/server/adapter-resolve.ts +20 -3
- package/src/server/auth-cors.ts +121 -28
- package/src/server/chat-completions.ts +57 -12
- package/src/server/claude-messages.ts +85 -13
- package/src/server/index.ts +239 -97
- package/src/server/lifecycle.ts +155 -25
- package/src/server/management/agent-settings-routes.ts +75 -32
- package/src/server/management/api-key-usage.ts +167 -0
- package/src/server/management/body.ts +35 -0
- package/src/server/management/combo-routes.ts +5 -1
- package/src/server/management/config-routes.ts +37 -12
- package/src/server/management/logs-usage-routes.ts +41 -21
- package/src/server/management/model-routes.ts +173 -53
- package/src/server/management/oauth-account-routes.ts +115 -26
- package/src/server/management/provider-routes.ts +56 -6
- package/src/server/management/sidebar-routes.ts +50 -1
- package/src/server/management/system-restart.ts +13 -6
- package/src/server/management/system-routes.ts +15 -3
- package/src/server/management/usage-summary-cache.ts +86 -0
- package/src/server/management-api.ts +39 -5
- package/src/server/management-auth.ts +65 -14
- package/src/server/port-reclaim.ts +58 -12
- package/src/server/ports.ts +2 -0
- package/src/server/proxy-liveness.ts +51 -12
- package/src/server/relay-eager.ts +20 -4
- package/src/server/relay.ts +548 -154
- package/src/server/request-decompress.ts +51 -4
- package/src/server/request-log.ts +134 -15
- package/src/server/responses/collaboration.ts +15 -4
- package/src/server/responses/compact.ts +3 -0
- package/src/server/responses/core.ts +241 -66
- package/src/server/responses-image-gen-repair.ts +19 -5
- package/src/server/responses-item-id-repair.ts +23 -5
- package/src/server/sse-payload-rewrite.ts +71 -12
- package/src/server/startup-health-cache.ts +14 -1
- package/src/server/system-env.ts +8 -1
- package/src/server/windows-tcp-drop.ts +15 -5
- package/src/server/ws-bridge.ts +25 -0
- package/src/service.ts +23 -3
- package/src/storage/policy-job.ts +93 -23
- package/src/storage/policy-worker.ts +6 -0
- package/src/storage/restore-job.ts +62 -16
- package/src/storage/restore-worker.ts +6 -0
- package/src/storage/storage-mutation-coordinator.ts +36 -6
- package/src/storage/worker-lifecycle.ts +181 -47
- package/src/tray/windows.ts +45 -18
- package/src/types.ts +34 -6
- package/src/update/index.ts +24 -5
- package/src/update/job.ts +598 -73
- package/src/usage/log.ts +115 -17
- package/src/usage/summary.ts +67 -2
- package/src/vision/index.ts +112 -22
- package/src/web-search/loop.ts +38 -6
- package/src/web-search/progress-stream.ts +14 -3
- package/gui/dist/assets/index-BHsKRFh9.css +0 -1
- package/gui/dist/assets/index-GC0Vlu1Z.js +0 -67
|
@@ -2,77 +2,211 @@
|
|
|
2
2
|
* Deterministic teardown for the storage Bun Workers.
|
|
3
3
|
*
|
|
4
4
|
* `Worker.terminate()` returns void and does NOT wait for the thread to be
|
|
5
|
-
* reclaimed.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* run.
|
|
5
|
+
* reclaimed. Callers that fire-and-forget leave a window where
|
|
6
|
+
* `bun test --isolate` reclaims the file realm while a Windows worker thread
|
|
7
|
+
* is still exiting — Bun then panics with
|
|
8
|
+
* `workers_spawned(N) workers_terminated(N-1)` / Internal assertion failure.
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
10
|
+
* Invariant: every registered worker has a `close` listener attached at spawn
|
|
11
|
+
* time (so we cannot miss `self.close()` / early exit), stays in `liveWorkers`
|
|
12
|
+
* until that close settles, and `drainStorageWorkers()` joins every in-flight
|
|
13
|
+
* terminate. Spawns are serialized through `withStorageWorkerSpawnGate` so the
|
|
14
|
+
* next Worker cannot be created until prior threads have exited. On Windows and
|
|
15
|
+
* macOS, a post-close settle covers the OS join gap Bun does not expose
|
|
16
|
+
* (Windows unbalanced join panic; macOS Silicon balanced-count segfault under
|
|
17
|
+
* `bun test --isolate`).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { createAdmissionGate, type AdmissionMetrics, type AdmissionReservation } from "../lib/admission";
|
|
21
|
+
|
|
22
|
+
type TrackedWorker = {
|
|
23
|
+
worker: Worker;
|
|
24
|
+
closed: Promise<void>;
|
|
25
|
+
resolveClosed: () => void;
|
|
26
|
+
terminatePromise?: Promise<void>;
|
|
27
|
+
reservation?: AdmissionReservation<Worker>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const MAX_RESERVED_STORAGE_WORKER_SPAWNS = 16;
|
|
31
|
+
export class StorageWorkerAdmissionBusyError extends Error {
|
|
32
|
+
readonly code = "storage_mutation_busy";
|
|
33
|
+
|
|
34
|
+
constructor() {
|
|
35
|
+
super("storage worker spawn queue is busy");
|
|
36
|
+
this.name = "StorageWorkerAdmissionBusyError";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const liveWorkers = new Map<Worker, TrackedWorker>();
|
|
41
|
+
const workerGate = createAdmissionGate("storage_worker_reservations", MAX_RESERVED_STORAGE_WORKER_SPAWNS);
|
|
42
|
+
|
|
43
|
+
/** Serialize spawns so a new Worker never overlaps a still-exiting predecessor. */
|
|
44
|
+
let spawnGate: Promise<void> = Promise.resolve();
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Bumped by teardown so a spawn still queued on `spawnGate` (not yet in
|
|
48
|
+
* `liveWorkers`) cannot create a Worker after reset/shutdown reported idle.
|
|
49
|
+
*/
|
|
50
|
+
let spawnCancelEpoch = 0;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* OS-join gap after the `close` event on platforms where Bun's Worker reclaim
|
|
54
|
+
* races the isolate/file boundary (not a CI job-timeout bump).
|
|
55
|
+
* Windows GHA at 250ms and 750ms still left `workers_spawned(N)
|
|
56
|
+
* workers_terminated(N-1)` panics under isolate (seen mid
|
|
57
|
+
* `storage-mutation-race` with 11/10). 1500ms covers deferred reclaim under
|
|
58
|
+
* stacked policy/restore workers. Darwin uses 250ms.
|
|
23
59
|
*/
|
|
60
|
+
const WORKER_OS_JOIN_MS = process.platform === "win32" ? 1_500 : 250;
|
|
24
61
|
|
|
25
|
-
|
|
62
|
+
function needsWorkerOsJoinSettle(): boolean {
|
|
63
|
+
return process.platform === "win32" || process.platform === "darwin";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Invalidate spawn callbacks still waiting on the gate (reset / server drain). */
|
|
67
|
+
export function cancelQueuedStorageWorkerSpawns(): void {
|
|
68
|
+
spawnCancelEpoch += 1;
|
|
69
|
+
}
|
|
26
70
|
|
|
27
71
|
/** Track a freshly spawned worker so teardown can wait for it later. */
|
|
28
72
|
export function registerStorageWorker(worker: Worker): void {
|
|
29
|
-
liveWorkers.
|
|
73
|
+
if (liveWorkers.has(worker)) return;
|
|
74
|
+
|
|
75
|
+
let resolveClosed!: () => void;
|
|
76
|
+
const closed = new Promise<void>(resolve => {
|
|
77
|
+
resolveClosed = resolve;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const tracked: TrackedWorker = { worker, closed, resolveClosed };
|
|
81
|
+
liveWorkers.set(worker, tracked);
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
worker.addEventListener("close", () => {
|
|
85
|
+
resolveClosed();
|
|
86
|
+
}, { once: true });
|
|
87
|
+
} catch {
|
|
88
|
+
// No close event — terminate path will still resolve via timeout/settle.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function tryReserveStorageWorker(): AdmissionReservation<Worker> | null {
|
|
93
|
+
const gateLease = workerGate.tryAcquire();
|
|
94
|
+
if (!gateLease) return null;
|
|
95
|
+
let active = true;
|
|
96
|
+
let bound: Worker | undefined;
|
|
97
|
+
const reservation: AdmissionReservation<Worker> = {
|
|
98
|
+
bind(worker) {
|
|
99
|
+
if (!active) return;
|
|
100
|
+
bound = worker;
|
|
101
|
+
registerStorageWorker(worker);
|
|
102
|
+
const tracked = liveWorkers.get(worker);
|
|
103
|
+
if (tracked) tracked.reservation = reservation;
|
|
104
|
+
},
|
|
105
|
+
release() {
|
|
106
|
+
if (!active) return;
|
|
107
|
+
active = false;
|
|
108
|
+
if (bound) {
|
|
109
|
+
const tracked = liveWorkers.get(bound);
|
|
110
|
+
if (tracked?.reservation === reservation) tracked.reservation = undefined;
|
|
111
|
+
}
|
|
112
|
+
bound = undefined;
|
|
113
|
+
gateLease.release();
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
return reservation;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function storageWorkerAdmissionMetrics(): AdmissionMetrics {
|
|
120
|
+
return workerGate.metrics();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Run `fn` only after every previously gated spawn has finished tearing down.
|
|
125
|
+
* Used around `new Worker(...)` so tests cannot overlap Windows thread exit.
|
|
126
|
+
*/
|
|
127
|
+
export function withStorageWorkerSpawnGate<T>(fn: () => Promise<T>): Promise<T> {
|
|
128
|
+
const epochAtEnqueue = spawnCancelEpoch;
|
|
129
|
+
const run = spawnGate.then(async () => {
|
|
130
|
+
if (epochAtEnqueue !== spawnCancelEpoch) {
|
|
131
|
+
throw new Error("storage_worker_spawn_cancelled");
|
|
132
|
+
}
|
|
133
|
+
await drainStorageWorkers();
|
|
134
|
+
if (epochAtEnqueue !== spawnCancelEpoch) {
|
|
135
|
+
throw new Error("storage_worker_spawn_cancelled");
|
|
136
|
+
}
|
|
137
|
+
return fn();
|
|
138
|
+
});
|
|
139
|
+
spawnGate = run.then(
|
|
140
|
+
() => undefined,
|
|
141
|
+
() => undefined,
|
|
142
|
+
);
|
|
143
|
+
return run;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function queuedStorageWorkerSpawnCount(): number {
|
|
147
|
+
return Math.max(0, workerGate.metrics().active - liveWorkers.size);
|
|
30
148
|
}
|
|
31
149
|
|
|
32
150
|
/**
|
|
33
151
|
* Terminate a worker and resolve once its thread has actually exited.
|
|
34
152
|
*
|
|
35
|
-
* Safe to call twice: the second call
|
|
36
|
-
* resolves immediately.
|
|
153
|
+
* Safe to call twice: the second call joins the in-flight terminate promise.
|
|
37
154
|
*/
|
|
38
155
|
export function terminateStorageWorker(worker: Worker, timeoutMs = 5_000): Promise<void> {
|
|
39
|
-
|
|
156
|
+
const tracked = liveWorkers.get(worker);
|
|
157
|
+
if (!tracked) {
|
|
40
158
|
try { worker.terminate(); } catch { /* already gone */ }
|
|
41
159
|
return Promise.resolve();
|
|
42
160
|
}
|
|
43
|
-
|
|
161
|
+
if (tracked.terminatePromise) return tracked.terminatePromise;
|
|
162
|
+
|
|
163
|
+
tracked.terminatePromise = (async () => {
|
|
164
|
+
let timedOut = false;
|
|
165
|
+
const timer = setTimeout(() => {
|
|
166
|
+
timedOut = true;
|
|
167
|
+
tracked.resolveClosed();
|
|
168
|
+
}, timeoutMs);
|
|
44
169
|
|
|
45
|
-
return new Promise<void>(resolve => {
|
|
46
|
-
let done = false;
|
|
47
|
-
const settle = (): void => {
|
|
48
|
-
if (done) return;
|
|
49
|
-
done = true;
|
|
50
|
-
clearTimeout(timer);
|
|
51
|
-
resolve();
|
|
52
|
-
};
|
|
53
|
-
// A worker that refuses to exit must not wedge teardown; the proxy path
|
|
54
|
-
// never awaits this, and a test teardown would rather continue than hang.
|
|
55
|
-
const timer = setTimeout(settle, timeoutMs);
|
|
56
|
-
try {
|
|
57
|
-
worker.addEventListener("close", settle, { once: true });
|
|
58
|
-
} catch {
|
|
59
|
-
// No close event available — fall back to the timeout above.
|
|
60
|
-
}
|
|
61
170
|
try {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
171
|
+
try {
|
|
172
|
+
worker.terminate();
|
|
173
|
+
} catch {
|
|
174
|
+
tracked.resolveClosed();
|
|
175
|
+
}
|
|
176
|
+
await tracked.closed;
|
|
177
|
+
// Disarm before the OS-join settle: a late timer firing during the sleep
|
|
178
|
+
// would set timedOut after close already won and throw a false timeout.
|
|
179
|
+
clearTimeout(timer);
|
|
180
|
+
// Always run the OS-join settle before throwing on timeout: the timer
|
|
181
|
+
// only forces `closed`, it does not prove the OS thread has exited.
|
|
182
|
+
// Callers that catch and continue (e.g. drainAndShutdown) still need
|
|
183
|
+
// that gap before the next isolate reclaim or server.stop.
|
|
184
|
+
if (needsWorkerOsJoinSettle()) {
|
|
185
|
+
await Bun.sleep(0);
|
|
186
|
+
await Bun.sleep(WORKER_OS_JOIN_MS);
|
|
187
|
+
}
|
|
188
|
+
if (timedOut) {
|
|
189
|
+
throw new Error(`storage worker did not exit within ${timeoutMs}ms`);
|
|
190
|
+
}
|
|
191
|
+
} finally {
|
|
192
|
+
clearTimeout(timer);
|
|
193
|
+
liveWorkers.delete(worker);
|
|
194
|
+
tracked.reservation?.release();
|
|
195
|
+
tracked.reservation = undefined;
|
|
65
196
|
}
|
|
66
|
-
});
|
|
197
|
+
})();
|
|
198
|
+
|
|
199
|
+
return tracked.terminatePromise;
|
|
67
200
|
}
|
|
68
201
|
|
|
69
202
|
/**
|
|
70
|
-
* Await every worker this module still tracks
|
|
71
|
-
* storage worker outlives the file that
|
|
203
|
+
* Await every worker this module still tracks (including terminations already
|
|
204
|
+
* in flight). Used by test resets so no storage worker outlives the file that
|
|
205
|
+
* spawned it.
|
|
72
206
|
*/
|
|
73
207
|
export async function drainStorageWorkers(timeoutMs = 5_000): Promise<void> {
|
|
74
|
-
const pending = [...liveWorkers];
|
|
75
|
-
await Promise.all(pending
|
|
208
|
+
const pending = [...liveWorkers.keys()].map(worker => terminateStorageWorker(worker, timeoutMs));
|
|
209
|
+
await Promise.all(pending);
|
|
76
210
|
}
|
|
77
211
|
|
|
78
212
|
/** Live worker count — exported so a regression test can assert the invariant. */
|
package/src/tray/windows.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { homedir } from "node:os";
|
|
|
5
5
|
import { join, resolve } from "node:path";
|
|
6
6
|
import { expandUserPath, getConfigDir } from "../config";
|
|
7
7
|
import { durableBunPath } from "../lib/bun-runtime";
|
|
8
|
-
import { hardenSecretDir, hardenSecretPath } from "../lib/windows-secret-acl";
|
|
8
|
+
import { forgetHardenedSecretPath, hardenSecretDir, hardenSecretPath } from "../lib/windows-secret-acl";
|
|
9
9
|
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
10
10
|
|
|
11
11
|
const RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
|
@@ -218,18 +218,45 @@ function readState(): WindowsTrayState | null {
|
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
|
|
221
|
+
export interface WindowsTrayOwnedFileIO {
|
|
222
|
+
write(path: string, contents: string | Buffer): void;
|
|
223
|
+
harden(path: string): void;
|
|
224
|
+
rename(source: string, destination: string): void;
|
|
225
|
+
unlink(path: string): void;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function replaceWindowsTrayOwnedFile(
|
|
229
|
+
path: string,
|
|
230
|
+
contents: string | Buffer,
|
|
231
|
+
io: WindowsTrayOwnedFileIO = {
|
|
232
|
+
write: (target, value) => writeFileSync(target, value, { mode: 0o600 }),
|
|
233
|
+
harden: target => {
|
|
234
|
+
try { chmodSync(target, 0o600); } catch { /* best-effort */ }
|
|
235
|
+
if (process.platform !== "win32") return;
|
|
236
|
+
const hardened = hardenSecretPath(target, { required: true });
|
|
237
|
+
if (!hardened.ok) throw new Error("Windows tray ACL hardening did not complete; refusing to persist executable state.");
|
|
238
|
+
},
|
|
239
|
+
rename: renameSync,
|
|
240
|
+
unlink: unlinkSync,
|
|
241
|
+
},
|
|
242
|
+
): void {
|
|
222
243
|
const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
223
|
-
|
|
244
|
+
io.write(temporary, contents);
|
|
245
|
+
let renamed = false;
|
|
224
246
|
try {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
renameSync(temporary, path);
|
|
247
|
+
io.harden(temporary);
|
|
248
|
+
io.rename(temporary, path);
|
|
249
|
+
renamed = true;
|
|
250
|
+
forgetHardenedSecretPath(temporary);
|
|
231
251
|
} finally {
|
|
232
|
-
|
|
252
|
+
if (!renamed) {
|
|
253
|
+
try {
|
|
254
|
+
io.unlink(temporary);
|
|
255
|
+
forgetHardenedSecretPath(temporary);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") forgetHardenedSecretPath(temporary);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
233
260
|
}
|
|
234
261
|
}
|
|
235
262
|
|
|
@@ -239,7 +266,7 @@ function writeState(
|
|
|
239
266
|
runCommand: string,
|
|
240
267
|
): void {
|
|
241
268
|
const path = trayStatePath();
|
|
242
|
-
|
|
269
|
+
replaceWindowsTrayOwnedFile(path, JSON.stringify({ version: TRAY_STATE_VERSION, ...entry, runValue, runCommand }, null, 2) + "\n");
|
|
243
270
|
}
|
|
244
271
|
|
|
245
272
|
export function parseWindowsTrayRunValue(output: string, runValue: string): string | null {
|
|
@@ -580,21 +607,21 @@ export function installWindowsTray(startNow = true): WindowsTrayStatus {
|
|
|
580
607
|
]));
|
|
581
608
|
const restorePreviousInstall = () => {
|
|
582
609
|
try {
|
|
583
|
-
if (previousScriptBytes)
|
|
610
|
+
if (previousScriptBytes) replaceWindowsTrayOwnedFile(entry.script, previousScriptBytes);
|
|
584
611
|
else if (existsSync(entry.script)) unlinkSync(entry.script);
|
|
585
612
|
} catch { /* rollback best-effort */ }
|
|
586
613
|
try {
|
|
587
|
-
if (previousLauncherBytes)
|
|
614
|
+
if (previousLauncherBytes) replaceWindowsTrayOwnedFile(launcherPath, previousLauncherBytes);
|
|
588
615
|
else if (existsSync(launcherPath)) unlinkSync(launcherPath);
|
|
589
616
|
} catch { /* rollback best-effort */ }
|
|
590
617
|
for (const [path, contents] of previousIconBytes) {
|
|
591
618
|
try {
|
|
592
|
-
if (contents)
|
|
619
|
+
if (contents) replaceWindowsTrayOwnedFile(path, contents);
|
|
593
620
|
else if (existsSync(path)) unlinkSync(path);
|
|
594
621
|
} catch { /* rollback best-effort */ }
|
|
595
622
|
}
|
|
596
623
|
try {
|
|
597
|
-
if (previousStateBytes)
|
|
624
|
+
if (previousStateBytes) replaceWindowsTrayOwnedFile(trayStatePath(), previousStateBytes);
|
|
598
625
|
else if (existsSync(trayStatePath())) unlinkSync(trayStatePath());
|
|
599
626
|
} catch { /* rollback best-effort */ }
|
|
600
627
|
try {
|
|
@@ -612,9 +639,9 @@ export function installWindowsTray(startNow = true): WindowsTrayStatus {
|
|
|
612
639
|
try {
|
|
613
640
|
const hardenedDir = hardenSecretDir(getConfigDir(), { required: true });
|
|
614
641
|
if (!hardenedDir.ok) throw new Error("Windows tray directory ACL hardening did not complete; refusing to install persistence.");
|
|
615
|
-
|
|
616
|
-
for (const pair of iconPairs)
|
|
617
|
-
|
|
642
|
+
replaceWindowsTrayOwnedFile(entry.script, readFileSync(sourceScript));
|
|
643
|
+
for (const pair of iconPairs) replaceWindowsTrayOwnedFile(pair.installed, readFileSync(pair.source));
|
|
644
|
+
replaceWindowsTrayOwnedFile(launcherPath, Buffer.from("\uFEFF" + buildWindowsTrayLauncherScript(entry), "utf16le"));
|
|
618
645
|
runRegistry(["add", RUN_KEY, "/v", runValue, "/t", "REG_SZ", "/d", runCommand, "/f", "/reg:64"]);
|
|
619
646
|
writeState(entryWithLauncher, runValue, runCommand);
|
|
620
647
|
} catch (error) {
|
package/src/types.ts
CHANGED
|
@@ -516,8 +516,22 @@ export interface OcxCustomModel {
|
|
|
516
516
|
addedAt?: string;
|
|
517
517
|
}
|
|
518
518
|
|
|
519
|
+
/**
|
|
520
|
+
* A generated `ocx_` data-plane key. `key` is the secret itself and never leaves
|
|
521
|
+
* the server except in the one-time POST /api/keys response; every other surface
|
|
522
|
+
* sees only the masked prefix.
|
|
523
|
+
*/
|
|
524
|
+
export interface OcxApiKeyEntry {
|
|
525
|
+
id: string;
|
|
526
|
+
name: string;
|
|
527
|
+
key: string;
|
|
528
|
+
createdAt: string;
|
|
529
|
+
}
|
|
530
|
+
|
|
519
531
|
export interface OcxConfig {
|
|
520
532
|
port: number;
|
|
533
|
+
/** Maximum usage-log bytes read for one management snapshot. */
|
|
534
|
+
managementUsageMaxReadBytes?: number;
|
|
521
535
|
providers: Record<string, OcxProviderConfig>;
|
|
522
536
|
defaultProvider: string;
|
|
523
537
|
/** OpenAI provider-contract migration marker (v2 = single `openai` provider with account mode). */
|
|
@@ -579,11 +593,12 @@ export interface OcxConfig {
|
|
|
579
593
|
*/
|
|
580
594
|
fastMode?: boolean;
|
|
581
595
|
/**
|
|
582
|
-
* Windows SSE passthrough stream shape (#314 mitigation).
|
|
583
|
-
* "auto" (default)
|
|
584
|
-
* Bun#32111 fix
|
|
585
|
-
*
|
|
586
|
-
*
|
|
596
|
+
* Windows/macOS SSE passthrough stream shape (#314 mitigation).
|
|
597
|
+
* On Windows, "auto" (default) selects eager relay only on a runtime proven
|
|
598
|
+
* to carry the Bun#32111 fix. On macOS, "auto" always stays on legacy tee and
|
|
599
|
+
* eager relay is explicit-only. "eager-relay" opts into the new relay (and
|
|
600
|
+
* accepts #32111 crash risk on Bun 1.3.14); "legacy-tee" pins the tee path.
|
|
601
|
+
* Persisted in config.json so service users can select the stream shape.
|
|
587
602
|
* See src/lib/bun-stream-caps.ts.
|
|
588
603
|
*/
|
|
589
604
|
streamMode?: "auto" | "legacy-tee" | "eager-relay";
|
|
@@ -675,7 +690,7 @@ export interface OcxConfig {
|
|
|
675
690
|
*/
|
|
676
691
|
storageCleanupPolicy?: StorageCleanupPolicy;
|
|
677
692
|
/** Generated API keys for external access to the proxy's /v1/responses endpoint. */
|
|
678
|
-
apiKeys?:
|
|
693
|
+
apiKeys?: OcxApiKeyEntry[];
|
|
679
694
|
/** Auto-start/sync the proxy from the Codex shim before launching Codex. Default true. */
|
|
680
695
|
codexAutoStart?: boolean;
|
|
681
696
|
/** Restore an installed shim after a stable external Codex update replaces it. Default true. */
|
|
@@ -689,6 +704,8 @@ export interface OcxConfig {
|
|
|
689
704
|
syncResumeHistory?: boolean;
|
|
690
705
|
/** Freshness window (ms) for the per-provider live `/models` cache. Defaults to 5 min. */
|
|
691
706
|
modelCacheTtlMs?: number;
|
|
707
|
+
/** Evictable retained app-state budget in MiB. Default 256; valid 64..4096. */
|
|
708
|
+
appOwnedMemoryBudgetMb?: number;
|
|
692
709
|
/** Anthropic prompt-cache retention: "short" = 5-min ephemeral (default), "long" = 1-hour extended, "none" = disabled. */
|
|
693
710
|
cacheRetention?: "none" | "short" | "long";
|
|
694
711
|
/** Web-search sidecar: route web_search for non-OpenAI models through a gpt-mini via ChatGPT passthrough. */
|
|
@@ -890,6 +907,10 @@ export interface ResponsesItemIdRepairConfig {
|
|
|
890
907
|
|
|
891
908
|
export interface OcxProviderConfig {
|
|
892
909
|
adapter: string;
|
|
910
|
+
/** Cursor MCP compatibility bounds; positive integers when configured. */
|
|
911
|
+
mcpMaxTools?: number;
|
|
912
|
+
mcpMaxSchemaBytes?: number;
|
|
913
|
+
mcpMaxResultBytes?: number;
|
|
893
914
|
/**
|
|
894
915
|
* Per-model wire override, keyed by the upstream native model id (after namespace
|
|
895
916
|
* and combo resolution). A single gateway can front models that speak different
|
|
@@ -908,6 +929,13 @@ export interface OcxProviderConfig {
|
|
|
908
929
|
* the legacy `/v1/responses` construction.
|
|
909
930
|
*/
|
|
910
931
|
responsesPath?: string;
|
|
932
|
+
/**
|
|
933
|
+
* Responses upstream that stores nothing server-side (DeepSeek documents "the API
|
|
934
|
+
* is stateless"). Stateful request parameters are dropped, `store` is pinned false,
|
|
935
|
+
* and orphaned tool results left by a replay miss are repaired rather than
|
|
936
|
+
* forwarded to an upstream that cannot resolve their pair.
|
|
937
|
+
*/
|
|
938
|
+
statelessResponses?: boolean;
|
|
911
939
|
/**
|
|
912
940
|
* Explicit opt-in for non-registry private-network destinations such as localhost, RFC1918,
|
|
913
941
|
* link-local, or unique-local upstreams. Metadata endpoints remain blocked.
|
package/src/update/index.ts
CHANGED
|
@@ -321,16 +321,35 @@ export async function runUpdate(): Promise<void> {
|
|
|
321
321
|
windowsHide: true,
|
|
322
322
|
});
|
|
323
323
|
if (svcStdio === "pipe") logSpawnOutput("", svc);
|
|
324
|
-
|
|
324
|
+
const serviceRefreshed = svc.status === 0;
|
|
325
|
+
let serviceViable = serviceRefreshed;
|
|
326
|
+
if (serviceRefreshed) {
|
|
327
|
+
try {
|
|
328
|
+
const { isServiceViable } = await import("../service");
|
|
329
|
+
serviceViable = isServiceViable();
|
|
330
|
+
} catch {
|
|
331
|
+
serviceViable = false;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (!serviceRefreshed || !serviceViable) {
|
|
325
335
|
// On Windows, schtasks /create requires elevation. The CLI inherits the
|
|
326
336
|
// user's (non-admin) token, so the service reinstall can fail with access
|
|
327
|
-
// denied
|
|
328
|
-
//
|
|
337
|
+
// denied — or exit 0 while leaving stale/missing assets that never start
|
|
338
|
+
// the proxy. Fall back to a direct detached proxy start so the update
|
|
339
|
+
// never leaves the user without a running proxy — but only when the port is free.
|
|
329
340
|
if (!freed) {
|
|
330
|
-
console.warn(
|
|
341
|
+
console.warn(
|
|
342
|
+
serviceRefreshed
|
|
343
|
+
? "⚠️ Service refresh left a non-viable manager and the captured port is still busy; not starting on another port."
|
|
344
|
+
: "⚠️ Service refresh failed and the captured port is still busy; not starting on another port.",
|
|
345
|
+
);
|
|
331
346
|
console.warn(` Run 'ocx service install' as administrator, then 'ocx start --port ${capturedListen.port}'.`);
|
|
332
347
|
} else {
|
|
333
|
-
console.warn(
|
|
348
|
+
console.warn(
|
|
349
|
+
serviceRefreshed
|
|
350
|
+
? "⚠️ Service refresh left a non-viable manager (stale or missing assets) — starting the proxy directly instead."
|
|
351
|
+
: "⚠️ Service refresh failed — starting the proxy directly instead.",
|
|
352
|
+
);
|
|
334
353
|
console.warn(" Run 'ocx service install' as administrator to refresh the background service.");
|
|
335
354
|
const env = { ...process.env };
|
|
336
355
|
delete env.OCX_SERVICE;
|