@bitkyc08/opencodex 2.7.43-preview.20260728 → 2.8.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 +8 -1
- package/bin/ocx.mjs +47 -22
- package/gui/dist/assets/index-BDjpkcRN.js +67 -0
- package/gui/dist/assets/index-BHsKRFh9.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic.ts +15 -6
- package/src/adapters/cursor/discovery.ts +4 -1
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/cursor/native-exec-shell.ts +18 -6
- package/src/adapters/cursor/protobuf-events.ts +24 -2
- package/src/adapters/cursor/protobuf-request.ts +1 -2
- package/src/adapters/cursor/tool-definitions.ts +68 -29
- package/src/adapters/google-wire-compiler.ts +4 -0
- package/src/adapters/google.ts +128 -2
- package/src/adapters/identity.ts +12 -2
- package/src/adapters/kiro.ts +64 -7
- package/src/adapters/mimo-free.ts +2 -0
- package/src/adapters/openai-responses.ts +246 -59
- package/src/claude/agents-inject.ts +5 -0
- package/src/claude/alias.ts +94 -14
- package/src/claude/inbound.ts +26 -9
- package/src/claude/outbound.ts +6 -3
- package/src/cli/account-auth.ts +1 -1
- package/src/cli/agent-driven.ts +37 -0
- package/src/cli/catalog-prewarm.ts +24 -0
- package/src/cli/claude.ts +35 -10
- package/src/cli/doctor.ts +71 -19
- package/src/cli/help.ts +42 -6
- package/src/cli/index.ts +93 -19
- package/src/cli/interactive-confirm.ts +133 -0
- package/src/cli/opencode.ts +701 -0
- package/src/cli/provider-runtime.ts +3 -0
- package/src/cli/provider.ts +31 -10
- package/src/cli/star-prompt.ts +79 -18
- package/src/cli/status.ts +47 -13
- package/src/cli/v2.ts +10 -1
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-lifecycle.ts +4 -1
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +149 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-store.ts +2 -0
- package/src/codex/account-usability.ts +6 -1
- package/src/codex/app-server-processes.ts +511 -0
- package/src/codex/auth-api.ts +293 -34
- package/src/codex/auth-collision.ts +2 -1
- package/src/codex/auth-context.ts +60 -17
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/parsing.ts +42 -2
- package/src/codex/catalog/provider-fetch.ts +264 -70
- package/src/codex/catalog/sync.ts +45 -8
- package/src/codex/catalog.ts +2 -2
- package/src/codex/features.ts +524 -5
- package/src/codex/history-provider.ts +145 -1
- package/src/codex/inject.ts +114 -14
- package/src/codex/main-account.ts +2 -8
- package/src/codex/pool-rotation.ts +186 -0
- package/src/codex/quota.ts +92 -2
- package/src/codex/routing.ts +695 -106
- package/src/codex/runtime.ts +10 -1
- package/src/codex/shim.ts +4 -1
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +2 -0
- package/src/codex/sync.ts +3 -0
- package/src/config.ts +574 -25
- package/src/generated/jawcode-model-metadata.ts +12 -12
- package/src/github/star-state.ts +191 -0
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +111 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +789 -0
- package/src/images/plan.ts +133 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/bun-binary-validator.d.mts +3 -0
- package/src/lib/bun-binary-validator.mjs +18 -0
- package/src/lib/bun-runtime.ts +6 -20
- package/src/lib/config-ownership.ts +327 -0
- package/src/lib/crash-guard.ts +2 -0
- package/src/lib/destination-policy.ts +132 -7
- package/src/lib/pinned-http.ts +151 -0
- package/src/lib/process-control.ts +2 -2
- package/src/lib/provider-outbound.ts +167 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/shadow-call.ts +30 -0
- package/src/lib/test-home-guard.ts +90 -0
- package/src/lib/win-exec.ts +12 -2
- package/src/lib/windows-elevation.ts +81 -3
- package/src/lib/windows-secret-acl.ts +189 -12
- package/src/lib/winsw.ts +2 -0
- package/src/oauth/anthropic-routing.ts +570 -0
- package/src/oauth/health.ts +6 -0
- package/src/oauth/index.ts +310 -75
- package/src/oauth/key-providers.ts +38 -8
- package/src/oauth/kimi.ts +2 -0
- package/src/oauth/kiro-credentials.ts +373 -12
- package/src/oauth/kiro.ts +424 -43
- package/src/oauth/login-cli.ts +33 -6
- package/src/oauth/store.ts +56 -4
- package/src/oauth/types.ts +11 -0
- package/src/providers/alibaba-region-migration.ts +16 -3
- package/src/providers/antigravity-models.ts +3 -0
- package/src/providers/api-keys.ts +13 -6
- package/src/providers/derive.ts +8 -2
- package/src/providers/key-failover.ts +24 -4
- package/src/providers/model-discovery.ts +356 -0
- package/src/providers/quota.ts +233 -29
- package/src/providers/registry.ts +125 -3
- package/src/responses/parser.ts +11 -0
- package/src/responses/state.ts +22 -8
- package/src/responses/tool-groups.ts +19 -0
- package/src/router.ts +19 -7
- package/src/server/auth-cors.ts +114 -24
- package/src/server/claude-messages.ts +8 -1
- package/src/server/gui-static.ts +30 -6
- package/src/server/images.ts +303 -9
- package/src/server/index.ts +77 -9
- package/src/server/lifecycle.ts +25 -1
- package/src/server/live.ts +75 -25
- package/src/server/management/agent-settings-routes.ts +106 -8
- package/src/server/management/combo-routes.ts +7 -0
- package/src/server/management/config-routes.ts +22 -7
- package/src/server/management/context.ts +11 -1
- package/src/server/management/logs-usage-routes.ts +167 -3
- package/src/server/management/model-routes.ts +46 -13
- package/src/server/management/oauth-account-routes.ts +163 -17
- package/src/server/management/provider-routes.ts +73 -10
- package/src/server/management/shared.ts +2 -2
- package/src/server/management/sidebar-routes.ts +39 -0
- package/src/server/management/system-restart.ts +172 -0
- package/src/server/management/system-routes.ts +33 -10
- package/src/server/management-api.ts +5 -3
- package/src/server/management-auth.ts +216 -0
- package/src/server/proxy-liveness.ts +14 -3
- package/src/server/responses/compact.ts +21 -13
- package/src/server/responses/core.ts +614 -172
- package/src/server/responses/upstream-error.ts +48 -0
- package/src/server/responses-image-gen-repair.ts +118 -0
- package/src/server/responses-item-id-repair.ts +10 -85
- package/src/server/sse-payload-rewrite.ts +116 -0
- package/src/server/startup-action-control.ts +30 -14
- package/src/server/system-env.ts +28 -10
- package/src/service.ts +284 -19
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +1504 -28
- package/src/storage/policy-job.ts +387 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +53 -0
- package/src/storage/policy.ts +522 -0
- package/src/storage/restore-job.ts +253 -0
- package/src/storage/restore-worker.ts +52 -0
- package/src/storage/storage-mutation-coordinator.ts +109 -0
- package/src/storage/worker-lifecycle.ts +81 -0
- package/src/tray/windows.ts +34 -4
- package/src/types.ts +107 -1
- package/src/update/badge.ts +72 -0
- package/src/update/index.ts +36 -18
- package/src/update/job.ts +111 -16
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/usage/debug.ts +2 -0
- package/src/usage/expected-prices.ts +6 -5
- package/src/usage/log.ts +12 -0
- package/src/web-search/loop.ts +57 -16
- package/gui/dist/assets/index-CjKFJHSC.js +0 -65
- package/gui/dist/assets/index-DfVGuN88.css +0 -1
package/src/service.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { isWslRuntime } from "./codex/home";
|
|
|
17
17
|
import { durableBunPath, durableBunRuntime } from "./lib/bun-runtime";
|
|
18
18
|
import { isProcessAlive, stopProxy } from "./lib/process-control";
|
|
19
19
|
import { serviceApiTokenFilePath } from "./lib/service-secrets";
|
|
20
|
+
import { findLiveProxy } from "./server/proxy-liveness";
|
|
20
21
|
import { randomUUID } from "node:crypto";
|
|
21
22
|
import {
|
|
22
23
|
ELEVATION_REQUEST_TIMEOUT_MS,
|
|
@@ -34,6 +35,8 @@ import {
|
|
|
34
35
|
import { defaultWinswEntry, installWinswService, startWinswService, stopWinswService, statusWinswRaw, uninstallWinswService, winswStatusSummary, WINSW_SERVICE_ID, WINSW_SHA256, WINSW_VERSION } from "./lib/winsw";
|
|
35
36
|
import { hardenSecretDir, hardenSecretPath } from "./lib/windows-secret-acl";
|
|
36
37
|
import { windowsEnvIndirectBatchPathList, windowsEnvIndirectBatchValue } from "./lib/win-paths";
|
|
38
|
+
import { recordOwnedConfigPath } from "./lib/config-ownership";
|
|
39
|
+
import { maybeShowStarPrompt } from "./cli/star-prompt";
|
|
37
40
|
|
|
38
41
|
const LABEL = "com.opencodex.proxy";
|
|
39
42
|
const TASK = "opencodex-proxy";
|
|
@@ -146,6 +149,7 @@ function writeServiceInstallState(backend: ServiceBackend = "scheduler"): void {
|
|
|
146
149
|
};
|
|
147
150
|
for (const path of serviceStatePaths()) {
|
|
148
151
|
const dir = dirname(path);
|
|
152
|
+
recordOwnedConfigPath(getConfigDir(), path);
|
|
149
153
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
150
154
|
writeFileSync(path, JSON.stringify(state, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
|
|
151
155
|
try { chmodSync(path, 0o600); } catch { /* best-effort */ }
|
|
@@ -254,6 +258,7 @@ function writeServiceApiTokenFile(): string | null {
|
|
|
254
258
|
if (!token) return null;
|
|
255
259
|
const path = serviceApiTokenFilePath();
|
|
256
260
|
const dir = getConfigDir();
|
|
261
|
+
recordOwnedConfigPath(dir, path);
|
|
257
262
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
258
263
|
if (process.platform === "win32") hardenSecretDir(dir, { required: true });
|
|
259
264
|
writeFileSync(path, `${token}\n`, { encoding: "utf8", mode: 0o600 });
|
|
@@ -351,8 +356,41 @@ function sh(cmd: string): string {
|
|
|
351
356
|
return execSync(cmd, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
352
357
|
}
|
|
353
358
|
|
|
359
|
+
/**
|
|
360
|
+
* Decode schtasks stdout. `/query /xml` emits UTF-16LE (often with BOM) because the
|
|
361
|
+
* registered task document is UTF-16; reading that as UTF-8 makes every health check
|
|
362
|
+
* fail ("registration present but unhealthy") and rolls back a successful elevated create.
|
|
363
|
+
*/
|
|
364
|
+
export function decodeSchtasksOutput(buffer: Buffer): string {
|
|
365
|
+
if (buffer.length === 0) return "";
|
|
366
|
+
const bomUtf16Le = buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xfe;
|
|
367
|
+
const bomUtf16Be = buffer.length >= 2 && buffer[0] === 0xfe && buffer[1] === 0xff;
|
|
368
|
+
const looksUtf16Le = buffer.length >= 4
|
|
369
|
+
&& buffer[1] === 0x00
|
|
370
|
+
&& buffer[3] === 0x00
|
|
371
|
+
&& buffer[0] !== 0x00;
|
|
372
|
+
if (bomUtf16Le || looksUtf16Le) {
|
|
373
|
+
return buffer.toString("utf16le").replace(/^\uFEFF/, "").trim();
|
|
374
|
+
}
|
|
375
|
+
if (bomUtf16Be) {
|
|
376
|
+
// Swap pairs then decode as utf16le.
|
|
377
|
+
const swapped = Buffer.alloc(buffer.length - 2);
|
|
378
|
+
for (let i = 2; i + 1 < buffer.length; i += 2) {
|
|
379
|
+
swapped[i - 2] = buffer[i + 1]!;
|
|
380
|
+
swapped[i - 1] = buffer[i]!;
|
|
381
|
+
}
|
|
382
|
+
return swapped.toString("utf16le").trim();
|
|
383
|
+
}
|
|
384
|
+
return buffer.toString("utf8").replace(/^\uFEFF/, "").trim();
|
|
385
|
+
}
|
|
386
|
+
|
|
354
387
|
function runFile(file: string, args: string[]): string {
|
|
355
|
-
|
|
388
|
+
const buffer = execFileSync(file, args, {
|
|
389
|
+
encoding: "buffer",
|
|
390
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
391
|
+
windowsHide: true,
|
|
392
|
+
}) as Buffer;
|
|
393
|
+
return decodeSchtasksOutput(buffer);
|
|
356
394
|
}
|
|
357
395
|
|
|
358
396
|
function windowsSchtasks(): string {
|
|
@@ -390,6 +428,70 @@ export type WindowsSchedulerTaskProbe =
|
|
|
390
428
|
| { status: "absent" }
|
|
391
429
|
| { status: "unknown"; detail: string };
|
|
392
430
|
|
|
431
|
+
export type WindowsSchedulerProxyProbe =
|
|
432
|
+
| { status: "running"; port: number }
|
|
433
|
+
| { status: "not-running" }
|
|
434
|
+
| { status: "unknown" };
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Render Task Scheduler status without exposing localized `schtasks` table output.
|
|
438
|
+
* The task probe answers installation state; the identity-checked health probe answers
|
|
439
|
+
* runtime state. Keep probe details out of this user-facing line because they can contain
|
|
440
|
+
* incorrectly decoded, locale-specific command output.
|
|
441
|
+
*/
|
|
442
|
+
export function formatWindowsSchedulerServiceStatus(
|
|
443
|
+
task: WindowsSchedulerTaskProbe,
|
|
444
|
+
proxy: WindowsSchedulerProxyProbe,
|
|
445
|
+
): string {
|
|
446
|
+
if (task.status === "present") {
|
|
447
|
+
if (proxy.status === "running") {
|
|
448
|
+
return `✅ service installed (Task Scheduler); OpenCodex proxy running on port ${proxy.port}.`;
|
|
449
|
+
}
|
|
450
|
+
if (proxy.status === "not-running") {
|
|
451
|
+
return "⚠️ service installed (Task Scheduler); OpenCodex proxy not running.";
|
|
452
|
+
}
|
|
453
|
+
return "⚠️ service installed (Task Scheduler); OpenCodex proxy status unknown.";
|
|
454
|
+
}
|
|
455
|
+
if (task.status === "absent") {
|
|
456
|
+
if (proxy.status === "running") {
|
|
457
|
+
return `❌ service not installed (Task Scheduler); OpenCodex proxy is running independently on port ${proxy.port}.`;
|
|
458
|
+
}
|
|
459
|
+
if (proxy.status === "unknown") {
|
|
460
|
+
return "❌ service not installed (Task Scheduler); OpenCodex proxy status unknown.";
|
|
461
|
+
}
|
|
462
|
+
return "❌ service not installed (Task Scheduler).";
|
|
463
|
+
}
|
|
464
|
+
if (proxy.status === "running") {
|
|
465
|
+
return `⚠️ Task Scheduler registration unknown; OpenCodex proxy running on port ${proxy.port}.`;
|
|
466
|
+
}
|
|
467
|
+
if (proxy.status === "not-running") {
|
|
468
|
+
return "⚠️ service status unknown (Task Scheduler query failed); OpenCodex proxy not running.";
|
|
469
|
+
}
|
|
470
|
+
return "⚠️ service status unknown (Task Scheduler and proxy checks failed).";
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export async function inspectWindowsSchedulerServiceStatus(io: {
|
|
474
|
+
probeTask?: () => WindowsSchedulerTaskProbe;
|
|
475
|
+
findProxy?: () => Promise<{ port: number } | null>;
|
|
476
|
+
} = {}): Promise<string> {
|
|
477
|
+
let task: WindowsSchedulerTaskProbe;
|
|
478
|
+
try {
|
|
479
|
+
task = (io.probeTask ?? probeWindowsSchedulerTask)();
|
|
480
|
+
} catch (error) {
|
|
481
|
+
task = { status: "unknown", detail: schtasksErrorDetail(error) };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
let proxy: WindowsSchedulerProxyProbe;
|
|
485
|
+
try {
|
|
486
|
+
const live = await (io.findProxy ?? findLiveProxy)();
|
|
487
|
+
proxy = live ? { status: "running", port: live.port } : { status: "not-running" };
|
|
488
|
+
} catch {
|
|
489
|
+
proxy = { status: "unknown" };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return formatWindowsSchedulerServiceStatus(task, proxy);
|
|
493
|
+
}
|
|
494
|
+
|
|
393
495
|
function schtasksErrorDetail(error: unknown): string {
|
|
394
496
|
return error instanceof Error ? error.message : String(error);
|
|
395
497
|
}
|
|
@@ -483,7 +585,9 @@ export function evaluateWindowsSchedulerInstallVerification(inputs: {
|
|
|
483
585
|
: !assetsHealthy
|
|
484
586
|
? "Required scheduler service assets are missing."
|
|
485
587
|
: !registrationHealthy
|
|
486
|
-
?
|
|
588
|
+
? (inputs.xml.trim()
|
|
589
|
+
? "Task Scheduler registration is present but unhealthy."
|
|
590
|
+
: "Task Scheduler task is present but its XML could not be read.")
|
|
487
591
|
: nativeStatusUnknown
|
|
488
592
|
? "The Task Scheduler task was created, but OpenCodex could not verify that the native WinSW service is absent."
|
|
489
593
|
: "ok";
|
|
@@ -502,9 +606,18 @@ export function evaluateWindowsSchedulerInstallVerification(inputs: {
|
|
|
502
606
|
/** Conflict-free postcondition check for an elevated scheduler install. */
|
|
503
607
|
export function verifyWindowsSchedulerInstall(taskName = TASK): WindowsSchedulerInstallVerification {
|
|
504
608
|
const taskInstalled = windowsSchedulerTaskInstalled(taskName);
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
609
|
+
let xml = "";
|
|
610
|
+
if (taskInstalled) {
|
|
611
|
+
try { xml = querySchtasks(["/query", "/tn", taskName, "/xml"]); } catch { xml = ""; }
|
|
612
|
+
}
|
|
613
|
+
// After elevated create, non-elevated `/query /xml` can fail or return empty while the
|
|
614
|
+
// task is still listed. Fall back to the on-disk document we registered.
|
|
615
|
+
if (taskInstalled && !xml.trim()) {
|
|
616
|
+
const diskPath = windowsTaskXmlPath();
|
|
617
|
+
if (existsSync(diskPath)) {
|
|
618
|
+
try { xml = decodeSchtasksOutput(readFileSync(diskPath)); } catch { /* keep empty */ }
|
|
619
|
+
}
|
|
620
|
+
}
|
|
508
621
|
return evaluateWindowsSchedulerInstallVerification({
|
|
509
622
|
taskInstalled,
|
|
510
623
|
xml,
|
|
@@ -892,6 +1005,22 @@ function taskXmlString(value: string): string {
|
|
|
892
1005
|
.replace(/'/g, "'");
|
|
893
1006
|
}
|
|
894
1007
|
|
|
1008
|
+
/**
|
|
1009
|
+
* RunLevel check. Schema default is LeastPrivilege (omitted on export). Elevated
|
|
1010
|
+
* `schtasks /create` often rewrites the registered task to HighestAvailable even when
|
|
1011
|
+
* the source XML asked for LeastPrivilege — still InteractiveToken / same user.
|
|
1012
|
+
* Keep accepting HighestAvailable here: rejecting it would false-fail healthy elevated
|
|
1013
|
+
* installs, and windowsTaskRegistrationHealthy tests encode that contract.
|
|
1014
|
+
*/
|
|
1015
|
+
function taskXmlRunLevelAcceptable(principal: string): boolean {
|
|
1016
|
+
if (taskXmlHasPrefixedTag(principal, "RunLevel")) return false;
|
|
1017
|
+
const count = taskXmlElementCount(principal, "RunLevel");
|
|
1018
|
+
if (count === 0) return true;
|
|
1019
|
+
if (count > 1) return false;
|
|
1020
|
+
const value = new RegExp(`<RunLevel(?:\\s[^>]*?)?>\\s*([^<]*?)\\s*<\\/RunLevel>`, "i").exec(principal)?.[1]?.trim().toLowerCase();
|
|
1021
|
+
return value === "leastprivilege" || value === "highestavailable";
|
|
1022
|
+
}
|
|
1023
|
+
|
|
895
1024
|
export function buildWindowsServiceScript(entry = cliEntry(), port = resolveServiceListenPort()): string {
|
|
896
1025
|
const { bun, cli } = entry;
|
|
897
1026
|
const bunRuntime = durableBunRuntime();
|
|
@@ -1039,6 +1168,43 @@ function taskXmlHasPrefixedTag(xml: string, tag: string): boolean {
|
|
|
1039
1168
|
* Absence means the documented schema default (#432); a present element must still
|
|
1040
1169
|
* match exactly, so a malformed or explicitly unsafe value never reads as healthy.
|
|
1041
1170
|
*/
|
|
1171
|
+
/**
|
|
1172
|
+
* Decode XML's five predefined entities, exactly once.
|
|
1173
|
+
*
|
|
1174
|
+
* Task Scheduler re-encodes element text when it exports a task, so a needle we
|
|
1175
|
+
* escaped ourselves can never match its output (#608). Compare decoded values
|
|
1176
|
+
* instead of encoded ones.
|
|
1177
|
+
*
|
|
1178
|
+
* The single pass is the point: decoding twice would turn `&quot;` into `"`,
|
|
1179
|
+
* letting a doubly-encoded value impersonate the expected launcher path.
|
|
1180
|
+
*/
|
|
1181
|
+
function taskXmlDecodeEntities(value: string): string {
|
|
1182
|
+
return value.replace(/&(amp|lt|gt|quot|apos);/g, (_, name: string) => (
|
|
1183
|
+
name === "amp" ? "&"
|
|
1184
|
+
: name === "lt" ? "<"
|
|
1185
|
+
: name === "gt" ? ">"
|
|
1186
|
+
: name === "quot" ? "\""
|
|
1187
|
+
: "'"
|
|
1188
|
+
));
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/**
|
|
1192
|
+
* Exactly one unprefixed `<tag>` whose DECODED text equals `expected`.
|
|
1193
|
+
*
|
|
1194
|
+
* Unlike taskXmlOptionalValueEquals(), an absent element is NOT a pass: these
|
|
1195
|
+
* elements name what actually gets executed, so a missing <Command>/<Arguments>
|
|
1196
|
+
* must fail the health check rather than inherit a schema default.
|
|
1197
|
+
*/
|
|
1198
|
+
function taskXmlDecodedValueEquals(xml: string, tag: string, expected: string): boolean {
|
|
1199
|
+
// Same reasoning as the optional helper: `<t:Arguments>` must not read as absent.
|
|
1200
|
+
if (taskXmlHasPrefixedTag(xml, tag)) return false;
|
|
1201
|
+
if (taskXmlElementCount(xml, tag) !== 1) return false;
|
|
1202
|
+
// `[^<]*` refuses nested markup, so a decoy inside a child element cannot match.
|
|
1203
|
+
const value = new RegExp(`<${tag}(?:\\s[^>]*?)?>([^<]*)<\\/${tag}>`, "i").exec(xml)?.[1];
|
|
1204
|
+
if (value === undefined) return false;
|
|
1205
|
+
return taskXmlDecodeEntities(value).trim().toLowerCase() === expected.trim().toLowerCase();
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1042
1208
|
function taskXmlOptionalValueEquals(xml: string, tag: string, expected: string): boolean {
|
|
1043
1209
|
// Check the prefixed form first: treating `<t:Enabled>false</t:Enabled>` as an
|
|
1044
1210
|
// omission would turn an explicitly disabled task into a healthy one.
|
|
@@ -1072,12 +1238,16 @@ export function windowsTaskRegistrationHealthy(
|
|
|
1072
1238
|
return taskXmlElementCount(triggers, "LogonTrigger") > 0
|
|
1073
1239
|
&& taskXmlOptionalValueEquals(trigger, "Enabled", "true")
|
|
1074
1240
|
&& /<LogonType>\s*InteractiveToken\s*<\/LogonType>/i.test(principal)
|
|
1075
|
-
&&
|
|
1241
|
+
&& taskXmlRunLevelAcceptable(principal)
|
|
1076
1242
|
&& taskXmlOptionalValueEquals(settings, "Enabled", "true")
|
|
1077
1243
|
&& /<MultipleInstancesPolicy>\s*IgnoreNew\s*<\/MultipleInstancesPolicy>/i.test(settings)
|
|
1078
1244
|
&& /<ExecutionTimeLimit>\s*PT0S\s*<\/ExecutionTimeLimit>/i.test(settings)
|
|
1079
|
-
|
|
1080
|
-
|
|
1245
|
+
// Compare decoded VALUES, not encodings: Task Scheduler canonicalizes the
|
|
1246
|
+
// quotes we wrote as `"` back to literal `"` on export, so an escaped
|
|
1247
|
+
// needle never matched and a healthy task read as permanently stale (#608).
|
|
1248
|
+
// Case-insensitive: elevated `schtasks /create` may rewrite System32 casing.
|
|
1249
|
+
&& taskXmlDecodedValueEquals(action, "Command", wscript)
|
|
1250
|
+
&& taskXmlDecodedValueEquals(action, "Arguments", `/b /nologo "${launcher}"`);
|
|
1081
1251
|
}
|
|
1082
1252
|
|
|
1083
1253
|
export interface WindowsSchedulerXmlState {
|
|
@@ -1112,6 +1282,7 @@ export function readWindowsSchedulerXmlState(
|
|
|
1112
1282
|
function installLaunchd(): void {
|
|
1113
1283
|
const dir = join(homedir(), "Library", "LaunchAgents");
|
|
1114
1284
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
1285
|
+
recordOwnedConfigPath(getConfigDir(), serviceStatePath());
|
|
1115
1286
|
if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
|
|
1116
1287
|
writeServiceApiTokenFile();
|
|
1117
1288
|
const p = plistPath();
|
|
@@ -1147,9 +1318,23 @@ function writeServiceAssetWithRetry(path: string, content: string, encoding: "ut
|
|
|
1147
1318
|
}
|
|
1148
1319
|
}
|
|
1149
1320
|
|
|
1150
|
-
|
|
1321
|
+
/**
|
|
1322
|
+
* Rewrite on-disk scheduler assets (script/VBS/XML) without re-registering the task.
|
|
1323
|
+
* Used by fresh install (before schtasks /create) and by repair (no elevation).
|
|
1324
|
+
*/
|
|
1325
|
+
function writeWindowsSchedulerAssets(): void {
|
|
1151
1326
|
if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
|
|
1152
1327
|
writeServiceApiTokenFile();
|
|
1328
|
+
const script = windowsServiceScriptPath();
|
|
1329
|
+
writeServiceAssetWithRetry(script, buildWindowsServiceScript(), "utf8");
|
|
1330
|
+
// UTF-16LE + BOM: a BOM-less UTF-8 VBS mis-decodes non-ASCII (e.g. Korean) profile
|
|
1331
|
+
// paths on some WSH/codepage combinations — same contract as the task XML below.
|
|
1332
|
+
writeServiceAssetWithRetry(windowsLauncherVbsPath(), `\uFEFF${buildWindowsLauncherVbs(script)}`, "utf16le");
|
|
1333
|
+
writeServiceAssetWithRetry(windowsTaskXmlPath(), `\uFEFF${buildWindowsTaskXml(script)}`, "utf16le");
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
function installWindows(): void {
|
|
1337
|
+
recordOwnedConfigPath(getConfigDir(), serviceStatePath());
|
|
1153
1338
|
// Transactional backend switch: installing the scheduler backend removes a native
|
|
1154
1339
|
// service first — two live managers would both respawn the proxy (conflict).
|
|
1155
1340
|
if (statusWinswRaw() !== "nonexistent") {
|
|
@@ -1166,23 +1351,85 @@ function installWindows(): void {
|
|
|
1166
1351
|
// End a running task BEFORE rewriting the assets it is executing — cmd.exe reading the
|
|
1167
1352
|
// script mid-rewrite runs a torn batch file, and its open handle can fail the write.
|
|
1168
1353
|
try { stopWindows(); } catch { /* not running */ }
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
// UTF-16LE + BOM: a BOM-less UTF-8 VBS mis-decodes non-ASCII (e.g. Korean) profile
|
|
1172
|
-
// paths on some WSH/codepage combinations — same contract as the task XML below.
|
|
1173
|
-
writeServiceAssetWithRetry(windowsLauncherVbsPath(), `\uFEFF${buildWindowsLauncherVbs(script)}`, "utf16le");
|
|
1174
|
-
writeServiceAssetWithRetry(windowsTaskXmlPath(), `\uFEFF${buildWindowsTaskXml(script)}`, "utf16le");
|
|
1175
|
-
schtasks(buildWindowsSchtasksCreateArgs(script));
|
|
1354
|
+
writeWindowsSchedulerAssets();
|
|
1355
|
+
schtasks(buildWindowsSchtasksCreateArgs(windowsServiceScriptPath()));
|
|
1176
1356
|
schtasks(["/run", "/tn", TASK]);
|
|
1177
1357
|
writeServiceInstallState("scheduler");
|
|
1178
1358
|
}
|
|
1179
1359
|
|
|
1360
|
+
export interface RepairServiceDeps {
|
|
1361
|
+
diagnose?: () => ServiceDiagnostic;
|
|
1362
|
+
assertEnv?: () => void;
|
|
1363
|
+
assertAuth?: () => void;
|
|
1364
|
+
writeSchedulerAssets?: () => void;
|
|
1365
|
+
stopScheduler?: () => void;
|
|
1366
|
+
startScheduler?: () => void;
|
|
1367
|
+
writeSchedulerState?: () => void;
|
|
1368
|
+
writeNativeState?: () => void;
|
|
1369
|
+
repairNative?: () => void | Promise<void>;
|
|
1370
|
+
repairLaunchd?: () => void;
|
|
1371
|
+
repairSystemd?: () => void;
|
|
1372
|
+
/** Test seam — defaults to process.platform so Linux CI cannot hit real installSystemd. */
|
|
1373
|
+
platform?: NodeJS.Platform;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
/**
|
|
1377
|
+
* Repair an already-installed background service without Task Scheduler re-registration.
|
|
1378
|
+
*
|
|
1379
|
+
* Windows scheduler: rewrite assets + stop/start — no `schtasks /create`, no UAC.
|
|
1380
|
+
* Windows native: WinSW asset rewrite + restart (skips `install /p` when present).
|
|
1381
|
+
* macOS/Linux: re-run the user-level install/reload path.
|
|
1382
|
+
*/
|
|
1383
|
+
export async function repairService(deps: RepairServiceDeps = {}): Promise<void> {
|
|
1384
|
+
const diagnose = deps.diagnose ?? diagnoseService;
|
|
1385
|
+
const platform = deps.platform ?? process.platform;
|
|
1386
|
+
const diag = diagnose();
|
|
1387
|
+
if (!diag.supported) {
|
|
1388
|
+
throw new Error(`Background service is unsupported (${diag.summary}).`);
|
|
1389
|
+
}
|
|
1390
|
+
if (diag.conflict) {
|
|
1391
|
+
throw new Error(
|
|
1392
|
+
"Cannot repair while Task Scheduler and native WinSW are both present. "
|
|
1393
|
+
+ "Run 'ocx service uninstall' then reinstall one backend with 'ocx service install'.",
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
if (!diag.installed) {
|
|
1397
|
+
throw new Error("Background service is not installed. Run 'ocx service install' first.");
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
(deps.assertEnv ?? assertServiceEnvironmentMatchesInstall)();
|
|
1401
|
+
(deps.assertAuth ?? assertServiceAuthEnvironment)();
|
|
1402
|
+
|
|
1403
|
+
if (platform === "win32") {
|
|
1404
|
+
if (diag.backend === "native") {
|
|
1405
|
+
await (deps.repairNative ?? (() => installWinswService(defaultWinswEntry(import.meta.dir))))();
|
|
1406
|
+
(deps.writeNativeState ?? (() => writeServiceInstallState("native")))();
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
try { (deps.stopScheduler ?? stopWindows)(); } catch { /* not running */ }
|
|
1410
|
+
(deps.writeSchedulerAssets ?? writeWindowsSchedulerAssets)();
|
|
1411
|
+
(deps.startScheduler ?? startWindows)();
|
|
1412
|
+
(deps.writeSchedulerState ?? (() => writeServiceInstallState("scheduler")))();
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
if (platform === "darwin") {
|
|
1416
|
+
(deps.repairLaunchd ?? installLaunchd)();
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
if (platform === "linux") {
|
|
1420
|
+
(deps.repairSystemd ?? installSystemd)();
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
throw new Error(`Background service repair is unsupported on ${platform}.`);
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1180
1426
|
/**
|
|
1181
1427
|
* Opt-in native backend (`ocx service install --native`). Transactional: removes the
|
|
1182
1428
|
* scheduler backend first; on failure the machine is left with NO service (explicitly
|
|
1183
1429
|
* reported) — never a silent fallback to the scheduler.
|
|
1184
1430
|
*/
|
|
1185
1431
|
async function installWindowsNative(): Promise<void> {
|
|
1432
|
+
recordOwnedConfigPath(getConfigDir(), serviceStatePath());
|
|
1186
1433
|
if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
|
|
1187
1434
|
writeServiceApiTokenFile();
|
|
1188
1435
|
let hadScheduler = false;
|
|
@@ -1321,6 +1568,7 @@ function installSystemd(): void {
|
|
|
1321
1568
|
ensureUserBusEnv(); // reach the user bus over a bare SSH session (F9)
|
|
1322
1569
|
const dir = unitDir();
|
|
1323
1570
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
1571
|
+
recordOwnedConfigPath(getConfigDir(), serviceStatePath());
|
|
1324
1572
|
if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
|
|
1325
1573
|
writeServiceApiTokenFile();
|
|
1326
1574
|
writeFileSync(unitPath(), buildUnit(), "utf8");
|
|
@@ -1664,6 +1912,13 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
1664
1912
|
console.error("--native (WinSW) is Windows-only.");
|
|
1665
1913
|
process.exit(1);
|
|
1666
1914
|
}
|
|
1915
|
+
if (command === "repair") {
|
|
1916
|
+
assertServiceEnvironmentMatchesInstall();
|
|
1917
|
+
assertServiceAuthEnvironment();
|
|
1918
|
+
await repairService();
|
|
1919
|
+
console.log("✅ opencodex background service repaired (assets refreshed, no Task Scheduler re-registration).");
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1667
1922
|
// Non-install subcommands follow the backend recorded at install time (state v2).
|
|
1668
1923
|
const backend: ServiceBackend = parsed.backend ?? (process.platform === "win32" ? readServiceBackend() : "scheduler");
|
|
1669
1924
|
const ops = platformOps(backend);
|
|
@@ -1680,6 +1935,11 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
1680
1935
|
? "✅ opencodex native service installed + started (windowless, starts at boot, auto-restarts on crash)."
|
|
1681
1936
|
: "✅ opencodex service installed + started (auto-starts on login, auto-restarts on crash).");
|
|
1682
1937
|
if (process.platform === "linux") console.log(" For auto-start on boot: loginctl enable-linger $USER");
|
|
1938
|
+
// Service users never reach the `ocx start` prompt: the proxy they run is the
|
|
1939
|
+
// supervised child, which always carries OCX_SERVICE=1. This command, though, is
|
|
1940
|
+
// hand-typed in a real terminal, so it is the one interactive moment they get.
|
|
1941
|
+
// Same one-time marker and same guards (TTY, gh auth, agent deferral) apply.
|
|
1942
|
+
await maybeShowStarPrompt();
|
|
1683
1943
|
break;
|
|
1684
1944
|
case "start":
|
|
1685
1945
|
ops.start();
|
|
@@ -1703,8 +1963,12 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
1703
1963
|
}
|
|
1704
1964
|
break;
|
|
1705
1965
|
case "status": {
|
|
1706
|
-
|
|
1707
|
-
|
|
1966
|
+
if (process.platform === "win32" && backend === "scheduler") {
|
|
1967
|
+
console.log(await inspectWindowsSchedulerServiceStatus());
|
|
1968
|
+
} else {
|
|
1969
|
+
const s = ops.status();
|
|
1970
|
+
console.log(s ? `✅ running:\n${s}` : "❌ service not installed/running.");
|
|
1971
|
+
}
|
|
1708
1972
|
console.log(`Diagnostics: ${serviceDiagnosticsSummary()}`);
|
|
1709
1973
|
break;
|
|
1710
1974
|
}
|
|
@@ -1736,8 +2000,9 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
1736
2000
|
console.log("✅ service uninstalled.");
|
|
1737
2001
|
break;
|
|
1738
2002
|
default:
|
|
1739
|
-
console.error("Usage: ocx service [install|start|stop|status|uninstall|remove] [--native|--scheduler]");
|
|
2003
|
+
console.error("Usage: ocx service [install|repair|start|stop|status|uninstall|remove] [--native|--scheduler]");
|
|
1740
2004
|
console.error(" With no subcommand, installs/updates and starts the background service.");
|
|
2005
|
+
console.error(" repair: refresh assets and restart an already-installed service (no admin re-prompt).");
|
|
1741
2006
|
console.error(" --native (Windows only): register a real SCM service via WinSW instead of Task Scheduler.");
|
|
1742
2007
|
process.exit(1);
|
|
1743
2008
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-flight wrapper for manual archived cleanup (management API).
|
|
3
|
+
*
|
|
4
|
+
* Shares the CODEX_HOME storage-mutation coordinator with trash restore and
|
|
5
|
+
* (Phase 3) policy-driven cleanup.
|
|
6
|
+
*/
|
|
7
|
+
import { resolveCodexHomeDir } from "../codex/home";
|
|
8
|
+
import {
|
|
9
|
+
executeArchivedCleanup,
|
|
10
|
+
type CleanupResult,
|
|
11
|
+
type ExecuteCleanupOptions,
|
|
12
|
+
} from "./cleanup";
|
|
13
|
+
import {
|
|
14
|
+
resetStorageMutationCoordinatorForTests,
|
|
15
|
+
setStorageMutationCoordinatorTestHooks,
|
|
16
|
+
withStorageMutationSlot,
|
|
17
|
+
type StorageMutationCoordinatorTestHooks,
|
|
18
|
+
} from "./storage-mutation-coordinator";
|
|
19
|
+
|
|
20
|
+
export type CleanupJobTestHooks = StorageMutationCoordinatorTestHooks;
|
|
21
|
+
|
|
22
|
+
export function setArchivedCleanupJobTestHooks(hooks: CleanupJobTestHooks | null): void {
|
|
23
|
+
setStorageMutationCoordinatorTestHooks(hooks);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function resetArchivedCleanupJobForTests(): void {
|
|
27
|
+
resetStorageMutationCoordinatorForTests();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Run archived cleanup under the shared storage-mutation gate.
|
|
32
|
+
* Returns immediately with `storage_mutation_busy` when restore or another
|
|
33
|
+
* cleanup is in flight for the same CODEX_HOME.
|
|
34
|
+
*/
|
|
35
|
+
export async function runArchivedCleanupJob(
|
|
36
|
+
options: ExecuteCleanupOptions,
|
|
37
|
+
): Promise<CleanupResult> {
|
|
38
|
+
const codexHome = options.codexHome ?? resolveCodexHomeDir();
|
|
39
|
+
const mode = options.mode;
|
|
40
|
+
const percent = options.percent;
|
|
41
|
+
const result = await withStorageMutationSlot("cleanup", codexHome, () =>
|
|
42
|
+
executeArchivedCleanup(options),
|
|
43
|
+
);
|
|
44
|
+
if (result && typeof result === "object" && "ok" in result && result.ok === false
|
|
45
|
+
&& "error" in result && result.error === "storage_mutation_busy") {
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
mode,
|
|
49
|
+
percent,
|
|
50
|
+
count: 0,
|
|
51
|
+
bytes: 0,
|
|
52
|
+
removedPaths: [],
|
|
53
|
+
error: "storage_mutation_busy",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return result as CleanupResult;
|
|
57
|
+
}
|