@bitkyc08/opencodex 2.80.0 → 2.82.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/bin/ocx.mjs +166 -73
- package/gui/dist/assets/App-Ds_qL7o8.js +54 -0
- package/gui/dist/assets/Tray-BqHF5Trd.js +1 -0
- package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
- package/gui/dist/assets/index-D3FBCNiP.js +94 -0
- package/gui/dist/assets/quota-summary-DTBC8n7-.js +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/claude-green.svg +1 -0
- package/package.json +3 -2
- package/src/adapters/anthropic-model-contract.ts +8 -2
- package/src/adapters/anthropic.ts +8 -3
- package/src/adapters/codebuddy/mcp-server.ts +2 -25
- package/src/adapters/codebuddy/tool-bridge.ts +1 -7
- package/src/adapters/coding-agent/mcp-server.ts +33 -0
- package/src/adapters/coding-agent/protocol.ts +122 -17
- package/src/adapters/coding-agent/tool-catalog.ts +75 -0
- package/src/adapters/coding-agent/turn.ts +101 -44
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +2 -0
- package/src/adapters/devin/live-models.ts +2 -0
- package/src/adapters/kiro-retry.ts +9 -4
- package/src/adapters/openai-chat/messages.ts +7 -6
- package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
- package/src/adapters/openai-responses/passthrough.ts +9 -4
- package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
- package/src/adapters/physical-send.ts +8 -5
- package/src/adapters/qoder/adapter.ts +41 -6
- package/src/adapters/qoder/scaffold-guard.ts +2 -3
- package/src/adapters/zed.ts +16 -1
- package/src/claude/agents-inject.ts +14 -3
- package/src/claude/context-windows.ts +19 -17
- package/src/claude/desktop-3p.ts +88 -12
- package/src/claude/desktop-profile.ts +2 -1
- package/src/claude/gateway-cache.ts +18 -1
- package/src/claude/inbound-cache-stabilize.ts +4 -9
- package/src/claude/inbound.ts +78 -3
- package/src/claude/intercept/cli-catalog.ts +3 -0
- package/src/claude/intercept/model-bindings.ts +1 -0
- package/src/claude/intercept/picker-ca-persistence.ts +7 -6
- package/src/claude/intercept/picker-listener.ts +71 -4
- 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-anthropic-threshold.ts +14 -5
- package/src/cli/account-api.ts +3 -1
- package/src/cli/account-extended.ts +13 -9
- package/src/cli/account-policy-dto.ts +1 -1
- package/src/cli/account-policy.ts +13 -4
- package/src/cli/account.ts +3 -3
- package/src/cli/agent.ts +9 -5
- package/src/cli/aside-profile-recovery.ts +33 -0
- package/src/cli/capabilities-accounts.ts +2 -2
- package/src/cli/capabilities-agents-routing.ts +24 -0
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-integrations.ts +18 -2
- package/src/cli/cli-path-diagnostics.ts +132 -0
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/desktop-runtime-guidance.ts +65 -0
- package/src/cli/dispatch.ts +15 -1
- package/src/cli/doctor.ts +16 -3
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +25 -14
- package/src/cli/integration-aside-sync.ts +2 -1
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/integration-preview.ts +8 -1
- package/src/cli/integrations.ts +48 -6
- package/src/cli/message-args.ts +34 -0
- package/src/cli/message-command.ts +43 -0
- package/src/cli/message-runtime.ts +6 -0
- package/src/cli/observe.ts +3 -1
- package/src/cli/provider.ts +29 -6
- package/src/cli/registry.ts +24 -2
- package/src/cli/resolve.ts +25 -2
- 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/commandcode.ts +131 -0
- package/src/clients/config-export/contracts.ts +12 -0
- package/src/clients/config-export.ts +64 -1
- package/src/clients/lazycodex.ts +1 -1
- package/src/clients/omo-role-models.ts +32 -7
- package/src/codex/auth-api/account-list.ts +11 -3
- package/src/codex/auth-api/account-selection.ts +107 -0
- package/src/codex/auth-api/main-account-probe.ts +25 -8
- package/src/codex/auth-api/pool-mode-gate.ts +33 -9
- package/src/codex/auth-api/pool-quota-probe.ts +15 -1
- package/src/codex/auth-api/routes.ts +11 -49
- package/src/codex/auth-context.ts +20 -1
- package/src/codex/autostart-health.ts +12 -3
- package/src/codex/catalog/auto-review.ts +13 -6
- package/src/codex/catalog/build-entries.ts +33 -7
- package/src/codex/catalog/bundled.ts +155 -26
- package/src/codex/catalog/combo-member.ts +3 -2
- package/src/codex/catalog/control-plane.ts +45 -0
- package/src/codex/catalog/effort.ts +37 -6
- package/src/codex/catalog/gather-capture.ts +10 -3
- package/src/codex/catalog/model-hints.ts +15 -8
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/operator-tiers.ts +38 -0
- package/src/codex/catalog/parsing.ts +2 -1
- package/src/codex/catalog/provider-models.ts +58 -44
- 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/control-plane-models.ts +6 -0
- package/src/codex/convergence.ts +2 -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/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-decision-contract.ts +25 -2
- package/src/combos/jev-service-exchange.ts +281 -0
- package/src/combos/jev.ts +34 -219
- package/src/combos/types.ts +1 -1
- package/src/config/atomic-write.ts +1 -1
- package/src/config/codex-ws-reuse-setting.ts +10 -0
- package/src/config/diagnostics.ts +27 -16
- package/src/config/live-reconcile.ts +17 -0
- package/src/config/load-degrade.ts +42 -0
- package/src/config/rebase-provenance.ts +11 -0
- package/src/config/schema/anthropic-account-pool.ts +143 -0
- package/src/config/schema/config-schema.ts +11 -1
- package/src/config/schema/leaf-validators.ts +4 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +566 -298
- package/src/generated/model-metadata.ts +7 -6
- package/src/grok/inject.ts +50 -4
- package/src/grok/sync.ts +2 -1
- package/src/images/loop.ts +30 -11
- package/src/integrations/aside-profiles.ts +10 -3
- package/src/integrations/catalog-refresh.ts +19 -3
- package/src/integrations/mutation-plan.ts +7 -0
- package/src/integrations/registry.ts +8 -0
- package/src/integrations/state.ts +9 -2
- package/src/integrations/writer.ts +17 -5
- 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/lab-live-route-production.ts +9 -1
- package/src/lib/request-execution-budget.ts +244 -58
- package/src/lib/spend-pool-alias-validation.ts +79 -0
- package/src/lib/spend-pool-continuity.ts +34 -0
- package/src/lib/spend-reservation-ledger.ts +412 -50
- package/src/lib/state-store-registrations.ts +20 -3
- 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/upstream-retry.ts +17 -5
- 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/lib/workflow-budget.ts +53 -11
- package/src/messaging/budget.ts +45 -0
- package/src/messaging/discovery.ts +57 -0
- package/src/messaging/envelope.ts +49 -0
- package/src/messaging/input.ts +37 -0
- package/src/messaging/rpc.ts +196 -0
- package/src/messaging/send.ts +58 -0
- package/src/messaging/socket.ts +72 -0
- package/src/messaging/types.ts +29 -0
- package/src/oauth/anthropic-account-refusal.ts +90 -18
- package/src/oauth/anthropic-account-threshold.ts +8 -2
- package/src/oauth/anthropic-continuity.ts +15 -1
- package/src/oauth/anthropic-model-quota.ts +120 -48
- package/src/oauth/anthropic-model-routes.ts +11 -3
- package/src/oauth/anthropic-oauth-definitions.ts +98 -0
- package/src/oauth/anthropic-pool-config.ts +30 -0
- package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
- package/src/oauth/anthropic-routing.ts +1202 -866
- package/src/oauth/anthropic-send-ownership.ts +41 -0
- package/src/oauth/anthropic.ts +4 -1
- package/src/oauth/chatgpt.ts +113 -41
- package/src/oauth/generic-account-failover.ts +3 -1
- package/src/oauth/health.ts +8 -6
- package/src/oauth/index.ts +96 -175
- package/src/oauth/kiro.ts +22 -9
- package/src/oauth/login-cli.ts +2 -0
- package/src/oauth/model-discovery-auth.ts +43 -0
- package/src/oauth/pool-kernel.ts +11 -5
- package/src/oauth/pool-settings-capability.ts +5 -3
- package/src/oauth/store-anthropic-instance.ts +87 -0
- package/src/oauth/store.ts +68 -10
- package/src/oauth/token-guardian.ts +2 -0
- package/src/oauth/types.ts +2 -0
- package/src/protocols/plan-snapshot.ts +5 -0
- package/src/protocols/settings.ts +20 -5
- package/src/providers/anthropic-instance-id.ts +47 -0
- package/src/providers/anthropic-instance.ts +48 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
- package/src/providers/azure-model-metadata.ts +115 -0
- package/src/providers/derive.ts +23 -0
- package/src/providers/kiro-models.ts +3 -0
- package/src/providers/label.ts +1 -1
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/quota/account-cache.ts +37 -11
- package/src/providers/quota/anthropic-account-quota.ts +93 -0
- package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
- package/src/providers/quota/report-cache.ts +24 -1
- package/src/providers/quota/vendor-probes-key.ts +127 -17
- package/src/providers/quota/vendor-probes-oauth.ts +46 -14
- package/src/providers/quota.ts +23 -33
- package/src/providers/registry/entries-core.ts +35 -30
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/model-seeds.ts +7 -4
- package/src/providers/registry/types.ts +3 -0
- package/src/providers/registry.ts +3 -3
- package/src/responses/reasoning-envelope.ts +29 -1
- package/src/responses/reasoning-replay-cache.ts +17 -1
- package/src/responses/spill-store.ts +20 -3
- package/src/responses/state/metrics.ts +95 -0
- package/src/responses/state/snapshot-codec.ts +6 -0
- package/src/responses/state/spill-failure.ts +12 -0
- package/src/responses/state/spill-queue.ts +13 -5
- package/src/responses/state/unforced-store-false.ts +55 -0
- package/src/responses/state.ts +68 -76
- package/src/responses/task-input.ts +17 -0
- package/src/router.ts +28 -4
- package/src/routing/compatibility/assemble.ts +1 -1
- package/src/routing/quota.ts +7 -3
- package/src/server/auth-cors.ts +5 -0
- package/src/server/background-lifecycle.ts +27 -0
- package/src/server/chat-completions.ts +7 -1
- package/src/server/chat-native.ts +64 -28
- package/src/server/claude-messages.ts +59 -8
- package/src/server/gui-static.ts +38 -4
- package/src/server/inbound-body-admission.ts +1 -0
- package/src/server/index/claude-intercept-lifecycle.ts +3 -1
- package/src/server/index/serve-options.ts +10 -2
- package/src/server/index/spend-ledger-lifecycle.ts +12 -5
- package/src/server/index/startup-warnings.ts +19 -0
- package/src/server/index.ts +5 -2
- package/src/server/inference/context.ts +1 -1
- package/src/server/lifecycle.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +123 -38
- package/src/server/management/anthropic-account-threshold.ts +12 -9
- package/src/server/management/anthropic-pool-settings.ts +72 -0
- package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
- 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 +85 -14
- package/src/server/management/context.ts +2 -2
- package/src/server/management/decision-routes.ts +2 -1
- package/src/server/management/integration-routes.ts +25 -2
- package/src/server/management/oauth-account-routes.ts +79 -98
- package/src/server/management/provider-instance-ownership.ts +44 -0
- package/src/server/management/provider-routes.ts +21 -28
- package/src/server/management/route-registry.ts +3 -1
- package/src/server/management/routing-profile-routes.ts +4 -4
- package/src/server/management/shared.ts +3 -3
- package/src/server/management/system-restart.ts +44 -14
- package/src/server/management/system-routes.ts +24 -2
- package/src/server/management/vision-sidecar-options.ts +9 -6
- package/src/server/management/web-search-sidecar-options.ts +51 -2
- package/src/server/management-auth.ts +6 -0
- package/src/server/messages-native-eligibility.ts +3 -2
- package/src/server/messages-native-oauth.ts +91 -45
- package/src/server/messages-native-selector.ts +39 -0
- package/src/server/messages-native.ts +133 -38
- package/src/server/messages-response-headers.ts +14 -0
- package/src/server/request-log-conversation.ts +48 -5
- package/src/server/request-log.ts +36 -2
- package/src/server/responses/adapter-continuation.ts +31 -19
- package/src/server/responses/adapter-dispatch.ts +75 -24
- package/src/server/responses/antigravity-validation-refusal.ts +20 -6
- 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 +25 -8
- package/src/server/responses/compaction-recovery.ts +1 -1
- package/src/server/responses/core-codex-account.ts +22 -1
- package/src/server/responses/core-combo-native.ts +24 -4
- package/src/server/responses/core-combo.ts +38 -25
- package/src/server/responses/core-lifetime.ts +3 -36
- package/src/server/responses/core-normalize.ts +1 -0
- package/src/server/responses/core-options.ts +8 -1
- package/src/server/responses/core-replay.ts +78 -0
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/fetch-helpers.ts +26 -3
- package/src/server/responses/passthrough-delivery.ts +30 -4
- package/src/server/responses/passthrough-dispatch.ts +39 -9
- package/src/server/responses/request-prepare.ts +41 -5
- package/src/server/responses/request-send-budget.ts +150 -50
- package/src/server/responses/request-spend.ts +325 -33
- package/src/server/responses/request-transport.ts +132 -52
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +13 -3
- package/src/server/responses/sidecar-execution.ts +113 -22
- package/src/server/responses/sidecar-send-budget.ts +58 -0
- package/src/server/responses/ws-upstream.ts +19 -6
- package/src/server/responses-undeclared-tool-guard.ts +5 -1
- package/src/server/startup-health-cache.ts +2 -1
- package/src/server/workflow-refusal.ts +33 -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/ownership-mutation-lease.d.mts +1 -1
- package/src/service/ownership-mutation-lease.mjs +5 -7
- 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/sidecar/anthropic-binding.ts +79 -0
- package/src/sidecar/auth.ts +76 -2
- 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/anthropic-account-pool.ts +33 -0
- package/src/types/config.ts +33 -28
- package/src/types/provider.ts +5 -0
- package/src/types/request.ts +5 -0
- package/src/types/tools.ts +7 -0
- package/src/types/wire.ts +1 -0
- package/src/types.ts +4 -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/cost.ts +3 -2
- package/src/usage/expected-prices.ts +66 -14
- package/src/vision/anthropic-describe.ts +16 -3
- package/src/vision/index.ts +3 -0
- package/src/vision/plan.ts +19 -6
- package/src/web-search/alpha-search.ts +7 -1
- package/src/web-search/anthropic-executor.ts +16 -4
- package/src/web-search/index.ts +7 -3
- package/src/web-search/loop.ts +32 -7
- package/src/web-search/passthrough-bridge.ts +4 -1
- package/src/web-search/sidecar-providers.ts +4 -3
- package/gui/dist/assets/App-MbDVfUeC.js +0 -54
- package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
- package/gui/dist/assets/index-UJt6npXk.js +0 -94
- package/gui/dist/assets/quota-summary-DU3sD6Z-.js +0 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createSupervisionLatch, inspectDesktopSupervision } from "./desktop-supervision.mjs";
|
|
2
|
+
|
|
3
|
+
export { createSupervisionLatch };
|
|
4
|
+
export type DesktopSupervision = ReturnType<typeof inspectDesktopSupervision>;
|
|
5
|
+
export type SupervisionInspector = (deps?: Parameters<typeof inspectDesktopSupervision>[0]) => DesktopSupervision;
|
|
6
|
+
export type SupervisionLatch = ReturnType<typeof createSupervisionLatch>;
|
|
7
|
+
|
|
8
|
+
export function desktopServiceRefusal(evidence: DesktopSupervision, blocked: boolean): string | null {
|
|
9
|
+
if (!blocked) return null;
|
|
10
|
+
const runtime = evidence.kind === "desktop" ? ` (pid ${evidence.runtimePid}, ${evidence.app})` : "";
|
|
11
|
+
return `OpenCodex Desktop supervises the running proxy${runtime}. `
|
|
12
|
+
+ "Quit OpenCodex, then rerun 'ocx service install' to move startup management to this CLI.";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function assertNoDesktopSupervision(
|
|
16
|
+
inspect: SupervisionInspector = inspectDesktopSupervision,
|
|
17
|
+
latch: SupervisionLatch = createSupervisionLatch(),
|
|
18
|
+
): void {
|
|
19
|
+
const evidence = inspect();
|
|
20
|
+
const refusal = desktopServiceRefusal(evidence, latch.observe(evidence));
|
|
21
|
+
if (refusal) throw new Error(refusal);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function desktopServiceCommandRefusal(
|
|
25
|
+
command: string,
|
|
26
|
+
inspect: SupervisionInspector = inspectDesktopSupervision,
|
|
27
|
+
latch: SupervisionLatch = createSupervisionLatch(),
|
|
28
|
+
): string | null {
|
|
29
|
+
if (!["install", "repair", "start", "restart"].includes(command)) return null;
|
|
30
|
+
const evidence = inspect();
|
|
31
|
+
return desktopServiceRefusal(evidence, latch.observe(evidence));
|
|
32
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { accessSync, constants, readFileSync,
|
|
2
|
+
import { accessSync, constants, readFileSync, realpathSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
5
|
import { readPid } from "../config/process-state";
|
|
6
6
|
import { resolveServiceOwnership, sameServiceOwnershipSubject, type ServiceOwnershipResolution } from "./state";
|
|
7
|
+
import { inspectDesktopSupervision, processIdentity, procfs, type ProcReader } from "./desktop-supervision.mjs";
|
|
7
8
|
|
|
8
9
|
export interface DesktopStartupDiagnostic {
|
|
9
10
|
/** Durable desktop claim; failed supervision does not release ownership. */
|
|
@@ -11,11 +12,13 @@ export interface DesktopStartupDiagnostic {
|
|
|
11
12
|
loginEnabled: boolean;
|
|
12
13
|
running: boolean;
|
|
13
14
|
viable: boolean;
|
|
15
|
+
/** Live parentage when no durable claim exists; never converts supervision into ownership. */
|
|
16
|
+
supervisor?: { supervisorPid: number; runtimePid: number; app: string };
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
/** A login item alone is insufficient: the same app must
|
|
19
|
+
/** A login item alone is insufficient: the same app must supervise this proxy. */
|
|
17
20
|
export function deriveDesktopStartup(facts: Omit<DesktopStartupDiagnostic, "viable">): DesktopStartupDiagnostic {
|
|
18
|
-
return { ...facts, viable: facts.owned && facts.loginEnabled && facts.running };
|
|
21
|
+
return { ...facts, viable: (facts.owned || facts.supervisor !== undefined) && facts.loginEnabled && facts.running };
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
function run(command: string, args: string[]): string {
|
|
@@ -28,25 +31,12 @@ interface DesktopStartupDeps {
|
|
|
28
31
|
uid?: number;
|
|
29
32
|
ownership?: () => ServiceOwnershipResolution;
|
|
30
33
|
readPid?: () => number | null;
|
|
34
|
+
readRuntimePortPid?: () => number | null;
|
|
31
35
|
run?: typeof run;
|
|
32
36
|
env?: NodeJS.ProcessEnv;
|
|
33
37
|
proc?: ProcReader;
|
|
34
38
|
}
|
|
35
39
|
|
|
36
|
-
/** Linux process identity from procfs: executable target and parent pid. */
|
|
37
|
-
interface ProcReader {
|
|
38
|
-
exe(pid: number): string;
|
|
39
|
-
parent(pid: number): number;
|
|
40
|
-
}
|
|
41
|
-
const procfs: ProcReader = {
|
|
42
|
-
exe: pid => readlinkSync(`/proc/${pid}/exe`),
|
|
43
|
-
// comm may contain spaces or parentheses, so fields are counted after the last ')'.
|
|
44
|
-
parent: pid => {
|
|
45
|
-
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
46
|
-
return Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
|
|
47
|
-
},
|
|
48
|
-
};
|
|
49
|
-
|
|
50
40
|
/** Cheap ownership-only snapshot: no launchd/process probes on the server request path. */
|
|
51
41
|
export function desktopStartupOwnership(deps: DesktopStartupDeps = {}): DesktopStartupDiagnostic | undefined {
|
|
52
42
|
const platform = deps.platform ?? process.platform;
|
|
@@ -56,9 +46,47 @@ export function desktopStartupOwnership(deps: DesktopStartupDeps = {}): DesktopS
|
|
|
56
46
|
? deriveDesktopStartup({ owned: true, loginEnabled: false, running: false }) : undefined;
|
|
57
47
|
}
|
|
58
48
|
|
|
59
|
-
function
|
|
60
|
-
const
|
|
61
|
-
|
|
49
|
+
function macLoginRegistration(deps: DesktopStartupDeps) {
|
|
50
|
+
const execute = deps.run ?? run;
|
|
51
|
+
const path = join(deps.home ?? homedir(), "Library", "LaunchAgents", "OpenCodex.plist");
|
|
52
|
+
const plist = JSON.parse(execute("/usr/bin/plutil", ["-convert", "json", "-o", "-", path]));
|
|
53
|
+
const args = plist.ProgramArguments;
|
|
54
|
+
if (plist.Label !== "OpenCodex" || plist.RunAtLoad !== true || !Array.isArray(args)
|
|
55
|
+
|| args.length !== 2 || args[1] !== "--autostart" || typeof args[0] !== "string"
|
|
56
|
+
|| !args[0].endsWith("/Contents/MacOS/opencodex-desktop")
|
|
57
|
+
|| (plist.Program !== undefined && plist.Program !== args[0])) return null;
|
|
58
|
+
const app = realpathSync(args[0]);
|
|
59
|
+
const proxy = realpathSync(join(dirname(app), "ocx"));
|
|
60
|
+
accessSync(app, constants.X_OK);
|
|
61
|
+
accessSync(proxy, constants.X_OK);
|
|
62
|
+
const domain = `gui/${deps.uid ?? process.getuid!()}`;
|
|
63
|
+
const disabled = execute("/bin/launchctl", ["print-disabled", domain]);
|
|
64
|
+
const loaded = execute("/bin/launchctl", ["print", `${domain}/OpenCodex`]);
|
|
65
|
+
const program = /^\s*program = (.+)$/m.exec(loaded)?.[1];
|
|
66
|
+
const loadedPath = /^\s*path = (.+)$/m.exec(loaded)?.[1];
|
|
67
|
+
const loginEnabled = /^\s*disabled services = \{[\s\S]*\}\s*$/.test(disabled)
|
|
68
|
+
&& !/"OpenCodex"\s*=>\s*(?:disabled|true)/.test(disabled)
|
|
69
|
+
&& program !== undefined && realpathSync(program) === app
|
|
70
|
+
&& loadedPath !== undefined && realpathSync(loadedPath) === realpathSync(path);
|
|
71
|
+
return { app, proxy, loginEnabled };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function diagnoseSupervisedStartup(deps: DesktopStartupDeps, ownerRevision: number, loginFor: (app: string) => boolean): DesktopStartupDiagnostic | undefined {
|
|
75
|
+
const evidence = inspectDesktopSupervision(deps);
|
|
76
|
+
if (evidence.kind !== "desktop") return undefined;
|
|
77
|
+
let loginEnabled = false;
|
|
78
|
+
try { loginEnabled = loginFor(evidence.app); }
|
|
79
|
+
catch { /* Login failure revokes restart viability, not the observed live supervisor. */ }
|
|
80
|
+
if ((deps.ownership ?? resolveServiceOwnership)().kind !== "none") return undefined;
|
|
81
|
+
// Login probes can outlive the runtime or its parent; only the same live chain gets credit.
|
|
82
|
+
const final = inspectDesktopSupervision({ ...deps, targetPid: evidence.runtimePid });
|
|
83
|
+
if (final.kind !== "desktop" || final.runtimePid !== evidence.runtimePid
|
|
84
|
+
|| final.supervisorPid !== evidence.supervisorPid || final.app !== evidence.app) return undefined;
|
|
85
|
+
// A new claim or revision during the last process probe invalidates the unowned projection.
|
|
86
|
+
const finalOwner = (deps.ownership ?? resolveServiceOwnership)();
|
|
87
|
+
if (finalOwner.kind !== "none" || finalOwner.revision !== ownerRevision) return undefined;
|
|
88
|
+
const { supervisorPid, runtimePid, app } = evidence;
|
|
89
|
+
return deriveDesktopStartup({ owned: false, loginEnabled, running: true, supervisor: { supervisorPid, runtimePid, app } });
|
|
62
90
|
}
|
|
63
91
|
|
|
64
92
|
/** Read-only macOS desktop ownership, login registration and live parent/child checks. */
|
|
@@ -66,6 +94,10 @@ export function diagnoseMacDesktopStartup(deps: DesktopStartupDeps = {}): Deskto
|
|
|
66
94
|
if ((deps.platform ?? process.platform) !== "darwin") return undefined;
|
|
67
95
|
const ownership = deps.ownership ?? resolveServiceOwnership;
|
|
68
96
|
const owner = ownership();
|
|
97
|
+
if (owner.kind === "none") return diagnoseSupervisedStartup(deps, owner.revision, app => {
|
|
98
|
+
const login = macLoginRegistration(deps);
|
|
99
|
+
return login !== null && login.app === app && login.loginEnabled;
|
|
100
|
+
});
|
|
69
101
|
if (owner.kind !== "owned" || owner.ownership.owner !== "desktop") return undefined;
|
|
70
102
|
const facts = { owned: true, loginEnabled: false, running: false };
|
|
71
103
|
const execute = deps.run ?? run;
|
|
@@ -74,26 +106,10 @@ export function diagnoseMacDesktopStartup(deps: DesktopStartupDeps = {}): Deskto
|
|
|
74
106
|
const home = deps.home ?? homedir();
|
|
75
107
|
const id = readFileSync(join(home, "Library", "Application Support", "com.opencodex.desktop", "install-id"), "utf8").trim();
|
|
76
108
|
if (id !== owner.ownership.installId) return deriveDesktopStartup(facts);
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|| args.length !== 2 || args[1] !== "--autostart" || typeof args[0] !== "string"
|
|
82
|
-
|| !args[0].endsWith("/Contents/MacOS/opencodex-desktop")
|
|
83
|
-
|| (plist.Program !== undefined && plist.Program !== args[0])) return deriveDesktopStartup(facts);
|
|
84
|
-
const app = realpathSync(args[0]);
|
|
85
|
-
const proxy = realpathSync(join(dirname(app), "ocx"));
|
|
86
|
-
accessSync(app, constants.X_OK);
|
|
87
|
-
accessSync(proxy, constants.X_OK);
|
|
88
|
-
const domain = `gui/${deps.uid ?? process.getuid!()}`;
|
|
89
|
-
const disabled = execute("/bin/launchctl", ["print-disabled", domain]);
|
|
90
|
-
const loaded = execute("/bin/launchctl", ["print", `${domain}/OpenCodex`]);
|
|
91
|
-
const program = /^\s*program = (.+)$/m.exec(loaded)?.[1];
|
|
92
|
-
const loadedPath = /^\s*path = (.+)$/m.exec(loaded)?.[1];
|
|
93
|
-
facts.loginEnabled = /^\s*disabled services = \{[\s\S]*\}\s*$/.test(disabled)
|
|
94
|
-
&& !/"OpenCodex"\s*=>\s*(?:disabled|true)/.test(disabled)
|
|
95
|
-
&& program !== undefined && realpathSync(program) === app
|
|
96
|
-
&& loadedPath !== undefined && realpathSync(loadedPath) === realpathSync(path);
|
|
109
|
+
const login = macLoginRegistration(deps);
|
|
110
|
+
if (!login) return deriveDesktopStartup(facts);
|
|
111
|
+
const { app, proxy } = login;
|
|
112
|
+
facts.loginEnabled = login.loginEnabled;
|
|
97
113
|
const pid = pidReader();
|
|
98
114
|
if (pid !== null) {
|
|
99
115
|
const child = processIdentity(pid, execute);
|
|
@@ -135,6 +151,16 @@ export function diagnoseLinuxDesktopStartup(deps: DesktopStartupDeps = {}): Desk
|
|
|
135
151
|
if ((deps.platform ?? process.platform) !== "linux") return undefined;
|
|
136
152
|
const ownership = deps.ownership ?? resolveServiceOwnership;
|
|
137
153
|
const owner = ownership();
|
|
154
|
+
if (owner.kind === "none") return diagnoseSupervisedStartup(deps, owner.revision, app => {
|
|
155
|
+
const home = deps.home ?? homedir();
|
|
156
|
+
const configured = (deps.env ?? process.env).XDG_CONFIG_HOME;
|
|
157
|
+
const config = configured && isAbsolute(configured) ? configured : join(home, ".config");
|
|
158
|
+
// auto-launch's HOME entry must remain inside the session's XDG autostart search path.
|
|
159
|
+
if (resolve(config) !== resolve(home, ".config")) return false;
|
|
160
|
+
const entry = readFileSync(join(home, ".config", "autostart", "OpenCodex.desktop"), "utf8");
|
|
161
|
+
const login = linuxLoginApp(entry);
|
|
162
|
+
return login !== null && realpathSync(login) === app;
|
|
163
|
+
});
|
|
138
164
|
if (owner.kind !== "owned" || owner.ownership.owner !== "desktop") return undefined;
|
|
139
165
|
const facts = { owned: true, loginEnabled: false, running: false };
|
|
140
166
|
const proc = deps.proc ?? procfs;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type SupervisionEvidence =
|
|
2
|
+
| { kind: "desktop"; runtimePid: number; supervisorPid: number; app: string; proxy: string }
|
|
3
|
+
| { kind: "none" }
|
|
4
|
+
| { kind: "unknown"; reason: string; desktopSeen: boolean }
|
|
5
|
+
| { kind: "unsupported" };
|
|
6
|
+
|
|
7
|
+
export interface ProcReader {
|
|
8
|
+
exe(pid: number): string;
|
|
9
|
+
parent(pid: number): number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface DesktopSupervisionDeps {
|
|
13
|
+
targetPid?: number;
|
|
14
|
+
platform?: NodeJS.Platform;
|
|
15
|
+
run?: (command: string, args: string[]) => string;
|
|
16
|
+
proc?: ProcReader;
|
|
17
|
+
readPid?: () => number | null;
|
|
18
|
+
readRuntimePortPid?: () => number | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function desktopSupervisionPaths(): { pid: string; runtimePort: string };
|
|
22
|
+
export const procfs: ProcReader;
|
|
23
|
+
export function processIdentity(pid: number, execute?: DesktopSupervisionDeps["run"], onExecutable?: (path: string) => void):
|
|
24
|
+
{ parent: number; executable: string } | null;
|
|
25
|
+
export function inspectDesktopSupervision(deps?: DesktopSupervisionDeps): SupervisionEvidence;
|
|
26
|
+
export function createSupervisionLatch(): { observe(evidence: SupervisionEvidence): boolean };
|
|
@@ -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) {
|
|
@@ -5,7 +5,7 @@ export interface OwnershipMutationLeaseOptions {
|
|
|
5
5
|
readonly processAlive?: (pid: number) => boolean;
|
|
6
6
|
readonly beforeRelease?: (lockPath: string) => void;
|
|
7
7
|
readonly joinToken?: string;
|
|
8
|
-
/** Names the current executable at the recorded PID, identity unverified;
|
|
8
|
+
/** Names the current executable at the recorded PID, identity unverified; uses `ps` on POSIX and omits the default image lookup on Windows. */
|
|
9
9
|
readonly processImage?: (pid: number) => string | null;
|
|
10
10
|
}
|
|
11
11
|
|
|
@@ -112,13 +112,11 @@ function readIncompleteOwner(path) {
|
|
|
112
112
|
/** The current executable at the recorded PID, identity unverified; null if unreadable within a second. */
|
|
113
113
|
function processImage(pid) {
|
|
114
114
|
try {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return listed.status === 0 && match && Number(match[2]) === pid ? match[1] : null;
|
|
121
|
-
}
|
|
115
|
+
// This module also runs in the Node npm/pnpm launcher, before Bun is available.
|
|
116
|
+
// Without a kernel-backed system-directory resolver, omit the optional Windows
|
|
117
|
+
// image label rather than searching the caller's directory/PATH or trusting env roots.
|
|
118
|
+
// PID, liveness, age and lease ownership decisions do not depend on this label.
|
|
119
|
+
if (process.platform === "win32") return null;
|
|
122
120
|
const listed = spawnSync("ps", ["-o", "comm=", "-p", String(pid)], { encoding: "utf8", timeout: 1_000 });
|
|
123
121
|
const command = (listed.stdout ?? "").trim();
|
|
124
122
|
return listed.status === 0 && command ? basename(command) : null;
|