@bitkyc08/opencodex 2.8.0 → 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 +126 -10
- 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 +143 -17
- 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/claude-desktop.ts +2 -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/init.ts +129 -102
- 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/metadata.ts +6 -0
- 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/catalog.ts +2 -2
- 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/destination-policy.ts +12 -1
- 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/provider-outbound.ts +3 -0
- 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 +14 -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-credentials.ts +72 -1
- package/src/oauth/kiro.ts +23 -4
- 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/free-directory.ts +4 -1
- 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 +242 -100
- package/src/server/lifecycle.ts +155 -25
- package/src/server/management/agent-settings-routes.ts +79 -36
- 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 +42 -12
- package/src/server/management/logs-usage-routes.ts +41 -21
- package/src/server/management/model-routes.ts +188 -54
- package/src/server/management/oauth-account-routes.ts +115 -26
- package/src/server/management/provider-routes.ts +56 -6
- package/src/server/management/shared.ts +16 -3
- 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 +60 -14
- 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 +179 -13
- 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 +97 -25
- package/src/types.ts +39 -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-BDjpkcRN.js +0 -67
- package/gui/dist/assets/index-BHsKRFh9.css +0 -1
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { resolve } from "node:path";
|
|
9
9
|
import { resolveCodexHomeDir } from "../codex/home";
|
|
10
|
+
import { createAdmissionGate, type AdmissionLease, type AdmissionMetrics } from "../lib/admission";
|
|
10
11
|
|
|
11
12
|
export type StorageMutationKind = "cleanup" | "restore" | "policy";
|
|
12
13
|
|
|
@@ -20,9 +21,13 @@ export interface StorageMutationCoordinatorTestHooks {
|
|
|
20
21
|
interface ActiveSlot {
|
|
21
22
|
kind: StorageMutationKind;
|
|
22
23
|
startedAt: number;
|
|
24
|
+
lease: AdmissionLease;
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
export const MAX_ACTIVE_STORAGE_HOME_SLOTS = 32;
|
|
25
28
|
const slots = new Map<string, ActiveSlot>();
|
|
29
|
+
const slotGate = createAdmissionGate("storage_home_slots", MAX_ACTIVE_STORAGE_HOME_SLOTS);
|
|
30
|
+
let releaseMisses = 0;
|
|
26
31
|
let testHooks: StorageMutationCoordinatorTestHooks | null = null;
|
|
27
32
|
|
|
28
33
|
function slotKey(codexHome?: string): string {
|
|
@@ -37,9 +42,15 @@ export function setStorageMutationCoordinatorTestHooks(
|
|
|
37
42
|
|
|
38
43
|
export function resetStorageMutationCoordinatorForTests(): void {
|
|
39
44
|
testHooks = null;
|
|
45
|
+
for (const slot of slots.values()) slot.lease.release();
|
|
40
46
|
slots.clear();
|
|
41
47
|
}
|
|
42
48
|
|
|
49
|
+
export function storageMutationAdmissionMetrics(): AdmissionMetrics {
|
|
50
|
+
const metrics = slotGate.metrics();
|
|
51
|
+
return { ...metrics, releaseMisses: metrics.releaseMisses + releaseMisses };
|
|
52
|
+
}
|
|
53
|
+
|
|
43
54
|
export function getActiveStorageMutation(
|
|
44
55
|
codexHome?: string,
|
|
45
56
|
): { kind: StorageMutationKind; startedAt: number } | null {
|
|
@@ -50,17 +61,36 @@ export function getActiveStorageMutation(
|
|
|
50
61
|
export function tryBeginStorageMutation(
|
|
51
62
|
kind: StorageMutationKind,
|
|
52
63
|
codexHome?: string,
|
|
53
|
-
): { acquired: true } | { acquired: false; error: StorageMutationBusyError } {
|
|
64
|
+
): { acquired: true; lease: AdmissionLease } | { acquired: false; error: StorageMutationBusyError } {
|
|
54
65
|
const key = slotKey(codexHome);
|
|
55
66
|
if (slots.has(key)) {
|
|
56
67
|
return { acquired: false, error: "storage_mutation_busy" };
|
|
57
68
|
}
|
|
58
|
-
|
|
59
|
-
return { acquired:
|
|
69
|
+
const lease = slotGate.tryAcquire();
|
|
70
|
+
if (!lease) return { acquired: false, error: "storage_mutation_busy" };
|
|
71
|
+
let active = true;
|
|
72
|
+
const ownerLease: AdmissionLease = {
|
|
73
|
+
release() {
|
|
74
|
+
if (!active) return;
|
|
75
|
+
active = false;
|
|
76
|
+
const owner = slots.get(key);
|
|
77
|
+
if (owner?.lease === ownerLease) slots.delete(key);
|
|
78
|
+
lease.release();
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
slots.set(key, { kind, startedAt: Date.now(), lease: ownerLease });
|
|
82
|
+
return { acquired: true, lease: ownerLease };
|
|
60
83
|
}
|
|
61
84
|
|
|
62
85
|
export function endStorageMutation(codexHome?: string): void {
|
|
63
|
-
|
|
86
|
+
const key = slotKey(codexHome);
|
|
87
|
+
const slot = slots.get(key);
|
|
88
|
+
if (!slot) {
|
|
89
|
+
releaseMisses += 1;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
slots.delete(key);
|
|
93
|
+
slot.lease.release();
|
|
64
94
|
}
|
|
65
95
|
|
|
66
96
|
async function applyCoordinatorBlock(): Promise<void> {
|
|
@@ -87,7 +117,7 @@ export async function runPolicyStorageMutation<T>(
|
|
|
87
117
|
await applyCoordinatorBlock();
|
|
88
118
|
return await work();
|
|
89
119
|
} finally {
|
|
90
|
-
|
|
120
|
+
gate.lease.release();
|
|
91
121
|
}
|
|
92
122
|
}
|
|
93
123
|
|
|
@@ -104,6 +134,6 @@ export async function withStorageMutationSlot<T>(
|
|
|
104
134
|
await applyCoordinatorBlock();
|
|
105
135
|
return await work();
|
|
106
136
|
} finally {
|
|
107
|
-
|
|
137
|
+
gate.lease.release();
|
|
108
138
|
}
|
|
109
139
|
}
|
|
@@ -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";
|
|
@@ -150,8 +150,16 @@ function quoteRunValue(value: string): string {
|
|
|
150
150
|
return `\"${value}\"`;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
function installedTrayLauncherPath(): string {
|
|
154
|
+
return join(getConfigDir(), "opencodex-tray.vbs");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function quoteVbsPath(value: string): string {
|
|
158
|
+
return value.replace(/"/g, '""');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Full PowerShell invocation used by the owned VBS launcher (not written to HKCU Run). */
|
|
162
|
+
export function buildWindowsTrayPowerShellCommand(entry: WindowsTrayEntry, powershell = windowsPowerShellPath()): string {
|
|
155
163
|
return [
|
|
156
164
|
quoteRunValue(powershell),
|
|
157
165
|
"-NoLogo",
|
|
@@ -169,6 +177,27 @@ export function buildWindowsTrayRunCommand(entry: WindowsTrayEntry, powershell =
|
|
|
169
177
|
].join(" ");
|
|
170
178
|
}
|
|
171
179
|
|
|
180
|
+
/** Short HKCU Run command (must stay ≤260 chars under long Windows user/npm paths). */
|
|
181
|
+
export function buildWindowsTrayRunCommand(entry: WindowsTrayEntry & { launcherPath: string }): string {
|
|
182
|
+
const wscript = join(process.env.SystemRoot ?? "C:\\Windows", "System32", "wscript.exe");
|
|
183
|
+
return `${quoteRunValue(wscript)} //B //NoLogo ${quoteRunValue(entry.launcherPath)}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function buildWindowsTrayLauncherScript(entry: WindowsTrayEntry, powershell = windowsPowerShellPath()): string {
|
|
187
|
+
const command = buildWindowsTrayPowerShellCommand(entry, powershell);
|
|
188
|
+
// VBS CreateObject("WScript.Shell").Run command, 0, False — hidden, non-blocking.
|
|
189
|
+
return [
|
|
190
|
+
"' OpenCodex owned tray launcher — do not edit by hand.",
|
|
191
|
+
`CreateObject("WScript.Shell").Run "${quoteVbsPath(command)}", 0, False`,
|
|
192
|
+
"",
|
|
193
|
+
].join("\r\n");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** @deprecated Prefer buildWindowsTrayPowerShellCommand; kept for callers that still expect the long form. */
|
|
197
|
+
export function buildWindowsTrayLegacyRunCommand(entry: WindowsTrayEntry, powershell = windowsPowerShellPath()): string {
|
|
198
|
+
return buildWindowsTrayPowerShellCommand(entry, powershell);
|
|
199
|
+
}
|
|
200
|
+
|
|
172
201
|
function readState(): WindowsTrayState | null {
|
|
173
202
|
try {
|
|
174
203
|
const state = JSON.parse(readFileSync(trayStatePath(), "utf8")) as Partial<WindowsTrayState>;
|
|
@@ -189,24 +218,55 @@ function readState(): WindowsTrayState | null {
|
|
|
189
218
|
}
|
|
190
219
|
}
|
|
191
220
|
|
|
192
|
-
|
|
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 {
|
|
193
243
|
const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
194
|
-
|
|
244
|
+
io.write(temporary, contents);
|
|
245
|
+
let renamed = false;
|
|
195
246
|
try {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
renameSync(temporary, path);
|
|
247
|
+
io.harden(temporary);
|
|
248
|
+
io.rename(temporary, path);
|
|
249
|
+
renamed = true;
|
|
250
|
+
forgetHardenedSecretPath(temporary);
|
|
202
251
|
} finally {
|
|
203
|
-
|
|
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
|
+
}
|
|
204
260
|
}
|
|
205
261
|
}
|
|
206
262
|
|
|
207
|
-
function writeState(
|
|
263
|
+
function writeState(
|
|
264
|
+
entry: WindowsTrayEntry & { launcherPath: string },
|
|
265
|
+
runValue: string,
|
|
266
|
+
runCommand: string,
|
|
267
|
+
): void {
|
|
208
268
|
const path = trayStatePath();
|
|
209
|
-
|
|
269
|
+
replaceWindowsTrayOwnedFile(path, JSON.stringify({ version: TRAY_STATE_VERSION, ...entry, runValue, runCommand }, null, 2) + "\n");
|
|
210
270
|
}
|
|
211
271
|
|
|
212
272
|
export function parseWindowsTrayRunValue(output: string, runValue: string): string | null {
|
|
@@ -376,7 +436,8 @@ function trayStatusFrom(registered: string | null): WindowsTrayStatus {
|
|
|
376
436
|
const running = heartbeatProcessAlive(heartbeat);
|
|
377
437
|
const registrationOwned = state !== null
|
|
378
438
|
&& registered === state.runCommand
|
|
379
|
-
&& [state.bun, state.cli, state.script, ...
|
|
439
|
+
&& [state.bun, state.cli, state.script, ...(state.launcherPath ? [state.launcherPath] : []), ...installedTrayIconPaths()]
|
|
440
|
+
.every(path => existsSync(path));
|
|
380
441
|
const stale = windowsTrayRegistrationIsStale({
|
|
381
442
|
registered: registered !== null,
|
|
382
443
|
registrationOwned,
|
|
@@ -507,7 +568,12 @@ export function installWindowsTray(startNow = true): WindowsTrayStatus {
|
|
|
507
568
|
}
|
|
508
569
|
recordOwnedConfigPath(getConfigDir(), trayStatePath());
|
|
509
570
|
if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true, mode: 0o700 });
|
|
510
|
-
const
|
|
571
|
+
const launcherPath = installedTrayLauncherPath();
|
|
572
|
+
const entryWithLauncher = { ...entry, launcherPath };
|
|
573
|
+
const runCommand = buildWindowsTrayRunCommand(entryWithLauncher);
|
|
574
|
+
if (runCommand.length > 260) {
|
|
575
|
+
throw new Error(`Tray Run command exceeds the Windows 260-character limit (${runCommand.length} chars).`);
|
|
576
|
+
}
|
|
511
577
|
const runValue = windowsTrayRunValue(entry.opencodexHome);
|
|
512
578
|
const existing = readOwnedRunValue(runValue);
|
|
513
579
|
const state = readState();
|
|
@@ -517,6 +583,9 @@ export function installWindowsTray(startNow = true): WindowsTrayStatus {
|
|
|
517
583
|
if (existsSync(entry.script) && (!state || resolve(state.script) !== resolve(entry.script))) {
|
|
518
584
|
throw new Error(`Refusing to overwrite an unowned tray script at ${entry.script}.`);
|
|
519
585
|
}
|
|
586
|
+
if (existsSync(launcherPath) && (!state?.launcherPath || resolve(state.launcherPath) !== resolve(launcherPath))) {
|
|
587
|
+
throw new Error(`Refusing to overwrite an unowned tray launcher at ${launcherPath}.`);
|
|
588
|
+
}
|
|
520
589
|
if (!state && iconPairs.some(pair => existsSync(pair.installed))) {
|
|
521
590
|
throw new Error("Refusing to overwrite unowned Windows tray icon assets.");
|
|
522
591
|
}
|
|
@@ -531,23 +600,28 @@ export function installWindowsTray(startNow = true): WindowsTrayStatus {
|
|
|
531
600
|
|
|
532
601
|
const previousStateBytes = existsSync(trayStatePath()) ? readFileSync(trayStatePath()) : null;
|
|
533
602
|
const previousScriptBytes = existsSync(entry.script) ? readFileSync(entry.script) : null;
|
|
603
|
+
const previousLauncherBytes = existsSync(launcherPath) ? readFileSync(launcherPath) : null;
|
|
534
604
|
const previousIconBytes = new Map(iconPairs.map(pair => [
|
|
535
605
|
pair.installed,
|
|
536
606
|
existsSync(pair.installed) ? readFileSync(pair.installed) : null,
|
|
537
607
|
]));
|
|
538
608
|
const restorePreviousInstall = () => {
|
|
539
609
|
try {
|
|
540
|
-
if (previousScriptBytes)
|
|
610
|
+
if (previousScriptBytes) replaceWindowsTrayOwnedFile(entry.script, previousScriptBytes);
|
|
541
611
|
else if (existsSync(entry.script)) unlinkSync(entry.script);
|
|
542
612
|
} catch { /* rollback best-effort */ }
|
|
613
|
+
try {
|
|
614
|
+
if (previousLauncherBytes) replaceWindowsTrayOwnedFile(launcherPath, previousLauncherBytes);
|
|
615
|
+
else if (existsSync(launcherPath)) unlinkSync(launcherPath);
|
|
616
|
+
} catch { /* rollback best-effort */ }
|
|
543
617
|
for (const [path, contents] of previousIconBytes) {
|
|
544
618
|
try {
|
|
545
|
-
if (contents)
|
|
619
|
+
if (contents) replaceWindowsTrayOwnedFile(path, contents);
|
|
546
620
|
else if (existsSync(path)) unlinkSync(path);
|
|
547
621
|
} catch { /* rollback best-effort */ }
|
|
548
622
|
}
|
|
549
623
|
try {
|
|
550
|
-
if (previousStateBytes)
|
|
624
|
+
if (previousStateBytes) replaceWindowsTrayOwnedFile(trayStatePath(), previousStateBytes);
|
|
551
625
|
else if (existsSync(trayStatePath())) unlinkSync(trayStatePath());
|
|
552
626
|
} catch { /* rollback best-effort */ }
|
|
553
627
|
try {
|
|
@@ -565,10 +639,11 @@ export function installWindowsTray(startNow = true): WindowsTrayStatus {
|
|
|
565
639
|
try {
|
|
566
640
|
const hardenedDir = hardenSecretDir(getConfigDir(), { required: true });
|
|
567
641
|
if (!hardenedDir.ok) throw new Error("Windows tray directory ACL hardening did not complete; refusing to install persistence.");
|
|
568
|
-
|
|
569
|
-
for (const pair of iconPairs)
|
|
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"));
|
|
570
645
|
runRegistry(["add", RUN_KEY, "/v", runValue, "/t", "REG_SZ", "/d", runCommand, "/f", "/reg:64"]);
|
|
571
|
-
writeState(
|
|
646
|
+
writeState(entryWithLauncher, runValue, runCommand);
|
|
572
647
|
} catch (error) {
|
|
573
648
|
restorePreviousInstall();
|
|
574
649
|
throw error;
|
|
@@ -578,9 +653,6 @@ export function installWindowsTray(startNow = true): WindowsTrayStatus {
|
|
|
578
653
|
restorePreviousInstall();
|
|
579
654
|
throw new Error("The tray startup registration was installed, but the tray process did not become healthy.");
|
|
580
655
|
}
|
|
581
|
-
if (state?.launcherPath && existsSync(state.launcherPath)) {
|
|
582
|
-
try { unlinkSync(state.launcherPath); } catch { /* old owned VBS is inert after a committed Run replacement */ }
|
|
583
|
-
}
|
|
584
656
|
return getWindowsTrayStatus();
|
|
585
657
|
}
|
|
586
658
|
|
package/src/types.ts
CHANGED
|
@@ -455,6 +455,11 @@ export interface OcxClaudeCodeConfig {
|
|
|
455
455
|
desktopProfile?: OcxClaudeDesktopProfile;
|
|
456
456
|
/** Auto-reconcile Desktop 3P config when provider catalog changes. Default: enabled. */
|
|
457
457
|
desktopAutoApply?: boolean;
|
|
458
|
+
/**
|
|
459
|
+
* When false, omit `native/*` rows from Claude Desktop show/export/apply. Default: enabled.
|
|
460
|
+
* Routing-sidecar alias decoding is unchanged — only the Desktop model list writer.
|
|
461
|
+
*/
|
|
462
|
+
desktopNativeModels?: boolean;
|
|
458
463
|
}
|
|
459
464
|
|
|
460
465
|
export type OcxClaudeDesktopFamily = "opus" | "fable" | "sonnet" | "haiku";
|
|
@@ -511,8 +516,22 @@ export interface OcxCustomModel {
|
|
|
511
516
|
addedAt?: string;
|
|
512
517
|
}
|
|
513
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
|
+
|
|
514
531
|
export interface OcxConfig {
|
|
515
532
|
port: number;
|
|
533
|
+
/** Maximum usage-log bytes read for one management snapshot. */
|
|
534
|
+
managementUsageMaxReadBytes?: number;
|
|
516
535
|
providers: Record<string, OcxProviderConfig>;
|
|
517
536
|
defaultProvider: string;
|
|
518
537
|
/** OpenAI provider-contract migration marker (v2 = single `openai` provider with account mode). */
|
|
@@ -574,11 +593,12 @@ export interface OcxConfig {
|
|
|
574
593
|
*/
|
|
575
594
|
fastMode?: boolean;
|
|
576
595
|
/**
|
|
577
|
-
* Windows SSE passthrough stream shape (#314 mitigation).
|
|
578
|
-
* "auto" (default)
|
|
579
|
-
* Bun#32111 fix
|
|
580
|
-
*
|
|
581
|
-
*
|
|
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.
|
|
582
602
|
* See src/lib/bun-stream-caps.ts.
|
|
583
603
|
*/
|
|
584
604
|
streamMode?: "auto" | "legacy-tee" | "eager-relay";
|
|
@@ -670,7 +690,7 @@ export interface OcxConfig {
|
|
|
670
690
|
*/
|
|
671
691
|
storageCleanupPolicy?: StorageCleanupPolicy;
|
|
672
692
|
/** Generated API keys for external access to the proxy's /v1/responses endpoint. */
|
|
673
|
-
apiKeys?:
|
|
693
|
+
apiKeys?: OcxApiKeyEntry[];
|
|
674
694
|
/** Auto-start/sync the proxy from the Codex shim before launching Codex. Default true. */
|
|
675
695
|
codexAutoStart?: boolean;
|
|
676
696
|
/** Restore an installed shim after a stable external Codex update replaces it. Default true. */
|
|
@@ -684,6 +704,8 @@ export interface OcxConfig {
|
|
|
684
704
|
syncResumeHistory?: boolean;
|
|
685
705
|
/** Freshness window (ms) for the per-provider live `/models` cache. Defaults to 5 min. */
|
|
686
706
|
modelCacheTtlMs?: number;
|
|
707
|
+
/** Evictable retained app-state budget in MiB. Default 256; valid 64..4096. */
|
|
708
|
+
appOwnedMemoryBudgetMb?: number;
|
|
687
709
|
/** Anthropic prompt-cache retention: "short" = 5-min ephemeral (default), "long" = 1-hour extended, "none" = disabled. */
|
|
688
710
|
cacheRetention?: "none" | "short" | "long";
|
|
689
711
|
/** Web-search sidecar: route web_search for non-OpenAI models through a gpt-mini via ChatGPT passthrough. */
|
|
@@ -885,6 +907,10 @@ export interface ResponsesItemIdRepairConfig {
|
|
|
885
907
|
|
|
886
908
|
export interface OcxProviderConfig {
|
|
887
909
|
adapter: string;
|
|
910
|
+
/** Cursor MCP compatibility bounds; positive integers when configured. */
|
|
911
|
+
mcpMaxTools?: number;
|
|
912
|
+
mcpMaxSchemaBytes?: number;
|
|
913
|
+
mcpMaxResultBytes?: number;
|
|
888
914
|
/**
|
|
889
915
|
* Per-model wire override, keyed by the upstream native model id (after namespace
|
|
890
916
|
* and combo resolution). A single gateway can front models that speak different
|
|
@@ -903,6 +929,13 @@ export interface OcxProviderConfig {
|
|
|
903
929
|
* the legacy `/v1/responses` construction.
|
|
904
930
|
*/
|
|
905
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;
|
|
906
939
|
/**
|
|
907
940
|
* Explicit opt-in for non-registry private-network destinations such as localhost, RFC1918,
|
|
908
941
|
* link-local, or unique-local upstreams. Metadata endpoints remain blocked.
|