@bitkyc08/opencodex 2.10.0 → 2.10.1
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/AGENTS_INSTALL.md +77 -0
- package/README.md +4 -10
- package/bin/ocx.mjs +71 -18
- package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
- package/gui/dist/assets/index-ChZQsmBY.js +70 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +8 -4
- package/src/adapters/anthropic.ts +208 -14
- package/src/adapters/base.ts +16 -5
- package/src/adapters/cursor/effort-map.ts +3 -2
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/live-transport.ts +105 -95
- package/src/adapters/cursor/native-exec.ts +32 -6
- package/src/adapters/cursor/protobuf-request.ts +20 -15
- package/src/adapters/cursor/request-builder.ts +21 -7
- package/src/adapters/cursor/types.ts +7 -0
- package/src/adapters/google-antigravity-replay.ts +237 -21
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +50 -9
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +10 -2
- package/src/adapters/kiro-tools.ts +10 -1
- package/src/adapters/kiro.ts +37 -11
- package/src/adapters/openai-chat.ts +284 -83
- package/src/adapters/openai-responses.ts +182 -24
- package/src/bridge.ts +177 -7
- package/src/chat/outbound.ts +78 -23
- package/src/claude/agents-inject.ts +27 -5
- package/src/claude/inbound.ts +11 -1
- package/src/claude/model-info.ts +13 -10
- package/src/claude/outbound.ts +17 -0
- package/src/cli/account-api.ts +24 -0
- package/src/cli/account-auth.ts +31 -6
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +5 -0
- package/src/cli/claude.ts +2 -1
- package/src/cli/doctor.ts +93 -22
- package/src/cli/export-command.ts +26 -12
- package/src/cli/help.ts +8 -6
- package/src/cli/index.ts +56 -22
- package/src/cli/integrations.ts +84 -1
- package/src/cli/observe.ts +54 -1
- package/src/cli/opencode.ts +2 -1
- package/src/cli/provider-runtime.ts +18 -1
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +6 -3
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +1 -1
- package/src/cli/v2.ts +36 -9
- package/src/clients/config-export.ts +687 -10
- package/src/codex/account-lifecycle.ts +30 -5
- package/src/codex/account-usability.ts +22 -2
- package/src/codex/admission.ts +255 -0
- package/src/codex/auth-api.ts +427 -140
- package/src/codex/auth-context.ts +155 -30
- package/src/codex/autostart-health.ts +8 -1
- package/src/codex/catalog/account-models.ts +62 -0
- package/src/codex/catalog/aggregation.ts +14 -1
- package/src/codex/catalog/bundled.ts +282 -32
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/metadata.ts +51 -6
- package/src/codex/catalog/parsing.ts +6 -3
- package/src/codex/catalog/provider-fetch.ts +576 -41
- package/src/codex/catalog/sync.ts +505 -66
- package/src/codex/catalog-admission.ts +197 -0
- package/src/codex/catalog-write-serialization.ts +241 -0
- package/src/codex/catalog.ts +2 -1
- package/src/codex/codex-write-lock.ts +372 -0
- package/src/codex/convergence-types.ts +593 -0
- package/src/codex/convergence.ts +441 -0
- package/src/codex/desired-state.ts +177 -0
- package/src/codex/features.ts +52 -8
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +257 -0
- package/src/codex/history-lock.ts +241 -0
- package/src/codex/history-migration-guardian.ts +18 -5
- package/src/codex/history-provider.ts +9 -2
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +176 -0
- package/src/codex/inject-coordination.ts +245 -0
- package/src/codex/inject.ts +605 -124
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +80 -0
- package/src/codex/journal.ts +10 -1
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/management-convergence.ts +114 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +167 -0
- package/src/codex/native-main-lock-file.ts +158 -0
- package/src/codex/native-main-owner.ts +315 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1512 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +340 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +557 -0
- package/src/codex/project-config-warnings.ts +18 -4
- package/src/codex/prompt-journal.ts +311 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +224 -0
- package/src/codex/quota.ts +86 -3
- package/src/codex/routing.ts +299 -62
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +39 -13
- package/src/codex/subagent-model-fallback.ts +73 -12
- package/src/codex/transition-state.ts +604 -0
- package/src/codex/upstream-host-health.ts +70 -0
- package/src/codex/user-identity.ts +266 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/config.ts +562 -26
- package/src/generated/jawcode-model-metadata.ts +2 -2
- package/src/grok/inject.ts +15 -4
- package/src/grok/inspect.ts +45 -0
- package/src/images/loop.ts +113 -20
- package/src/integrations/config-io.ts +151 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/native/ownership-preflight.ts +165 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +101 -0
- package/src/integrations/serialize.ts +235 -0
- package/src/integrations/state.ts +290 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer.ts +492 -0
- package/src/lib/bounded-body.ts +46 -8
- package/src/lib/bun-runtime.ts +110 -1
- package/src/lib/bun-stream-caps.ts +2 -1
- package/src/lib/redact.ts +407 -2
- package/src/lib/shadow-call.ts +24 -0
- package/src/lib/translator-budget.ts +10 -0
- package/src/lib/upstream-reachability.ts +91 -0
- package/src/lib/upstream-retry.ts +154 -2
- package/src/lib/windows-secret-acl.ts +212 -11
- package/src/lib/winsw.ts +9 -3
- package/src/oauth/index.ts +61 -3
- package/src/oauth/key-providers.ts +4 -0
- package/src/oauth/kiro.ts +50 -6
- package/src/oauth/store.ts +31 -6
- package/src/oauth/token-guardian.ts +9 -3
- package/src/providers/codex-capacity.ts +288 -0
- package/src/providers/derive.ts +33 -1
- package/src/providers/free-directory.ts +3 -1
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/openai-sidecar.ts +64 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota.ts +334 -26
- package/src/providers/registry.ts +284 -16
- package/src/providers/xai-transport.ts +11 -4
- package/src/responses/compaction.ts +8 -1
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/parser.ts +44 -2
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +105 -0
- package/src/responses/spill-store.ts +45 -8
- package/src/responses/state.ts +161 -17
- package/src/router.ts +302 -16
- package/src/routing/analytics.ts +377 -0
- package/src/routing/capability.ts +204 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +444 -0
- package/src/routing/health.ts +401 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +590 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile.ts +423 -0
- package/src/routing/quota.ts +124 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +686 -0
- package/src/server/auth-cors.ts +46 -6
- package/src/server/chat-completions.ts +28 -13
- package/src/server/claude-messages.ts +23 -15
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +10 -1
- package/src/server/index.ts +238 -52
- package/src/server/lifecycle.ts +228 -9
- package/src/server/live.ts +14 -3
- package/src/server/management/agent-settings-routes.ts +64 -14
- package/src/server/management/combo-routes.ts +5 -5
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +42 -2
- package/src/server/management/integration-routes.ts +538 -0
- package/src/server/management/logs-usage-routes.ts +1 -1
- package/src/server/management/model-routes.ts +32 -113
- package/src/server/management/model-rows.ts +117 -0
- package/src/server/management/native-integration-routes.ts +587 -0
- package/src/server/management/oauth-account-routes.ts +1 -1
- package/src/server/management/provider-routes.ts +218 -117
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +177 -0
- package/src/server/management/shared.ts +2 -2
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +276 -24
- package/src/server/management/system-routes.ts +4 -0
- package/src/server/management-api.ts +84 -9
- package/src/server/management-auth.ts +43 -5
- package/src/server/relay-eager.ts +82 -42
- package/src/server/relay.ts +120 -6
- package/src/server/request-log.ts +26 -6
- package/src/server/responses/collaboration.ts +63 -8
- package/src/server/responses/compact.ts +272 -41
- package/src/server/responses/core.ts +730 -132
- package/src/server/responses/fetch-helpers.ts +15 -1
- package/src/server/responses-item-id-repair.ts +32 -3
- package/src/server/responses-json-events.ts +52 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/search.ts +51 -6
- package/src/server/sse-payload-rewrite.ts +89 -12
- package/src/server/startup-health-cache.ts +7 -1
- package/src/server/ws-bridge.ts +11 -17
- package/src/service-manager-probe.ts +297 -0
- package/src/service.ts +222 -32
- package/src/tray/windows-tray.ps1 +9 -0
- package/src/tray/windows.ts +15 -7
- package/src/types.ts +194 -14
- package/src/update/index.ts +13 -13
- package/src/update/job.ts +24 -21
- package/src/update/notify.ts +7 -3
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +129 -10
- package/src/usage/log.ts +50 -15
- package/src/usage/summary.ts +4 -4
- package/src/vision/index.ts +6 -1
- package/src/web-search/loop.ts +161 -34
- package/gui/dist/assets/index-OY43ubAq.css +0 -1
- package/gui/dist/assets/index-YwNnKZcL.js +0 -67
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
package/src/service.ts
CHANGED
|
@@ -12,10 +12,11 @@ import { homedir } from "node:os";
|
|
|
12
12
|
import { dirname, join, resolve } from "node:path";
|
|
13
13
|
import { expandUserPath, getConfigDir, readPid, removePid, removeRuntimePort, verifyPidIdentity } from "./config";
|
|
14
14
|
import { loadConfig } from "./config";
|
|
15
|
-
import { restoreNativeCodex } from "./codex/inject";
|
|
15
|
+
import { restoreNativeCodex, restoreNativeCodexAsync } from "./codex/inject";
|
|
16
16
|
import { stripGrokConfig } from "./grok/inject";
|
|
17
17
|
import { isWslRuntime } from "./codex/home";
|
|
18
|
-
import {
|
|
18
|
+
import { BUN_RUNTIME_PATH_ENV, BUN_RUNTIME_SOURCE_ENV, durableBunRuntime } from "./lib/bun-runtime";
|
|
19
|
+
import type { BunRuntimeSource } from "./lib/bun-runtime";
|
|
19
20
|
import { isProcessAlive, stopProxy } from "./lib/process-control";
|
|
20
21
|
import { serviceApiTokenFilePath } from "./lib/service-secrets";
|
|
21
22
|
import { randomUUID } from "node:crypto";
|
|
@@ -43,11 +44,15 @@ const TASK = "opencodex-proxy";
|
|
|
43
44
|
|
|
44
45
|
export type ServiceBackend = "scheduler" | "native";
|
|
45
46
|
|
|
46
|
-
function cliEntry(): { bun: string; cli: string } {
|
|
47
|
+
function cliEntry(): { bun: string; bunRuntimeSource: BunRuntimeSource; cli: string } {
|
|
47
48
|
// Bake the bundled Bun (npm global prefix, survives `ocx update`) rather than
|
|
48
49
|
// a transient system Bun, so launchd/systemd/schtasks keep resolving even if a
|
|
49
50
|
// standalone Bun is later removed. The CLI entry lives at src/cli/index.ts.
|
|
50
|
-
|
|
51
|
+
//
|
|
52
|
+
// Path and provenance come from ONE resolution so the marker can never describe a
|
|
53
|
+
// different binary than the one actually baked.
|
|
54
|
+
const runtime = durableBunRuntime();
|
|
55
|
+
return { bun: runtime.path, bunRuntimeSource: runtime.source, cli: join(import.meta.dir, "cli", "index.ts") };
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
function plistPath(): string {
|
|
@@ -169,13 +174,80 @@ function readServiceInstallState(): ServiceInstallState | null {
|
|
|
169
174
|
return null;
|
|
170
175
|
}
|
|
171
176
|
|
|
172
|
-
/**
|
|
177
|
+
/** What ONE state path said. Absent, unreadable and invalid are different answers. */
|
|
178
|
+
export type ServiceStateEvidence =
|
|
179
|
+
| { readonly path: string; readonly kind: "absent" }
|
|
180
|
+
| { readonly path: string; readonly kind: "unreadable"; readonly reason: string }
|
|
181
|
+
| { readonly path: string; readonly kind: "invalid" }
|
|
182
|
+
| { readonly path: string; readonly kind: "valid"; readonly state: ServiceInstallState };
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Every state path, with what each one said.
|
|
186
|
+
*
|
|
187
|
+
* `readServiceInstallState` returns the FIRST path that parsed and discards the
|
|
188
|
+
* rest, so a valid mirror beside a corrupt one reads as clean. That is the right
|
|
189
|
+
* behavior for callers that just need the install state; it is the wrong input
|
|
190
|
+
* for deciding ownership, where a disagreement between mirrors is exactly the
|
|
191
|
+
* evidence that matters.
|
|
192
|
+
*/
|
|
193
|
+
export function inspectServiceStateEvidence(
|
|
194
|
+
paths: readonly string[] = serviceStatePaths(),
|
|
195
|
+
): readonly ServiceStateEvidence[] {
|
|
196
|
+
return paths.map((path): ServiceStateEvidence => {
|
|
197
|
+
let raw: string;
|
|
198
|
+
try {
|
|
199
|
+
raw = readFileSync(path, "utf8");
|
|
200
|
+
} catch (error) {
|
|
201
|
+
const code = error && typeof error === "object" && "code" in error
|
|
202
|
+
? String((error as { code?: unknown }).code)
|
|
203
|
+
: "";
|
|
204
|
+
// ENOENT is an answer. EACCES, ENOTDIR and the rest are a failure to ask,
|
|
205
|
+
// and collapsing them into absence is how a locked-down state file would
|
|
206
|
+
// become permission to write.
|
|
207
|
+
if (code === "ENOENT") return { path, kind: "absent" };
|
|
208
|
+
return { path, kind: "unreadable", reason: code || String(error) };
|
|
209
|
+
}
|
|
210
|
+
let parsed: ServiceInstallState | null;
|
|
211
|
+
try {
|
|
212
|
+
parsed = parseServiceInstallState(JSON.parse(raw));
|
|
213
|
+
} catch {
|
|
214
|
+
return { path, kind: "invalid" };
|
|
215
|
+
}
|
|
216
|
+
return parsed ? { path, kind: "valid", state: parsed } : { path, kind: "invalid" };
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** The homes this process is actually using, for comparison against a claim. */
|
|
221
|
+
export function currentServiceHomes(): { codexHome: string; opencodexHome: string } {
|
|
222
|
+
return { codexHome: currentCodexHome(), opencodexHome: currentOpenCodexHome() };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function serviceHomeMatches(a: string, b: string): boolean {
|
|
226
|
+
return normalizePathForCompare(a) === normalizePathForCompare(b);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Single accessor for backend-sensitive service code — v1/legacy state maps to scheduler. */
|
|
173
230
|
export function readServiceBackend(): ServiceBackend {
|
|
174
231
|
return readServiceInstallState()?.backend === "native" ? "native" : "scheduler";
|
|
175
232
|
}
|
|
176
233
|
|
|
177
|
-
/**
|
|
234
|
+
/**
|
|
235
|
+
* The `ocx` argv that refreshes an already-installed service after an update.
|
|
236
|
+
*
|
|
237
|
+
* `repair` discovers the installed backend itself and, on Windows scheduler installs,
|
|
238
|
+
* rewrites the wrapper assets and restarts the existing task WITHOUT `schtasks /create`
|
|
239
|
+
* (see repairService below). `install` always reaches `/create`, which requires
|
|
240
|
+
* elevation — so an ordinary non-elevated `ocx update` used to stop a working proxy and
|
|
241
|
+
* then fail to bring its service back.
|
|
242
|
+
*
|
|
243
|
+
* The historical export name is kept for callers outside this module.
|
|
244
|
+
*/
|
|
178
245
|
export function serviceReinstallArgs(): string[] {
|
|
246
|
+
return ["service", "repair"];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** The `ocx` argv that registers a service from scratch, preserving the chosen backend. */
|
|
250
|
+
export function serviceInstallArgs(): string[] {
|
|
179
251
|
return readServiceBackend() === "native" ? ["service", "install", "--native"] : ["service", "install"];
|
|
180
252
|
}
|
|
181
253
|
|
|
@@ -229,6 +301,7 @@ export function assertServiceEnvironmentMatchesInstall(): void {
|
|
|
229
301
|
}
|
|
230
302
|
}
|
|
231
303
|
|
|
304
|
+
|
|
232
305
|
function plistString(value: string): string {
|
|
233
306
|
return value
|
|
234
307
|
.replace(/&/g, "&")
|
|
@@ -243,13 +316,31 @@ function isLoopbackHostname(hostname: string | undefined): boolean {
|
|
|
243
316
|
return normalized === "" || normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1" || normalized === "[::1]";
|
|
244
317
|
}
|
|
245
318
|
|
|
319
|
+
/**
|
|
320
|
+
* The `ocx` command a user should rerun for the service state they actually have.
|
|
321
|
+
*
|
|
322
|
+
* `installed` alone is not enough: `repairService()` refuses a Task-Scheduler-plus-WinSW
|
|
323
|
+
* conflict outright, so recommending repair there names a command guaranteed to fail.
|
|
324
|
+
* Install IS the valid conflict recovery, because `installWindows` removes the native
|
|
325
|
+
* backend first. Exported so the guard tests the real selector rather than a copy of it.
|
|
326
|
+
*/
|
|
327
|
+
export function serviceRetryCommand(
|
|
328
|
+
diag: Pick<ServiceDiagnostic, "installed" | "conflict"> = diagnoseService(),
|
|
329
|
+
): string {
|
|
330
|
+
return diag.installed && !diag.conflict ? "ocx service repair" : "ocx service install";
|
|
331
|
+
}
|
|
332
|
+
|
|
246
333
|
export function assertServiceAuthEnvironment(): void {
|
|
247
334
|
const config = loadConfig();
|
|
248
335
|
if (isLoopbackHostname(config.hostname)) return;
|
|
249
336
|
if (process.env.OPENCODEX_API_AUTH_TOKEN?.trim()) return;
|
|
337
|
+
// Reached from `service repair` as well as `install`, so name a command that can
|
|
338
|
+
// actually succeed (see serviceRetryCommand).
|
|
339
|
+
const diag = diagnoseService();
|
|
340
|
+
const retry = serviceRetryCommand(diag);
|
|
250
341
|
throw new Error(
|
|
251
|
-
|
|
252
|
-
|
|
342
|
+
`OPENCODEX_API_AUTH_TOKEN is required before ${diag.installed ? "refreshing" : "installing"} a service `
|
|
343
|
+
+ `for non-loopback hostname. Set it in the same shell, then rerun \`${retry}\`.`,
|
|
253
344
|
);
|
|
254
345
|
}
|
|
255
346
|
|
|
@@ -268,13 +359,15 @@ function writeServiceApiTokenFile(): string | null {
|
|
|
268
359
|
}
|
|
269
360
|
|
|
270
361
|
export function buildPlist(): string {
|
|
271
|
-
const { bun, cli } = cliEntry();
|
|
362
|
+
const { bun, bunRuntimeSource, cli } = cliEntry();
|
|
272
363
|
const log = logPath();
|
|
273
364
|
const path = process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin";
|
|
274
365
|
const codexHome = process.env.CODEX_HOME?.trim();
|
|
275
366
|
const opencodexHome = process.env.OPENCODEX_HOME?.trim();
|
|
276
367
|
const envLines = [
|
|
277
368
|
` <key>OCX_SERVICE</key><string>1</string>`,
|
|
369
|
+
` <key>${BUN_RUNTIME_SOURCE_ENV}</key><string>${bunRuntimeSource}</string>`,
|
|
370
|
+
` <key>${BUN_RUNTIME_PATH_ENV}</key><string>${plistString(bun)}</string>`,
|
|
278
371
|
` <key>PATH</key><string>${plistString(path)}</string>`,
|
|
279
372
|
codexHome ? ` <key>CODEX_HOME</key><string>${plistString(codexHome)}</string>` : null,
|
|
280
373
|
opencodexHome ? ` <key>OPENCODEX_HOME</key><string>${plistString(opencodexHome)}</string>` : null,
|
|
@@ -491,17 +584,14 @@ async function reportServiceServing(
|
|
|
491
584
|
}
|
|
492
585
|
|
|
493
586
|
/**
|
|
494
|
-
* The
|
|
587
|
+
* The command that repairs the CURRENTLY INSTALLED backend without re-registering it.
|
|
495
588
|
*
|
|
496
|
-
*
|
|
497
|
-
*
|
|
498
|
-
*
|
|
499
|
-
* backend, so the hint has to carry `--native` when that is what is installed.
|
|
589
|
+
* `ocx service repair` reads the recorded backend itself, so it cannot silently switch a
|
|
590
|
+
* WinSW install to Task Scheduler the way a plain `ocx service install` would, and on
|
|
591
|
+
* Windows it needs no elevation because it never calls `schtasks /create`.
|
|
500
592
|
*/
|
|
501
593
|
function serviceRepairCommand(): string {
|
|
502
|
-
return
|
|
503
|
-
? "ocx service install --native"
|
|
504
|
-
: "ocx service install";
|
|
594
|
+
return "ocx service repair";
|
|
505
595
|
}
|
|
506
596
|
|
|
507
597
|
function systemdQuote(value: string): string {
|
|
@@ -544,16 +634,28 @@ function sh(cmd: string): string {
|
|
|
544
634
|
export function runLaunchctl(
|
|
545
635
|
args: string[],
|
|
546
636
|
deps: { run?: typeof spawnSync } = {},
|
|
547
|
-
): { ok: boolean; stdout: string; stderr: string } {
|
|
637
|
+
): { ok: boolean; stdout: string; stderr: string; status: number | null } {
|
|
548
638
|
const run = deps.run ?? spawnSync;
|
|
549
639
|
const result = run("/bin/launchctl", args, { encoding: "utf8", windowsHide: true });
|
|
550
640
|
// `error` is set when the spawn itself failed (ENOENT off macOS) and `status` is
|
|
551
641
|
// null for a signalled child; neither may be reported as success.
|
|
552
|
-
if (result.error)
|
|
642
|
+
if (result.error) {
|
|
643
|
+
return { ok: false, stdout: "", stderr: String(result.error.message ?? ""), status: null };
|
|
644
|
+
}
|
|
553
645
|
return {
|
|
554
646
|
ok: result.status === 0,
|
|
555
647
|
stdout: String(result.stdout ?? "").trim(),
|
|
556
648
|
stderr: String(result.stderr ?? "").trim(),
|
|
649
|
+
/*
|
|
650
|
+
* The NUMBER, not just its zero-ness.
|
|
651
|
+
*
|
|
652
|
+
* `launchctl print` distinguishes "that domain does not exist" (112) from
|
|
653
|
+
* "the domain answered and has no such service" (113), and an ownership
|
|
654
|
+
* probe needs that difference: the second proves absence, the first only
|
|
655
|
+
* proves we could not look. Collapsing both into `ok: false` forced callers
|
|
656
|
+
* to parse stderr, which Apple does not treat as a stable interface.
|
|
657
|
+
*/
|
|
658
|
+
status: result.status ?? null,
|
|
557
659
|
};
|
|
558
660
|
}
|
|
559
661
|
|
|
@@ -788,6 +890,10 @@ export function windowsSchedulerTaskInstalled(taskName = TASK): boolean {
|
|
|
788
890
|
export interface WindowsSchedulerInstallVerification {
|
|
789
891
|
taskInstalled: boolean;
|
|
790
892
|
registrationHealthy: boolean;
|
|
893
|
+
/** Well-formed XML that is PUBLISHED but policy-violating — permanent, never
|
|
894
|
+
* worth a settle retry (vs an empty/unreadable view, which is publication
|
|
895
|
+
* lag and transient). */
|
|
896
|
+
registrationInvalid: boolean;
|
|
791
897
|
assetsHealthy: boolean;
|
|
792
898
|
nativeServiceAbsent: boolean;
|
|
793
899
|
/** True when SCM probe failed; not a proven WinSW presence. */
|
|
@@ -808,6 +914,10 @@ export function evaluateWindowsSchedulerInstallVerification(inputs: {
|
|
|
808
914
|
}): WindowsSchedulerInstallVerification {
|
|
809
915
|
const registrationHealthy = inputs.xml.length > 0
|
|
810
916
|
&& windowsTaskRegistrationHealthy(inputs.xml, inputs.wscript, inputs.launcher);
|
|
917
|
+
// Permanent invalidity: the XML IS published but violates the registration
|
|
918
|
+
// contract — no amount of settling changes it. Empty/unreadable XML stays
|
|
919
|
+
// transient (publication lag).
|
|
920
|
+
const registrationInvalid = inputs.taskInstalled && inputs.xml.length > 0 && !registrationHealthy;
|
|
811
921
|
const assetsHealthy = inputs.assetsExist;
|
|
812
922
|
const nativeServiceAbsent = inputs.nativeStatus === "nonexistent";
|
|
813
923
|
const nativeStatusUnknown = inputs.nativeStatus === "unknown";
|
|
@@ -831,6 +941,7 @@ export function evaluateWindowsSchedulerInstallVerification(inputs: {
|
|
|
831
941
|
return {
|
|
832
942
|
taskInstalled: inputs.taskInstalled,
|
|
833
943
|
registrationHealthy,
|
|
944
|
+
registrationInvalid,
|
|
834
945
|
assetsHealthy,
|
|
835
946
|
nativeServiceAbsent,
|
|
836
947
|
nativeStatusUnknown,
|
|
@@ -909,6 +1020,8 @@ type FinalizeHooks = {
|
|
|
909
1020
|
/** Defense-in-depth: late reconciliation must still own this attempt. */
|
|
910
1021
|
stillOwnsAttempt?: (attemptId: string) => boolean;
|
|
911
1022
|
requestTimeoutMs?: number;
|
|
1023
|
+
/** Test-only seam for the post-create settle backoff; real installs use a timer. */
|
|
1024
|
+
settleDelay?: (ms: number) => Promise<void>;
|
|
912
1025
|
};
|
|
913
1026
|
|
|
914
1027
|
let finalizeHooks: FinalizeHooks | null = null;
|
|
@@ -973,6 +1086,68 @@ function attemptStillOwned(options: ApplyElevatedOptions): boolean {
|
|
|
973
1086
|
return !check || check(options.attemptId);
|
|
974
1087
|
}
|
|
975
1088
|
|
|
1089
|
+
/**
|
|
1090
|
+
* Bounded post-create backoff, 1.1s total. Task Scheduler's non-elevated view can
|
|
1091
|
+
* lag an elevated `/create` by a few hundred milliseconds, so a single verification
|
|
1092
|
+
* would roll back a task that is merely not visible yet.
|
|
1093
|
+
*/
|
|
1094
|
+
const SCHEDULER_SETTLE_DELAYS_MS = [50, 150, 300, 600] as const;
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Whether a failed verification is still worth re-checking after a short delay.
|
|
1098
|
+
*
|
|
1099
|
+
* Retrying is confined to states that a lagging scheduler view actually produces:
|
|
1100
|
+
* the task is not visible yet, or it is visible but its registration has not been
|
|
1101
|
+
* published in full. Everything else keeps its existing fail-closed meaning and is
|
|
1102
|
+
* rejected here so no delay can turn it into a pass:
|
|
1103
|
+
*
|
|
1104
|
+
* - a proven conflict (both backends present) is a real dual-backend install;
|
|
1105
|
+
* - missing assets are missing on disk, which no amount of waiting creates;
|
|
1106
|
+
* - a WinSW service that is proven present (`started`/`stopped`) is never absent
|
|
1107
|
+
* later. This is checked independently of `conflict`, which only becomes true
|
|
1108
|
+
* once the task itself is visible — while the task is still invisible the pair
|
|
1109
|
+
* is `conflict: false` with `nativeServiceAbsent: false`, and that must not retry;
|
|
1110
|
+
* - unknown SCM status is unproven rather than transient, and has its own
|
|
1111
|
+
* task-preserving branch below.
|
|
1112
|
+
*/
|
|
1113
|
+
/** Exported for tests: the transient-vs-permanent settle decision. */
|
|
1114
|
+
export function schedulerVerificationMaySettle(v: WindowsSchedulerInstallVerification): boolean {
|
|
1115
|
+
if (v.ok) return false;
|
|
1116
|
+
if (v.conflict) return false;
|
|
1117
|
+
if (!v.assetsHealthy) return false;
|
|
1118
|
+
if (!v.nativeServiceAbsent) return false;
|
|
1119
|
+
// A published-but-invalid registration is permanent: no delay repairs it.
|
|
1120
|
+
if (v.registrationInvalid) return false;
|
|
1121
|
+
return !v.taskInstalled || !v.registrationHealthy;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
function settleDelay(ms: number): Promise<void> {
|
|
1125
|
+
const hook = finalizeHooks?.settleDelay;
|
|
1126
|
+
if (hook) return hook(ms);
|
|
1127
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
/**
|
|
1131
|
+
* Verify the elevated install, re-checking only while the failure looks like a
|
|
1132
|
+
* scheduler view that has not caught up yet. Returns `null` when this attempt lost
|
|
1133
|
+
* ownership mid-settle: a newer attempt owns the task, so this one must neither
|
|
1134
|
+
* write install state nor roll anything back.
|
|
1135
|
+
*/
|
|
1136
|
+
async function verifyWindowsSchedulerInstallAfterSettle(
|
|
1137
|
+
options: ApplyElevatedOptions,
|
|
1138
|
+
): Promise<WindowsSchedulerInstallVerification | null> {
|
|
1139
|
+
const verify = finalizeHooks?.verify ?? verifyWindowsSchedulerInstall;
|
|
1140
|
+
let verification = verify();
|
|
1141
|
+
for (const delayMs of SCHEDULER_SETTLE_DELAYS_MS) {
|
|
1142
|
+
if (!schedulerVerificationMaySettle(verification)) break;
|
|
1143
|
+
if (!attemptStillOwned(options)) return null;
|
|
1144
|
+
await settleDelay(delayMs);
|
|
1145
|
+
if (!attemptStillOwned(options)) return null;
|
|
1146
|
+
verification = verify();
|
|
1147
|
+
}
|
|
1148
|
+
return verification;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
976
1151
|
async function applyElevatedSchedulerResult(
|
|
977
1152
|
result: ElevatedSchtasksCreateAndRunResult,
|
|
978
1153
|
options: ApplyElevatedOptions,
|
|
@@ -1002,7 +1177,9 @@ async function applyElevatedSchedulerResult(
|
|
|
1002
1177
|
await reconcileUnknownElevatedOutcome(result.exitCode);
|
|
1003
1178
|
}
|
|
1004
1179
|
|
|
1005
|
-
const verification =
|
|
1180
|
+
const verification = await verifyWindowsSchedulerInstallAfterSettle(options);
|
|
1181
|
+
// Ownership moved to a newer attempt while settling; that attempt owns the outcome.
|
|
1182
|
+
if (!verification) return;
|
|
1006
1183
|
if (!verification.ok) {
|
|
1007
1184
|
// Preserve a healthy elevated task when WinSW absence cannot be proven (unknown SCM status).
|
|
1008
1185
|
// Unknown is not a confirmed dual-backend conflict; install state is still withheld.
|
|
@@ -1019,6 +1196,9 @@ async function applyElevatedSchedulerResult(
|
|
|
1019
1196
|
"Installation state was not written.",
|
|
1020
1197
|
]);
|
|
1021
1198
|
}
|
|
1199
|
+
// Rollback deletes a real task, so it needs the same ownership fence as the
|
|
1200
|
+
// state write below: a stale attempt must never delete a newer attempt's task.
|
|
1201
|
+
if (!attemptStillOwned(options)) return;
|
|
1022
1202
|
const rollbackError = await rollbackElevatedSchedulerTask();
|
|
1023
1203
|
const parts = [
|
|
1024
1204
|
"Elevated Task Scheduler registration did not produce a conflict-free install.",
|
|
@@ -1259,8 +1439,9 @@ function taskXmlRunLevelAcceptable(principal: string): boolean {
|
|
|
1259
1439
|
}
|
|
1260
1440
|
|
|
1261
1441
|
export function buildWindowsServiceScript(entry = cliEntry(), port = resolveServiceListenPort()): string {
|
|
1262
|
-
|
|
1263
|
-
|
|
1442
|
+
// Provenance rides along with the entry: a second durableBunRuntime() call here could
|
|
1443
|
+
// resolve differently from the binary the caller actually baked.
|
|
1444
|
+
const { bun, bunRuntimeSource, cli } = entry;
|
|
1264
1445
|
const path = process.env.PATH ?? "";
|
|
1265
1446
|
const lines = [
|
|
1266
1447
|
"@echo off",
|
|
@@ -1269,6 +1450,8 @@ export function buildWindowsServiceScript(entry = cliEntry(), port = resolveServ
|
|
|
1269
1450
|
// it to UTF-8 is safe (no leak into user shells) and lets cmd parse UTF-8 remnants.
|
|
1270
1451
|
"chcp 65001 >nul",
|
|
1271
1452
|
windowsBatchSet("OCX_SERVICE", "1"),
|
|
1453
|
+
windowsBatchSet(BUN_RUNTIME_SOURCE_ENV, bunRuntimeSource),
|
|
1454
|
+
windowsBatchSet(BUN_RUNTIME_PATH_ENV, bun, "path"),
|
|
1272
1455
|
windowsBatchSet("PATH", path, "pathList"),
|
|
1273
1456
|
windowsBatchSet("CODEX_HOME", process.env.CODEX_HOME?.trim(), "path"),
|
|
1274
1457
|
windowsBatchSet("OPENCODEX_HOME", process.env.OPENCODEX_HOME?.trim(), "path"),
|
|
@@ -1282,7 +1465,7 @@ export function buildWindowsServiceScript(entry = cliEntry(), port = resolveServ
|
|
|
1282
1465
|
":loop",
|
|
1283
1466
|
'>>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] opencodex service wrapper start',
|
|
1284
1467
|
'>>"%OCX_SERVICE_LOG%" echo bun="%OCX_BUN%"',
|
|
1285
|
-
`>>"%OCX_SERVICE_LOG%" echo bun_source="${
|
|
1468
|
+
`>>"%OCX_SERVICE_LOG%" echo bun_source="${bunRuntimeSource}"`,
|
|
1286
1469
|
'>>"%OCX_SERVICE_LOG%" echo cli="%OCX_CLI%"',
|
|
1287
1470
|
'>>"%OCX_SERVICE_LOG%" echo opencodex_home="%OPENCODEX_HOME%"',
|
|
1288
1471
|
'>>"%OCX_SERVICE_LOG%" echo codex_home="%CODEX_HOME%"',
|
|
@@ -1523,6 +1706,9 @@ function installLaunchd(): void {
|
|
|
1523
1706
|
if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
|
|
1524
1707
|
writeServiceApiTokenFile();
|
|
1525
1708
|
const p = plistPath();
|
|
1709
|
+
// Capture this BEFORE writing: the write below makes the plist exist unconditionally,
|
|
1710
|
+
// so a post-write existsSync would call every fresh install an "installed" service.
|
|
1711
|
+
const wasInstalled = existsSync(p);
|
|
1526
1712
|
writeFileSync(p, buildPlist(), "utf8");
|
|
1527
1713
|
// Best-effort: an absent job is fine here, and a failed unload is caught by the
|
|
1528
1714
|
// load verification below with a better message than a raw unload error.
|
|
@@ -1535,7 +1721,9 @@ function installLaunchd(): void {
|
|
|
1535
1721
|
`launchctl could not load ${p}: ${loaded.stderr || "load reported failure"}\n`
|
|
1536
1722
|
+ "A previous job may still be bootstrapped. Try:\n"
|
|
1537
1723
|
+ ` launchctl bootout ${launchdGuiDomain()}/${LABEL}\n`
|
|
1538
|
-
|
|
1724
|
+
// macOS `service repair` delegates straight to installLaunchd, so this fires for
|
|
1725
|
+
// an already-installed service too; repair reloads it without re-registering.
|
|
1726
|
+
+ `then re-run '${wasInstalled ? "ocx service repair" : "ocx service install"}'.`,
|
|
1539
1727
|
);
|
|
1540
1728
|
}
|
|
1541
1729
|
writeServiceInstallState();
|
|
@@ -1572,8 +1760,8 @@ export function startLaunchd(deps: {
|
|
|
1572
1760
|
throw new Error(
|
|
1573
1761
|
`launchctl could not load ${p}: ${loaded.stderr || "load reported failure"}\n`
|
|
1574
1762
|
+ (live.loaded
|
|
1575
|
-
? `launchd is running an OLDER plist. Fix:\n launchctl bootout ${launchdGuiDomain()}/${LABEL}\n ocx service
|
|
1576
|
-
: "The job is not loaded. Run 'ocx service
|
|
1763
|
+
? `launchd is running an OLDER plist. Fix:\n launchctl bootout ${launchdGuiDomain()}/${LABEL}\n ocx service repair`
|
|
1764
|
+
: "The job is not loaded. Run 'ocx service repair' to reload it."),
|
|
1577
1765
|
);
|
|
1578
1766
|
}
|
|
1579
1767
|
function stopLaunchd(): void { try { sh(`launchctl unload "${plistPath()}"`); } catch { /* not loaded */ } }
|
|
@@ -1836,7 +2024,7 @@ export function bakedServicePathsDiagnostic(): string | null {
|
|
|
1836
2024
|
if (!state?.bunPath || !state?.cliPath) return null;
|
|
1837
2025
|
const missing = [state.bunPath, state.cliPath].filter(path => !existsSync(path));
|
|
1838
2026
|
if (missing.length === 0) return null;
|
|
1839
|
-
return `STALE baked paths (missing: ${missing.join(", ")}) — run 'ocx service
|
|
2027
|
+
return `STALE baked paths (missing: ${missing.join(", ")}) — run 'ocx service repair' to re-bake`;
|
|
1840
2028
|
}
|
|
1841
2029
|
|
|
1842
2030
|
function serviceDiagnosticsSummary(): string {
|
|
@@ -1854,13 +2042,15 @@ function unitPath(): string {
|
|
|
1854
2042
|
}
|
|
1855
2043
|
|
|
1856
2044
|
export function buildUnit(): string {
|
|
1857
|
-
const { bun, cli } = cliEntry();
|
|
2045
|
+
const { bun, bunRuntimeSource, cli } = cliEntry();
|
|
1858
2046
|
const log = logPath();
|
|
1859
2047
|
const path = process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin";
|
|
1860
2048
|
const codexHome = systemdEnvironmentAssignment("CODEX_HOME", process.env.CODEX_HOME?.trim());
|
|
1861
2049
|
const opencodexHome = systemdEnvironmentAssignment("OPENCODEX_HOME", process.env.OPENCODEX_HOME?.trim());
|
|
1862
2050
|
const envLines = [
|
|
1863
2051
|
systemdEnvironmentAssignment("OCX_SERVICE", "1"),
|
|
2052
|
+
systemdEnvironmentAssignment(BUN_RUNTIME_SOURCE_ENV, bunRuntimeSource),
|
|
2053
|
+
systemdEnvironmentAssignment(BUN_RUNTIME_PATH_ENV, bun),
|
|
1864
2054
|
systemdEnvironmentAssignment("PATH", path),
|
|
1865
2055
|
codexHome,
|
|
1866
2056
|
opencodexHome,
|
|
@@ -2251,7 +2441,7 @@ export function deriveWindowsServiceDiagnostic(inputs: WindowsServiceDiagnosticI
|
|
|
2251
2441
|
const detail = conflict
|
|
2252
2442
|
? "CONFLICT: Task Scheduler and native WinSW are both present — run 'ocx service uninstall' then reinstall one"
|
|
2253
2443
|
: stale
|
|
2254
|
-
? "stale or missing service assets — run 'ocx service
|
|
2444
|
+
? "stale or missing service assets — run 'ocx service repair'"
|
|
2255
2445
|
: schedulerInstalled
|
|
2256
2446
|
? schedulerEnabled ? "Task Scheduler enabled" : "Task Scheduler disabled"
|
|
2257
2447
|
: nativeInstalled
|
|
@@ -2373,7 +2563,7 @@ export async function serviceStatusReport(
|
|
|
2373
2563
|
: null);
|
|
2374
2564
|
const staleLine = stalePlist && stalePlist.loaded && !stalePlist.matchesPlist
|
|
2375
2565
|
? " launchd is running an OLDER plist than the one on disk.\n"
|
|
2376
|
-
+ ` Fix: launchctl bootout gui/$(id -u)/${LABEL} && ocx service
|
|
2566
|
+
+ ` Fix: launchctl bootout gui/$(id -u)/${LABEL} && ocx service repair\n`
|
|
2377
2567
|
: "";
|
|
2378
2568
|
|
|
2379
2569
|
return `⚠️ ${diag.summary}\n`
|
|
@@ -2494,7 +2684,7 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
2494
2684
|
process.exitCode = 1;
|
|
2495
2685
|
break;
|
|
2496
2686
|
}
|
|
2497
|
-
const restore =
|
|
2687
|
+
const restore = await restoreNativeCodexAsync();
|
|
2498
2688
|
if (restore.success) console.log("✅ service stopped + native Codex restored.");
|
|
2499
2689
|
else console.error(`⚠️ service stopped, but native Codex restore FAILED: ${restore.message}\nRun \`ocx restore\` (or check $CODEX_HOME/config.toml) before using native Codex.`);
|
|
2500
2690
|
// The Grok fence is the other managed config this command owns. Leaving it behind
|
|
@@ -2532,7 +2722,7 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
2532
2722
|
process.exit(1);
|
|
2533
2723
|
}
|
|
2534
2724
|
{
|
|
2535
|
-
const restore =
|
|
2725
|
+
const restore = await restoreNativeCodexAsync();
|
|
2536
2726
|
if (!restore.success) {
|
|
2537
2727
|
console.error(`⚠️ native Codex restore FAILED: ${restore.message}\nRun \`ocx restore\` before using native Codex.`);
|
|
2538
2728
|
}
|
|
@@ -3,6 +3,9 @@ param(
|
|
|
3
3
|
[Parameter(Mandatory = $true)][string]$CliPath,
|
|
4
4
|
[Parameter(Mandatory = $true)][string]$CodexHome,
|
|
5
5
|
[Parameter(Mandatory = $true)][string]$OpenCodexHome,
|
|
6
|
+
# Provenance of $BunPath, chosen when the tray entry was built. Optional so an
|
|
7
|
+
# already-installed launcher command from an older version still starts.
|
|
8
|
+
[ValidateSet("", "override", "bundled", "process")][string]$BunRuntimeSource = "",
|
|
6
9
|
[ValidateSet("Run", "Stop")][string]$Mode = "Run",
|
|
7
10
|
[int]$HostPid = 0
|
|
8
11
|
)
|
|
@@ -92,6 +95,12 @@ function Start-OcxCommand([string[]]$CommandArgs) {
|
|
|
92
95
|
$psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
|
|
93
96
|
$psi.EnvironmentVariables["CODEX_HOME"] = $CodexHome
|
|
94
97
|
$psi.EnvironmentVariables["OPENCODEX_HOME"] = $OpenCodexHome
|
|
98
|
+
if ($BunRuntimeSource) {
|
|
99
|
+
$psi.EnvironmentVariables["OCX_BUN_RUNTIME_SOURCE"] = $BunRuntimeSource
|
|
100
|
+
# Paired with the source so a later relaunch can tell the marker still describes
|
|
101
|
+
# this binary rather than one it merely inherited.
|
|
102
|
+
$psi.EnvironmentVariables["OCX_BUN_RUNTIME_PATH"] = $BunPath
|
|
103
|
+
}
|
|
95
104
|
$process = [System.Diagnostics.Process]::Start($psi)
|
|
96
105
|
if ($null -ne $process) { $process.Dispose() }
|
|
97
106
|
Write-ActionLog "dispatched $($CommandArgs -join ' ')"
|
package/src/tray/windows.ts
CHANGED
|
@@ -4,8 +4,9 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync,
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { join, resolve } from "node:path";
|
|
6
6
|
import { expandUserPath, getConfigDir } from "../config";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { durableBunRuntime } from "../lib/bun-runtime";
|
|
8
|
+
import type { BunRuntimeSource } from "../lib/bun-runtime";
|
|
9
|
+
import { forgetEphemeralSecretPath, hardenSecretDir, hardenSecretPath } from "../lib/windows-secret-acl";
|
|
9
10
|
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
10
11
|
|
|
11
12
|
const RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
|
@@ -20,6 +21,8 @@ const TRAY_ICON_FILES = [
|
|
|
20
21
|
|
|
21
22
|
export interface WindowsTrayEntry {
|
|
22
23
|
bun: string;
|
|
24
|
+
/** Provenance of `bun`, resolved together with it. */
|
|
25
|
+
bunRuntimeSource: BunRuntimeSource;
|
|
23
26
|
cli: string;
|
|
24
27
|
script: string;
|
|
25
28
|
codexHome: string;
|
|
@@ -81,8 +84,10 @@ function currentCodexHome(): string {
|
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
function currentEntry(): WindowsTrayEntry {
|
|
87
|
+
const runtime = durableBunRuntime();
|
|
84
88
|
return {
|
|
85
|
-
bun:
|
|
89
|
+
bun: runtime.path,
|
|
90
|
+
bunRuntimeSource: runtime.source,
|
|
86
91
|
cli: join(import.meta.dir, "..", "cli", "index.ts"),
|
|
87
92
|
script: installedTrayScriptPath(),
|
|
88
93
|
codexHome: currentCodexHome(),
|
|
@@ -136,6 +141,7 @@ export function windowsTrayProcessArgs(entry: WindowsTrayEntry, mode: "Run" | "S
|
|
|
136
141
|
"-WindowStyle", "Hidden",
|
|
137
142
|
"-File", safePath(entry.script),
|
|
138
143
|
"-BunPath", safePath(entry.bun),
|
|
144
|
+
"-BunRuntimeSource", entry.bunRuntimeSource,
|
|
139
145
|
"-CliPath", safePath(entry.cli),
|
|
140
146
|
"-CodexHome", safePath(entry.codexHome),
|
|
141
147
|
"-OpenCodexHome", safePath(entry.opencodexHome),
|
|
@@ -170,6 +176,7 @@ export function buildWindowsTrayPowerShellCommand(entry: WindowsTrayEntry, power
|
|
|
170
176
|
"-WindowStyle", "Hidden",
|
|
171
177
|
"-File", quoteRunValue(entry.script),
|
|
172
178
|
"-BunPath", quoteRunValue(entry.bun),
|
|
179
|
+
"-BunRuntimeSource", entry.bunRuntimeSource,
|
|
173
180
|
"-CliPath", quoteRunValue(entry.cli),
|
|
174
181
|
"-CodexHome", quoteRunValue(entry.codexHome),
|
|
175
182
|
"-OpenCodexHome", quoteRunValue(entry.opencodexHome),
|
|
@@ -233,7 +240,8 @@ export function replaceWindowsTrayOwnedFile(
|
|
|
233
240
|
harden: target => {
|
|
234
241
|
try { chmodSync(target, 0o600); } catch { /* best-effort */ }
|
|
235
242
|
if (process.platform !== "win32") return;
|
|
236
|
-
|
|
243
|
+
// Destination-keyed timeout memo: retries share one memo per final path.
|
|
244
|
+
const hardened = hardenSecretPath(target, { required: true, timeoutMemoKey: path });
|
|
237
245
|
if (!hardened.ok) throw new Error("Windows tray ACL hardening did not complete; refusing to persist executable state.");
|
|
238
246
|
},
|
|
239
247
|
rename: renameSync,
|
|
@@ -247,14 +255,14 @@ export function replaceWindowsTrayOwnedFile(
|
|
|
247
255
|
io.harden(temporary);
|
|
248
256
|
io.rename(temporary, path);
|
|
249
257
|
renamed = true;
|
|
250
|
-
|
|
258
|
+
forgetEphemeralSecretPath(temporary);
|
|
251
259
|
} finally {
|
|
252
260
|
if (!renamed) {
|
|
253
261
|
try {
|
|
254
262
|
io.unlink(temporary);
|
|
255
|
-
|
|
263
|
+
forgetEphemeralSecretPath(temporary);
|
|
256
264
|
} catch (error) {
|
|
257
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT")
|
|
265
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") forgetEphemeralSecretPath(temporary);
|
|
258
266
|
}
|
|
259
267
|
}
|
|
260
268
|
}
|