@iislee/opencodex 2.31.0 → 2.35.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 +3 -3
- package/bin/ocx.mjs +99 -70
- package/gui/dist/assets/index-Be1sDpWw.css +1 -0
- package/gui/dist/assets/index-D4OKgUV4.js +114 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -1
- package/src/adapters/anthropic.ts +104 -15
- package/src/adapters/base.ts +29 -1
- package/src/adapters/command-code.ts +42 -6
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +69 -18
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +290 -0
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +14 -0
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-desktop.ts +23 -0
- package/src/adapters/cursor/native-exec-fs.ts +2 -2
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +5 -2
- package/src/adapters/cursor/native-exec.ts +20 -0
- package/src/adapters/cursor/protobuf-request.ts +68 -14
- package/src/adapters/cursor/request-builder.ts +40 -10
- package/src/adapters/cursor/tool-definitions.ts +14 -4
- package/src/adapters/cursor/tool-result-normalize.ts +28 -5
- package/src/adapters/cursor/types.ts +20 -0
- package/src/adapters/cursor.ts +209 -38
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +173 -25
- package/src/adapters/google-antigravity-wire.ts +5 -0
- package/src/adapters/google-errors.ts +44 -12
- package/src/adapters/google.ts +136 -24
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +27 -0
- package/src/adapters/kiro-tools.ts +55 -16
- package/src/adapters/kiro.ts +182 -23
- package/src/adapters/openai-chat.ts +300 -242
- package/src/adapters/openai-responses.ts +353 -27
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-catalog-nudge.ts +4 -3
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/adapters/xai-web-search.ts +17 -16
- package/src/bridge.ts +90 -37
- package/src/chat/inbound.ts +14 -1
- package/src/chat/outbound.ts +15 -7
- package/src/claude/agents-inject.ts +8 -1
- package/src/claude/context-windows.ts +16 -9
- package/src/claude/outbound.ts +24 -11
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +116 -19
- package/src/cli/account-extended.ts +262 -29
- package/src/cli/account-main.ts +12 -12
- package/src/cli/account.ts +69 -15
- package/src/cli/agent.ts +8 -1
- package/src/cli/alias.ts +66 -0
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +496 -0
- package/src/cli/claude-desktop.ts +31 -11
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +256 -28
- package/src/cli/doctor.ts +126 -14
- package/src/cli/help.ts +17 -6
- package/src/cli/index.ts +39 -9
- package/src/cli/init.ts +1 -0
- package/src/cli/inspect.ts +230 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +26 -10
- package/src/cli/observe.ts +31 -8
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/provider.ts +2 -1
- package/src/cli/registry.ts +49 -10
- package/src/cli/runtime-api.ts +51 -7
- package/src/cli/status.ts +41 -1
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/system-restart-client.ts +1 -1
- package/src/cli/usage-report.ts +184 -0
- package/src/cli/v2.ts +34 -10
- package/src/cli/version-skew.ts +46 -0
- package/src/clients/config-export.ts +85 -3
- package/src/clients/effective-status.ts +30 -2
- package/src/clients/probes/cline.ts +81 -0
- package/src/codex/account-label.ts +21 -0
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +83 -7
- package/src/codex/auth-api.ts +50 -10
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/autostart-health.ts +16 -0
- package/src/codex/catalog/aggregation.ts +18 -0
- package/src/codex/catalog/effort.ts +18 -3
- package/src/codex/catalog/metadata.ts +27 -1
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +72 -27
- package/src/codex/catalog/provider-fetch.ts +187 -31
- package/src/codex/catalog/sync.ts +131 -5
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-write-lock.ts +3 -1
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +27 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +355 -0
- package/src/codex/history-job.ts +32 -3
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +5 -5
- package/src/codex/history-provider.ts +825 -247
- package/src/codex/history-worker.ts +8 -5
- package/src/codex/inject-coordination.ts +83 -0
- package/src/codex/inject.ts +63 -22
- package/src/codex/injected-marker.ts +1 -1
- package/src/codex/internal/history-writer.ts +4 -3
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/native-profile-startup.ts +157 -27
- package/src/codex/native-residue.ts +26 -33
- package/src/codex/prompt-layers.ts +371 -25
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota.ts +91 -10
- package/src/codex/routing.ts +9 -0
- package/src/codex/shim.ts +56 -3
- package/src/codex/subagent-model-fallback.ts +104 -25
- package/src/codex/transition-state.ts +116 -8
- package/src/combos/failover.ts +27 -0
- package/src/combos/types.ts +10 -0
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +121 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +309 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +206 -824
- package/src/generated/compatibility-version.json +373 -213
- package/src/images/loop.ts +5 -4
- package/src/images/plan.ts +5 -4
- package/src/integrations/ownership-policy.ts +160 -0
- package/src/integrations/ownership.ts +46 -2
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/state.ts +79 -7
- package/src/integrations/writer.ts +26 -2
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/admin-secrets.ts +24 -0
- package/src/lib/bounded-body.ts +14 -2
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +36 -3
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/lib/process-control.ts +2 -1
- package/src/lib/service-secrets.ts +15 -0
- package/src/lib/tool-argument-integers.ts +56 -5
- package/src/oauth/callback-server.ts +22 -2
- package/src/oauth/cline.ts +203 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/health.ts +1 -1
- package/src/oauth/index.ts +65 -6
- package/src/oauth/key-providers.ts +10 -1
- package/src/oauth/kimi.ts +9 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/local-token-detect.ts +2 -0
- package/src/oauth/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +14 -5
- package/src/oauth/types.ts +15 -0
- package/src/providers/auto-compact-budget.ts +65 -0
- package/src/providers/command-code-efforts.ts +38 -6
- package/src/providers/context-cap.ts +4 -3
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +28 -0
- package/src/providers/fastwire.ts +7 -1
- package/src/providers/label.ts +34 -1
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/provider-id-rewrite.ts +2 -1
- package/src/providers/quota.ts +65 -7
- package/src/providers/registry.ts +184 -69
- package/src/providers/slug-codec.ts +52 -0
- package/src/providers/xai-transport.ts +21 -0
- package/src/reasoning-effort.ts +19 -2
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +157 -39
- package/src/responses/namespace-tool-compat.ts +84 -4
- package/src/responses/parser.ts +7 -2
- package/src/responses/reasoning-replay-cache.ts +81 -3
- package/src/responses/schema.ts +5 -1
- package/src/responses/state.ts +94 -4
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/responses/turn-termination.ts +107 -0
- package/src/router.ts +40 -2
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +27 -7
- package/src/server/chat-completions.ts +24 -16
- package/src/server/chat-native-sse.ts +3 -3
- package/src/server/chat-native.ts +30 -11
- package/src/server/claude-messages.ts +1 -1
- package/src/server/index.ts +179 -32
- package/src/server/lifecycle.ts +52 -1
- package/src/server/local-management-read-client.ts +1 -1
- package/src/server/local-provider-reload-client.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +45 -23
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +2 -1
- package/src/server/management/config-routes.ts +59 -12
- package/src/server/management/context.ts +10 -1
- package/src/server/management/logs-usage-routes.ts +30 -24
- package/src/server/management/model-routes.ts +274 -4
- package/src/server/management/model-rows.ts +4 -0
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/oauth-account-routes.ts +10 -4
- package/src/server/management/provider-capability-config.ts +1 -1
- package/src/server/management/provider-routes.ts +81 -11
- package/src/server/management/route-registry.ts +332 -0
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management/system-restart.ts +1 -1
- package/src/server/management-api.ts +18 -16
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/proxy-liveness.ts +29 -5
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +51 -0
- package/src/server/request-log.ts +70 -16
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/collaboration.ts +37 -3
- package/src/server/responses/combo-stream-preflight.ts +178 -0
- package/src/server/responses/compact.ts +6 -21
- package/src/server/responses/core.ts +810 -208
- package/src/server/responses/empty-completion-guard.ts +63 -6
- package/src/server/responses/fetch-helpers.ts +17 -102
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/responses-field-backfill.ts +105 -13
- package/src/server/responses/ws-upstream.ts +75 -2
- package/src/server/responses-custom-tool-repair.ts +87 -6
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/responses-undeclared-tool-guard.ts +241 -18
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +14 -2
- package/src/service.ts +42 -4
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +88 -23
- package/src/types/config.ts +66 -0
- package/src/types/provider.ts +69 -0
- package/src/types/request.ts +17 -4
- package/src/types/tools.ts +116 -11
- package/src/types.ts +3 -1
- package/src/update/index.ts +5 -4
- package/src/update/job.ts +3 -1
- package/src/update/transactional-install.mjs +8 -1
- package/src/usage/expected-prices.ts +43 -24
- package/src/usage/log.ts +63 -12
- package/src/usage/summary.ts +211 -8
- package/src/vision/describe.ts +18 -13
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/executor.ts +10 -3
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-BF38heuV.js +0 -104
- package/gui/dist/assets/index-DMiI18Kv.css +0 -1
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { isLocalAttestationSecret } from "../lib/local-management-attestation";
|
|
5
|
+
import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
|
|
6
|
+
import {
|
|
7
|
+
resolveTrustedWindowsPowerShellExe,
|
|
8
|
+
resolveTrustedWindowsSystemDirectory,
|
|
9
|
+
} from "../lib/windows-elevation";
|
|
10
|
+
import { atomicWriteFile } from "./atomic-write";
|
|
11
|
+
import { getConfigDir, hardenConfigDir } from "./paths";
|
|
12
|
+
|
|
13
|
+
export function getPidPath(): string {
|
|
14
|
+
return join(getConfigDir(), "ocx.pid");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getRuntimePortPath(): string {
|
|
18
|
+
return join(getConfigDir(), "runtime-port.json");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function ensureProcessStateDir(): void {
|
|
22
|
+
const dir = getConfigDir();
|
|
23
|
+
assertNotRealHomeUnderTest(dir);
|
|
24
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
25
|
+
else hardenConfigDir();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function writePid(pid: number): void {
|
|
29
|
+
ensureProcessStateDir();
|
|
30
|
+
atomicWriteFile(getPidPath(), String(pid));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type RuntimePortState = {
|
|
34
|
+
pid: number;
|
|
35
|
+
port: number;
|
|
36
|
+
hostname?: string;
|
|
37
|
+
/** Per-process proof key; protected by the config directory and never served. */
|
|
38
|
+
attestationSecret?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function isValidRuntimePortState(value: unknown): value is RuntimePortState {
|
|
42
|
+
if (!value || typeof value !== "object") return false;
|
|
43
|
+
const state = value as Record<string, unknown>;
|
|
44
|
+
const hostnameOk = state.hostname === undefined || typeof state.hostname === "string";
|
|
45
|
+
const attestationOk = state.attestationSecret === undefined || isLocalAttestationSecret(state.attestationSecret);
|
|
46
|
+
return Number.isSafeInteger(state.pid)
|
|
47
|
+
&& Number(state.pid) > 0
|
|
48
|
+
&& Number.isInteger(state.port)
|
|
49
|
+
&& Number(state.port) > 0
|
|
50
|
+
&& Number(state.port) <= 65535
|
|
51
|
+
&& hostnameOk
|
|
52
|
+
&& attestationOk;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function writeRuntimePort(state: RuntimePortState): void {
|
|
56
|
+
ensureProcessStateDir();
|
|
57
|
+
atomicWriteFile(getRuntimePortPath(), JSON.stringify(state, null, 2) + "\n");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function parsePidFile(raw: string): number | null {
|
|
61
|
+
const trimmed = raw.trim();
|
|
62
|
+
if (!/^\d+$/.test(trimmed)) return null;
|
|
63
|
+
const pid = Number.parseInt(trimmed, 10);
|
|
64
|
+
return Number.isSafeInteger(pid) && pid > 0 ? pid : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function readPidFileValue(): number | null {
|
|
68
|
+
try {
|
|
69
|
+
return parsePidFile(readFileSync(getPidPath(), "utf-8"));
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function readRuntimePort(expectedPid?: number): RuntimePortState | null {
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(readFileSync(getRuntimePortPath(), "utf-8"));
|
|
78
|
+
if (!isValidRuntimePortState(parsed)) return null;
|
|
79
|
+
if (expectedPid !== undefined && parsed.pid !== expectedPid) return null;
|
|
80
|
+
return parsed;
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function removePid(expectedPid?: number): void {
|
|
87
|
+
if (expectedPid !== undefined && readPidFileValue() !== expectedPid) return;
|
|
88
|
+
try { unlinkSync(getPidPath()); } catch { /* ignore */ }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function removeRuntimePort(expectedPid?: number): void {
|
|
92
|
+
if (expectedPid !== undefined && readRuntimePort(expectedPid) === null) return;
|
|
93
|
+
try { unlinkSync(getRuntimePortPath()); } catch { /* ignore */ }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Snapshot-guarded stale-state purge. A replacement `ocx start` can publish a
|
|
98
|
+
* fresh record while a liveness probe is in flight, so deletion is authorized
|
|
99
|
+
* only by the exact value observed before that probe.
|
|
100
|
+
*/
|
|
101
|
+
export function removePidIfValueIs(snapshot: number | null): void {
|
|
102
|
+
if (!existsSync(getPidPath())) return;
|
|
103
|
+
if (readPidFileValue() !== snapshot) return;
|
|
104
|
+
try { unlinkSync(getPidPath()); } catch { /* ignore */ }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function removeRuntimePortIfPidIs(snapshotPid: number | null): void {
|
|
108
|
+
const current = readRuntimePort();
|
|
109
|
+
if ((current?.pid ?? null) !== snapshotPid) return;
|
|
110
|
+
try { unlinkSync(getRuntimePortPath()); } catch { /* ignore */ }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function isOcxStartCommandLine(commandLine: string): boolean {
|
|
114
|
+
const normalized = commandLine.toLowerCase().replace(/\\/g, "/");
|
|
115
|
+
// Match both the upstream scope (@bitkyc08) and this fork's publish scope (@iislee).
|
|
116
|
+
const hasOcxEntrypoint = normalized.includes("src/cli.ts")
|
|
117
|
+
|| normalized.includes("src/cli/index.ts")
|
|
118
|
+
|| normalized.includes("@bitkyc08/opencodex")
|
|
119
|
+
|| normalized.includes("@iislee/opencodex")
|
|
120
|
+
|| /@bitkyc08\/\.opencodex-/.test(normalized)
|
|
121
|
+
|| /@iislee\/\.opencodex-/.test(normalized)
|
|
122
|
+
|| /(?:^|[\s/"'])(?:ocx|opencodex)(?:\.cmd)?(?:$|[\s"'])/.test(normalized);
|
|
123
|
+
return hasOcxEntrypoint && /(?:^|[\s"'])start(?:$|[\s"'])/.test(normalized);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Avoid spawning WMIC/PowerShell on every short liveness poll. */
|
|
127
|
+
const ocxStartProcessCache = new Map<number, boolean>();
|
|
128
|
+
let ocxStartProcessSweepCursor = 0;
|
|
129
|
+
let ocxStartProcessProbe: (pid: number) => void = pid => { process.kill(pid, 0); };
|
|
130
|
+
|
|
131
|
+
export function setOcxStartProcessProbeForTests(probe: ((pid: number) => void) | null): void {
|
|
132
|
+
ocxStartProcessProbe = probe ?? (pid => { process.kill(pid, 0); });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function setOcxStartProcessCacheForTests(entries: Iterable<readonly [number, boolean]>): void {
|
|
136
|
+
ocxStartProcessCache.clear();
|
|
137
|
+
for (const [pid, value] of entries) ocxStartProcessCache.set(pid, value);
|
|
138
|
+
ocxStartProcessSweepCursor = 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function sweepDeadOcxStartProcessCache(maxProbes = 64): number {
|
|
142
|
+
const pids: number[] = [];
|
|
143
|
+
let removed = 0;
|
|
144
|
+
for (const pid of ocxStartProcessCache.keys()) {
|
|
145
|
+
if (Number.isSafeInteger(pid) && pid > 0) pids.push(pid);
|
|
146
|
+
else if (ocxStartProcessCache.delete(pid)) removed += 1;
|
|
147
|
+
}
|
|
148
|
+
if (pids.length === 0 || maxProbes <= 0) {
|
|
149
|
+
ocxStartProcessSweepCursor = 0;
|
|
150
|
+
return removed;
|
|
151
|
+
}
|
|
152
|
+
const probeCount = Math.min(Math.floor(maxProbes), pids.length);
|
|
153
|
+
const start = ocxStartProcessSweepCursor % pids.length;
|
|
154
|
+
for (let offset = 0; offset < probeCount; offset += 1) {
|
|
155
|
+
const pid = pids[(start + offset) % pids.length]!;
|
|
156
|
+
try {
|
|
157
|
+
ocxStartProcessProbe(pid);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if ((error as NodeJS.ErrnoException).code !== "ESRCH") continue;
|
|
160
|
+
if (ocxStartProcessCache.delete(pid)) removed += 1;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
ocxStartProcessSweepCursor = (start + probeCount) % pids.length;
|
|
164
|
+
return removed;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function ocxStartProcessCacheSizeForTests(): number {
|
|
168
|
+
return ocxStartProcessCache.size;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function isLikelyOcxStartProcess(pid: number): boolean {
|
|
172
|
+
const cached = ocxStartProcessCache.get(pid);
|
|
173
|
+
if (cached !== undefined) return cached;
|
|
174
|
+
const commandLine = readProcessCommandLine(pid);
|
|
175
|
+
if (commandLine === undefined) return false;
|
|
176
|
+
const ok = isOcxStartCommandLine(commandLine);
|
|
177
|
+
ocxStartProcessCache.set(pid, ok);
|
|
178
|
+
return ok;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function readPid(): number | null {
|
|
182
|
+
const pidPath = getPidPath();
|
|
183
|
+
if (!existsSync(pidPath)) return null;
|
|
184
|
+
try {
|
|
185
|
+
const raw = readFileSync(pidPath, "utf-8").trim();
|
|
186
|
+
const pid = parsePidFile(raw);
|
|
187
|
+
if (pid === null) return null;
|
|
188
|
+
try {
|
|
189
|
+
process.kill(pid, 0);
|
|
190
|
+
return isLikelyOcxStartProcess(pid) ? pid : null;
|
|
191
|
+
} catch (error) {
|
|
192
|
+
if ((error as NodeJS.ErrnoException).code === "EPERM") {
|
|
193
|
+
return isLikelyOcxStartProcess(pid) ? pid : null;
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
} catch {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Cheap non-destructive liveness check. Destructive callers must additionally
|
|
204
|
+
* call `verifyPidIdentity()` before acting on the returned PID.
|
|
205
|
+
*/
|
|
206
|
+
export function readAlivePid(): number | null {
|
|
207
|
+
const pid = readPidFileValue();
|
|
208
|
+
if (pid === null) return null;
|
|
209
|
+
try {
|
|
210
|
+
process.kill(pid, 0);
|
|
211
|
+
return pid;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if ((error as NodeJS.ErrnoException).code === "EPERM") return pid;
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Full identity check for a known candidate. The returned PID must equal the
|
|
220
|
+
* candidate, preventing a pidfile rewrite from swapping in another process.
|
|
221
|
+
*/
|
|
222
|
+
export function verifyPidIdentity(candidatePid: number): number | null {
|
|
223
|
+
try {
|
|
224
|
+
process.kill(candidatePid, 0);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
if ((error as NodeJS.ErrnoException).code !== "EPERM") return null;
|
|
227
|
+
}
|
|
228
|
+
return isLikelyOcxStartProcess(candidatePid) ? candidatePid : null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
type ProcessCommandLineExec = (
|
|
232
|
+
executable: string,
|
|
233
|
+
args: string[],
|
|
234
|
+
options: {
|
|
235
|
+
encoding: BufferEncoding;
|
|
236
|
+
stdio: ["ignore", "pipe", "ignore"];
|
|
237
|
+
timeout: number;
|
|
238
|
+
windowsHide: boolean;
|
|
239
|
+
},
|
|
240
|
+
) => string;
|
|
241
|
+
|
|
242
|
+
const defaultProcessCommandLineExec: ProcessCommandLineExec = (executable, args, options) =>
|
|
243
|
+
execFileSync(executable, args, options);
|
|
244
|
+
let processCommandLineExec = defaultProcessCommandLineExec;
|
|
245
|
+
let processCommandLinePlatformForTests: NodeJS.Platform | null = null;
|
|
246
|
+
|
|
247
|
+
export function setProcessCommandLineExecForTests(next: ProcessCommandLineExec | null): void {
|
|
248
|
+
processCommandLineExec = next ?? defaultProcessCommandLineExec;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function setProcessCommandLinePlatformForTests(next: NodeJS.Platform | null): void {
|
|
252
|
+
processCommandLinePlatformForTests = next;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function readProcessCommandLine(pid: number): string | undefined {
|
|
256
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return undefined;
|
|
257
|
+
const platform = processCommandLinePlatformForTests ?? process.platform;
|
|
258
|
+
try {
|
|
259
|
+
if (platform === "linux") {
|
|
260
|
+
try {
|
|
261
|
+
const output = readFileSync(`/proc/${pid}/cmdline`, "utf-8");
|
|
262
|
+
const value = output.replace(/\0/g, " ").trim();
|
|
263
|
+
if (value) return value;
|
|
264
|
+
} catch {
|
|
265
|
+
/* procfs unavailable — use the fixed ps fallback below */
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (platform === "win32") {
|
|
269
|
+
// WMIC is the fast path. Newer Windows images may omit it, so fall back to
|
|
270
|
+
// the trusted fixed PowerShell binary without consulting PATH or env roots.
|
|
271
|
+
const wmic = join(resolveTrustedWindowsSystemDirectory(), "wbem", "WMIC.exe");
|
|
272
|
+
try {
|
|
273
|
+
const output = processCommandLineExec(wmic, [
|
|
274
|
+
"process", "where", `ProcessId=${pid}`, "get", "CommandLine", "/VALUE",
|
|
275
|
+
], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 3000, windowsHide: true });
|
|
276
|
+
const match = /^CommandLine=(.*)$/m.exec(output.replace(/\r/g, ""));
|
|
277
|
+
const value = match?.[1]?.trim();
|
|
278
|
+
if (value) return value;
|
|
279
|
+
} catch {
|
|
280
|
+
/* WMIC missing or failed — fall through */
|
|
281
|
+
}
|
|
282
|
+
const output = processCommandLineExec(resolveTrustedWindowsPowerShellExe(), [
|
|
283
|
+
"-NoProfile",
|
|
284
|
+
"-NoLogo",
|
|
285
|
+
"-NonInteractive",
|
|
286
|
+
"-Command",
|
|
287
|
+
`(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}").CommandLine`,
|
|
288
|
+
], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 3000, windowsHide: true });
|
|
289
|
+
return output.trim() || undefined;
|
|
290
|
+
}
|
|
291
|
+
for (const ps of ["/bin/ps", "/usr/bin/ps"]) {
|
|
292
|
+
try {
|
|
293
|
+
const output = processCommandLineExec(ps, ["-p", String(pid), "-o", "command="], {
|
|
294
|
+
encoding: "utf-8",
|
|
295
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
296
|
+
timeout: 1000,
|
|
297
|
+
windowsHide: true,
|
|
298
|
+
});
|
|
299
|
+
const value = output.trim();
|
|
300
|
+
if (value) return value;
|
|
301
|
+
} catch {
|
|
302
|
+
/* try the other fixed system path */
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return undefined;
|
|
306
|
+
} catch {
|
|
307
|
+
return undefined;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
|
|
2
|
+
import { modelRecordValue } from "../reasoning-effort";
|
|
3
|
+
import {
|
|
4
|
+
isWirePinnedModel,
|
|
5
|
+
MODEL_ADAPTER_OVERRIDE_ALLOWED,
|
|
6
|
+
REASONING_SUMMARY_DELIVERY_VALUES,
|
|
7
|
+
UPSTREAM_HTTP_VERSION_VALUES,
|
|
8
|
+
type OcxProviderConfig,
|
|
9
|
+
} from "../types";
|
|
10
|
+
|
|
11
|
+
const HEADER_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
12
|
+
const SENSITIVE_PROVIDER_HEADERS = new Set([
|
|
13
|
+
"authorization",
|
|
14
|
+
"cookie",
|
|
15
|
+
"set-cookie",
|
|
16
|
+
"proxy-authorization",
|
|
17
|
+
"x-api-key",
|
|
18
|
+
"x-goog-api-key",
|
|
19
|
+
"x-amz-security-token",
|
|
20
|
+
]);
|
|
21
|
+
const REASONING_SUMMARY_DELIVERY_SET = new Set<string>(REASONING_SUMMARY_DELIVERY_VALUES);
|
|
22
|
+
|
|
23
|
+
/** Validate a provider destination without coupling DTO callers to config persistence. */
|
|
24
|
+
export function providerBaseUrlConfigError(baseUrl: string): string | null {
|
|
25
|
+
try {
|
|
26
|
+
const parsed = new URL(baseUrl.trim());
|
|
27
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return "baseUrl must be an http(s) URL";
|
|
28
|
+
if (parsed.username || parsed.password) return "baseUrl must not include embedded credentials";
|
|
29
|
+
if (parsed.search || parsed.hash) return "baseUrl must not include query strings or fragments";
|
|
30
|
+
} catch {
|
|
31
|
+
return "baseUrl must be a valid URL";
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Validate user-configured provider headers while keeping auth headers on owned fields. */
|
|
37
|
+
export function providerHeadersConfigError(headers: unknown): string | null {
|
|
38
|
+
if (headers === undefined) return null;
|
|
39
|
+
if (!headers || typeof headers !== "object" || Array.isArray(headers)) return "headers must be an object";
|
|
40
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
41
|
+
const normalized = name.trim().toLowerCase();
|
|
42
|
+
if (!normalized || !HEADER_NAME_PATTERN.test(name)) return "headers must use valid HTTP header names";
|
|
43
|
+
if (SENSITIVE_PROVIDER_HEADERS.has(normalized)) return `headers must not include sensitive header "${name}"; use apiKey/authMode instead`;
|
|
44
|
+
if (typeof value !== "string") return `header "${name}" value must be a string`;
|
|
45
|
+
if (/[\r\n]/.test(value)) return `header "${name}" value must not include line breaks`;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Keep the configured API-key header style scoped to Anthropic-compatible key auth. */
|
|
51
|
+
export function apiKeyTransportConfigError(
|
|
52
|
+
provider: Pick<OcxProviderConfig, "adapter" | "authMode" | "apiKeyTransport">,
|
|
53
|
+
): string | null {
|
|
54
|
+
if (provider.apiKeyTransport === undefined) return null;
|
|
55
|
+
if (provider.apiKeyTransport !== "x-api-key" && provider.apiKeyTransport !== "bearer") {
|
|
56
|
+
return 'apiKeyTransport must be "x-api-key" or "bearer"';
|
|
57
|
+
}
|
|
58
|
+
if (provider.adapter !== "anthropic") {
|
|
59
|
+
return "apiKeyTransport is supported only by the anthropic adapter";
|
|
60
|
+
}
|
|
61
|
+
if (provider.authMode === "oauth" || provider.authMode === "forward" || provider.authMode === "local") {
|
|
62
|
+
return "apiKeyTransport requires Anthropic API-key authentication";
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Shared strict boundary for the per-provider upstream HTTP-version pin. */
|
|
68
|
+
export function upstreamHttpVersionConfigError(value: unknown): string | null {
|
|
69
|
+
if (value === undefined || value === null) return null;
|
|
70
|
+
if (typeof value !== "string" || !(UPSTREAM_HTTP_VERSION_VALUES as readonly string[]).includes(value)) {
|
|
71
|
+
return 'upstreamHttpVersion must be one of "auto", "http1.1", "h1", "http2", "h2", or null to clear';
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function positiveIntegerRecordConfigError(value: unknown, field: string): string | null {
|
|
77
|
+
if (value === undefined) return null;
|
|
78
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return `${field} must be a plain object`;
|
|
79
|
+
const prototype = Object.getPrototypeOf(value);
|
|
80
|
+
if (prototype !== Object.prototype && prototype !== null) return `${field} must be a plain object with own properties`;
|
|
81
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
82
|
+
if (!key.trim()) return `${field} keys must be nonblank model ids`;
|
|
83
|
+
if (typeof entry !== "number" || !Number.isFinite(entry) || !Number.isInteger(entry) || entry <= 0) {
|
|
84
|
+
return `${field}.${key} must be a positive finite integer`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function positiveIntegerConfigError(value: unknown, field: string): string | null {
|
|
91
|
+
if (value === undefined) return null;
|
|
92
|
+
if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
|
|
93
|
+
return `${field} must be a positive finite integer`;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function nonBlankStringArrayConfigError(value: unknown, field: string): string | null {
|
|
99
|
+
if (value === undefined) return null;
|
|
100
|
+
if (!Array.isArray(value)) return `${field} must be an array`;
|
|
101
|
+
for (const [index, entry] of value.entries()) {
|
|
102
|
+
if (typeof entry !== "string" || !entry.trim()) {
|
|
103
|
+
return `${field}.${index} must be a nonblank model id`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Normalize only after validation so whitespace-only entries cannot silently disappear. */
|
|
110
|
+
export function normalizeNonBlankStringArray(value: readonly string[]): string[] {
|
|
111
|
+
return [...new Set(value.map(entry => entry.trim()))];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function booleanRecordConfigError(value: unknown, field: string): string | null {
|
|
115
|
+
if (value === undefined) return null;
|
|
116
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return `${field} must be a plain object`;
|
|
117
|
+
const prototype = Object.getPrototypeOf(value);
|
|
118
|
+
if (prototype !== Object.prototype && prototype !== null) return `${field} must be a plain object with own properties`;
|
|
119
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
120
|
+
if (!key.trim()) return `${field} keys must be nonblank model ids`;
|
|
121
|
+
if (typeof entry !== "boolean") return `${field}.${key} must be a boolean`;
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function reasoningSummaryDeliveryRecordConfigError(
|
|
127
|
+
value: unknown,
|
|
128
|
+
supportsReasoningSummaries: unknown,
|
|
129
|
+
field = "modelReasoningSummaryDelivery",
|
|
130
|
+
): string | null {
|
|
131
|
+
if (value === undefined) return null;
|
|
132
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return `${field} must be a plain object`;
|
|
133
|
+
const prototype = Object.getPrototypeOf(value);
|
|
134
|
+
if (prototype !== Object.prototype && prototype !== null) return `${field} must be a plain object with own properties`;
|
|
135
|
+
|
|
136
|
+
const supports = booleanRecordConfigError(supportsReasoningSummaries, "modelSupportsReasoningSummaries") === null
|
|
137
|
+
&& supportsReasoningSummaries && typeof supportsReasoningSummaries === "object"
|
|
138
|
+
? supportsReasoningSummaries as Record<string, boolean>
|
|
139
|
+
: undefined;
|
|
140
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
141
|
+
if (!key.trim()) return `${field} keys must be nonblank model ids`;
|
|
142
|
+
if (typeof entry !== "string" || !REASONING_SUMMARY_DELIVERY_SET.has(entry)) {
|
|
143
|
+
return `${field}.${key} must be one of: ${REASONING_SUMMARY_DELIVERY_VALUES.join(", ")}`;
|
|
144
|
+
}
|
|
145
|
+
if (modelRecordValue(supports, key) === false) {
|
|
146
|
+
return `${field}.${key} conflicts with modelSupportsReasoningSummaries=false`;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Validate a provider's per-model wire override map against runtime routing rules. */
|
|
153
|
+
export function modelAdapterRecordConfigError(
|
|
154
|
+
value: unknown,
|
|
155
|
+
field: string,
|
|
156
|
+
providerName: string,
|
|
157
|
+
provider: { adapter?: unknown; authMode?: unknown; baseUrl?: unknown },
|
|
158
|
+
): string | null {
|
|
159
|
+
if (value === undefined) return null;
|
|
160
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return `${field} must be a plain object`;
|
|
161
|
+
const prototype = Object.getPrototypeOf(value);
|
|
162
|
+
if (prototype !== Object.prototype && prototype !== null) return `${field} must be a plain object with own properties`;
|
|
163
|
+
const entries = Object.entries(value);
|
|
164
|
+
if (entries.length > 0 && isCanonicalOpenAiForwardProvider(provider as OcxProviderConfig)) {
|
|
165
|
+
return `${field} is not supported on the canonical ChatGPT forward provider`;
|
|
166
|
+
}
|
|
167
|
+
for (const [key, entry] of entries) {
|
|
168
|
+
if (!key.trim()) return `${field} keys must be nonblank model ids`;
|
|
169
|
+
if (typeof entry !== "string" || !MODEL_ADAPTER_OVERRIDE_ALLOWED.has(entry)) {
|
|
170
|
+
return `${field}.${key} must be one of: ${[...MODEL_ADAPTER_OVERRIDE_ALLOWED].join(", ")}`;
|
|
171
|
+
}
|
|
172
|
+
if (isWirePinnedModel(providerName, key.trim())) {
|
|
173
|
+
return `${field}.${key} cannot be overridden: the upstream only speaks one wire for this model`;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { OcxConfig } from "../types";
|
|
2
|
+
|
|
3
|
+
const pendingTopLevelDeletions = new WeakMap<OcxConfig, Set<string>>();
|
|
4
|
+
export const CONFIG_REBASE_PROVENANCE_KEY = "configRebaseProvenance";
|
|
5
|
+
|
|
6
|
+
export function parsedConfigRebaseDeletionKeys(config: OcxConfig): Set<string> | null {
|
|
7
|
+
const value = config.configRebaseProvenance;
|
|
8
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
9
|
+
const record = value as Record<string, unknown>;
|
|
10
|
+
if (record.version !== 1 || !Array.isArray(record.deletedTopLevelKeys)) return null;
|
|
11
|
+
if (!record.deletedTopLevelKeys.every(key => typeof key === "string" && key !== CONFIG_REBASE_PROVENANCE_KEY)) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return new Set(record.deletedTopLevelKeys as string[]);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function configRebaseDeletionKeys(config: OcxConfig): Set<string> {
|
|
18
|
+
const deleted = new Set([
|
|
19
|
+
...(parsedConfigRebaseDeletionKeys(config) ?? []),
|
|
20
|
+
...(pendingTopLevelDeletions.get(config) ?? []),
|
|
21
|
+
]);
|
|
22
|
+
const record = config as unknown as Record<string, unknown>;
|
|
23
|
+
for (const key of [...deleted]) {
|
|
24
|
+
if (Object.hasOwn(record, key) && record[key] !== undefined) deleted.delete(key);
|
|
25
|
+
}
|
|
26
|
+
return deleted;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function configHasRebaseProvenance(config: OcxConfig): boolean {
|
|
30
|
+
return parsedConfigRebaseDeletionKeys(config) !== null || pendingTopLevelDeletions.has(config);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function projectConfigRebaseProvenance(config: OcxConfig): OcxConfig {
|
|
34
|
+
const pending = pendingTopLevelDeletions.get(config);
|
|
35
|
+
const parsed = parsedConfigRebaseDeletionKeys(config);
|
|
36
|
+
// Preserve unknown future metadata byte-for-value. It carries no authority here.
|
|
37
|
+
if (config.configRebaseProvenance !== undefined && parsed === null) return config;
|
|
38
|
+
const deleted = new Set([...(parsed ?? []), ...(pending ?? [])]);
|
|
39
|
+
const record = config as unknown as Record<string, unknown>;
|
|
40
|
+
for (const key of [...deleted]) {
|
|
41
|
+
if (Object.hasOwn(record, key) && record[key] !== undefined) deleted.delete(key);
|
|
42
|
+
}
|
|
43
|
+
// A SHALLOW copy, deliberately. A provider entry may carry a non-cloneable value — a test
|
|
44
|
+
// fixture injects its own `fetch`, and `structuredClone` throws DataCloneError on a function,
|
|
45
|
+
// which took every provider-probe and CLI-parity suite down. Only the provenance key is
|
|
46
|
+
// rewritten here, so nothing below the top level needs to be copied at all; the deep clone
|
|
47
|
+
// was doing work this projection never asked for.
|
|
48
|
+
const projected = { ...config };
|
|
49
|
+
if (deleted.size === 0) delete projected.configRebaseProvenance;
|
|
50
|
+
else projected.configRebaseProvenance = {
|
|
51
|
+
version: 1,
|
|
52
|
+
deletedTopLevelKeys: [...deleted].sort(),
|
|
53
|
+
};
|
|
54
|
+
return projected;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Delete one top-level config key and retain the writer's explicit intent for rebasing. */
|
|
58
|
+
export function deleteConfigTopLevelKey<K extends keyof OcxConfig>(config: OcxConfig, key: K): void {
|
|
59
|
+
delete config[key];
|
|
60
|
+
if (key === CONFIG_REBASE_PROVENANCE_KEY) return;
|
|
61
|
+
const deleted = pendingTopLevelDeletions.get(config) ?? new Set<string>();
|
|
62
|
+
deleted.add(key);
|
|
63
|
+
pendingTopLevelDeletions.set(config, deleted);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function clearPendingConfigTopLevelDeletions(config: OcxConfig): void {
|
|
67
|
+
pendingTopLevelDeletions.delete(config);
|
|
68
|
+
}
|