@bitkyc08/opencodex 2.17.0 → 2.18.2
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/gui/dist/assets/{index-DOKr6RBR.js → index-CXI1262_.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/client-fingerprint.ts +2 -2
- package/src/adapters/cursor/live-transport.ts +17 -5
- package/src/adapters/cursor/protobuf-events.ts +662 -20
- package/src/adapters/cursor/tool-definitions.ts +12 -6
- package/src/adapters/google.ts +21 -2
- package/src/bridge.ts +12 -2
- package/src/cli/index.ts +11 -0
- package/src/codex/app-server-processes.ts +3 -3
- package/src/codex/user-identity.ts +36 -6
- package/src/config.ts +0 -2
- package/src/generated/compatibility-version.json +37 -33
- package/src/lib/token-estimate.ts +19 -2
- package/src/lib/windows-elevation.ts +37 -0
- package/src/lib/windows-secret-acl.ts +7 -0
- package/src/lib/windows-text.ts +106 -0
- package/src/lib/windows-user-principal.ts +0 -2
- package/src/oauth/index.ts +1 -1
- package/src/oauth/store.ts +32 -18
- package/src/providers/antigravity-models.ts +25 -5
- package/src/providers/free-directory.ts +1 -1
- package/src/providers/registry.ts +4 -3
- package/src/server/index.ts +5 -1
- package/src/server/management/logs-usage-routes.ts +7 -22
- package/src/server/request-log.ts +48 -3
- package/src/server/responses/core.ts +38 -13
- package/src/server/responses/encrypted-payload.ts +58 -38
- package/src/server/responses/fetch-helpers.ts +12 -4
- package/src/server/responses/policy-fallback.ts +13 -2
- package/src/server/responses/ws-upstream.ts +115 -6
- package/src/service-manager-probe.ts +21 -34
- package/src/service.ts +233 -25
- package/src/tray/windows.ts +0 -2
- package/src/update/job.ts +2 -2
- package/src/usage/summary.ts +21 -4
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
resolveTrustedWindowsSchtasksExe,
|
|
27
27
|
resolveTrustedWindowsSystemDirectory,
|
|
28
28
|
} from "./lib/windows-elevation";
|
|
29
|
+
import { decodeWindowsTextBytes } from "./lib/windows-text";
|
|
29
30
|
import { WINSW_SERVICE_ID } from "./lib/winsw";
|
|
30
31
|
|
|
31
32
|
/** Short: this runs inside admission, and a slow answer is the same as none. */
|
|
@@ -119,6 +120,8 @@ export interface ProbeDeps {
|
|
|
119
120
|
readonly configDir?: string;
|
|
120
121
|
/** Test seam for WinSW SCM status. Production uses bounded trusted `sc.exe query`. */
|
|
121
122
|
readonly winswStatus?: () => "started" | "stopped" | "nonexistent" | "unknown";
|
|
123
|
+
/** Test seam for redirected Windows legacy-codepage output. */
|
|
124
|
+
readonly windowsLocale?: string;
|
|
122
125
|
}
|
|
123
126
|
|
|
124
127
|
const LABEL = "com.opencodex.proxy";
|
|
@@ -338,29 +341,6 @@ function windowsConfigDirPath(deps: { home: string; configDir?: string }): strin
|
|
|
338
341
|
return join(deps.home, ".opencodex");
|
|
339
342
|
}
|
|
340
343
|
|
|
341
|
-
/** Decode an on-disk Windows text asset (task XML, VBS), which is UTF-16LE (often BOM-prefixed). */
|
|
342
|
-
function decodeWindowsText(buffer: Buffer): string {
|
|
343
|
-
if (buffer.length === 0) return "";
|
|
344
|
-
const bomUtf16Le = buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xfe;
|
|
345
|
-
const bomUtf16Be = buffer.length >= 2 && buffer[0] === 0xfe && buffer[1] === 0xff;
|
|
346
|
-
const looksUtf16Le = buffer.length >= 4
|
|
347
|
-
&& buffer[1] === 0x00
|
|
348
|
-
&& buffer[3] === 0x00
|
|
349
|
-
&& buffer[0] !== 0x00;
|
|
350
|
-
if (bomUtf16Le || looksUtf16Le) {
|
|
351
|
-
return buffer.toString("utf16le").replace(/^\uFEFF/, "").trim();
|
|
352
|
-
}
|
|
353
|
-
if (bomUtf16Be) {
|
|
354
|
-
const swapped = Buffer.alloc(buffer.length - 2);
|
|
355
|
-
for (let i = 2; i + 1 < buffer.length; i += 2) {
|
|
356
|
-
swapped[i - 2] = buffer[i + 1]!;
|
|
357
|
-
swapped[i - 1] = buffer[i]!;
|
|
358
|
-
}
|
|
359
|
-
return swapped.toString("utf16le").trim();
|
|
360
|
-
}
|
|
361
|
-
return buffer.toString("utf8").replace(/^\uFEFF/, "").trim();
|
|
362
|
-
}
|
|
363
|
-
|
|
364
344
|
/** Decode the XML entities emitted by the service-definition writers. */
|
|
365
345
|
function decodeXmlEntities(value: string): string {
|
|
366
346
|
return value
|
|
@@ -478,7 +458,9 @@ const SCHTASKS_TASK_NOT_FOUND_EN = /cannot find the file specified/i;
|
|
|
478
458
|
* other nonzero responses use a bounded full listing as the locale-neutral
|
|
479
459
|
* fallback, and only a successful list without our task proves absence.
|
|
480
460
|
*/
|
|
481
|
-
function probeWindowsTaskRegistration(
|
|
461
|
+
function probeWindowsTaskRegistration(
|
|
462
|
+
deps: Required<Pick<ProbeDeps, "runRaw">> & Pick<ProbeDeps, "windowsLocale">,
|
|
463
|
+
): {
|
|
482
464
|
registered: "present" | "absent" | "unknown";
|
|
483
465
|
registeredXml: string;
|
|
484
466
|
} {
|
|
@@ -492,13 +474,14 @@ function probeWindowsTaskRegistration(deps: Required<Pick<ProbeDeps, "runRaw">>)
|
|
|
492
474
|
const queried = deps.runRaw(schtasks, ["/query", "/tn", windowsTaskName(), "/xml"]);
|
|
493
475
|
if (queried.spawnFailed || queried.timedOut) return { registered: "unknown", registeredXml: "" };
|
|
494
476
|
if (queried.status === 0) {
|
|
495
|
-
const registeredXml =
|
|
477
|
+
const registeredXml = decodeWindowsTextBytes(queried.stdout, { locale: deps.windowsLocale })
|
|
478
|
+
|| decodeWindowsTextBytes(queried.stderr, { locale: deps.windowsLocale });
|
|
496
479
|
return registeredXml
|
|
497
480
|
? { registered: "present", registeredXml }
|
|
498
481
|
: { registered: "unknown", registeredXml: "" };
|
|
499
482
|
}
|
|
500
483
|
|
|
501
|
-
const queryText = `${
|
|
484
|
+
const queryText = `${decodeWindowsTextBytes(queried.stdout, { locale: deps.windowsLocale })}\n${decodeWindowsTextBytes(queried.stderr, { locale: deps.windowsLocale })}`;
|
|
502
485
|
if (queried.status !== null && SCHTASKS_TASK_NOT_FOUND_EN.test(queryText)) {
|
|
503
486
|
return { registered: "absent", registeredXml: "" };
|
|
504
487
|
}
|
|
@@ -507,7 +490,8 @@ function probeWindowsTaskRegistration(deps: Required<Pick<ProbeDeps, "runRaw">>)
|
|
|
507
490
|
if (listed.spawnFailed || listed.timedOut || listed.status !== 0) {
|
|
508
491
|
return { registered: "unknown", registeredXml: "" };
|
|
509
492
|
}
|
|
510
|
-
const listing =
|
|
493
|
+
const listing = decodeWindowsTextBytes(listed.stdout, { locale: deps.windowsLocale })
|
|
494
|
+
|| decodeWindowsTextBytes(listed.stderr, { locale: deps.windowsLocale });
|
|
511
495
|
return windowsTaskListContains(listing, windowsTaskName())
|
|
512
496
|
? { registered: "unknown", registeredXml: "" }
|
|
513
497
|
: { registered: "absent", registeredXml: "" };
|
|
@@ -545,7 +529,8 @@ function probeWinswRegistration(
|
|
|
545
529
|
}
|
|
546
530
|
|
|
547
531
|
function inspectWindows(
|
|
548
|
-
deps: Required<Pick<ProbeDeps, "runRaw" | "home">>
|
|
532
|
+
deps: Required<Pick<ProbeDeps, "runRaw" | "home">>
|
|
533
|
+
& Pick<ProbeDeps, "configDir" | "winswStatus" | "windowsLocale">,
|
|
549
534
|
): ServiceManagerInstallation {
|
|
550
535
|
const configDir = windowsConfigDirPath(deps);
|
|
551
536
|
const taskXmlPath = join(configDir, "opencodex-service-task.xml");
|
|
@@ -557,7 +542,7 @@ function inspectWindows(
|
|
|
557
542
|
let xml = "";
|
|
558
543
|
if (task !== "absent") {
|
|
559
544
|
try {
|
|
560
|
-
xml =
|
|
545
|
+
xml = decodeWindowsTextBytes(readFileSync(taskXmlPath), { locale: deps.windowsLocale });
|
|
561
546
|
} catch (error) {
|
|
562
547
|
return unknown(`the scheduled-task XML exists but could not be read: ${String(error)}`);
|
|
563
548
|
}
|
|
@@ -659,7 +644,7 @@ function homesEqual(
|
|
|
659
644
|
* generated service-asset directory.
|
|
660
645
|
*/
|
|
661
646
|
function walkWindowsChain(
|
|
662
|
-
deps: Required<Pick<ProbeDeps, "home">> & Pick<ProbeDeps, "configDir">,
|
|
647
|
+
deps: Required<Pick<ProbeDeps, "home">> & Pick<ProbeDeps, "configDir" | "windowsLocale">,
|
|
663
648
|
xml: string,
|
|
664
649
|
definitionPath: string,
|
|
665
650
|
): ServiceManagerInstallation {
|
|
@@ -683,7 +668,7 @@ function walkWindowsChain(
|
|
|
683
668
|
}
|
|
684
669
|
let launcherBody: string;
|
|
685
670
|
try {
|
|
686
|
-
launcherBody =
|
|
671
|
+
launcherBody = decodeWindowsTextBytes(readFileSync(launcherPath), { locale: deps.windowsLocale });
|
|
687
672
|
} catch (error) {
|
|
688
673
|
return unknown(`the scheduled-task launcher could not be read: ${String(error)}`);
|
|
689
674
|
}
|
|
@@ -702,7 +687,7 @@ function walkWindowsChain(
|
|
|
702
687
|
}
|
|
703
688
|
let wrapperBody: string;
|
|
704
689
|
try {
|
|
705
|
-
wrapperBody =
|
|
690
|
+
wrapperBody = decodeWindowsTextBytes(readFileSync(wrapperPath), { locale: deps.windowsLocale });
|
|
706
691
|
} catch (error) {
|
|
707
692
|
return unknown(`the launcher wrapper could not be read: ${String(error)}`);
|
|
708
693
|
}
|
|
@@ -734,7 +719,8 @@ function walkWindowsChain(
|
|
|
734
719
|
* this read-only ownership probe.
|
|
735
720
|
*/
|
|
736
721
|
function walkWinswChain(
|
|
737
|
-
deps: Required<Pick<ProbeDeps, "runRaw" | "home">>
|
|
722
|
+
deps: Required<Pick<ProbeDeps, "runRaw" | "home">>
|
|
723
|
+
& Pick<ProbeDeps, "configDir" | "winswStatus" | "windowsLocale">,
|
|
738
724
|
): ServiceManagerInstallation {
|
|
739
725
|
const configDir = windowsConfigDirPath(deps);
|
|
740
726
|
const exePath = join(configDir, "winsw", `${WINSW_SERVICE_ID}.exe`);
|
|
@@ -753,7 +739,7 @@ function walkWinswChain(
|
|
|
753
739
|
|
|
754
740
|
let body: string;
|
|
755
741
|
try {
|
|
756
|
-
body =
|
|
742
|
+
body = decodeWindowsTextBytes(readFileSync(xmlPath), { locale: deps.windowsLocale });
|
|
757
743
|
} catch (error) {
|
|
758
744
|
return unknown(`the WinSW XML could not be read: ${String(error)}`);
|
|
759
745
|
}
|
|
@@ -801,6 +787,7 @@ export function inspectServiceManagerInstallation(deps: ProbeDeps = {}): Service
|
|
|
801
787
|
home,
|
|
802
788
|
configDir: deps.configDir,
|
|
803
789
|
winswStatus: deps.winswStatus,
|
|
790
|
+
windowsLocale: deps.windowsLocale,
|
|
804
791
|
});
|
|
805
792
|
}
|
|
806
793
|
return unknown(`no service manager probe for platform ${platform}`);
|
package/src/service.ts
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { execFileSync, execSync, spawnSync } from "node:child_process";
|
|
9
9
|
import { findLiveProxy, proxyIdentityAt, SERVICE_STOP_LIVENESS } from "./server/proxy-liveness";
|
|
10
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
11
|
-
import { homedir } from "node:os";
|
|
10
|
+
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { homedir, tmpdir } from "node:os";
|
|
12
12
|
import { dirname, join, resolve, win32 } from "node:path";
|
|
13
13
|
import { expandUserPath, getConfigDir, readPid, removePid, removeRuntimePort, verifyPidIdentity } from "./config";
|
|
14
14
|
import { loadConfig } from "./config";
|
|
@@ -24,9 +24,11 @@ import {
|
|
|
24
24
|
ELEVATION_REQUEST_TIMEOUT_MS,
|
|
25
25
|
OCX_ELEVATED_PROTOCOL_FAILED,
|
|
26
26
|
raceWithTimeout,
|
|
27
|
+
resolveTrustedWindowsPowerShellExe,
|
|
27
28
|
resolveTrustedWindowsSchtasksExe,
|
|
28
29
|
startElevatedSchtasksCreateAndRun,
|
|
29
30
|
runWindowsElevated,
|
|
31
|
+
runWindowsElevatedScheduledTaskRegistration,
|
|
30
32
|
toWindowsSchtasksError,
|
|
31
33
|
WindowsElevationError,
|
|
32
34
|
WindowsSchtasksError,
|
|
@@ -35,7 +37,12 @@ import {
|
|
|
35
37
|
type ElevatedSchtasksCreateAndRunResult,
|
|
36
38
|
} from "./lib/windows-elevation";
|
|
37
39
|
import { defaultWinswEntry, installWinswService, startWinswService, stopWinswService, statusWinswRaw, uninstallWinswService, winswStatusSummary, winswXmlPath, WINSW_SERVICE_ID, WINSW_SHA256, WINSW_VERSION, type WinswStatus } from "./lib/winsw";
|
|
38
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
forgetEphemeralSecretDir,
|
|
42
|
+
forgetEphemeralSecretPath,
|
|
43
|
+
hardenSecretDir,
|
|
44
|
+
hardenSecretPath,
|
|
45
|
+
} from "./lib/windows-secret-acl";
|
|
39
46
|
import { windowsEnvIndirectBatchPathList, windowsEnvIndirectBatchValue } from "./lib/win-paths";
|
|
40
47
|
import { recordOwnedConfigPath } from "./lib/config-ownership";
|
|
41
48
|
import { maybeShowStarPrompt } from "./cli/star-prompt";
|
|
@@ -1896,22 +1903,115 @@ function writeWindowsSchedulerAssets(): void {
|
|
|
1896
1903
|
writeServiceAssetWithRetry(windowsTaskXmlPath(), `\uFEFF${buildWindowsTaskXml(script)}`, "utf16le");
|
|
1897
1904
|
}
|
|
1898
1905
|
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1906
|
+
const WINDOWS_SCHEDULER_STAGE_PREFIX = "opencodex-service-stage-";
|
|
1907
|
+
const ownedWindowsSchedulerStages = new Set<string>();
|
|
1908
|
+
|
|
1909
|
+
export interface WindowsSchedulerRegistrationStageDeps {
|
|
1910
|
+
createStageDir?: () => string;
|
|
1911
|
+
hardenDir?: (path: string) => void;
|
|
1912
|
+
writeXml?: (path: string, contents: string) => void;
|
|
1913
|
+
hardenPath?: (path: string) => void;
|
|
1914
|
+
removeStageDir?: (path: string) => void;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
function cleanupWindowsSchedulerStage(
|
|
1918
|
+
stageDir: string,
|
|
1919
|
+
xmlPath: string,
|
|
1920
|
+
removeStageDir: (path: string) => void,
|
|
1921
|
+
): void {
|
|
1922
|
+
let cleanupError: unknown;
|
|
1923
|
+
try {
|
|
1924
|
+
unlinkSync(xmlPath);
|
|
1925
|
+
forgetEphemeralSecretPath(xmlPath);
|
|
1926
|
+
} catch (error) {
|
|
1927
|
+
if ((error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") {
|
|
1928
|
+
forgetEphemeralSecretPath(xmlPath);
|
|
1929
|
+
} else {
|
|
1930
|
+
cleanupError = error;
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
try {
|
|
1934
|
+
removeStageDir(stageDir);
|
|
1935
|
+
forgetEphemeralSecretDir(stageDir);
|
|
1936
|
+
} catch (error) {
|
|
1937
|
+
if ((error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") {
|
|
1938
|
+
forgetEphemeralSecretDir(stageDir);
|
|
1939
|
+
} else if (cleanupError) {
|
|
1940
|
+
throw new AggregateError([cleanupError, error], "Task Scheduler staging cleanup failed.");
|
|
1941
|
+
} else {
|
|
1942
|
+
cleanupError = error;
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
if (cleanupError) throw cleanupError;
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
export function stageWindowsSchedulerRegistrationXml(
|
|
1949
|
+
attemptNonce: string,
|
|
1950
|
+
deps: WindowsSchedulerRegistrationStageDeps = {},
|
|
1951
|
+
): string {
|
|
1952
|
+
const createStageDir = deps.createStageDir
|
|
1953
|
+
?? (() => mkdtempSync(join(tmpdir(), WINDOWS_SCHEDULER_STAGE_PREFIX)));
|
|
1954
|
+
const hardenDir = deps.hardenDir
|
|
1955
|
+
?? ((path: string) => { hardenSecretDir(path, { required: true }); });
|
|
1956
|
+
const writeXml = deps.writeXml ?? ((path: string, contents: string) => {
|
|
1957
|
+
writeFileSync(path, contents, { encoding: "utf16le", flag: "wx", mode: 0o600 });
|
|
1958
|
+
});
|
|
1959
|
+
const hardenPath = deps.hardenPath
|
|
1960
|
+
?? ((path: string) => { hardenSecretPath(path, { required: true }); });
|
|
1961
|
+
const removeStageDir = deps.removeStageDir
|
|
1962
|
+
?? ((path: string) => { rmdirSync(path); });
|
|
1963
|
+
|
|
1964
|
+
let stageDir: string | null = null;
|
|
1965
|
+
let xmlPath: string | null = null;
|
|
1966
|
+
try {
|
|
1967
|
+
stageDir = createStageDir();
|
|
1968
|
+
try { chmodSync(stageDir, 0o700); } catch { /* required Windows ACL is authoritative */ }
|
|
1969
|
+
hardenDir(stageDir);
|
|
1970
|
+
xmlPath = join(stageDir, "task.xml");
|
|
1971
|
+
// This document points at the canonical launcher but does not publish or rewrite it.
|
|
1972
|
+
// The hardened private directory prevents another local account from replacing the
|
|
1973
|
+
// document while UAC is pending; the file harden independently proves its identity.
|
|
1974
|
+
writeXml(
|
|
1975
|
+
xmlPath,
|
|
1976
|
+
`\uFEFF${buildWindowsTaskXml(windowsServiceScriptPath(), windowsLauncherVbsPath(), attemptNonce)}`,
|
|
1977
|
+
);
|
|
1978
|
+
hardenPath(xmlPath);
|
|
1979
|
+
ownedWindowsSchedulerStages.add(xmlPath);
|
|
1980
|
+
return xmlPath;
|
|
1981
|
+
} catch (error) {
|
|
1982
|
+
if (stageDir) {
|
|
1983
|
+
try {
|
|
1984
|
+
cleanupWindowsSchedulerStage(stageDir, xmlPath ?? join(stageDir, "task.xml"), removeStageDir);
|
|
1985
|
+
} catch (cleanupError) {
|
|
1986
|
+
throw new AggregateError(
|
|
1987
|
+
[error, cleanupError],
|
|
1988
|
+
"Task Scheduler staging failed and its private temporary directory could not be removed.",
|
|
1989
|
+
);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
throw error;
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
function removeWindowsSchedulerRegistrationStage(xmlPath: string): void {
|
|
1997
|
+
if (!ownedWindowsSchedulerStages.has(xmlPath)) {
|
|
1998
|
+
throw new Error("Refusing to remove an unrecognized Task Scheduler staging path.");
|
|
1999
|
+
}
|
|
2000
|
+
const stageDir = dirname(xmlPath);
|
|
2001
|
+
cleanupWindowsSchedulerStage(
|
|
2002
|
+
stageDir,
|
|
2003
|
+
xmlPath,
|
|
2004
|
+
path => { rmdirSync(path); },
|
|
1908
2005
|
);
|
|
1909
|
-
|
|
2006
|
+
if (existsSync(stageDir)) {
|
|
2007
|
+
throw new Error("The private Task Scheduler staging directory still exists after cleanup.");
|
|
2008
|
+
}
|
|
2009
|
+
ownedWindowsSchedulerStages.delete(xmlPath);
|
|
1910
2010
|
}
|
|
1911
2011
|
|
|
1912
2012
|
export interface FreshWindowsSchedulerRegistrationDeps {
|
|
1913
2013
|
create?: (args: string[]) => void;
|
|
1914
|
-
elevate?: (
|
|
2014
|
+
elevate?: (taskName: string, xml: string) => Promise<void>;
|
|
1915
2015
|
probe?: () => WindowsSchedulerTaskProbe;
|
|
1916
2016
|
queryXml?: () => string;
|
|
1917
2017
|
rollback?: () => Promise<string | null>;
|
|
@@ -1923,6 +2023,16 @@ export async function registerFreshWindowsSchedulerTask(
|
|
|
1923
2023
|
deps: FreshWindowsSchedulerRegistrationDeps = {},
|
|
1924
2024
|
): Promise<void> {
|
|
1925
2025
|
const args = buildWindowsSchtasksCreateArgsForXml(xmlPath);
|
|
2026
|
+
// Capture and validate the exact definition before an access-denied attempt can
|
|
2027
|
+
// cross the UAC boundary. The elevated fallback receives these immutable bytes,
|
|
2028
|
+
// never the caller-writable staging pathname.
|
|
2029
|
+
const expectedXml = decodeSchtasksOutput(readFileSync(xmlPath));
|
|
2030
|
+
if (
|
|
2031
|
+
!windowsTaskRegistrationHealthy(expectedXml)
|
|
2032
|
+
|| !windowsTaskRegistrationOwnedByAttempt(expectedXml, attemptNonce)
|
|
2033
|
+
) {
|
|
2034
|
+
throw new Error("The staged Task Scheduler registration failed OpenCodex ownership or shape validation.");
|
|
2035
|
+
}
|
|
1926
2036
|
try {
|
|
1927
2037
|
(deps.create ?? schtasks)(args);
|
|
1928
2038
|
} catch (error) {
|
|
@@ -1933,9 +2043,13 @@ export async function registerFreshWindowsSchedulerTask(
|
|
|
1933
2043
|
) {
|
|
1934
2044
|
throw error;
|
|
1935
2045
|
}
|
|
1936
|
-
//
|
|
1937
|
-
//
|
|
1938
|
-
|
|
2046
|
+
// Register from the captured XML string inside the elevated process. Another
|
|
2047
|
+
// same-user process can mutate its own temp files, but cannot change this command.
|
|
2048
|
+
const elevate = deps.elevate ?? (async (taskName: string, xml: string) => {
|
|
2049
|
+
const exitCode = await runWindowsElevatedScheduledTaskRegistration(taskName, xml);
|
|
2050
|
+
if (exitCode !== 0) throw new Error(`Background service install failed with exit code ${exitCode}.`);
|
|
2051
|
+
});
|
|
2052
|
+
await elevate(TASK, expectedXml);
|
|
1939
2053
|
}
|
|
1940
2054
|
|
|
1941
2055
|
const rollbackTask = deps.rollback ?? (() => rollbackWindowsSchedulerTaskOwnedByAttempt(attemptNonce, TASK));
|
|
@@ -1978,21 +2092,47 @@ export async function registerFreshWindowsSchedulerTask(
|
|
|
1978
2092
|
}
|
|
1979
2093
|
}
|
|
1980
2094
|
|
|
1981
|
-
function
|
|
1982
|
-
|
|
2095
|
+
function recordWindowsSchedulerOwnership(): boolean {
|
|
2096
|
+
// Ownership claiming is deliberately conservative: a legacy non-empty config root
|
|
2097
|
+
// without metadata stays unclaimed, but that must not turn a service reinstall into
|
|
2098
|
+
// an outage after prepareServiceInstall has stopped the previous manager.
|
|
2099
|
+
return recordOwnedConfigPath(getConfigDir(), serviceStatePath());
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
export interface RemoveNativeWindowsServiceDeps {
|
|
2103
|
+
status?: () => WinswStatus;
|
|
2104
|
+
uninstall?: () => void;
|
|
2105
|
+
sleep?: (ms: number) => void;
|
|
2106
|
+
settleChecks?: number;
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
export function removeNativeWindowsServiceForScheduler(
|
|
2110
|
+
deps: RemoveNativeWindowsServiceDeps = {},
|
|
2111
|
+
): void {
|
|
2112
|
+
const status = deps.status ?? statusWinswRaw;
|
|
2113
|
+
const uninstall = deps.uninstall ?? uninstallWinswService;
|
|
2114
|
+
const sleep = deps.sleep ?? Bun.sleepSync;
|
|
2115
|
+
const settleChecks = Math.max(1, deps.settleChecks ?? 20);
|
|
1983
2116
|
// Transactional backend switch: installing the scheduler backend removes a native
|
|
1984
2117
|
// service first — two live managers would both respawn the proxy (conflict).
|
|
1985
|
-
if (
|
|
2118
|
+
if (status() !== "nonexistent") {
|
|
1986
2119
|
console.log("🔁 Removing the native (WinSW) service before installing the Task Scheduler backend...");
|
|
1987
2120
|
try {
|
|
1988
|
-
|
|
2121
|
+
uninstall();
|
|
1989
2122
|
} catch (err) {
|
|
1990
2123
|
throw new Error(`Cannot remove the native service before switching to Task Scheduler: ${err instanceof Error ? err.message : String(err)}. Remove it manually with 'sc delete ${WINSW_SERVICE_ID}' or retry.`);
|
|
1991
2124
|
}
|
|
1992
|
-
|
|
1993
|
-
|
|
2125
|
+
for (let check = 0; check < settleChecks; check++) {
|
|
2126
|
+
if (status() === "nonexistent") return;
|
|
2127
|
+
if (check + 1 < settleChecks) sleep(250);
|
|
1994
2128
|
}
|
|
2129
|
+
throw new Error(`Native service registration could not be re-verified after the removal attempt — aborting switch. Check 'sc.exe query ${WINSW_SERVICE_ID}' and remove it manually if present.`);
|
|
1995
2130
|
}
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
function installWindows(): void {
|
|
2134
|
+
recordWindowsSchedulerOwnership();
|
|
2135
|
+
removeNativeWindowsServiceForScheduler();
|
|
1996
2136
|
// End a running task BEFORE rewriting the assets it is executing — cmd.exe reading the
|
|
1997
2137
|
// script mid-rewrite runs a torn batch file, and its open handle can fail the write.
|
|
1998
2138
|
try { stopWindows(); } catch { /* not running */ }
|
|
@@ -2164,6 +2304,50 @@ export function stopWindows(): void {
|
|
|
2164
2304
|
}
|
|
2165
2305
|
function statusWindows(): string { try { return schtasks(["/query", "/tn", TASK]); } catch { return ""; } }
|
|
2166
2306
|
function statusWindowsXml(): string { try { return schtasks(["/query", "/tn", TASK, "/xml"]); } catch { return ""; } }
|
|
2307
|
+
|
|
2308
|
+
/**
|
|
2309
|
+
* Best-effort termination of surviving Windows scheduler launcher/wrapper processes.
|
|
2310
|
+
* `schtasks /end` ends the task instance but often leaves wscript/cmd running the
|
|
2311
|
+
* `:loop` batch, which brings the proxy back during a stop or restart. Same killer
|
|
2312
|
+
* the update job uses, so both teardown paths share the guarantee.
|
|
2313
|
+
*
|
|
2314
|
+
* Matching is scoped to the CANONICAL paths of THIS installation (opencodex-service.cmd
|
|
2315
|
+
* and opencodex-service-launcher.vbs under the current config dir), never a bare
|
|
2316
|
+
* filename: a wrapper from another OpenCodex home — or an unrelated process whose
|
|
2317
|
+
* command line merely contains the filename — must not be force-terminated.
|
|
2318
|
+
* The path must appear as a COMPLETE command-line token (wscript.exe spawns the
|
|
2319
|
+
* .vbs as an argument; cmd.exe /c runs the .cmd), so a substring-only match is
|
|
2320
|
+
* excluded.
|
|
2321
|
+
*/
|
|
2322
|
+
function killWindowsServiceWrapperProcesses(): void {
|
|
2323
|
+
if (process.platform !== "win32") return;
|
|
2324
|
+
try {
|
|
2325
|
+
const script = windowsServiceScriptPath();
|
|
2326
|
+
const launcher = windowsLauncherVbsPath();
|
|
2327
|
+
// Quote for PowerShell: single-quote the value and double any embedded quote.
|
|
2328
|
+
const quote = (value: string) => `'${value.replace(/'/g, "''")}'`;
|
|
2329
|
+
const ps = [
|
|
2330
|
+
`$pats = @(${quote(script)}, ${quote(launcher)});`,
|
|
2331
|
+
"Get-CimInstance Win32_Process | Where-Object {",
|
|
2332
|
+
" if ($_.ProcessId -eq $PID) { return $false };",
|
|
2333
|
+
" $c = $_.CommandLine; if (-not $c) { return $false };",
|
|
2334
|
+
" foreach ($p in $pats) {",
|
|
2335
|
+
" $i = $c.IndexOf($p, [System.StringComparison]::OrdinalIgnoreCase);",
|
|
2336
|
+
" if ($i -lt 0) { continue };",
|
|
2337
|
+
" $before = if ($i -gt 0) { $c.Substring($i - 1, 1) } else { ' ' };",
|
|
2338
|
+
" $end = $i + $p.Length;",
|
|
2339
|
+
" $after = if ($end -lt $c.Length) { $c.Substring($end, 1) } else { ' ' };",
|
|
2340
|
+
" if ($before -match '[\\s\"'']' -and $after -match '[\\s\"'']') { return $true };",
|
|
2341
|
+
" };",
|
|
2342
|
+
" $false",
|
|
2343
|
+
"} | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }",
|
|
2344
|
+
].join(" ");
|
|
2345
|
+
spawnSync(resolveTrustedWindowsPowerShellExe(), [
|
|
2346
|
+
"-NoProfile", "-NoLogo", "-NonInteractive", "-WindowStyle", "Hidden",
|
|
2347
|
+
"-Command", ps,
|
|
2348
|
+
], { stdio: "ignore", timeout: 5000, windowsHide: true });
|
|
2349
|
+
} catch { /* best-effort */ }
|
|
2350
|
+
}
|
|
2167
2351
|
function uninstallWindows(): void {
|
|
2168
2352
|
const probe = probeWindowsSchedulerTask(TASK);
|
|
2169
2353
|
if (probe.status === "present") {
|
|
@@ -2595,7 +2779,9 @@ export async function installServiceSafely(
|
|
|
2595
2779
|
export interface FreshWindowsSchedulerInstallDeps {
|
|
2596
2780
|
stageRegistrationXml?: (attemptNonce: string) => string;
|
|
2597
2781
|
register?: (xmlPath: string, attemptNonce: string) => Promise<void>;
|
|
2782
|
+
recordOwnership?: () => boolean;
|
|
2598
2783
|
prepare?: () => Promise<void>;
|
|
2784
|
+
removeNativeService?: () => void;
|
|
2599
2785
|
publishAssets?: () => void;
|
|
2600
2786
|
runTask?: () => void;
|
|
2601
2787
|
writeState?: () => void;
|
|
@@ -2616,7 +2802,9 @@ export async function installFreshWindowsSchedulerSafely(
|
|
|
2616
2802
|
): Promise<void> {
|
|
2617
2803
|
const stage = deps.stageRegistrationXml ?? stageWindowsSchedulerRegistrationXml;
|
|
2618
2804
|
const register = deps.register ?? registerFreshWindowsSchedulerTask;
|
|
2805
|
+
const recordOwnership = deps.recordOwnership ?? recordWindowsSchedulerOwnership;
|
|
2619
2806
|
const prepare = deps.prepare ?? (() => prepareServiceInstall("scheduler"));
|
|
2807
|
+
const removeNativeService = deps.removeNativeService ?? removeNativeWindowsServiceForScheduler;
|
|
2620
2808
|
const publishAssets = deps.publishAssets ?? writeWindowsSchedulerAssets;
|
|
2621
2809
|
const runTask = deps.runTask ?? startWindows;
|
|
2622
2810
|
const writeState = deps.writeState ?? (() => writeServiceInstallState("scheduler"));
|
|
@@ -2624,11 +2812,12 @@ export async function installFreshWindowsSchedulerSafely(
|
|
|
2624
2812
|
rollbackWindowsSchedulerTaskOwnedByAttempt(attemptNonce, TASK)
|
|
2625
2813
|
));
|
|
2626
2814
|
const removeStagedXml = deps.removeStagedXml ?? ((path: string) => {
|
|
2627
|
-
|
|
2815
|
+
removeWindowsSchedulerRegistrationStage(path);
|
|
2628
2816
|
});
|
|
2629
2817
|
|
|
2630
2818
|
let stagedXml: string | null = null;
|
|
2631
2819
|
const attemptNonce = randomUUID();
|
|
2820
|
+
const configRootWasAbsent = !existsSync(getConfigDir());
|
|
2632
2821
|
let registered = false;
|
|
2633
2822
|
let started = false;
|
|
2634
2823
|
try {
|
|
@@ -2637,7 +2826,19 @@ export async function installFreshWindowsSchedulerSafely(
|
|
|
2637
2826
|
registered = true;
|
|
2638
2827
|
|
|
2639
2828
|
// The destructive boundary begins only after Task Scheduler accepted the definition.
|
|
2829
|
+
// The registration has consumed its temporary XML. Remove it before claiming a newly
|
|
2830
|
+
// created config root, because ownership initialization intentionally requires emptiness.
|
|
2831
|
+
removeStagedXml(stagedXml);
|
|
2832
|
+
stagedXml = null;
|
|
2833
|
+
const ownershipRecorded = recordOwnership();
|
|
2834
|
+
if (!ownershipRecorded && configRootWasAbsent) {
|
|
2835
|
+
throw new Error(
|
|
2836
|
+
"The fresh OpenCodex config root could not be claimed for safe uninstall; "
|
|
2837
|
+
+ "aborting before service-manager cleanup or asset publication.",
|
|
2838
|
+
);
|
|
2839
|
+
}
|
|
2640
2840
|
await prepare();
|
|
2841
|
+
removeNativeService();
|
|
2641
2842
|
publishAssets();
|
|
2642
2843
|
runTask();
|
|
2643
2844
|
started = true;
|
|
@@ -2663,8 +2864,11 @@ export async function installFreshWindowsSchedulerSafely(
|
|
|
2663
2864
|
} finally {
|
|
2664
2865
|
if (stagedXml) {
|
|
2665
2866
|
try { removeStagedXml(stagedXml); } catch (error) {
|
|
2867
|
+
const code = error && typeof error === "object" && "code" in error
|
|
2868
|
+
? String((error as NodeJS.ErrnoException).code)
|
|
2869
|
+
: "";
|
|
2666
2870
|
console.error(
|
|
2667
|
-
`⚠️ Failed to remove
|
|
2871
|
+
`⚠️ Failed to remove the private Task Scheduler staging directory${code ? ` (${code})` : ""}.`,
|
|
2668
2872
|
);
|
|
2669
2873
|
}
|
|
2670
2874
|
}
|
|
@@ -2692,6 +2896,10 @@ export function stopServiceIfInstalled(): boolean {
|
|
|
2692
2896
|
if (statusWinswRaw() !== "nonexistent") {
|
|
2693
2897
|
try { stopWinswService(); stopped = true; } catch { /* best-effort */ }
|
|
2694
2898
|
}
|
|
2899
|
+
// `schtasks /end` ends the task instance but the cmd `:loop` wrapper survives and
|
|
2900
|
+
// respawns its child seconds later (issue #764), resurrecting the proxy during a
|
|
2901
|
+
// stop or a tray restart. Kill the launcher/wrapper processes outright.
|
|
2902
|
+
killWindowsServiceWrapperProcesses();
|
|
2695
2903
|
if (stopped) return true;
|
|
2696
2904
|
} else if (process.platform === "linux" && isSystemd() && existsSync(unitPath())) {
|
|
2697
2905
|
try { stopSystemd(); return true; } catch { return false; }
|
package/src/tray/windows.ts
CHANGED
|
@@ -138,7 +138,6 @@ export function windowsTrayProcessArgs(entry: WindowsTrayEntry, mode: "Run" | "S
|
|
|
138
138
|
"-NonInteractive",
|
|
139
139
|
"-STA",
|
|
140
140
|
"-ExecutionPolicy", "Bypass",
|
|
141
|
-
"-WindowStyle", "Hidden",
|
|
142
141
|
"-File", safePath(entry.script),
|
|
143
142
|
"-BunPath", safePath(entry.bun),
|
|
144
143
|
"-BunRuntimeSource", entry.bunRuntimeSource,
|
|
@@ -173,7 +172,6 @@ export function buildWindowsTrayPowerShellCommand(entry: WindowsTrayEntry, power
|
|
|
173
172
|
"-NonInteractive",
|
|
174
173
|
"-STA",
|
|
175
174
|
"-ExecutionPolicy", "Bypass",
|
|
176
|
-
"-WindowStyle", "Hidden",
|
|
177
175
|
"-File", quoteRunValue(entry.script),
|
|
178
176
|
"-BunPath", quoteRunValue(entry.bun),
|
|
179
177
|
"-BunRuntimeSource", entry.bunRuntimeSource,
|
package/src/update/job.ts
CHANGED
|
@@ -578,7 +578,7 @@ export function spawnGuiUpdateWorker(
|
|
|
578
578
|
].join("; ");
|
|
579
579
|
const launched = spawnSync(
|
|
580
580
|
resolveTrustedWindowsPowerShellExe(),
|
|
581
|
-
["-NoProfile", "-NoLogo", "-NonInteractive", "-
|
|
581
|
+
["-NoProfile", "-NoLogo", "-NonInteractive", "-Command", ps],
|
|
582
582
|
{ encoding: "utf8", windowsHide: true, timeout: 15_000 },
|
|
583
583
|
);
|
|
584
584
|
const pid = Number(String(launched.stdout ?? "").trim().split(/\r?\n/).pop());
|
|
@@ -1386,7 +1386,7 @@ function killWindowsServiceWrapperProcesses(): void {
|
|
|
1386
1386
|
"} | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }",
|
|
1387
1387
|
].join(" ");
|
|
1388
1388
|
spawnSync(resolveTrustedWindowsPowerShellExe(), [
|
|
1389
|
-
"-NoProfile", "-NoLogo", "-NonInteractive",
|
|
1389
|
+
"-NoProfile", "-NoLogo", "-NonInteractive",
|
|
1390
1390
|
"-Command", ps,
|
|
1391
1391
|
], { stdio: "ignore", timeout: 5000, windowsHide: true });
|
|
1392
1392
|
} catch {
|
package/src/usage/summary.ts
CHANGED
|
@@ -136,9 +136,23 @@ export function parseUsageSurface(input: string | null | undefined): UsageSurfac
|
|
|
136
136
|
return "all";
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
function
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
function startOfLocalDay(ts: number): number {
|
|
140
|
+
const d = new Date(ts);
|
|
141
|
+
d.setHours(0, 0, 0, 0);
|
|
142
|
+
return d.getTime();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function rangeWindow(range: UsageRange, now: number): { since: number | null; days: number } {
|
|
146
|
+
if (range === "7d") {
|
|
147
|
+
const start = new Date(startOfLocalDay(now));
|
|
148
|
+
start.setDate(start.getDate() - 6);
|
|
149
|
+
return { since: start.getTime(), days: 7 };
|
|
150
|
+
}
|
|
151
|
+
if (range === "30d") {
|
|
152
|
+
const start = new Date(startOfLocalDay(now));
|
|
153
|
+
start.setDate(start.getDate() - 29);
|
|
154
|
+
return { since: start.getTime(), days: 30 };
|
|
155
|
+
}
|
|
142
156
|
return { since: null, days: 0 };
|
|
143
157
|
}
|
|
144
158
|
|
|
@@ -347,8 +361,11 @@ function buildDayGrid(range: UsageRange, since: number | null, now: number, entr
|
|
|
347
361
|
m.attemptCount += 1;
|
|
348
362
|
m.totalTokens += usageDisplayTotalTokens(attribution.usage, attribution.totalTokens) ?? 0;
|
|
349
363
|
};
|
|
364
|
+
const startOfToday = startOfLocalDay(now);
|
|
350
365
|
for (let i = days - 1; i >= 0; i--) {
|
|
351
|
-
const
|
|
366
|
+
const d = new Date(startOfToday);
|
|
367
|
+
d.setDate(d.getDate() - i);
|
|
368
|
+
const key = localDateKey(d.getTime());
|
|
352
369
|
grid.set(key, { date: key, requests: 0, measuredRequests: 0, reportedRequests: 0, totalTokens: 0, models: [] });
|
|
353
370
|
}
|
|
354
371
|
for (const entry of entries) {
|