@bitkyc08/opencodex 2.81.0 → 2.83.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/bin/ocx.mjs +166 -73
- package/gui/dist/assets/App-Ba8uG696.js +54 -0
- package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-IImbNPRy.js} +1 -1
- package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
- package/gui/dist/assets/{index-RsoaAxXY.js → index-BHRZdJuv.js} +13 -13
- package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DiJO6sYY.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +1 -1
- package/src/adapters/cursor/catalog.ts +2 -2
- package/src/adapters/cursor/effort-map.ts +1 -1
- package/src/adapters/devin/live-models.ts +2 -2
- package/src/adapters/google-truncation.ts +22 -0
- package/src/adapters/google.ts +7 -3
- package/src/adapters/ollama-native.ts +18 -14
- package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
- package/src/adapters/openai-responses/passthrough.ts +11 -4
- package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
- package/src/adapters/openai-responses/reasoning.ts +11 -1
- package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
- package/src/bridge/errors.ts +7 -1
- package/src/claude/agents-inject.ts +14 -3
- package/src/claude/context-windows.ts +22 -20
- package/src/claude/desktop-3p.ts +88 -12
- package/src/claude/desktop-picker.ts +1 -1
- package/src/claude/desktop-profile.ts +2 -1
- package/src/claude/gateway-cache.ts +18 -1
- package/src/claude/inbound.ts +77 -3
- package/src/claude/intercept/cli-catalog.ts +3 -0
- package/src/claude/intercept/picker-ca-persistence.ts +7 -6
- package/src/claude/intercept/picker-models.ts +14 -6
- package/src/claude/intercept/settings.ts +3 -0
- package/src/claude/long-context.ts +66 -0
- package/src/claude/model-info.ts +19 -16
- package/src/claude/outbound.ts +94 -9
- package/src/claude/subagent-model.ts +40 -11
- package/src/cli/account-auth.ts +20 -12
- package/src/cli/agent.ts +9 -5
- package/src/cli/aside-profile-recovery.ts +33 -0
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-integrations.ts +2 -2
- package/src/cli/cli-path-diagnostics.ts +132 -0
- package/src/cli/desktop-runtime-guidance.ts +65 -0
- package/src/cli/dispatch.ts +5 -0
- package/src/cli/doctor.ts +16 -3
- package/src/cli/index.ts +14 -9
- package/src/cli/integration-aside-sync.ts +2 -1
- package/src/cli/integrations.ts +16 -3
- package/src/cli/resolve.ts +26 -3
- package/src/cli/root.ts +4 -2
- package/src/cli/status.ts +64 -12
- package/src/cli/update-restart-child.ts +4 -2
- package/src/cli/update-restart-home.ts +28 -10
- package/src/cli/update-restart-service-record.ts +116 -0
- package/src/cli/update-restart-supervision.ts +96 -0
- package/src/cli/update-restart.ts +59 -13
- package/src/cli/v2.ts +27 -20
- package/src/cli/version-skew-notice.ts +77 -0
- package/src/clients/config-export/model-metadata.ts +16 -0
- package/src/clients/config-export/zcode-store.ts +46 -11
- package/src/clients/config-export/zcode.ts +3 -5
- package/src/clients/config-export.ts +1 -1
- package/src/clients/lazycodex.ts +1 -1
- package/src/clients/omo-role-models.ts +32 -7
- package/src/codex/auth-api/account-selection.ts +107 -0
- package/src/codex/auth-api/pool-mode-gate.ts +4 -5
- package/src/codex/auth-api/routes.ts +10 -49
- package/src/codex/auth-context.ts +28 -4
- package/src/codex/autostart-health.ts +12 -3
- package/src/codex/catalog/build-entries.ts +18 -0
- package/src/codex/catalog/bundled.ts +155 -26
- package/src/codex/catalog/combo-member.ts +3 -2
- package/src/codex/catalog/effort.ts +26 -5
- package/src/codex/catalog/gather-capture.ts +5 -0
- package/src/codex/catalog/model-hints.ts +15 -8
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/native-models.ts +1 -1
- package/src/codex/catalog/operator-tiers.ts +38 -0
- package/src/codex/catalog/provider-models.ts +68 -35
- package/src/codex/catalog/retained-sync.ts +7 -5
- package/src/codex/catalog/routed-gather.ts +15 -1
- package/src/codex/catalog-auto-refresh-sources.ts +3 -4
- package/src/codex/catalog-auto-refresh.ts +19 -0
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-home-owner.ts +3 -0
- package/src/codex/config-write-lock.ts +333 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/desktop-app/darwin.ts +1 -1
- package/src/codex/desktop-app/handoff.ts +1 -1
- package/src/codex/desktop-app/linux.ts +1 -1
- package/src/codex/desktop-app/lock.ts +1 -1
- package/src/codex/desktop-app/types.ts +1 -1
- package/src/codex/desktop-app/windows.ts +1 -1
- package/src/codex/desktop-app-restart.ts +1 -1
- package/src/codex/features.ts +257 -200
- package/src/codex/inject/config-write-section.ts +34 -0
- package/src/codex/inject/multi-agent-v2.ts +14 -6
- package/src/codex/inject/remove.ts +36 -11
- package/src/codex/inject/restore.ts +85 -20
- package/src/codex/inject-coordination.ts +15 -6
- package/src/codex/inject.ts +62 -20
- package/src/codex/journal.ts +62 -17
- package/src/codex/main-account-cache.ts +43 -0
- package/src/codex/main-account.ts +6 -7
- package/src/codex/paths.ts +2 -0
- package/src/codex/prepared-home.ts +18 -0
- package/src/codex/prompt-journal.ts +25 -12
- package/src/codex/prompt-layers.ts +49 -26
- package/src/codex/prompt-lock-claim.ts +134 -0
- package/src/codex/prompt-lock-evidence.ts +26 -0
- package/src/codex/prompt-lock-io.ts +16 -0
- package/src/codex/prompt-lock-owner.ts +130 -0
- package/src/codex/prompt-lock.ts +84 -53
- package/src/codex/routing/failure-window.ts +146 -0
- package/src/codex/routing/selection.ts +4 -1
- package/src/codex/routing.ts +13 -17
- package/src/codex/runtime.ts +106 -18
- package/src/codex/shim.ts +45 -19
- package/src/combos/jev-service-exchange.ts +281 -0
- package/src/combos/jev.ts +34 -220
- package/src/config/atomic-write.ts +1 -1
- package/src/config/codex-ws-reuse-setting.ts +10 -0
- package/src/config/live-reconcile.ts +17 -0
- package/src/config/load-degrade.ts +8 -0
- package/src/config/rebase-provenance.ts +11 -0
- package/src/config/schema/config-schema.ts +4 -0
- package/src/generated/compatibility-version.json +375 -227
- package/src/generated/model-metadata.ts +2 -2
- package/src/grok/inject.ts +276 -206
- package/src/grok/reset-coupon-ledger.ts +223 -57
- package/src/grok/reset-coupons.ts +13 -6
- package/src/grok/sync.ts +2 -1
- package/src/images/loop.ts +29 -11
- package/src/integrations/aside-profiles.ts +10 -3
- package/src/integrations/catalog-refresh.ts +18 -2
- package/src/integrations/cursor-detect.ts +1 -1
- package/src/integrations/writer.ts +9 -3
- package/src/lib/bun-path-runtime.d.mts +32 -0
- package/src/lib/bun-path-runtime.mjs +95 -0
- package/src/lib/bun-runtime-preflight.ts +140 -0
- package/src/lib/desktop-cli-handoff.d.mts +20 -0
- package/src/lib/desktop-cli-handoff.mjs +92 -0
- package/src/lib/desktop-cli-record.d.mts +29 -0
- package/src/lib/desktop-cli-record.mjs +102 -0
- package/src/lib/errors.ts +25 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/request-execution-budget.ts +8 -0
- package/src/lib/synced-state-location.ts +46 -6
- package/src/lib/test-home-guard.ts +87 -1
- package/src/lib/translator-budget.ts +17 -4
- package/src/lib/windows-atomic-replace.ts +38 -16
- package/src/lib/windows-elevation.ts +6 -0
- package/src/lib/windows-secret-acl.ts +92 -25
- package/src/lib/winsw.ts +2 -3
- package/src/oauth/anthropic-account-refusal.ts +39 -1
- package/src/oauth/anthropic-routing.ts +1 -1
- package/src/oauth/anthropic-send-ownership.ts +3 -3
- package/src/oauth/chatgpt.ts +113 -41
- package/src/oauth/index.ts +7 -3
- package/src/oauth/meta-muse-device.ts +1 -1
- package/src/oauth/store.ts +1 -1
- package/src/oauth/types.ts +1 -1
- package/src/providers/anthropic-fast.ts +1 -1
- package/src/providers/anthropic-reset-grant-ledger.ts +1 -1
- package/src/providers/azure-model-metadata.ts +115 -0
- package/src/providers/derive.ts +21 -0
- package/src/providers/devin-cli-authmode-migration.ts +1 -1
- package/src/providers/devin-provider-merge-migration.ts +1 -1
- package/src/providers/github-copilot-auto.ts +169 -0
- package/src/providers/model-presets.ts +1 -1
- package/src/providers/muse-subscription-usage.ts +1 -1
- package/src/providers/quota/vendor-probes-key.ts +127 -17
- package/src/providers/quota.ts +1 -1
- package/src/providers/registry/entries-core.ts +6 -6
- package/src/providers/registry/entries-extended.ts +4 -0
- package/src/providers/registry/model-seeds.ts +3 -3
- package/src/providers/xai-fast-model.ts +1 -1
- package/src/responses/reasoning-envelope.ts +29 -1
- package/src/responses/reasoning-replay-cache.ts +17 -1
- package/src/responses/spill-store.ts +15 -2
- package/src/responses/state/metrics.ts +8 -0
- package/src/responses/state/spill-failure.ts +12 -0
- package/src/responses/state/spill-queue.ts +12 -5
- package/src/responses/state.ts +18 -10
- package/src/responses/task-input.ts +17 -0
- package/src/server/background-lifecycle.ts +27 -0
- package/src/server/chat-completions.ts +1 -0
- package/src/server/chat-native.ts +28 -12
- package/src/server/claude-messages.ts +43 -6
- package/src/server/inbound-body-admission.ts +25 -7
- package/src/server/index/claude-intercept-lifecycle.ts +3 -1
- package/src/server/index/link-listener.ts +30 -17
- package/src/server/index/serve-options.ts +18 -4
- package/src/server/index/spend-ledger-lifecycle.ts +2 -1
- package/src/server/index/startup-warnings.ts +19 -0
- package/src/server/index.ts +2 -1
- package/src/server/lifecycle.ts +31 -4
- package/src/server/management/agent-settings-routes.ts +93 -32
- package/src/server/management/codex-agent-role-routes.ts +4 -2
- package/src/server/management/codex-prompt-routes.ts +1 -0
- package/src/server/management/codex-role-auto-assign.ts +13 -2
- package/src/server/management/config-routes.ts +59 -8
- package/src/server/management/context.ts +2 -2
- package/src/server/management/grok-coupon-routes.ts +277 -52
- package/src/server/management/link-routes.ts +42 -20
- package/src/server/management/route-registry.ts +2 -2
- package/src/server/management/shared.ts +3 -3
- package/src/server/messages-native-oauth.ts +9 -8
- package/src/server/messages-native.ts +49 -13
- package/src/server/messages-response-headers.ts +14 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +18 -7
- package/src/server/request-log-conversation.ts +48 -5
- package/src/server/request-log-failure-attribution.ts +3 -0
- package/src/server/request-log-terminal-status.ts +186 -0
- package/src/server/request-log.ts +15 -89
- package/src/server/responses/adapter-continuation.ts +11 -3
- package/src/server/responses/adapter-dispatch.ts +99 -19
- package/src/server/responses/codex-ws-exchange.ts +17 -2
- package/src/server/responses/codex-ws-pool.ts +100 -13
- package/src/server/responses/codex-ws-wire.ts +9 -0
- package/src/server/responses/combo-requested-effort.ts +3 -1
- package/src/server/responses/compact.ts +3 -2
- package/src/server/responses/core-codex-account.ts +22 -1
- package/src/server/responses/core-combo-native.ts +20 -3
- package/src/server/responses/core-combo.ts +22 -14
- package/src/server/responses/core-lifetime.ts +3 -36
- package/src/server/responses/core-opaque-recovery.ts +5 -5
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-replay.ts +73 -0
- package/src/server/responses/core.ts +9 -9
- package/src/server/responses/empty-completion-guard.ts +18 -2
- package/src/server/responses/fetch-helpers.ts +26 -3
- package/src/server/responses/jev-model-invoke.ts +1 -1
- package/src/server/responses/passthrough-delivery.ts +24 -2
- package/src/server/responses/passthrough-dispatch.ts +134 -62
- package/src/server/responses/passthrough-execution.ts +3 -3
- package/src/server/responses/request-prepare.ts +4 -0
- package/src/server/responses/request-send-budget.ts +112 -49
- package/src/server/responses/request-transport.ts +125 -5
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +15 -5
- package/src/server/responses/sidecar-execution.ts +30 -16
- package/src/server/responses/sidecar-send-budget.ts +58 -0
- package/src/server/responses/spend-storage-error.ts +33 -0
- package/src/server/responses/ws-upstream.ts +19 -6
- package/src/server/startup-health-cache.ts +2 -1
- package/src/server/workflow-refusal.ts +3 -0
- package/src/service/cli.ts +20 -4
- package/src/service/desktop-command-guard.ts +32 -0
- package/src/service/desktop-startup.ts +66 -40
- package/src/service/desktop-supervision.d.mts +26 -0
- package/src/service/desktop-supervision.mjs +140 -0
- package/src/service/managing-cli.ts +1 -0
- package/src/service/orchestration.ts +46 -20
- package/src/service/repair.ts +25 -9
- package/src/service/state.ts +4 -3
- package/src/service/systemd.ts +17 -8
- package/src/service/windows-ops.ts +61 -22
- package/src/storage/policy-job.ts +5 -1
- package/src/storage/policy-worker.ts +3 -1
- package/src/storage/policy.ts +9 -0
- package/src/types/config.ts +21 -0
- package/src/types/request.ts +10 -0
- package/src/types/wire.ts +1 -0
- package/src/update/index.ts +57 -9
- package/src/update/restart-ownership.ts +10 -3
- package/src/update/runtime-ownership.d.mts +6 -0
- package/src/update/runtime-ownership.mjs +23 -7
- package/src/usage/expected-prices.ts +1 -1
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/App-7yhoys8a.js +0 -54
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { accessSync, constants, readFileSync, readlinkSync, realpathSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
// Node >=18 also imports this module from bin/ocx.mjs; do not import TS or Bun APIs.
|
|
7
|
+
function run(command, args) {
|
|
8
|
+
return execFileSync(command, args, {
|
|
9
|
+
encoding: "utf8", timeout: 750, maxBuffer: 128 * 1024, stdio: ["ignore", "pipe", "pipe"],
|
|
10
|
+
}).trim();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Mirror src/config/paths.ts getConfigDir/expandUserPath and process-state.ts paths.
|
|
14
|
+
// Keep this Node-safe copy in sync; the focused startup test pins both implementations.
|
|
15
|
+
export function desktopSupervisionPaths() {
|
|
16
|
+
const raw = process.env.OPENCODEX_HOME?.trim();
|
|
17
|
+
const expanded = raw === "~" ? homedir()
|
|
18
|
+
: raw?.startsWith("~/") || raw?.startsWith("~\\") ? join(homedir(), raw.slice(2)) : raw;
|
|
19
|
+
const home = expanded ? resolve(expanded) : join(homedir(), ".opencodex");
|
|
20
|
+
return { pid: join(home, "ocx.pid"), runtimePort: join(home, "runtime-port.json") };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function validPid(pid) {
|
|
24
|
+
return Number.isSafeInteger(pid) && pid > 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readState(path, parse) {
|
|
28
|
+
try { return parse(readFileSync(path, "utf8")); }
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error?.code === "ENOENT") return null;
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readPidFile() {
|
|
36
|
+
return readState(desktopSupervisionPaths().pid, raw => {
|
|
37
|
+
const text = raw.trim();
|
|
38
|
+
if (!/^\d+$/.test(text) || !validPid(Number(text))) throw new Error("invalid pid");
|
|
39
|
+
return Number(text);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readRuntimePortPid() {
|
|
44
|
+
return readState(desktopSupervisionPaths().runtimePort, raw => {
|
|
45
|
+
// Only project pid: no credential validation, serialization or returned port state.
|
|
46
|
+
const pid = JSON.parse(raw)?.pid;
|
|
47
|
+
if (!validPid(pid)) throw new Error("invalid runtime pid");
|
|
48
|
+
return pid;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Linux identity fields count from the end of comm, which may contain spaces or ')'. */
|
|
53
|
+
export const procfs = {
|
|
54
|
+
exe: pid => readlinkSync(`/proc/${pid}/exe`),
|
|
55
|
+
parent: pid => {
|
|
56
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
57
|
+
return Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export function processIdentity(pid, execute = run, onExecutable) {
|
|
62
|
+
const text = execute("/bin/ps", ["-p", String(pid), "-o", "ppid=,comm="]).trim();
|
|
63
|
+
const row = /^(\d+)\s+(.+)$/.exec(text);
|
|
64
|
+
if (!row) {
|
|
65
|
+
if (text) throw new Error("invalid process identity");
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
onExecutable?.(row[2]);
|
|
69
|
+
return { parent: Number(row[1]), executable: realpathSync(row[2]) };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Live supervision is evidence, never a durable ownership claim. */
|
|
73
|
+
export function inspectDesktopSupervision(deps = {}) {
|
|
74
|
+
const platform = deps.platform ?? process.platform;
|
|
75
|
+
if (platform !== "darwin" && platform !== "linux") return { kind: "unsupported" };
|
|
76
|
+
let desktopSeen = false;
|
|
77
|
+
const unknown = reason => ({ kind: "unknown", reason, desktopSeen });
|
|
78
|
+
const seen = executable => { if (basename(executable) === "opencodex-desktop") desktopSeen = true; };
|
|
79
|
+
const readPid = deps.readPid ?? readPidFile;
|
|
80
|
+
const readPortPid = deps.readRuntimePortPid ?? readRuntimePortPid;
|
|
81
|
+
const proc = deps.proc ?? procfs;
|
|
82
|
+
const execute = deps.run ?? run;
|
|
83
|
+
function identity(pid, parent = false) {
|
|
84
|
+
if (platform === "darwin") return processIdentity(pid, execute, parent ? seen : undefined);
|
|
85
|
+
const executable = proc.exe(pid);
|
|
86
|
+
if (parent) seen(executable);
|
|
87
|
+
const parentPid = proc.parent(pid);
|
|
88
|
+
if (!Number.isSafeInteger(parentPid) || parentPid < 0) throw new Error("invalid parent pid");
|
|
89
|
+
return { parent: parentPid, executable: realpathSync(executable) };
|
|
90
|
+
}
|
|
91
|
+
function capture() {
|
|
92
|
+
const pidFile = readPid();
|
|
93
|
+
const portPid = readPortPid();
|
|
94
|
+
const pids = [deps.targetPid, pidFile, portPid].filter(pid => pid !== undefined && pid !== null);
|
|
95
|
+
if (pids.some(pid => !validPid(pid))) return { error: "invalid-pid" };
|
|
96
|
+
if (pids.some(pid => pid !== pids[0])) return { error: "pid-mismatch" };
|
|
97
|
+
const runtimePid = pids[0] ?? null;
|
|
98
|
+
const child = runtimePid !== null ? identity(runtimePid) : null;
|
|
99
|
+
const parent = child && child.parent > 1 ? identity(child.parent, true) : null;
|
|
100
|
+
const snapshot = { pidFile, portPid, runtimePid, child, parent };
|
|
101
|
+
if (!runtimePid || !child || !parent || basename(parent.executable) !== "opencodex-desktop") {
|
|
102
|
+
return { ...snapshot, kind: "none" };
|
|
103
|
+
}
|
|
104
|
+
seen(parent.executable);
|
|
105
|
+
const app = parent.executable;
|
|
106
|
+
if (platform === "darwin" && (basename(dirname(app)) !== "MacOS"
|
|
107
|
+
|| basename(dirname(dirname(app))) !== "Contents")) return { error: "app-mismatch" };
|
|
108
|
+
const proxy = realpathSync(join(dirname(app), "ocx"));
|
|
109
|
+
if (child.executable !== proxy || (platform === "linux"
|
|
110
|
+
&& (dirname(proxy) !== dirname(app) || basename(proxy) !== "ocx"))) return { error: "proxy-mismatch" };
|
|
111
|
+
accessSync(app, constants.X_OK);
|
|
112
|
+
accessSync(proxy, constants.X_OK);
|
|
113
|
+
return { ...snapshot, kind: "desktop", app, proxy };
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const first = capture();
|
|
117
|
+
if (first.error) return unknown(first.error);
|
|
118
|
+
const final = capture();
|
|
119
|
+
if (final.error) return unknown(final.error);
|
|
120
|
+
if (JSON.stringify(first) !== JSON.stringify(final)) return unknown("snapshot-changed");
|
|
121
|
+
return final.kind === "desktop"
|
|
122
|
+
? { kind: "desktop", runtimePid: final.runtimePid, supervisorPid: final.child.parent, app: final.app, proxy: final.proxy }
|
|
123
|
+
: { kind: "none" };
|
|
124
|
+
} catch {
|
|
125
|
+
// Error messages can contain paths or file contents; expose only the bounded reason.
|
|
126
|
+
return unknown("probe-failed");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** One operation retains its Desktop veto through inconclusive later probes. */
|
|
131
|
+
export function createSupervisionLatch() {
|
|
132
|
+
let blocked = false;
|
|
133
|
+
return {
|
|
134
|
+
observe(evidence) {
|
|
135
|
+
if (evidence.kind === "none") blocked = false;
|
|
136
|
+
else if (evidence.kind === "desktop" || (evidence.kind === "unknown" && evidence.desktopSeen)) blocked = true;
|
|
137
|
+
return blocked;
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -81,6 +81,7 @@ function probeVersionOnce(
|
|
|
81
81
|
windowsShim ? ["/c", shimCommand] : [...args, "--version"],
|
|
82
82
|
{
|
|
83
83
|
timeout: VERSION_PROBE_TIMEOUT_MS, encoding: "utf8", stdio: "pipe",
|
|
84
|
+
env: { ...deps.env, OCX_NO_DESKTOP_HANDOFF: "1" },
|
|
84
85
|
windowsHide: true, ...(windowsShim ? { windowsVerbatimArguments: true } : {}),
|
|
85
86
|
},
|
|
86
87
|
) as SpawnSyncReturns<string>;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { findLiveProxy, SERVICE_STOP_LIVENESS } from "../server/proxy-liveness";
|
|
2
2
|
import { existsSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { durableBunRuntime, type DurableBunRuntime } from "../lib/bun-runtime";
|
|
4
|
+
import { assertSelectedRuntimeWritable, type RuntimePreflightDeps } from "../lib/bun-runtime-preflight";
|
|
3
5
|
import { getConfigDir } from "../config";
|
|
4
6
|
import { readPid, removePid, removeRuntimePort, verifyPidIdentity } from "../config/process-state";
|
|
5
7
|
import { isWslRuntime } from "../codex/home";
|
|
@@ -14,16 +16,17 @@ import { assertSchedulerRegistrationBeforeStart } from "./repair";
|
|
|
14
16
|
import { SERVICE_MANAGED_ENV, TASK, plistPath, removeServiceInstallStateRecords, writeServiceInstallState } from "./state";
|
|
15
17
|
import type { ServiceBackend } from "./state";
|
|
16
18
|
import { unitPath, isSystemd, installSystemd, startSystemd, stopSystemd, statusSystemd, uninstallSystemd, systemdServiceInstallCleanupOps } from "./systemd";
|
|
17
|
-
import { writeWindowsSchedulerAssets, stageWindowsSchedulerRegistrationXml, removeWindowsSchedulerRegistrationStage, registerFreshWindowsSchedulerTask, recordWindowsSchedulerOwnership, removeNativeWindowsServiceForScheduler,
|
|
19
|
+
import { writeWindowsSchedulerAssets, stageWindowsSchedulerRegistrationXml, removeWindowsSchedulerRegistrationStage, registerFreshWindowsSchedulerTask, recordWindowsSchedulerOwnership, removeNativeWindowsServiceForScheduler, commitWindowsInstall, commitWindowsNativeInstall, type WindowsServiceInstallDeps, startWindows, isWindowsSchedulerEndBenign, stopWindows, stopWindowsChecked, statusWindows, statusWindowsXml, killWindowsServiceWrapperProcesses, uninstallWindows, classifyWindowsServiceStop } from "./windows-ops";
|
|
18
20
|
import { schtasks, probeWindowsSchedulerTask, rollbackWindowsSchedulerTaskOwnedByAttempt, settleDelay } from "./windows-scheduler";
|
|
19
21
|
import type { WindowsSchedulerTaskProbe } from "./windows-scheduler";
|
|
20
22
|
import { windowsTaskRegistrationOwnedByAttempt, windowsTaskRegistrationHealthy } from "./windows-taskxml";
|
|
21
23
|
import { win32 } from "node:path";
|
|
22
24
|
import { launchdGuiDomain } from "./launchd";
|
|
23
25
|
import { LABEL } from "./state";
|
|
26
|
+
import { assertNoDesktopSupervision, createSupervisionLatch, type SupervisionInspector, type SupervisionLatch } from "./desktop-command-guard";
|
|
24
27
|
|
|
25
28
|
type ServiceOps = {
|
|
26
|
-
install: () => void | Promise<void>; start: () => void; stop: () => void;
|
|
29
|
+
install: (runtime: DurableBunRuntime) => void | Promise<void>; start: () => void; stop: () => void;
|
|
27
30
|
status: () => string; uninstall: () => void;
|
|
28
31
|
};
|
|
29
32
|
|
|
@@ -32,18 +35,19 @@ export type ServiceInstallCleanupOps = {
|
|
|
32
35
|
stop: () => void;
|
|
33
36
|
};
|
|
34
37
|
|
|
35
|
-
export function platformOps(backend: ServiceBackend = "scheduler"): ServiceOps | null {
|
|
36
|
-
|
|
38
|
+
export function platformOps(backend: ServiceBackend = "scheduler", deps: { platform?: NodeJS.Platform; windowsInstall?: WindowsServiceInstallDeps } = {}): ServiceOps | null {
|
|
39
|
+
const platform = deps.platform ?? process.platform;
|
|
40
|
+
if (platform === "darwin")
|
|
37
41
|
// Wrapped, not passed: `installLaunchd` reports whether it reloaded launchd, and only
|
|
38
42
|
// `repairService` (for the `restart` verb) has any use for that. `ServiceOps.install` is
|
|
39
43
|
// the generic install seam and deliberately promises nothing about a return value.
|
|
40
44
|
return { install: () => { installLaunchd(); }, start: startLaunchd, stop: stopLaunchd, status: statusLaunchd, uninstall: uninstallLaunchd };
|
|
41
|
-
if (
|
|
45
|
+
if (platform === "win32") {
|
|
42
46
|
if (backend === "native")
|
|
43
|
-
return { install:
|
|
44
|
-
return { install:
|
|
47
|
+
return { install: runtime => commitWindowsNativeInstall(runtime, deps.windowsInstall), start: startWinswService, stop: stopWinswService, status: winswStatusSummary, uninstall: uninstallWinswService };
|
|
48
|
+
return { install: runtime => commitWindowsInstall(runtime, deps.windowsInstall), start: startWindows, stop: stopWindows, status: statusWindows, uninstall: uninstallWindows };
|
|
45
49
|
}
|
|
46
|
-
if (
|
|
50
|
+
if (platform === "linux") {
|
|
47
51
|
if (existsSync("/.dockerenv")) {
|
|
48
52
|
console.error("Docker detected. Run 'ocx start' directly instead of using the service manager.");
|
|
49
53
|
process.exit(1);
|
|
@@ -229,7 +233,9 @@ export async function stopTrackedProxyForServiceCommand(): Promise<TrackedProxyC
|
|
|
229
233
|
}
|
|
230
234
|
}
|
|
231
235
|
|
|
232
|
-
export interface ServiceInstallPreparationDeps {
|
|
236
|
+
export interface ServiceInstallPreparationDeps extends RuntimePreflightDeps {
|
|
237
|
+
inspectSupervision?: SupervisionInspector;
|
|
238
|
+
supervisionLatch?: SupervisionLatch;
|
|
233
239
|
diagnose?: () => ServiceDiagnostic;
|
|
234
240
|
managerOps?: (backend: ServiceBackend) => ServiceInstallCleanupOps | null;
|
|
235
241
|
stopTrackedProxy?: () => Promise<unknown>;
|
|
@@ -245,6 +251,8 @@ export async function prepareServiceInstall(
|
|
|
245
251
|
requestedBackend: ServiceBackend,
|
|
246
252
|
deps: ServiceInstallPreparationDeps = {},
|
|
247
253
|
): Promise<void> {
|
|
254
|
+
const latch = deps.supervisionLatch ?? createSupervisionLatch();
|
|
255
|
+
assertNoDesktopSupervision(deps.inspectSupervision, latch);
|
|
248
256
|
const diagnostic = (deps.diagnose ?? diagnoseService)();
|
|
249
257
|
const platform = deps.platform ?? process.platform;
|
|
250
258
|
const resolveOps = deps.managerOps ?? platformServiceInstallCleanupOps;
|
|
@@ -276,27 +284,35 @@ export async function prepareServiceInstall(
|
|
|
276
284
|
|
|
277
285
|
export async function installServiceSafely(
|
|
278
286
|
requestedBackend: ServiceBackend,
|
|
279
|
-
install: () => void | Promise<void>,
|
|
287
|
+
install: (runtime: DurableBunRuntime) => void | Promise<void>,
|
|
280
288
|
deps: ServiceInstallPreparationDeps = {},
|
|
281
289
|
): Promise<void> {
|
|
282
|
-
|
|
283
|
-
|
|
290
|
+
const latch = deps.supervisionLatch ?? createSupervisionLatch();
|
|
291
|
+
assertNoDesktopSupervision(deps.inspectSupervision, latch);
|
|
292
|
+
const runtime = Object.freeze({ ...(deps.selectRuntime ?? durableBunRuntime)() });
|
|
293
|
+
const configDir = (deps.configDir ?? getConfigDir)();
|
|
294
|
+
(deps.assertRuntimeWritable ?? assertSelectedRuntimeWritable)(runtime, configDir, { platform: deps.platform, rootWasAbsent: !existsSync(configDir) });
|
|
295
|
+
await prepareServiceInstall(requestedBackend, { ...deps, supervisionLatch: latch });
|
|
296
|
+
assertNoDesktopSupervision(deps.inspectSupervision, latch);
|
|
297
|
+
await install(runtime);
|
|
284
298
|
}
|
|
285
299
|
|
|
286
|
-
export interface FreshWindowsSchedulerInstallDeps {
|
|
300
|
+
export interface FreshWindowsSchedulerInstallDeps extends RuntimePreflightDeps {
|
|
301
|
+
inspectSupervision?: SupervisionInspector;
|
|
302
|
+
supervisionLatch?: SupervisionLatch;
|
|
287
303
|
stageRegistrationXml?: (attemptNonce: string) => string;
|
|
288
304
|
register?: (xmlPath: string, attemptNonce: string) => Promise<void>;
|
|
289
305
|
recordOwnership?: () => boolean;
|
|
290
306
|
prepare?: () => Promise<void>;
|
|
291
307
|
removeNativeService?: () => void;
|
|
292
|
-
publishAssets?: () => void;
|
|
308
|
+
publishAssets?: (runtime: DurableBunRuntime) => void;
|
|
293
309
|
verifyBeforeRun?: (attemptNonce: string) => void | Promise<void>;
|
|
294
310
|
/** Reads the newly registered task; empty or throwing reads are retried before rollback. */
|
|
295
311
|
readSchedulerXml?: () => string;
|
|
296
312
|
/** Bounded wait before retrying an unreadable fresh-install registration. */
|
|
297
313
|
settleSchedulerRead?: (delayMs: number) => void | Promise<void>;
|
|
298
314
|
runTask?: () => void;
|
|
299
|
-
writeState?: () => void;
|
|
315
|
+
writeState?: (runtime: DurableBunRuntime) => void;
|
|
300
316
|
rollbackTask?: (attemptNonce: string) => Promise<string | null>;
|
|
301
317
|
removeStagedXml?: (xmlPath: string) => void;
|
|
302
318
|
}
|
|
@@ -312,10 +328,14 @@ export interface FreshWindowsSchedulerInstallDeps {
|
|
|
312
328
|
export async function installFreshWindowsSchedulerSafely(
|
|
313
329
|
deps: FreshWindowsSchedulerInstallDeps = {},
|
|
314
330
|
): Promise<void> {
|
|
331
|
+
const latch = deps.supervisionLatch ?? createSupervisionLatch();
|
|
332
|
+
assertNoDesktopSupervision(deps.inspectSupervision, latch);
|
|
315
333
|
const stage = deps.stageRegistrationXml ?? stageWindowsSchedulerRegistrationXml;
|
|
316
334
|
const register = deps.register ?? registerFreshWindowsSchedulerTask;
|
|
317
335
|
const recordOwnership = deps.recordOwnership ?? recordWindowsSchedulerOwnership;
|
|
318
|
-
const prepare = deps.prepare ?? (() => prepareServiceInstall("scheduler"
|
|
336
|
+
const prepare = deps.prepare ?? (() => prepareServiceInstall("scheduler", {
|
|
337
|
+
inspectSupervision: deps.inspectSupervision, supervisionLatch: latch,
|
|
338
|
+
}));
|
|
319
339
|
const removeNativeService = deps.removeNativeService ?? removeNativeWindowsServiceForScheduler;
|
|
320
340
|
const publishAssets = deps.publishAssets ?? writeWindowsSchedulerAssets;
|
|
321
341
|
const verifyBeforeRun = deps.verifyBeforeRun ?? ((nonce: string) => (
|
|
@@ -331,7 +351,7 @@ export async function installFreshWindowsSchedulerSafely(
|
|
|
331
351
|
)
|
|
332
352
|
));
|
|
333
353
|
const runTask = deps.runTask ?? startWindows;
|
|
334
|
-
const writeState = deps.writeState ?? (() => writeServiceInstallState("scheduler"));
|
|
354
|
+
const writeState = deps.writeState ?? ((runtime: DurableBunRuntime) => writeServiceInstallState("scheduler", undefined, {}, runtime));
|
|
335
355
|
const rollbackTask = deps.rollbackTask ?? ((attemptNonce: string) => (
|
|
336
356
|
rollbackWindowsSchedulerTaskOwnedByAttempt(attemptNonce, TASK)
|
|
337
357
|
));
|
|
@@ -341,13 +361,16 @@ export async function installFreshWindowsSchedulerSafely(
|
|
|
341
361
|
|
|
342
362
|
let stagedXml: string | null = null;
|
|
343
363
|
const attemptNonce = randomUUID();
|
|
344
|
-
const
|
|
364
|
+
const configDir = (deps.configDir ?? getConfigDir)();
|
|
365
|
+
const configRootWasAbsent = !existsSync(configDir);
|
|
366
|
+
const runtime = Object.freeze({ ...(deps.selectRuntime ?? durableBunRuntime)() });
|
|
345
367
|
let registered = false;
|
|
346
368
|
let started = false;
|
|
347
369
|
try {
|
|
348
370
|
stagedXml = stage(attemptNonce);
|
|
349
371
|
await register(stagedXml, attemptNonce);
|
|
350
372
|
registered = true;
|
|
373
|
+
assertNoDesktopSupervision(deps.inspectSupervision, latch);
|
|
351
374
|
|
|
352
375
|
// The destructive boundary begins only after Task Scheduler accepted the definition.
|
|
353
376
|
// The registration has consumed its temporary XML. Remove it before claiming a newly
|
|
@@ -361,13 +384,16 @@ export async function installFreshWindowsSchedulerSafely(
|
|
|
361
384
|
+ "aborting before service-manager cleanup or asset publication.",
|
|
362
385
|
);
|
|
363
386
|
}
|
|
387
|
+
(deps.assertRuntimeWritable ?? assertSelectedRuntimeWritable)(runtime, configDir, { platform: deps.platform, rootWasAbsent: false });
|
|
364
388
|
await prepare();
|
|
389
|
+
assertNoDesktopSupervision(deps.inspectSupervision, latch);
|
|
365
390
|
removeNativeService();
|
|
366
|
-
publishAssets();
|
|
391
|
+
publishAssets(runtime);
|
|
367
392
|
await verifyBeforeRun(attemptNonce);
|
|
393
|
+
assertNoDesktopSupervision(deps.inspectSupervision, latch);
|
|
368
394
|
runTask();
|
|
369
395
|
started = true;
|
|
370
|
-
writeState();
|
|
396
|
+
writeState(runtime);
|
|
371
397
|
} catch (error) {
|
|
372
398
|
const detail = error instanceof Error ? error.message : String(error);
|
|
373
399
|
if (registered && !started) {
|
package/src/service/repair.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { getConfigDir } from "../config";
|
|
3
|
+
import { durableBunRuntime, type DurableBunRuntime } from "../lib/bun-runtime";
|
|
4
|
+
import { assertSelectedRuntimeWritable, type RuntimePreflightDeps } from "../lib/bun-runtime-preflight";
|
|
1
5
|
import { randomUUID } from "node:crypto";
|
|
2
6
|
import { defaultWinswEntry, installWinswService } from "../lib/winsw";
|
|
3
7
|
import { resolveWindowsTaskDiagnosticUserId, diagnoseService } from "./diagnostics";
|
|
@@ -14,6 +18,7 @@ import type { WindowsSchedulerTaskProbe } from "./windows-scheduler";
|
|
|
14
18
|
import { taskXmlSection, taskXmlWithoutCommentsAndCdata, taskXmlElementCount, taskXmlOptionalValueEquals, windowsTaskRegistrationOwnedByAttempt, windowsTaskHasSessionRecoveryTriggers, windowsTaskRegistrationHealthy, windowsTaskRegistrationRefreshableLegacy } from "./windows-taskxml";
|
|
15
19
|
import type { ExpectedWindowsTaskUserId } from "./windows-taskxml";
|
|
16
20
|
import { win32 } from "node:path";
|
|
21
|
+
import { assertNoDesktopSupervision, createSupervisionLatch, type SupervisionInspector, type SupervisionLatch } from "./desktop-command-guard";
|
|
17
22
|
|
|
18
23
|
/**
|
|
19
24
|
* The two CLI verbs `repairService` serves. They differ on ONE platform and ONE case: a
|
|
@@ -22,16 +27,18 @@ import { win32 } from "node:path";
|
|
|
22
27
|
*/
|
|
23
28
|
export type ServiceRepairVerb = "repair" | "restart";
|
|
24
29
|
|
|
25
|
-
export interface RepairServiceDeps {
|
|
30
|
+
export interface RepairServiceDeps extends RuntimePreflightDeps {
|
|
31
|
+
inspectSupervision?: SupervisionInspector;
|
|
32
|
+
supervisionLatch?: SupervisionLatch;
|
|
26
33
|
diagnose?: () => ServiceDiagnostic;
|
|
27
34
|
assertEnv?: () => void;
|
|
28
35
|
assertAuth?: () => void;
|
|
29
|
-
writeSchedulerAssets?: () => void;
|
|
36
|
+
writeSchedulerAssets?: (runtime: DurableBunRuntime) => void;
|
|
30
37
|
stopScheduler?: () => void;
|
|
31
38
|
startScheduler?: () => void;
|
|
32
|
-
writeSchedulerState?: () => void;
|
|
33
|
-
writeNativeState?: () => void;
|
|
34
|
-
repairNative?: () => void | Promise<void>;
|
|
39
|
+
writeSchedulerState?: (runtime: DurableBunRuntime) => void;
|
|
40
|
+
writeNativeState?: (runtime: DurableBunRuntime) => void;
|
|
41
|
+
repairNative?: (runtime: DurableBunRuntime) => void | Promise<void>;
|
|
35
42
|
repairLaunchd?: () => LaunchdInstallOutcome | void;
|
|
36
43
|
repairSystemd?: () => void;
|
|
37
44
|
/** Restarts a launchd job the install path deliberately left alone. `restart` only. */
|
|
@@ -148,6 +155,8 @@ export async function assertSchedulerRegistrationBeforeStart(
|
|
|
148
155
|
* macOS/Linux: re-run the user-level install/reload path.
|
|
149
156
|
*/
|
|
150
157
|
export async function repairService(deps: RepairServiceDeps = {}): Promise<void> {
|
|
158
|
+
const latch = deps.supervisionLatch ?? createSupervisionLatch();
|
|
159
|
+
assertNoDesktopSupervision(deps.inspectSupervision, latch);
|
|
151
160
|
const diagnose = deps.diagnose ?? diagnoseService;
|
|
152
161
|
const platform = deps.platform ?? process.platform;
|
|
153
162
|
const diag = diagnose();
|
|
@@ -176,14 +185,18 @@ export async function repairService(deps: RepairServiceDeps = {}): Promise<void>
|
|
|
176
185
|
|
|
177
186
|
(deps.assertEnv ?? assertServiceEnvironmentMatchesInstall)();
|
|
178
187
|
(deps.assertAuth ?? assertServiceAuthEnvironment)();
|
|
188
|
+
const runtime = Object.freeze({ ...(deps.selectRuntime ?? durableBunRuntime)() });
|
|
189
|
+
const configDir = (deps.configDir ?? getConfigDir)();
|
|
190
|
+
(deps.assertRuntimeWritable ?? assertSelectedRuntimeWritable)(runtime, configDir, { platform, rootWasAbsent: !existsSync(configDir) });
|
|
179
191
|
|
|
180
192
|
const temporaryLauncher = (deps.launcherPathDiagnostic ?? (() => serviceLauncherPathDiagnostic(undefined, platform, true)))();
|
|
181
193
|
if (temporaryLauncher) console.warn(`⚠️ ${temporaryLauncher}`);
|
|
182
194
|
|
|
183
195
|
if (platform === "win32") {
|
|
184
196
|
if (diag.backend === "native") {
|
|
185
|
-
await (deps.repairNative ?? (() => installWinswService(defaultWinswEntry(serviceSourceDir))))();
|
|
186
|
-
(deps.
|
|
197
|
+
await (deps.repairNative ?? ((selected: DurableBunRuntime) => installWinswService(defaultWinswEntry(serviceSourceDir, selected))))(runtime);
|
|
198
|
+
assertNoDesktopSupervision(deps.inspectSupervision, latch);
|
|
199
|
+
(deps.writeNativeState ?? ((selected: DurableBunRuntime) => writeServiceInstallState("native", undefined, {}, selected)))(runtime);
|
|
187
200
|
return;
|
|
188
201
|
}
|
|
189
202
|
const readSchedulerXml = deps.readSchedulerXml ?? statusWindowsXml;
|
|
@@ -243,7 +256,7 @@ export async function repairService(deps: RepairServiceDeps = {}): Promise<void>
|
|
|
243
256
|
);
|
|
244
257
|
}
|
|
245
258
|
try { (deps.stopScheduler ?? stopWindows)(); } catch { /* not running */ }
|
|
246
|
-
(deps.writeSchedulerAssets ?? writeWindowsSchedulerAssets)();
|
|
259
|
+
(deps.writeSchedulerAssets ?? writeWindowsSchedulerAssets)(runtime);
|
|
247
260
|
// Rewriting the on-disk assets does not touch the definition Task Scheduler holds, so a
|
|
248
261
|
// task registered by an older version keeps its old triggers forever: status reports it
|
|
249
262
|
// stale, tells the user to run repair, and repair changes nothing it complains about.
|
|
@@ -311,6 +324,7 @@ export async function repairService(deps: RepairServiceDeps = {}): Promise<void>
|
|
|
311
324
|
}
|
|
312
325
|
if (probe.status === "absent") {
|
|
313
326
|
try {
|
|
327
|
+
assertNoDesktopSupervision(deps.inspectSupervision, latch);
|
|
314
328
|
await (deps.restoreSchedulerIfAbsent ?? restoreWindowsSchedulerTaskIfAbsent)(registeredXml);
|
|
315
329
|
restartExpectedXml = registeredXml;
|
|
316
330
|
} catch (error) {
|
|
@@ -333,6 +347,7 @@ export async function repairService(deps: RepairServiceDeps = {}): Promise<void>
|
|
|
333
347
|
"The Task Scheduler registration changed again before restart; the newer definition was preserved and not started.",
|
|
334
348
|
"Task Scheduler state remained unreadable before restart; the registration was preserved and not started.",
|
|
335
349
|
);
|
|
350
|
+
assertNoDesktopSupervision(deps.inspectSupervision, latch);
|
|
336
351
|
(deps.startScheduler ?? startWindows)();
|
|
337
352
|
} catch (error) {
|
|
338
353
|
recoveryErrors.push(error);
|
|
@@ -358,8 +373,9 @@ export async function repairService(deps: RepairServiceDeps = {}): Promise<void>
|
|
|
358
373
|
"Task Scheduler registration changed before restart; the current definition was preserved and not started.",
|
|
359
374
|
"Task Scheduler registration became unreadable before restart; it was preserved and not started.",
|
|
360
375
|
);
|
|
376
|
+
assertNoDesktopSupervision(deps.inspectSupervision, latch);
|
|
361
377
|
(deps.startScheduler ?? startWindows)();
|
|
362
|
-
(deps.writeSchedulerState ?? (() => writeServiceInstallState("scheduler")))();
|
|
378
|
+
(deps.writeSchedulerState ?? ((selected: DurableBunRuntime) => writeServiceInstallState("scheduler", undefined, {}, selected)))(runtime);
|
|
363
379
|
return;
|
|
364
380
|
}
|
|
365
381
|
if (platform === "darwin") {
|
package/src/service/state.ts
CHANGED
|
@@ -360,8 +360,8 @@ export function parseServiceInstallState(value: unknown): ServiceInstallState |
|
|
|
360
360
|
* it describes the install that just ran. {@link ServiceInstallState.ownership} deliberately
|
|
361
361
|
* is not part of it.
|
|
362
362
|
*/
|
|
363
|
-
function installProvenanceRecord(backend: ServiceBackend, launcherPath
|
|
364
|
-
const { bun, cli } = cliEntry();
|
|
363
|
+
function installProvenanceRecord(backend: ServiceBackend, launcherPath: string | null | undefined, runtime: DurableBunRuntime): ServiceInstallState {
|
|
364
|
+
const { bun, cli } = cliEntry(runtime);
|
|
365
365
|
const codexHome = currentCodexHome();
|
|
366
366
|
return {
|
|
367
367
|
version: 2,
|
|
@@ -391,9 +391,10 @@ export function writeServiceInstallState(
|
|
|
391
391
|
backend: ServiceBackend = "scheduler",
|
|
392
392
|
launcherPath?: string | null,
|
|
393
393
|
deps: ServiceStateSwapDeps = {},
|
|
394
|
+
runtime: DurableBunRuntime = durableBunRuntime(),
|
|
394
395
|
): void {
|
|
395
396
|
swapServiceInstallState(current => ({
|
|
396
|
-
...installProvenanceRecord(backend, launcherPath),
|
|
397
|
+
...installProvenanceRecord(backend, launcherPath, runtime),
|
|
397
398
|
...preservedConsent(current),
|
|
398
399
|
}), deps);
|
|
399
400
|
}
|
package/src/service/systemd.ts
CHANGED
|
@@ -102,13 +102,22 @@ WantedBy=default.target
|
|
|
102
102
|
|
|
103
103
|
/** The per-user runtime dir systemd creates (holds the user-bus socket), or null. */
|
|
104
104
|
function userRuntimeDir(): string | null {
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
105
|
+
const env = { ...process.env };
|
|
106
|
+
if (env.XDG_RUNTIME_DIR && !existsSync(env.XDG_RUNTIME_DIR)) delete env.XDG_RUNTIME_DIR;
|
|
107
|
+
return systemdUserBusEnvironment(env).XDG_RUNTIME_DIR || null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Return explicit SSH user-bus discovery without changing the caller's environment. */
|
|
111
|
+
export function systemdUserBusEnvironment(environment: NodeJS.ProcessEnv = process.env, deps: {
|
|
112
|
+
uid?: number; exists?: (path: string) => boolean;
|
|
113
|
+
} = {}): NodeJS.ProcessEnv {
|
|
114
|
+
const env = { ...environment };
|
|
115
|
+
if (!env.XDG_RUNTIME_DIR) {
|
|
116
|
+
const uid = deps.uid ?? process.getuid?.();
|
|
117
|
+
const candidate = uid === undefined ? null : `/run/user/${uid}`;
|
|
118
|
+
if (candidate && (deps.exists ?? existsSync)(candidate)) env.XDG_RUNTIME_DIR = candidate;
|
|
110
119
|
}
|
|
111
|
-
return
|
|
120
|
+
return env;
|
|
112
121
|
}
|
|
113
122
|
|
|
114
123
|
/**
|
|
@@ -119,8 +128,8 @@ function userRuntimeDir(): string | null {
|
|
|
119
128
|
*/
|
|
120
129
|
function ensureUserBusEnv(): void {
|
|
121
130
|
if (process.env.XDG_RUNTIME_DIR) return;
|
|
122
|
-
const
|
|
123
|
-
if (
|
|
131
|
+
const env = systemdUserBusEnvironment();
|
|
132
|
+
if (env.XDG_RUNTIME_DIR) process.env.XDG_RUNTIME_DIR = env.XDG_RUNTIME_DIR;
|
|
124
133
|
}
|
|
125
134
|
|
|
126
135
|
export function isSystemd(): boolean {
|
|
@@ -3,6 +3,8 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { win32 } from "node:path";
|
|
4
4
|
import { winswXmlPath } from "../lib/winsw";
|
|
5
5
|
import { hardenSecretPath } from "../lib/windows-secret-acl";
|
|
6
|
+
import { durableBunRuntime, type DurableBunRuntime } from "../lib/bun-runtime";
|
|
7
|
+
import { assertSelectedRuntimeWritable, type RuntimePreflightDeps } from "../lib/bun-runtime-preflight";
|
|
6
8
|
import { parseBakedListenPort } from "./health";
|
|
7
9
|
import { windowsServiceScriptPath } from "./state";
|
|
8
10
|
import { execFileSync } from "node:child_process";
|
|
@@ -18,7 +20,7 @@ import { killWindowsSchedulerWrappers } from "../lib/windows-service-wrappers";
|
|
|
18
20
|
import { isTestHomeGuardArmed } from "../lib/test-home-guard";
|
|
19
21
|
import { writeServiceApiTokenFile } from "./guards";
|
|
20
22
|
import type { ServiceStopOutcome } from "./orchestration";
|
|
21
|
-
import { TASK, windowsLauncherVbsPath, windowsTaskXmlPath, serviceStatePath, writeServiceInstallState, serviceSourceDir } from "./state";
|
|
23
|
+
import { TASK, windowsLauncherVbsPath, windowsTaskXmlPath, serviceStatePath, writeServiceInstallState, serviceSourceDir, cliEntry } from "./state";
|
|
22
24
|
import { decodeSchtasksOutput, querySchtasks, schtasks, type WindowsSchedulerTaskProbe, schtasksErrorDetail, probeWindowsSchedulerTask, rollbackWindowsSchedulerTaskOwnedByAttempt } from "./windows-scheduler";
|
|
23
25
|
import { buildWindowsServiceScript, buildWindowsSchtasksCreateArgs, buildWindowsSchtasksCreateArgsForXml, buildWindowsLauncherVbs, resolvedWindowsTaskSid, buildWindowsTaskXmlDocument, windowsTaskRegistrationOwnedByAttempt, windowsTaskRegistrationHealthy } from "./windows-taskxml";
|
|
24
26
|
|
|
@@ -108,11 +110,11 @@ function writeServiceAssetWithRetry(path: string, content: string, encoding: "ut
|
|
|
108
110
|
* Rewrite on-disk scheduler assets (script/VBS/XML) without itself registering the task.
|
|
109
111
|
* Fresh install creates it afterwards; repair does so only when the live definition is stale.
|
|
110
112
|
*/
|
|
111
|
-
export function writeWindowsSchedulerAssets(): void {
|
|
113
|
+
export function writeWindowsSchedulerAssets(runtime: DurableBunRuntime = durableBunRuntime()): void {
|
|
112
114
|
if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
|
|
113
115
|
writeServiceApiTokenFile();
|
|
114
116
|
const script = windowsServiceScriptPath();
|
|
115
|
-
writeServiceAssetWithRetry(script, buildWindowsServiceScript(), "utf8");
|
|
117
|
+
writeServiceAssetWithRetry(script, buildWindowsServiceScript(cliEntry(runtime)), "utf8");
|
|
116
118
|
// UTF-16LE + BOM: a BOM-less UTF-8 VBS mis-decodes non-ASCII (e.g. Korean) profile
|
|
117
119
|
// paths on some WSH/codepage combinations — same contract as the task XML below.
|
|
118
120
|
writeServiceAssetWithRetry(windowsLauncherVbsPath(), `\uFEFF${buildWindowsLauncherVbs(script)}`, "utf16le");
|
|
@@ -597,16 +599,40 @@ export function removeNativeWindowsServiceForScheduler(
|
|
|
597
599
|
}
|
|
598
600
|
}
|
|
599
601
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
602
|
+
/** External Windows effects shared by the gated entrypoints and admitted commit paths. */
|
|
603
|
+
export interface WindowsServiceInstallDeps extends RuntimePreflightDeps {
|
|
604
|
+
recordSchedulerOwnership?: () => boolean;
|
|
605
|
+
removeNativeService?: () => void;
|
|
606
|
+
stopScheduler?: () => void;
|
|
607
|
+
writeSchedulerAssets?: (runtime: DurableBunRuntime) => void;
|
|
608
|
+
schedulerCommand?: typeof schtasks;
|
|
609
|
+
writeState?: (backend: "scheduler" | "native", runtime: DurableBunRuntime) => void;
|
|
610
|
+
assertNativeAccount?: () => void;
|
|
611
|
+
prepareNativeConfig?: () => void;
|
|
612
|
+
uninstallScheduler?: () => void;
|
|
613
|
+
installNativeService?: typeof installWinswService;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
export function installWindows(selected?: DurableBunRuntime, deps: WindowsServiceInstallDeps = {}): void {
|
|
617
|
+
const runtime = Object.freeze({ ...(selected ?? (deps.selectRuntime ?? durableBunRuntime)()) });
|
|
618
|
+
const configDir = (deps.configDir ?? getConfigDir)();
|
|
619
|
+
(deps.assertRuntimeWritable ?? assertSelectedRuntimeWritable)(runtime, configDir, { platform: deps.platform, rootWasAbsent: !existsSync(configDir) });
|
|
620
|
+
commitWindowsInstall(runtime, deps);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** Internal commit boundary: the caller has admitted this frozen runtime before cleanup. */
|
|
624
|
+
export function commitWindowsInstall(runtime: DurableBunRuntime, deps: WindowsServiceInstallDeps = {}): void {
|
|
625
|
+
(deps.recordSchedulerOwnership ?? recordWindowsSchedulerOwnership)();
|
|
626
|
+
(deps.removeNativeService ?? removeNativeWindowsServiceForScheduler)();
|
|
603
627
|
// End a running task BEFORE rewriting the assets it is executing — cmd.exe reading the
|
|
604
628
|
// script mid-rewrite runs a torn batch file, and its open handle can fail the write.
|
|
605
|
-
try { stopWindows(); } catch { /* not running */ }
|
|
606
|
-
writeWindowsSchedulerAssets();
|
|
607
|
-
schtasks
|
|
608
|
-
|
|
609
|
-
|
|
629
|
+
try { (deps.stopScheduler ?? stopWindows)(); } catch { /* not running */ }
|
|
630
|
+
(deps.writeSchedulerAssets ?? writeWindowsSchedulerAssets)(runtime);
|
|
631
|
+
const command = deps.schedulerCommand ?? schtasks;
|
|
632
|
+
command(buildWindowsSchtasksCreateArgs(windowsServiceScriptPath()));
|
|
633
|
+
command(["/run", "/tn", TASK]);
|
|
634
|
+
if (deps.writeState) deps.writeState("scheduler", runtime);
|
|
635
|
+
else writeServiceInstallState("scheduler", undefined, {}, runtime);
|
|
610
636
|
}
|
|
611
637
|
|
|
612
638
|
/**
|
|
@@ -748,26 +774,38 @@ function readWindowsPrincipalSource(): string | null {
|
|
|
748
774
|
}
|
|
749
775
|
}
|
|
750
776
|
|
|
751
|
-
export async function installWindowsNative(): Promise<void> {
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
777
|
+
export async function installWindowsNative(selected?: DurableBunRuntime, deps: WindowsServiceInstallDeps = {}): Promise<void> {
|
|
778
|
+
const runtime = Object.freeze({ ...(selected ?? (deps.selectRuntime ?? durableBunRuntime)()) });
|
|
779
|
+
const configDir = (deps.configDir ?? getConfigDir)();
|
|
780
|
+
(deps.assertRuntimeWritable ?? assertSelectedRuntimeWritable)(runtime, configDir, { platform: deps.platform, rootWasAbsent: !existsSync(configDir) });
|
|
781
|
+
await commitWindowsNativeInstall(runtime, deps);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/** Internal commit boundary; direct native installs go through the public admission gate. */
|
|
785
|
+
export async function commitWindowsNativeInstall(runtime: DurableBunRuntime, deps: WindowsServiceInstallDeps = {}): Promise<void> {
|
|
786
|
+
(deps.assertNativeAccount ?? assertWindowsNativeServiceAccountSupported)();
|
|
787
|
+
if (deps.prepareNativeConfig) deps.prepareNativeConfig();
|
|
788
|
+
else {
|
|
789
|
+
recordOwnedConfigPath(getConfigDir(), serviceStatePath());
|
|
790
|
+
if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
|
|
791
|
+
writeServiceApiTokenFile();
|
|
792
|
+
}
|
|
793
|
+
const command = deps.schedulerCommand ?? schtasks;
|
|
756
794
|
let hadScheduler = false;
|
|
757
795
|
try {
|
|
758
|
-
hadScheduler =
|
|
796
|
+
hadScheduler = command(["/query", "/tn", TASK]).includes(TASK);
|
|
759
797
|
} catch { /* task absent */ }
|
|
760
798
|
if (hadScheduler) {
|
|
761
799
|
console.log("🔁 Removing the Task Scheduler backend before installing the native (WinSW) service...");
|
|
762
|
-
try { stopWindows(); } catch { /* not running */ }
|
|
800
|
+
try { (deps.stopScheduler ?? stopWindows)(); } catch { /* not running */ }
|
|
763
801
|
try {
|
|
764
|
-
uninstallWindows();
|
|
802
|
+
(deps.uninstallScheduler ?? uninstallWindows)();
|
|
765
803
|
} catch (err) {
|
|
766
804
|
throw new Error(`Cannot remove the Task Scheduler backend before switching to native: ${err instanceof Error ? err.message : String(err)}`);
|
|
767
805
|
}
|
|
768
806
|
// Verify removal — schtasks /delete can silently fail if UAC or policy blocks it.
|
|
769
807
|
try {
|
|
770
|
-
if (
|
|
808
|
+
if (command(["/query", "/tn", TASK]).includes(TASK)) {
|
|
771
809
|
throw new Error("Task Scheduler backend still present after removal — aborting switch.");
|
|
772
810
|
}
|
|
773
811
|
} catch (e) {
|
|
@@ -776,12 +814,13 @@ export async function installWindowsNative(): Promise<void> {
|
|
|
776
814
|
}
|
|
777
815
|
}
|
|
778
816
|
try {
|
|
779
|
-
await installWinswService(defaultWinswEntry(serviceSourceDir));
|
|
817
|
+
await (deps.installNativeService ?? installWinswService)(defaultWinswEntry(serviceSourceDir, runtime));
|
|
780
818
|
} catch (err) {
|
|
781
819
|
if (hadScheduler) console.error("⚠️ Native install failed AFTER removing the Task Scheduler backend — no service is installed now. Run `ocx service install` to restore the scheduler backend, or retry `--native`.");
|
|
782
820
|
throw err;
|
|
783
821
|
}
|
|
784
|
-
|
|
822
|
+
if (deps.writeState) deps.writeState("native", runtime);
|
|
823
|
+
else writeServiceInstallState("native", undefined, {}, runtime);
|
|
785
824
|
}
|
|
786
825
|
|
|
787
826
|
export function startWindows(): void { schtasks(["/run", "/tn", TASK]); }
|