@bitkyc08/opencodex 2.39.0 → 2.40.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 +49 -21
- package/gui/dist/assets/index-BHe2rl_C.js +112 -0
- package/gui/dist/assets/index-CJSb3HPe.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/cursor/catalog.ts +140 -7
- package/src/adapters/cursor/discovery.ts +79 -87
- package/src/adapters/cursor/effort-map.ts +11 -0
- package/src/adapters/cursor/request-builder.ts +31 -4
- package/src/adapters/cursor.ts +17 -0
- package/src/adapters/openai-chat.ts +95 -9
- package/src/adapters/openai-responses.ts +73 -9
- package/src/bridge.ts +58 -13
- package/src/claude/auth-mode.ts +6 -9
- package/src/claude/gateway-cache.ts +23 -3
- package/src/claude/model-info.ts +13 -2
- package/src/cli/access.ts +30 -0
- package/src/cli/account-extended.ts +19 -10
- package/src/cli/capabilities.ts +40 -1
- package/src/cli/claude-agent-startup-sync.ts +3 -0
- package/src/cli/claude.ts +134 -36
- package/src/cli/connect.ts +226 -0
- package/src/cli/dispatch.ts +126 -21
- package/src/cli/gui-pair-client.ts +170 -0
- package/src/cli/gui.ts +87 -0
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +76 -22
- package/src/cli/models-runtime-subcommands.ts +34 -0
- package/src/cli/models-runtime.ts +4 -0
- package/src/cli/models.ts +2 -1
- package/src/cli/opencode.ts +2 -1
- package/src/cli/provider-runtime.ts +31 -1
- package/src/cli/registry.ts +32 -2
- package/src/cli/runtime-api.ts +10 -1
- package/src/cli/status.ts +26 -0
- package/src/cli/system-command.ts +9 -3
- package/src/client/connect.ts +650 -0
- package/src/client/hub-client.ts +457 -0
- package/src/client/hub-relay.ts +288 -0
- package/src/client/machine-api.ts +139 -0
- package/src/client/machine-auth.ts +54 -0
- package/src/client/machine-listener.ts +143 -0
- package/src/client/runtime.ts +93 -0
- package/src/client/state.ts +175 -0
- package/src/clients/config-export.ts +16 -5
- package/src/codex/auth-api.ts +169 -15
- package/src/codex/auth-context.ts +61 -6
- package/src/codex/catalog/aggregation.ts +17 -13
- package/src/codex/catalog/metadata.ts +28 -0
- package/src/codex/catalog/provider-fetch.ts +38 -5
- package/src/codex/catalog.ts +1 -1
- package/src/codex/desired-state.ts +13 -3
- package/src/codex/inject.ts +212 -31
- package/src/codex/journal.ts +54 -6
- package/src/codex/main-account.ts +105 -33
- package/src/codex/native-main-claim.ts +23 -1
- package/src/codex/native-residue.ts +7 -0
- package/src/codex/reset-credit-auto-redeem.ts +237 -0
- package/src/combos/request.ts +15 -3
- package/src/combos/types.ts +12 -0
- package/src/config/paths.ts +15 -1
- package/src/config/provider-validation.ts +40 -0
- package/src/config.ts +447 -3
- package/src/generated/compatibility-version.json +224 -108
- package/src/images/artifacts.ts +73 -37
- package/src/images/fulfill.ts +6 -1
- package/src/images/index.ts +1 -1
- package/src/images/plan.ts +16 -2
- package/src/images/synthetic-tool.ts +5 -0
- package/src/images/xai-client.ts +34 -1
- package/src/integrations/cursor-detect.ts +133 -0
- package/src/integrations/cursor-seen.ts +31 -0
- package/src/lib/errors.ts +18 -0
- package/src/lib/gui-pair-capability.ts +104 -0
- package/src/lib/lab-live-route-production.ts +2 -1
- package/src/lib/process-control.ts +4 -3
- package/src/lib/service-secrets.ts +161 -1
- package/src/lib/windows-system-proxy.ts +115 -0
- package/src/oauth/index.ts +3 -2
- package/src/oauth/pool-settings-capability.ts +55 -0
- package/src/providers/api-keys.ts +3 -1
- package/src/providers/codex-capacity.ts +34 -4
- package/src/providers/derive.ts +10 -0
- package/src/providers/fastwire.ts +10 -2
- package/src/providers/key-store.ts +197 -0
- package/src/providers/model-rename-migration.ts +3 -0
- package/src/providers/openai-sidecar.ts +3 -3
- package/src/providers/quota.ts +91 -42
- package/src/providers/registry.ts +33 -3
- package/src/reasoning-effort.ts +32 -0
- package/src/remote/protocol.ts +109 -0
- package/src/responses/citation-markers.ts +101 -0
- package/src/responses/parser.ts +43 -3
- package/src/responses/spill-store.ts +9 -4
- package/src/router.ts +72 -8
- package/src/server/auth-cors.ts +37 -1
- package/src/server/gui-session.ts +449 -0
- package/src/server/gui-static.ts +24 -7
- package/src/server/images.ts +237 -10
- package/src/server/index.ts +356 -30
- package/src/server/management/agent-settings-routes.ts +7 -1
- package/src/server/management/api-key-rotation.ts +74 -0
- package/src/server/management/combo-routes.ts +18 -6
- package/src/server/management/config-routes.ts +22 -3
- package/src/server/management/context.ts +5 -1
- package/src/server/management/cursor-integration-routes.ts +98 -0
- package/src/server/management/logs-usage-routes.ts +2 -1
- package/src/server/management/model-routes.ts +79 -1
- package/src/server/management/model-rows.ts +23 -1
- package/src/server/management/oauth-account-routes.ts +135 -2
- package/src/server/management/provider-routes.ts +143 -9
- package/src/server/management/route-registry.ts +10 -0
- package/src/server/management/session-routes.ts +13 -0
- package/src/server/management/system-routes.ts +13 -1
- package/src/server/management-api.ts +8 -3
- package/src/server/management-auth.ts +124 -82
- package/src/server/models-capabilities.ts +124 -0
- package/src/server/ports.ts +7 -0
- package/src/server/proxy-liveness.ts +7 -0
- package/src/server/request-log.ts +11 -5
- package/src/server/responses/agent-task-recovery.ts +1 -0
- package/src/server/responses/codex-auth-error.ts +3 -1
- package/src/server/responses/collaboration.ts +26 -1
- package/src/server/responses/combo-stream-preflight.ts +25 -6
- package/src/server/responses/compact.ts +32 -12
- package/src/server/responses/context-overflow.ts +49 -0
- package/src/server/responses/core.ts +196 -12
- package/src/server/responses/fetch-helpers.ts +2 -1
- package/src/server/responses/outbound-body-guard.ts +110 -0
- package/src/server/responses/ws-upstream.ts +87 -6
- package/src/server/responses-self-named-namespace-scrub.ts +181 -0
- package/src/server/system-env.ts +78 -25
- package/src/service.ts +218 -42
- package/src/types/config.ts +134 -0
- package/src/types/provider.ts +54 -1
- package/src/types/request.ts +2 -0
- package/src/types.ts +5 -0
- package/src/update/job.ts +33 -5
- package/src/usage/cost.ts +32 -1
- package/src/usage/expected-prices.ts +16 -0
- package/src/usage/log.ts +6 -2
- package/src/usage/summary.ts +48 -6
- package/gui/dist/assets/index-D-lchsPw.js +0 -112
- package/gui/dist/assets/index-uvENYLin.css +0 -1
package/src/service.ts
CHANGED
|
@@ -660,6 +660,23 @@ export function installedServiceListenPort(): number {
|
|
|
660
660
|
|
|
661
661
|
export const SERVICE_INSTALL_HEALTH_MS = 20_000;
|
|
662
662
|
|
|
663
|
+
/**
|
|
664
|
+
* Windows gets a longer budget because its cold start does more before the
|
|
665
|
+
* listener exists: NTFS ACL hardening and previous-session journal recovery
|
|
666
|
+
* both run first, and #3009 recorded a service that bound a few seconds past
|
|
667
|
+
* the 20s deadline and then stayed healthy. Reporting that as a terminal
|
|
668
|
+
* repair failure is worse than waiting — the caller's fallback is to start a
|
|
669
|
+
* second proxy against a port that is about to be taken.
|
|
670
|
+
*/
|
|
671
|
+
export const SERVICE_INSTALL_HEALTH_WINDOWS_MS = 45_000;
|
|
672
|
+
|
|
673
|
+
/** The health budget for the platform this is running on. */
|
|
674
|
+
export function serviceInstallHealthMs(
|
|
675
|
+
platform: NodeJS.Platform = process.platform,
|
|
676
|
+
): number {
|
|
677
|
+
return platform === "win32" ? SERVICE_INSTALL_HEALTH_WINDOWS_MS : SERVICE_INSTALL_HEALTH_MS;
|
|
678
|
+
}
|
|
679
|
+
|
|
663
680
|
/**
|
|
664
681
|
* Whether a proxy actually answers on the port this install/start just produced.
|
|
665
682
|
*
|
|
@@ -690,12 +707,23 @@ export async function confirmServiceServing(
|
|
|
690
707
|
const now = deps.now ?? Date.now;
|
|
691
708
|
const sleep = deps.sleep ?? ((ms: number) => new Promise<void>(r => setTimeout(r, ms)));
|
|
692
709
|
const probe = deps.probe ?? (async (p, h) => !!(await proxyIdentityAt(p, { hostname: h })));
|
|
693
|
-
const deadline = now() + (deps.timeoutMs ??
|
|
710
|
+
const deadline = now() + (deps.timeoutMs ?? serviceInstallHealthMs());
|
|
711
|
+
let waited = false;
|
|
694
712
|
for (;;) {
|
|
695
713
|
if (await probe(port, hostname)) return { ok: true, port };
|
|
696
|
-
if (now() >= deadline)
|
|
714
|
+
if (now() >= deadline) break;
|
|
697
715
|
await sleep(500);
|
|
716
|
+
waited = true;
|
|
698
717
|
}
|
|
718
|
+
// The probe that ran last started before the deadline, so a service that binds
|
|
719
|
+
// during it is reported as dead (#3009). Knock once more after a short grace
|
|
720
|
+
// before calling it a failure. A zero budget means the caller asked not to
|
|
721
|
+
// wait, so it gets exactly the single probe it asked for and nothing more.
|
|
722
|
+
if (waited) {
|
|
723
|
+
await sleep(500);
|
|
724
|
+
if (await probe(port, hostname)) return { ok: true, port };
|
|
725
|
+
}
|
|
726
|
+
return { ok: false, port };
|
|
699
727
|
}
|
|
700
728
|
|
|
701
729
|
/**
|
|
@@ -707,18 +735,27 @@ export async function confirmServiceServing(
|
|
|
707
735
|
* fall back to a direct proxy start rather than reporting a successful update over a
|
|
708
736
|
* dead port.
|
|
709
737
|
*/
|
|
710
|
-
async function reportServiceServing(
|
|
738
|
+
export async function reportServiceServing(
|
|
711
739
|
verb: "installed" | "started" | "repaired",
|
|
712
740
|
deps: Parameters<typeof confirmServiceServing>[0] = {},
|
|
713
741
|
): Promise<void> {
|
|
714
|
-
const
|
|
742
|
+
const healthBudgetMs = deps.timeoutMs ?? serviceInstallHealthMs();
|
|
743
|
+
// Timed here rather than reported from the budget. confirmServiceServing knocks once
|
|
744
|
+
// more after a grace sleep whenever it waited at all, so the real wait is the budget
|
|
745
|
+
// plus that grace — and printing the budget states a number the run did not spend.
|
|
746
|
+
// What the reader is deciding is whether the service was still coming up, which is a
|
|
747
|
+
// judgement about elapsed time (#3009).
|
|
748
|
+
const now = deps.now ?? Date.now;
|
|
749
|
+
const startedAt = now();
|
|
750
|
+
const serving = await confirmServiceServing({ ...deps, timeoutMs: healthBudgetMs });
|
|
751
|
+
const waitedMs = Math.max(0, now() - startedAt);
|
|
715
752
|
if (serving.ok) {
|
|
716
753
|
console.log(`✅ opencodex service ${verb} and serving on port ${serving.port}.`);
|
|
717
754
|
return;
|
|
718
755
|
}
|
|
719
756
|
console.error(
|
|
720
|
-
`⚠️ Service ${verb}, but no proxy answered on port ${serving.port}
|
|
721
|
-
+ `${Math.
|
|
757
|
+
`⚠️ Service ${verb}, but no proxy answered on port ${serving.port} after `
|
|
758
|
+
+ `${Math.round(waitedMs / 1000)}s.\n`
|
|
722
759
|
+ ` The manager registered the job; that is not the same as serving.\n`
|
|
723
760
|
+ ` Log: ${serviceLogPath()}\n`
|
|
724
761
|
+ ` Meanwhile: ocx start (serves in the foreground)`,
|
|
@@ -1087,9 +1124,10 @@ export function evaluateWindowsSchedulerInstallVerification(inputs: {
|
|
|
1087
1124
|
nativeStatus: "started" | "stopped" | "nonexistent" | "unknown";
|
|
1088
1125
|
wscript?: string;
|
|
1089
1126
|
launcher?: string;
|
|
1127
|
+
expectedUserId?: ExpectedWindowsTaskUserId | null;
|
|
1090
1128
|
}): WindowsSchedulerInstallVerification {
|
|
1091
1129
|
const registrationHealthy = inputs.xml.length > 0
|
|
1092
|
-
&& windowsTaskRegistrationHealthy(inputs.xml, inputs.wscript, inputs.launcher);
|
|
1130
|
+
&& windowsTaskRegistrationHealthy(inputs.xml, inputs.wscript, inputs.launcher, inputs.expectedUserId);
|
|
1093
1131
|
// Permanent invalidity: the XML IS published but violates the registration
|
|
1094
1132
|
// contract — no amount of settling changes it. Empty/unreadable XML stays
|
|
1095
1133
|
// transient (publication lag).
|
|
@@ -1804,18 +1842,16 @@ export function buildWindowsTaskXml(
|
|
|
1804
1842
|
script = windowsServiceScriptPath(),
|
|
1805
1843
|
launcher = windowsLauncherVbsPath(),
|
|
1806
1844
|
attemptNonce?: string,
|
|
1807
|
-
sessionTriggerUserId = cachedCurrentWindowsIdentity()?.
|
|
1845
|
+
sessionTriggerUserId = cachedCurrentWindowsIdentity()?.sid,
|
|
1808
1846
|
): string {
|
|
1809
1847
|
const escapedWscript = taskXmlString(windowsWscript());
|
|
1810
1848
|
// Escape the launcher path independently for the <Arguments> element; quoting it
|
|
1811
1849
|
// keeps spaces intact, and /b (batch mode) suppresses script error popups.
|
|
1812
1850
|
const escapedLauncherArgs = taskXmlString(`/b /nologo "${launcher}"`);
|
|
1813
1851
|
// `UserId` is optional in the schema, and omitting it makes a SessionStateChangeTrigger
|
|
1814
|
-
// fire for ANY account's session change.
|
|
1815
|
-
// account
|
|
1816
|
-
// and
|
|
1817
|
-
// degrade to the unscoped trigger rather than leave the task with no recovery at all.
|
|
1818
|
-
// `LogonTrigger` above is unscoped for the same reason and predates this change.
|
|
1852
|
+
// fire for ANY account's session change. Production registration resolves and passes the
|
|
1853
|
+
// installing account SID explicitly; the optional parameter remains only for deterministic
|
|
1854
|
+
// builders/tests, and the live validator rejects an unscoped recovery trigger.
|
|
1819
1855
|
const sessionUserIdElement = sessionTriggerUserId
|
|
1820
1856
|
? `\n <UserId>${taskXmlString(sessionTriggerUserId)}</UserId>`
|
|
1821
1857
|
: "";
|
|
@@ -1866,6 +1902,34 @@ export function buildWindowsTaskXml(
|
|
|
1866
1902
|
`;
|
|
1867
1903
|
}
|
|
1868
1904
|
|
|
1905
|
+
type ExpectedWindowsTaskUserId = string | readonly string[];
|
|
1906
|
+
|
|
1907
|
+
function cachedWindowsTaskUserIds(): readonly string[] | null {
|
|
1908
|
+
const identity = cachedCurrentWindowsIdentity();
|
|
1909
|
+
return identity ? [identity.sid, identity.name] : null;
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
function resolvedWindowsTaskSid(): string {
|
|
1913
|
+
let identity = cachedCurrentWindowsIdentity();
|
|
1914
|
+
if (!identity) {
|
|
1915
|
+
const principal = resolveCurrentWindowsPrincipal(WINDOWS_PRINCIPAL_LOOKUP_TIMEOUT_MS);
|
|
1916
|
+
identity = cachedCurrentWindowsIdentity();
|
|
1917
|
+
if (!identity && /^\*S-1-(?:\d+-)+\d+$/i.test(principal)) return principal.slice(1).toUpperCase();
|
|
1918
|
+
}
|
|
1919
|
+
if (!identity) throw new Error("Windows Task Scheduler identity could not be resolved.");
|
|
1920
|
+
return identity.sid;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
/** Render the exact UTF-16 task document published by production registration paths. */
|
|
1924
|
+
export function buildWindowsTaskXmlDocument(
|
|
1925
|
+
script = windowsServiceScriptPath(),
|
|
1926
|
+
launcher = windowsLauncherVbsPath(),
|
|
1927
|
+
attemptNonce?: string,
|
|
1928
|
+
sessionTriggerUserId = resolvedWindowsTaskSid(),
|
|
1929
|
+
): string {
|
|
1930
|
+
return `\uFEFF${buildWindowsTaskXml(script, launcher, attemptNonce, sessionTriggerUserId)}`;
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1869
1933
|
function taskXmlSection(xml: string, tag: string): string {
|
|
1870
1934
|
return new RegExp(`<${tag}(?:\\s[^>]*)?>([\\s\\S]*?)<\\/${tag}>`, "i").exec(xml)?.[1] ?? "";
|
|
1871
1935
|
}
|
|
@@ -1935,6 +1999,67 @@ function taskXmlDecodedValueEquals(xml: string, tag: string, expected: string):
|
|
|
1935
1999
|
return taskXmlDecodeEntities(value).trim().toLowerCase() === expected.trim().toLowerCase();
|
|
1936
2000
|
}
|
|
1937
2001
|
|
|
2002
|
+
/**
|
|
2003
|
+
* Characters a console code page substitutes when it cannot carry the original.
|
|
2004
|
+
* Windows writes `?` per unrepresentable character, some layers write U+FFFD, and a
|
|
2005
|
+
* few drop them entirely.
|
|
2006
|
+
*/
|
|
2007
|
+
const CODE_PAGE_SUBSTITUTIONS = /^[?\uFFFD]*$/;
|
|
2008
|
+
|
|
2009
|
+
/**
|
|
2010
|
+
* Compare a value that OpenCodex itself wrote against what `schtasks /query /xml` read
|
|
2011
|
+
* back, tolerating ONLY the characters the console code page could not carry.
|
|
2012
|
+
*
|
|
2013
|
+
* `runFile` already reads the query as bytes, so this is not a spawn-decoding bug: the
|
|
2014
|
+
* conversion happens inside `schtasks` before the bytes exist. A profile named outside
|
|
2015
|
+
* the active code page — `C:\\Users\\김병준\\...` — comes back as `C:\\Users\\???\\...`, so an
|
|
2016
|
+
* exact comparison rejected a registration this process had just created correctly and
|
|
2017
|
+
* `ocx service install` rolled it back (#3064).
|
|
2018
|
+
*
|
|
2019
|
+
* The tolerance is deliberately narrow. Each unrepresentable RUN in the expected value
|
|
2020
|
+
* may match only a run of substitution characters — never arbitrary text, and never a
|
|
2021
|
+
* path separator. A wildcard as wide as `[^\\\\/]*` would leave a fully non-ASCII segment with
|
|
2022
|
+
* no anchors at all, so `C:\\Users\\김병준\\x.vbs` would match `C:\\Users\\Admin\\x.vbs` and this
|
|
2023
|
+
* process would adopt, repair, or delete another account's task. Accepting a foreign
|
|
2024
|
+
* live task is a worse failure than the rollback this fixes.
|
|
2025
|
+
*/
|
|
2026
|
+
function taskXmlLossyValueEquals(reported: string, expected: string): boolean {
|
|
2027
|
+
const a = reported.trim().toLowerCase();
|
|
2028
|
+
const b = expected.trim().toLowerCase();
|
|
2029
|
+
if (a === b) return true;
|
|
2030
|
+
// Nothing unrepresentable in the expectation means there was nothing to mangle,
|
|
2031
|
+
// so any difference is a real one.
|
|
2032
|
+
if (!/[^\x00-\x7F]/.test(b)) return false;
|
|
2033
|
+
const parts = b.split(/([^\x00-\x7F]+)/);
|
|
2034
|
+
let rest = a;
|
|
2035
|
+
for (let i = 0; i < parts.length; i += 1) {
|
|
2036
|
+
const part = parts[i]!;
|
|
2037
|
+
if (i % 2 === 0) {
|
|
2038
|
+
// Literal ASCII run: it must be present verbatim, which is what keeps every
|
|
2039
|
+
// directory boundary and file name in the path verified.
|
|
2040
|
+
if (!rest.startsWith(part)) return false;
|
|
2041
|
+
rest = rest.slice(part.length);
|
|
2042
|
+
continue;
|
|
2043
|
+
}
|
|
2044
|
+
// Unrepresentable run: consume only substitution characters, and stop at the
|
|
2045
|
+
// next literal so a trailing run cannot swallow the remainder of the string.
|
|
2046
|
+
const next = parts[i + 1] ?? "";
|
|
2047
|
+
const end = next === "" ? rest.length : rest.indexOf(next);
|
|
2048
|
+
if (end < 0) return false;
|
|
2049
|
+
if (!CODE_PAGE_SUBSTITUTIONS.test(rest.slice(0, end))) return false;
|
|
2050
|
+
rest = rest.slice(end);
|
|
2051
|
+
}
|
|
2052
|
+
return rest === "";
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
function taskXmlDecodedLossyValueEquals(xml: string, tag: string, expected: string): boolean {
|
|
2056
|
+
if (taskXmlHasPrefixedTag(xml, tag)) return false;
|
|
2057
|
+
if (taskXmlElementCount(xml, tag) !== 1) return false;
|
|
2058
|
+
const value = new RegExp(`<${tag}(?:\\s[^>]*?)?>([^<]*)<\\/${tag}>`, "i").exec(xml)?.[1];
|
|
2059
|
+
if (value === undefined) return false;
|
|
2060
|
+
return taskXmlLossyValueEquals(taskXmlDecodeEntities(value), expected);
|
|
2061
|
+
}
|
|
2062
|
+
|
|
1938
2063
|
function taskXmlOptionalValueEquals(xml: string, tag: string, expected: string): boolean {
|
|
1939
2064
|
// Check the prefixed form first: treating `<t:Enabled>false</t:Enabled>` as an
|
|
1940
2065
|
// omission would turn an explicitly disabled task into a healthy one.
|
|
@@ -1968,7 +2093,10 @@ export function windowsTaskRegistrationOwnedByAttempt(xml: string, attemptNonce:
|
|
|
1968
2093
|
* carrying one disabled trigger plus a different enabled one must not pass because the two
|
|
1969
2094
|
* halves were found in unrelated elements.
|
|
1970
2095
|
*/
|
|
1971
|
-
function windowsTaskHasSessionRecoveryTriggers(
|
|
2096
|
+
function windowsTaskHasSessionRecoveryTriggers(
|
|
2097
|
+
triggers: string,
|
|
2098
|
+
expectedUserId: ExpectedWindowsTaskUserId | undefined,
|
|
2099
|
+
): boolean {
|
|
1972
2100
|
const scoped = triggers.match(/<SessionStateChangeTrigger(?:\s[^>]*)?>[\s\S]*?<\/SessionStateChangeTrigger>/gi) ?? [];
|
|
1973
2101
|
return WINDOWS_SESSION_RECOVERY_STATE_CHANGES.every(stateChange =>
|
|
1974
2102
|
scoped.some(element =>
|
|
@@ -1978,26 +2106,34 @@ function windowsTaskHasSessionRecoveryTriggers(triggers: string, expectedUserId:
|
|
|
1978
2106
|
}
|
|
1979
2107
|
|
|
1980
2108
|
/**
|
|
1981
|
-
* A trigger's scope is acceptable when it
|
|
2109
|
+
* A trigger's scope is acceptable only when it names the expected account exactly.
|
|
1982
2110
|
*
|
|
1983
|
-
* An unscoped trigger is
|
|
1984
|
-
*
|
|
1985
|
-
*
|
|
1986
|
-
* An explicitly scoped trigger is accepted only when the current account is known and matches.
|
|
2111
|
+
* An unscoped recovery trigger is not identity proof. Production registration resolves a SID
|
|
2112
|
+
* before writing XML; a missing scope therefore means the fixed-name task is legacy or foreign
|
|
2113
|
+
* and must be refreshed from an exact legacy snapshot or preserved for manual review.
|
|
1987
2114
|
* Treating an unknown expected identity as a wildcard would let a fresh status process accept a
|
|
1988
2115
|
* task bound to another user's session and suppress the repair that should replace it.
|
|
1989
2116
|
*/
|
|
1990
|
-
function windowsTaskTriggerScopeAcceptable(
|
|
2117
|
+
function windowsTaskTriggerScopeAcceptable(
|
|
2118
|
+
element: string,
|
|
2119
|
+
expectedUserId: ExpectedWindowsTaskUserId | undefined,
|
|
2120
|
+
): boolean {
|
|
1991
2121
|
// A prefixed `<t:UserId>` is a real scope this validator cannot read: taskXmlElementCount()
|
|
1992
2122
|
// counts only unprefixed tags, so without this the element below would look ABSENT and the
|
|
1993
2123
|
// trigger would be accepted as unscoped even though it is bound to some other account.
|
|
1994
2124
|
// Reject it outright rather than guess, and do so before the optional-field check.
|
|
1995
2125
|
if (taskXmlHasPrefixedTag(element, "UserId")) return false;
|
|
1996
2126
|
const userIdCount = taskXmlElementCount(element, "UserId");
|
|
1997
|
-
if (userIdCount === 0) return
|
|
2127
|
+
if (userIdCount === 0) return false;
|
|
1998
2128
|
if (userIdCount !== 1) return false;
|
|
1999
2129
|
if (expectedUserId === undefined) return false;
|
|
2000
|
-
|
|
2130
|
+
// Scope is an identity boundary, unlike the launcher path. Newly generated tasks
|
|
2131
|
+
// use the locale-independent SID from cachedCurrentWindowsIdentity(), so there is
|
|
2132
|
+
// no reason to forgive code-page substitutions here. A lossy account-name compare
|
|
2133
|
+
// lets two non-ASCII users collapse to the same `???` value and can make repair
|
|
2134
|
+
// start another account's fixed-name task.
|
|
2135
|
+
const expectedValues = typeof expectedUserId === "string" ? [expectedUserId] : expectedUserId;
|
|
2136
|
+
return expectedValues.some(value => taskXmlDecodedValueEquals(element, "UserId", value));
|
|
2001
2137
|
}
|
|
2002
2138
|
|
|
2003
2139
|
/** Validate the stable OpenCodex action, principal, settings, and logon trigger. */
|
|
@@ -2005,6 +2141,7 @@ function windowsTaskRegistrationBaseHealthy(
|
|
|
2005
2141
|
xml: string,
|
|
2006
2142
|
wscript = windowsWscript(),
|
|
2007
2143
|
launcher = windowsLauncherVbsPath(),
|
|
2144
|
+
allowLossyPaths = true,
|
|
2008
2145
|
): boolean {
|
|
2009
2146
|
const scrubbed = taskXmlWithoutCommentsAndCdata(xml);
|
|
2010
2147
|
// taskXmlSection() takes the FIRST match and the schema allows arbitrary XML under
|
|
@@ -2030,8 +2167,15 @@ function windowsTaskRegistrationBaseHealthy(
|
|
|
2030
2167
|
// quotes we wrote as `"` back to literal `"` on export, so an escaped
|
|
2031
2168
|
// needle never matched and a healthy task read as permanently stale (#608).
|
|
2032
2169
|
// Case-insensitive: elevated `schtasks /create` may rewrite System32 casing.
|
|
2033
|
-
|
|
2034
|
-
|
|
2170
|
+
// Lossy on purpose: both name paths under the user profile, which the query
|
|
2171
|
+
// cannot carry when the profile is named outside the code page (#3064). Only
|
|
2172
|
+
// unrepresentable characters are forgiven; every ASCII segment and every
|
|
2173
|
+
// separator is still matched literally.
|
|
2174
|
+
&& (allowLossyPaths
|
|
2175
|
+
? taskXmlDecodedLossyValueEquals(action, "Command", wscript)
|
|
2176
|
+
&& taskXmlDecodedLossyValueEquals(action, "Arguments", `/b /nologo "${launcher}"`)
|
|
2177
|
+
: taskXmlDecodedValueEquals(action, "Command", wscript)
|
|
2178
|
+
&& taskXmlDecodedValueEquals(action, "Arguments", `/b /nologo "${launcher}"`));
|
|
2035
2179
|
}
|
|
2036
2180
|
|
|
2037
2181
|
/** Validate the security/lifecycle-critical fields of the registered scheduler task. */
|
|
@@ -2039,7 +2183,7 @@ export function windowsTaskRegistrationHealthy(
|
|
|
2039
2183
|
xml: string,
|
|
2040
2184
|
wscript = windowsWscript(),
|
|
2041
2185
|
launcher = windowsLauncherVbsPath(),
|
|
2042
|
-
expectedUserId:
|
|
2186
|
+
expectedUserId: ExpectedWindowsTaskUserId | null = cachedWindowsTaskUserIds(),
|
|
2043
2187
|
): boolean {
|
|
2044
2188
|
const scrubbed = taskXmlWithoutCommentsAndCdata(xml);
|
|
2045
2189
|
const triggers = taskXmlSection(scrubbed, "Triggers");
|
|
@@ -2052,13 +2196,17 @@ export function windowsTaskRegistrationHealthy(
|
|
|
2052
2196
|
|
|
2053
2197
|
/**
|
|
2054
2198
|
* The only stale definition repair may replace automatically: the previous OpenCodex task
|
|
2055
|
-
* shape whose action/principal/settings are
|
|
2199
|
+
* shape whose action/principal/settings are byte-exact and which has no session triggers yet.
|
|
2056
2200
|
* Arbitrary unhealthy or partially modified fixed-name tasks are preserved for manual review.
|
|
2057
2201
|
*/
|
|
2058
|
-
function windowsTaskRegistrationRefreshableLegacy(
|
|
2202
|
+
function windowsTaskRegistrationRefreshableLegacy(
|
|
2203
|
+
xml: string,
|
|
2204
|
+
wscript = windowsWscript(),
|
|
2205
|
+
launcher = windowsLauncherVbsPath(),
|
|
2206
|
+
): boolean {
|
|
2059
2207
|
const scrubbed = taskXmlWithoutCommentsAndCdata(xml);
|
|
2060
2208
|
const triggers = taskXmlSection(scrubbed, "Triggers");
|
|
2061
|
-
return windowsTaskRegistrationBaseHealthy(xml)
|
|
2209
|
+
return windowsTaskRegistrationBaseHealthy(xml, wscript, launcher, false)
|
|
2062
2210
|
&& taskXmlElementCount(triggers, "SessionStateChangeTrigger") === 0
|
|
2063
2211
|
&& !taskXmlHasPrefixedTag(triggers, "SessionStateChangeTrigger");
|
|
2064
2212
|
}
|
|
@@ -2078,7 +2226,7 @@ export function readWindowsSchedulerXmlState(
|
|
|
2078
2226
|
xml: string,
|
|
2079
2227
|
wscript?: string,
|
|
2080
2228
|
launcher?: string,
|
|
2081
|
-
expectedUserId:
|
|
2229
|
+
expectedUserId: ExpectedWindowsTaskUserId | null = cachedWindowsTaskUserIds(),
|
|
2082
2230
|
): WindowsSchedulerXmlState {
|
|
2083
2231
|
const installed = xml.length > 0;
|
|
2084
2232
|
if (!installed) return { installed: false, enabled: false, registrationHealthy: false };
|
|
@@ -2242,7 +2390,11 @@ function writeWindowsSchedulerAssets(): void {
|
|
|
2242
2390
|
// UTF-16LE + BOM: a BOM-less UTF-8 VBS mis-decodes non-ASCII (e.g. Korean) profile
|
|
2243
2391
|
// paths on some WSH/codepage combinations — same contract as the task XML below.
|
|
2244
2392
|
writeServiceAssetWithRetry(windowsLauncherVbsPath(), `\uFEFF${buildWindowsLauncherVbs(script)}`, "utf16le");
|
|
2245
|
-
writeServiceAssetWithRetry(
|
|
2393
|
+
writeServiceAssetWithRetry(
|
|
2394
|
+
windowsTaskXmlPath(),
|
|
2395
|
+
buildWindowsTaskXmlDocument(script, windowsLauncherVbsPath()),
|
|
2396
|
+
"utf16le",
|
|
2397
|
+
);
|
|
2246
2398
|
}
|
|
2247
2399
|
|
|
2248
2400
|
const WINDOWS_SCHEDULER_STAGE_PREFIX = "opencodex-service-stage-";
|
|
@@ -2315,7 +2467,12 @@ export function stageWindowsSchedulerRegistrationXml(
|
|
|
2315
2467
|
// document while UAC is pending; the file harden independently proves its identity.
|
|
2316
2468
|
writeXml(
|
|
2317
2469
|
xmlPath,
|
|
2318
|
-
|
|
2470
|
+
buildWindowsTaskXmlDocument(
|
|
2471
|
+
windowsServiceScriptPath(),
|
|
2472
|
+
windowsLauncherVbsPath(),
|
|
2473
|
+
attemptNonce,
|
|
2474
|
+
resolvedWindowsTaskSid(),
|
|
2475
|
+
),
|
|
2319
2476
|
);
|
|
2320
2477
|
hardenPath(xmlPath);
|
|
2321
2478
|
ownedWindowsSchedulerStages.add(xmlPath);
|
|
@@ -2655,7 +2812,10 @@ export interface RepairServiceDeps {
|
|
|
2655
2812
|
/** Publishes the captured registration only when the fixed task name remains absent. */
|
|
2656
2813
|
restoreSchedulerIfAbsent?: (registeredXml: string) => Promise<void>;
|
|
2657
2814
|
/** Resolves the account the registered triggers must match; null when it cannot be resolved. */
|
|
2658
|
-
resolveExpectedUserId?: (registeredXml: string) =>
|
|
2815
|
+
resolveExpectedUserId?: (registeredXml: string) => ExpectedWindowsTaskUserId | null;
|
|
2816
|
+
/** Exact scheduler action values used by validation; defaults to the installed paths. */
|
|
2817
|
+
schedulerWscript?: string;
|
|
2818
|
+
schedulerLauncher?: string;
|
|
2659
2819
|
/** Test seam — defaults to process.platform so Linux CI cannot hit real installSystemd. */
|
|
2660
2820
|
platform?: NodeJS.Platform;
|
|
2661
2821
|
}
|
|
@@ -2756,11 +2916,26 @@ export async function repairService(deps: RepairServiceDeps = {}): Promise<void>
|
|
|
2756
2916
|
const expectedUserId = (deps.resolveExpectedUserId ?? resolveWindowsTaskDiagnosticUserId)(registeredXml);
|
|
2757
2917
|
const registrationHealthy = windowsTaskRegistrationHealthy(
|
|
2758
2918
|
registeredXml,
|
|
2759
|
-
|
|
2760
|
-
|
|
2919
|
+
deps.schedulerWscript,
|
|
2920
|
+
deps.schedulerLauncher,
|
|
2761
2921
|
expectedUserId,
|
|
2762
2922
|
);
|
|
2763
|
-
|
|
2923
|
+
const expectedValues = expectedUserId === null
|
|
2924
|
+
? []
|
|
2925
|
+
: typeof expectedUserId === "string" ? [expectedUserId] : expectedUserId;
|
|
2926
|
+
const preferredSid = expectedValues[0];
|
|
2927
|
+
const triggers = taskXmlSection(taskXmlWithoutCommentsAndCdata(registeredXml), "Triggers");
|
|
2928
|
+
// An exact legacy account name is safe to recognize, but rewrite it to the
|
|
2929
|
+
// locale-independent SID while repair already owns the mutation boundary.
|
|
2930
|
+
const identityUpgradeNeeded = registrationHealthy
|
|
2931
|
+
&& preferredSid !== undefined
|
|
2932
|
+
&& !windowsTaskHasSessionRecoveryTriggers(triggers, preferredSid);
|
|
2933
|
+
const refreshableLegacy = windowsTaskRegistrationRefreshableLegacy(
|
|
2934
|
+
registeredXml,
|
|
2935
|
+
deps.schedulerWscript,
|
|
2936
|
+
deps.schedulerLauncher,
|
|
2937
|
+
);
|
|
2938
|
+
if (!registrationHealthy && !refreshableLegacy) {
|
|
2764
2939
|
const scopedButUnresolved = expectedUserId === null
|
|
2765
2940
|
&& taskXmlElementCount(
|
|
2766
2941
|
taskXmlSection(taskXmlWithoutCommentsAndCdata(registeredXml), "Triggers"),
|
|
@@ -2781,7 +2956,7 @@ export async function repairService(deps: RepairServiceDeps = {}): Promise<void>
|
|
|
2781
2956
|
// Re-register only when the registered XML is actually stale, so the ordinary repair
|
|
2782
2957
|
// stays free of `schtasks /create` and its UAC prompt.
|
|
2783
2958
|
let startExpectedXml = registeredXml;
|
|
2784
|
-
if (!registrationHealthy) {
|
|
2959
|
+
if (!registrationHealthy || identityUpgradeNeeded) {
|
|
2785
2960
|
// The task was stopped above, so a failed replacement must not exit here: `/create /f`
|
|
2786
2961
|
// can be rejected, elevation can be cancelled, and staging or verification can fail.
|
|
2787
2962
|
// Any of those would leave a previously runnable proxy stopped and the user worse off
|
|
@@ -3855,7 +4030,7 @@ export function serviceStartableFromTray(service: ServiceDiagnostic): boolean {
|
|
|
3855
4030
|
}
|
|
3856
4031
|
|
|
3857
4032
|
export interface WindowsTaskDiagnosticIdentityDeps {
|
|
3858
|
-
currentIdentity?: () => Readonly<{ name: string }> | null;
|
|
4033
|
+
currentIdentity?: () => Readonly<{ sid: string; name: string }> | null;
|
|
3859
4034
|
resolvePrincipal?: (timeoutMs: number) => string;
|
|
3860
4035
|
}
|
|
3861
4036
|
|
|
@@ -3867,10 +4042,10 @@ export interface WindowsTaskDiagnosticIdentityDeps {
|
|
|
3867
4042
|
export function resolveWindowsTaskDiagnosticUserId(
|
|
3868
4043
|
schedulerXml: string,
|
|
3869
4044
|
deps: WindowsTaskDiagnosticIdentityDeps = {},
|
|
3870
|
-
): string | null {
|
|
4045
|
+
): readonly string[] | null {
|
|
3871
4046
|
const currentIdentity = deps.currentIdentity ?? cachedCurrentWindowsIdentity;
|
|
3872
4047
|
const cached = currentIdentity();
|
|
3873
|
-
if (cached) return cached.name;
|
|
4048
|
+
if (cached) return [cached.sid, cached.name];
|
|
3874
4049
|
|
|
3875
4050
|
const scrubbed = taskXmlWithoutCommentsAndCdata(schedulerXml);
|
|
3876
4051
|
const triggers = taskXmlSection(scrubbed, "Triggers");
|
|
@@ -3881,7 +4056,8 @@ export function resolveWindowsTaskDiagnosticUserId(
|
|
|
3881
4056
|
} catch {
|
|
3882
4057
|
return null;
|
|
3883
4058
|
}
|
|
3884
|
-
|
|
4059
|
+
const resolved = currentIdentity();
|
|
4060
|
+
return resolved ? [resolved.sid, resolved.name] : null;
|
|
3885
4061
|
}
|
|
3886
4062
|
|
|
3887
4063
|
export interface WindowsServiceDiagnosticInputs {
|
|
@@ -3893,7 +4069,7 @@ export interface WindowsServiceDiagnosticInputs {
|
|
|
3893
4069
|
*/
|
|
3894
4070
|
schedulerXml: string;
|
|
3895
4071
|
/** Resolved effective account for explicit scheduler trigger scopes; null means unknown. */
|
|
3896
|
-
schedulerExpectedUserId?:
|
|
4072
|
+
schedulerExpectedUserId?: ExpectedWindowsTaskUserId | null;
|
|
3897
4073
|
/** Whether the on-disk service assets exist. A filesystem concern, not an XML one. */
|
|
3898
4074
|
schedulerAssetsPresent: boolean;
|
|
3899
4075
|
nativeStatus: "started" | "stopped" | "nonexistent" | "unknown";
|
|
@@ -3905,7 +4081,7 @@ export interface WindowsServiceDiagnosticInputs {
|
|
|
3905
4081
|
|
|
3906
4082
|
export function deriveWindowsServiceDiagnostic(inputs: WindowsServiceDiagnosticInputs): ServiceDiagnostic {
|
|
3907
4083
|
const expectedUserId = inputs.schedulerExpectedUserId === undefined
|
|
3908
|
-
?
|
|
4084
|
+
? cachedWindowsTaskUserIds()
|
|
3909
4085
|
: inputs.schedulerExpectedUserId;
|
|
3910
4086
|
const schedulerState = readWindowsSchedulerXmlState(
|
|
3911
4087
|
inputs.schedulerXml,
|
package/src/types/config.ts
CHANGED
|
@@ -219,6 +219,14 @@ export interface OcxApiKeyEntry {
|
|
|
219
219
|
name: string;
|
|
220
220
|
key: string;
|
|
221
221
|
createdAt: string;
|
|
222
|
+
pendingRotation?: OcxPendingApiKeyRotation;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface OcxPendingApiKeyRotation {
|
|
226
|
+
id: string;
|
|
227
|
+
key: string;
|
|
228
|
+
createdAt: string;
|
|
229
|
+
expiresAt: string;
|
|
222
230
|
}
|
|
223
231
|
|
|
224
232
|
/**
|
|
@@ -244,8 +252,87 @@ export interface OcxConfigRebaseProvenance {
|
|
|
244
252
|
deletedTopLevelKeys: string[];
|
|
245
253
|
}
|
|
246
254
|
|
|
255
|
+
export type OcxRuntimeRole = "standalone" | "hub" | "client";
|
|
256
|
+
|
|
257
|
+
export interface OcxHubConfig {
|
|
258
|
+
/** Canonical browser-reachable management origin advertised by a hub. */
|
|
259
|
+
managementPublicOrigin?: string;
|
|
260
|
+
/**
|
|
261
|
+
* Optional management-only listener for a local HTTPS frontend such as Tailscale Serve.
|
|
262
|
+
* The hostname is deliberately not configurable: when enabled the socket is always bound
|
|
263
|
+
* to 127.0.0.1, and only GUI, session-bootstrap, and management API routes are admitted.
|
|
264
|
+
*/
|
|
265
|
+
managementIngress?:
|
|
266
|
+
| { enabled: false }
|
|
267
|
+
| { enabled: true; port: number };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export interface OcxRemoteGuiConfig {
|
|
271
|
+
/** Exact Tailscale login identities permitted to receive an automatic remote GUI session. */
|
|
272
|
+
allowedTailscaleUsers?: string[];
|
|
273
|
+
/**
|
|
274
|
+
* Retired. Once permitted a one-time pairing exchange over non-loopback plaintext HTTP.
|
|
275
|
+
*
|
|
276
|
+
* Still parsed so an existing config file keeps loading, but it grants nothing: a pairing
|
|
277
|
+
* grant now crosses loopback or authenticated HTTPS only. A persisted `true` is reported
|
|
278
|
+
* once and otherwise ignored. Kept in the type rather than deleted because the schema is
|
|
279
|
+
* strict — dropping the key outright would make an older config fail to load entirely,
|
|
280
|
+
* which is a worse outcome than ignoring one retired field.
|
|
281
|
+
*
|
|
282
|
+
* @deprecated has no effect; remove it from your config.
|
|
283
|
+
*/
|
|
284
|
+
allowInsecureHttp?: boolean;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export type OcxConnectedClientId = "codex" | "claude";
|
|
288
|
+
|
|
289
|
+
export interface OcxClientConnectionConfig {
|
|
290
|
+
serverUrl: string;
|
|
291
|
+
managementUrl: string;
|
|
292
|
+
managementTransport: "direct" | "relay";
|
|
293
|
+
selectedClients: OcxConnectedClientId[];
|
|
294
|
+
tokenEnv: "OPENCODEX_API_AUTH_TOKEN";
|
|
295
|
+
apiKeyId: string;
|
|
296
|
+
tokenFingerprint: string;
|
|
297
|
+
protocolVersion: 1;
|
|
298
|
+
connectedAt: string;
|
|
299
|
+
/**
|
|
300
|
+
* sha256/base64url of the catalog bytes this connection wrote, used to tell "still ours"
|
|
301
|
+
* from "edited or replaced" before removing the file on disconnect.
|
|
302
|
+
*
|
|
303
|
+
* Our own hash rather than the hub's ETag: /v1/catalog emits no validator, and this was
|
|
304
|
+
* always an ownership check on local bytes rather than a cache concern.
|
|
305
|
+
*/
|
|
306
|
+
catalogFingerprint?: string;
|
|
307
|
+
/**
|
|
308
|
+
* The catalog that was on disk before connect overwrote it, base64-encoded, or the
|
|
309
|
+
* empty string when there was none.
|
|
310
|
+
*
|
|
311
|
+
* Durable because disconnect runs in a different process than connect: an in-memory
|
|
312
|
+
* snapshot only covers a connect that fails and rolls back on the spot. Without this,
|
|
313
|
+
* disconnect deletes the remote catalog and reports a restored native state while the
|
|
314
|
+
* user's own catalog is simply gone.
|
|
315
|
+
*/
|
|
316
|
+
priorCatalog?: string;
|
|
317
|
+
catalogSyncedAt?: string;
|
|
318
|
+
pendingOperation?: {
|
|
319
|
+
kind: "rotate";
|
|
320
|
+
rotationId: string;
|
|
321
|
+
newKeyIssuedAt: string;
|
|
322
|
+
oldKeyBackupPath: string;
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
247
326
|
export interface OcxConfig {
|
|
248
327
|
port: number;
|
|
328
|
+
/** Runtime topology role. Absence preserves the historical standalone behavior. */
|
|
329
|
+
runtimeRole?: OcxRuntimeRole;
|
|
330
|
+
/** Hub-only public management metadata. Presence is inert outside the hub role. */
|
|
331
|
+
hub?: OcxHubConfig;
|
|
332
|
+
/** Opt-in remote dashboard issuance policy. Presence is inert outside the hub role. */
|
|
333
|
+
remoteGui?: OcxRemoteGuiConfig;
|
|
334
|
+
/** Remote-hub client state. The admission secret is stored only in service-api-token. */
|
|
335
|
+
client?: OcxClientConnectionConfig;
|
|
249
336
|
/** Opt in to one identical-turn retry when a Responses completion has no text or tool call. */
|
|
250
337
|
emptyCompletionRetry?: boolean;
|
|
251
338
|
/**
|
|
@@ -511,6 +598,10 @@ export interface OcxConfig {
|
|
|
511
598
|
* Outbound HTTP(S) proxy URL for provider requests (e.g. "http://user:pass@proxy:8080", or
|
|
512
599
|
* "${HTTPS_PROXY}"-style env reference). Mirrored into HTTP_PROXY/HTTPS_PROXY at startup when
|
|
513
600
|
* those are unset — Bun's fetch honors them for all outbound calls; localhost is excluded.
|
|
601
|
+
* The literal `"auto"` reads the Windows WinINET static proxy (`ProxyEnable`/`ProxyServer`)
|
|
602
|
+
* once at process start; on other platforms, or when the system proxy is off, SOCKS-only,
|
|
603
|
+
* or unreadable, it degrades to direct egress with one log line (#1525). PAC/WPAD and live
|
|
604
|
+
* changes are not followed.
|
|
514
605
|
*/
|
|
515
606
|
proxy?: string;
|
|
516
607
|
/**
|
|
@@ -543,6 +634,13 @@ export interface OcxConfig {
|
|
|
543
634
|
codexAutoStart?: boolean;
|
|
544
635
|
/** Restore an installed shim after a stable external Codex update replaces it. Default true. */
|
|
545
636
|
codexShimAutoRestore?: boolean;
|
|
637
|
+
/**
|
|
638
|
+
* Opt-in authless Codex Desktop routing (#1107). On a loopback bind, inject the dedicated
|
|
639
|
+
* `[model_providers.opencodex]` table with `requires_openai_auth = false` instead of the root
|
|
640
|
+
* `openai_base_url` override, so Desktop opens without a ChatGPT login. Default off; ignored on
|
|
641
|
+
* non-loopback binds, whose admission token contract is unchanged.
|
|
642
|
+
*/
|
|
643
|
+
codexDesktopAuthless?: boolean;
|
|
546
644
|
/**
|
|
547
645
|
* Compatibility mode: temporarily rewrite Codex resume-history metadata while the proxy is active
|
|
548
646
|
* so Codex App can show old OpenAI chats and opencodex-created exec chats under its default
|
|
@@ -600,6 +698,14 @@ export interface OcxConfig {
|
|
|
600
698
|
* whole config.
|
|
601
699
|
*/
|
|
602
700
|
showCodexSparkQuota?: boolean;
|
|
701
|
+
/**
|
|
702
|
+
* Opt-in auto-redemption of a main-account Codex reset credit shortly before it expires
|
|
703
|
+
* (#822). Default off. `leadTimeMinutes` (1–60, default 10) is how long before
|
|
704
|
+
* `expires_at` the redeem is attempted; the credit list is re-read upstream right before
|
|
705
|
+
* every dispatch and the request id is journaled first, so a manual redeem or a crash never
|
|
706
|
+
* spends a second credit. A malformed value reads as off.
|
|
707
|
+
*/
|
|
708
|
+
resetCreditAutoRedeem?: { enabled?: boolean; leadTimeMinutes?: number };
|
|
603
709
|
/** Active pool account id for next session. undefined = main (passthrough as-is). */
|
|
604
710
|
activeCodexAccountId?: string;
|
|
605
711
|
/** Auto-switch threshold (0-100). Default 80. 0 = disabled. */
|
|
@@ -615,6 +721,17 @@ export interface OcxConfig {
|
|
|
615
721
|
* Default 0 (disabled); range 0..20. The circuit never counts timeouts or HTTP responses.
|
|
616
722
|
*/
|
|
617
723
|
upstreamHostCircuitThreshold?: number;
|
|
724
|
+
/**
|
|
725
|
+
* Opt-in ceiling, in bytes, for a serialized native Responses **passthrough** body. When the
|
|
726
|
+
* built body exceeds it OpenCodex refuses locally instead of sending, naming the size and any
|
|
727
|
+
* embedded image payload. Translated adapter paths are not covered.
|
|
728
|
+
*
|
|
729
|
+
* Omitted or 0 = disabled, which is the default: no implicit ceiling is inferred for any
|
|
730
|
+
* destination. The only measured limit in this codebase is the WebSocket create-frame size,
|
|
731
|
+
* and the same body still succeeds over HTTP SSE, so a default here would refuse requests
|
|
732
|
+
* that work today — on Azure and custom Responses gateways as well, whose limits are unknown.
|
|
733
|
+
*/
|
|
734
|
+
maxUpstreamBodyBytes?: number;
|
|
618
735
|
/**
|
|
619
736
|
* Opt-in Anthropic OAuth account pool (#294). Default OFF.
|
|
620
737
|
* Failover on 429 + sticky affinity; new sessions may pick lowest known 5h usage.
|
|
@@ -669,6 +786,18 @@ export type OcxAccountPoolQuotaWindow = "five-hour" | "weekly" | "max-utilizatio
|
|
|
669
786
|
export type OcxComboStrategy = "failover" | "round-robin" | "random" | "least-used" | "reset-window";
|
|
670
787
|
export type OcxComboDefaultEffort = "low" | "medium" | "high" | "xhigh" | "max" | "ultra";
|
|
671
788
|
|
|
789
|
+
/**
|
|
790
|
+
* How a combo derives the reasoning ladder it publishes to the picker.
|
|
791
|
+
*
|
|
792
|
+
* `strict` (default) intersects every advertised ladder, so a target that explicitly
|
|
793
|
+
* advertises no effort control (`reasoningEfforts: []`) empties the combo's picker.
|
|
794
|
+
* `adaptive` excludes those empty ladders from the published intersection, keeping the
|
|
795
|
+
* control usable for a mixed-capability group. Unknown (`undefined`) ladders stay
|
|
796
|
+
* wildcards in both modes. Dispatch is unchanged: each concrete target still resolves
|
|
797
|
+
* its own effort at request time.
|
|
798
|
+
*/
|
|
799
|
+
export type OcxComboReasoningEffortMode = "strict" | "adaptive";
|
|
800
|
+
|
|
672
801
|
export interface OcxComboTarget {
|
|
673
802
|
provider: string;
|
|
674
803
|
model: string;
|
|
@@ -684,6 +813,11 @@ export interface OcxComboConfig {
|
|
|
684
813
|
stickyLimit?: number;
|
|
685
814
|
/** Used when the client omits reasoning.effort. null/omitted leaves the target default unchanged. */
|
|
686
815
|
defaultEffort?: OcxComboDefaultEffort | null;
|
|
816
|
+
/**
|
|
817
|
+
* Picker-ladder derivation policy. Omitted / `"strict"` keeps the legacy rule where an
|
|
818
|
+
* explicitly empty target ladder suppresses the whole combo's effort control.
|
|
819
|
+
*/
|
|
820
|
+
reasoningEffortMode?: OcxComboReasoningEffortMode;
|
|
687
821
|
/**
|
|
688
822
|
* Disable image input even when every target supports it.
|
|
689
823
|
* Omitted / `"auto"` keeps automatic capability derivation (default: enabled when
|