@bitkyc08/opencodex 2.83.0 → 2.86.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 +0 -4
- package/bin/ocx.mjs +61 -3
- package/gui/dist/assets/App-BjlttVLy.js +58 -0
- package/gui/dist/assets/Tray-DyR9yBee.js +1 -0
- package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
- package/gui/dist/assets/index-BMssP4k3.js +94 -0
- package/gui/dist/assets/quota-summary-D8M1bc_R.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +2 -3
- package/src/adapters/anthropic/beta-allowlist.ts +87 -8
- package/src/adapters/anthropic/client-identity.ts +1 -1
- package/src/adapters/anthropic/passthrough.ts +33 -4
- package/src/adapters/azure.ts +19 -1
- package/src/adapters/client-fingerprint.ts +24 -0
- package/src/adapters/coding-agent/protocol.ts +15 -8
- package/src/adapters/coding-agent/turn.ts +22 -9
- package/src/adapters/cursor/arg-codec.ts +17 -5
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/cursor/freeform-args.ts +75 -0
- package/src/adapters/cursor/protobuf-events.ts +36 -23
- package/src/adapters/google-antigravity-wire.ts +22 -1
- package/src/adapters/google.ts +2 -2
- package/src/adapters/ollama-native.ts +29 -1
- package/src/adapters/openai-chat/messages.ts +8 -4
- package/src/adapters/openai-chat/response-events.ts +59 -1
- package/src/adapters/openai-chat.ts +8 -5
- package/src/adapters/openai-responses/empty-assistant.ts +32 -0
- package/src/adapters/openai-responses/passthrough.ts +3 -0
- package/src/adapters/openai-responses/request-strips.ts +10 -2
- package/src/adapters/qoder/adapter.ts +56 -15
- package/src/advisor/consult.ts +220 -0
- package/src/advisor/context.ts +226 -0
- package/src/advisor/disclosure.ts +20 -0
- package/src/advisor/runtime.ts +305 -0
- package/src/advisor/settings.ts +135 -0
- package/src/advisor/state.ts +369 -0
- package/src/advisor/synthetic-tool.ts +33 -0
- package/src/bridge/errors.ts +24 -0
- package/src/bridge.ts +1 -1
- package/src/chat/inbound.ts +30 -6
- package/src/chat/outbound.ts +5 -0
- package/src/claude/context-windows.ts +54 -1
- package/src/claude/desktop-3p-library.ts +43 -3
- package/src/claude/desktop-3p.ts +22 -8
- package/src/claude/desktop-picker-profile.ts +11 -4
- package/src/claude/desktop-profile.ts +10 -1
- package/src/claude/desktop-remote-store-io.ts +5 -3
- package/src/claude/inbound.ts +121 -50
- package/src/claude/intercept/connect-proxy.ts +10 -3
- package/src/claude/intercept/local-ca.ts +8 -1
- package/src/claude/intercept/picker-ca-persistence.ts +3 -1
- package/src/claude/intercept/picker-listener.ts +49 -1
- package/src/claude/intercept/settings-no-proxy.ts +146 -0
- package/src/claude/intercept/settings.ts +101 -9
- package/src/claude/model-default-settings.ts +154 -0
- package/src/claude/model-info.ts +7 -7
- package/src/claude/outbound.ts +13 -6
- package/src/cli/account-api.ts +65 -0
- package/src/cli/account-auth.ts +26 -2
- package/src/cli/account.ts +41 -5
- package/src/cli/advisor.ts +151 -0
- package/src/cli/auxiliary-listener-preflight.ts +37 -0
- package/src/cli/capabilities-access-remote.ts +8 -0
- package/src/cli/capabilities-agents-routing.ts +18 -0
- package/src/cli/capabilities-base.ts +14 -2
- package/src/cli/catalog-command-result.ts +2 -1
- package/src/cli/chatgpt-command.ts +35 -14
- package/src/cli/claude.ts +60 -11
- package/src/cli/config-command.ts +8 -2
- package/src/cli/debug.ts +8 -0
- package/src/cli/dispatch.ts +21 -4
- package/src/cli/doctor.ts +9 -0
- package/src/cli/ensure-desktop-cold-start.ts +329 -0
- package/src/cli/export-command.ts +15 -2
- package/src/cli/help.ts +2 -1
- package/src/cli/hub-disable.ts +99 -0
- package/src/cli/hub.ts +4 -2
- package/src/cli/index.ts +26 -8
- package/src/cli/integration-plan-dto.ts +2 -0
- package/src/cli/link-child.ts +104 -0
- package/src/cli/link-enrollment.ts +96 -0
- package/src/cli/link.ts +39 -24
- package/src/cli/observe-stream.ts +6 -1
- package/src/cli/observe.ts +10 -2
- package/src/cli/provider.ts +12 -0
- package/src/cli/registry.ts +20 -7
- package/src/cli/restart-scope.ts +3 -1
- package/src/cli/runtime-api.ts +27 -2
- package/src/cli/status.ts +104 -20
- package/src/client/connect.ts +39 -4
- package/src/client/hub-client.ts +14 -7
- package/src/client/hub-state.ts +25 -14
- package/src/client/link-home.ts +248 -0
- package/src/client/link-management-relay.ts +61 -0
- package/src/client/link-relay.ts +6 -1
- package/src/client/link-status.ts +14 -0
- package/src/client/machine-api.ts +14 -7
- package/src/client/machine-listener.ts +29 -4
- package/src/clients/config-export/contracts.ts +1 -0
- package/src/clients/config-export/model-metadata.ts +3 -2
- package/src/clients/config-export/qoder.ts +93 -0
- package/src/clients/config-export.ts +59 -16
- package/src/clients/omo-role-models.ts +6 -3
- package/src/codex/agent-role-models.ts +26 -8
- package/src/codex/auth-api/account-list.ts +17 -2
- package/src/codex/auth-api/account-selection.ts +17 -7
- package/src/codex/auth-api/pool-mode-gate.ts +5 -2
- package/src/codex/auth-api/routes.ts +5 -1
- package/src/codex/auth-context.ts +79 -5
- package/src/codex/autostart-health.ts +7 -1
- package/src/codex/catalog/build-entries.ts +11 -2
- package/src/codex/catalog/bundled.ts +5 -1
- package/src/codex/catalog/configured-tiers.ts +50 -0
- package/src/codex/catalog/effort.ts +6 -3
- package/src/codex/catalog/metadata.ts +4 -2
- package/src/codex/catalog/model-hints.ts +21 -3
- package/src/codex/catalog/native-models.ts +16 -2
- package/src/codex/catalog/native-ultrafast.ts +106 -0
- package/src/codex/catalog/parsing.ts +5 -0
- package/src/codex/catalog/provider-models.ts +4 -0
- package/src/codex/catalog/retained-sync.ts +2 -0
- package/src/codex/catalog/routed-gather.ts +22 -4
- package/src/codex/catalog/vendor-context.ts +82 -0
- package/src/codex/config-write-lock.ts +40 -2
- package/src/codex/convergence.ts +2 -0
- package/src/codex/desktop-app/types.ts +2 -0
- package/src/codex/desktop-app/windows.ts +29 -16
- package/src/codex/desktop-app-restart.ts +4 -2
- package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
- package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
- package/src/codex/desktop-compatibility/connection-store.ts +87 -0
- package/src/codex/desktop-compatibility/installed-build.ts +55 -0
- package/src/codex/desktop-compatibility/json-body.ts +29 -0
- package/src/codex/desktop-compatibility/native-identity.ts +62 -0
- package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
- package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
- package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
- package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
- package/src/codex/desktop-compatibility/runtime.ts +173 -0
- package/src/codex/desktop-compatibility/service.ts +30 -0
- package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
- package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
- package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
- package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
- package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
- package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
- package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
- package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
- package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
- package/src/codex/desktop-compatibility/windows-key-protection.ts +59 -0
- package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
- package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
- package/src/codex/history-provider.ts +1 -1
- package/src/codex/inject/config-write-section.ts +4 -1
- package/src/codex/inject/remove.ts +18 -6
- package/src/codex/journal.ts +16 -1
- package/src/codex/main-account.ts +99 -15
- package/src/codex/native-main-refresh-mode.ts +217 -0
- package/src/codex/native-profile-startup.ts +4 -1
- package/src/codex/prompt-journal.ts +20 -2
- package/src/codex/prompt-layers/encoding.ts +49 -0
- package/src/codex/prompt-layers/import-source.ts +30 -0
- package/src/codex/prompt-layers/toml-edit.ts +269 -37
- package/src/codex/prompt-layers/toml-read.ts +52 -4
- package/src/codex/prompt-layers.ts +383 -20
- package/src/codex/prompt-lock-claim.ts +13 -3
- package/src/codex/prompt-lock-evidence.ts +3 -3
- package/src/codex/prompt-lock-owner.ts +14 -6
- package/src/codex/prompt-text-probe.ts +34 -5
- package/src/codex/quota.ts +13 -6
- package/src/codex/role-auto-assign.ts +2 -5
- package/src/codex/routing/active-account.ts +21 -0
- package/src/codex/routing/health-store.ts +1 -1
- package/src/codex/routing/pool-recovery.ts +79 -0
- package/src/codex/routing.ts +1 -1
- package/src/codex/runtime.ts +196 -8
- package/src/codex/shim-diagnostics.ts +6 -0
- package/src/codex/shim-overlay.ts +15 -6
- package/src/codex/shim.ts +1 -1
- package/src/combos/failover.ts +3 -0
- package/src/config/api-key-reconcile.ts +34 -0
- package/src/config/diagnostics.ts +18 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/live-reconcile.ts +56 -29
- package/src/config/live-topology.ts +29 -0
- package/src/config/load-degrade.ts +8 -2
- package/src/config/paths.ts +13 -3
- package/src/config/salvage.ts +3 -1
- package/src/config/schema/config-schema.ts +19 -0
- package/src/config/schema/desktop-compatibility.ts +9 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +713 -285
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/grpc-web.ts +23 -1
- package/src/grok/reset-coupon-ledger.ts +75 -5
- package/src/grok/reset-coupons.ts +4 -2
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/config-io.ts +20 -2
- package/src/integrations/journal.ts +15 -1
- package/src/integrations/mutation-plan.ts +14 -7
- package/src/integrations/registry.ts +52 -11
- package/src/integrations/state.ts +19 -7
- package/src/integrations/store.ts +3 -0
- package/src/integrations/target.ts +42 -5
- package/src/integrations/writer.ts +24 -7
- package/src/lib/advisor-activation.ts +16 -0
- package/src/lib/app-owned-memory-stores.ts +4 -0
- package/src/lib/app-owned-memory.ts +11 -0
- package/src/lib/background-work.ts +65 -0
- package/src/lib/bounded-file-read.ts +58 -0
- package/src/lib/bun-path-runtime.d.mts +10 -1
- package/src/lib/bun-path-runtime.mjs +62 -3
- package/src/lib/desktop-proxy-route.ts +30 -0
- package/src/lib/desktop-upstream-tunnel.ts +107 -0
- package/src/lib/errors.ts +7 -0
- package/src/lib/json-byte-size.ts +31 -0
- package/src/lib/local-internal-call-capability.ts +55 -0
- package/src/lib/optional-shutdown-hooks.ts +35 -0
- package/src/lib/outbound-credential-registry.ts +210 -0
- package/src/lib/package-tree-integrity.ts +2 -2
- package/src/lib/passive-read.ts +35 -0
- package/src/lib/shadow-call.ts +44 -5
- package/src/lib/socks5-fetch.ts +48 -71
- package/src/lib/socks5-handshake.ts +112 -0
- package/src/lib/translator-budget.ts +7 -1
- package/src/link/codex-settings.ts +155 -0
- package/src/link/management-routes.ts +89 -0
- package/src/link/remote-probe.ts +38 -0
- package/src/link/ssh-argv.ts +26 -5
- package/src/link/ssh-runner.ts +58 -27
- package/src/link/status-projection.ts +22 -4
- package/src/link/supervisor.ts +144 -49
- package/src/link/tunnel-state.ts +11 -4
- package/src/oauth/anthropic-continuity.ts +4 -3
- package/src/oauth/anthropic-identity.ts +13 -2
- package/src/oauth/anthropic-model-quota.ts +13 -1
- package/src/oauth/anthropic-routing.ts +230 -23
- package/src/oauth/anthropic.ts +18 -4
- package/src/oauth/generic-account-failover.ts +29 -5
- package/src/oauth/google-antigravity.ts +24 -7
- package/src/oauth/index.ts +28 -17
- package/src/oauth/kiro-account-identity.ts +106 -0
- package/src/oauth/kiro-device-login.ts +2 -1
- package/src/oauth/kiro.ts +55 -6
- package/src/oauth/plan.ts +29 -0
- package/src/oauth/store.ts +40 -3
- package/src/oauth/types.ts +2 -0
- package/src/plugins/loader.ts +11 -0
- package/src/protocols/codecs/chat-ir.ts +467 -0
- package/src/protocols/codecs/chat.ts +10 -1
- package/src/protocols/codecs/messages-ir.ts +500 -0
- package/src/protocols/features.ts +10 -2
- package/src/protocols/path.ts +11 -1
- package/src/protocols/plan-snapshot.ts +39 -16
- package/src/protocols/plan.ts +9 -1
- package/src/protocols/settings.ts +8 -0
- package/src/providers/anthropic-billing-ownership.ts +74 -0
- package/src/providers/anthropic-billing-recovery.ts +31 -0
- package/src/providers/anthropic-billing-refusal.ts +55 -0
- package/src/providers/anthropic-reset-admission.ts +62 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +28 -6
- package/src/providers/antigravity-account-roster.ts +102 -0
- package/src/providers/antigravity-models.ts +85 -7
- package/src/providers/api-key-resolve.ts +17 -2
- package/src/providers/key-failover.ts +52 -5
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/model-rename-migration.ts +5 -3
- package/src/providers/model-rename-startup.ts +5 -3
- package/src/providers/quota/account-cache.ts +11 -0
- package/src/providers/quota/anthropic-account-quota.ts +4 -2
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +3 -3
- package/src/providers/quota.ts +4 -1
- package/src/providers/registry/entries-core.ts +21 -20
- package/src/providers/registry/entries-extended.ts +19 -1
- package/src/providers/registry/model-seeds.ts +17 -0
- package/src/providers/request-pacing.ts +9 -0
- package/src/providers/stale-no-vision-seed-migration.ts +72 -0
- package/src/responses/compaction.ts +14 -0
- package/src/responses/parser.ts +37 -20
- package/src/responses/state.ts +32 -29
- package/src/routing/history/build-file.ts +92 -0
- package/src/routing/history/indexer.ts +248 -216
- package/src/routing/history/ingest.ts +238 -0
- package/src/routing/history/rebuild-job.ts +111 -0
- package/src/routing/history/rebuild-protocol.ts +15 -0
- package/src/routing/history/rebuild-worker.ts +34 -0
- package/src/server/admission-model-scope.ts +4 -1
- package/src/server/antigravity-direct-eligibility.ts +208 -0
- package/src/server/auth-cors.ts +46 -0
- package/src/server/chat-completions.ts +75 -9
- package/src/server/claude-messages.ts +110 -20
- package/src/server/configured-credentials.ts +194 -0
- package/src/server/index/desktop-compatibility-startup.ts +56 -0
- package/src/server/index/link-management.ts +58 -0
- package/src/server/index/optional-listeners.ts +5 -1
- package/src/server/index/serve-options.ts +15 -2
- package/src/server/index/startup-warnings.ts +9 -0
- package/src/server/index/websocket-handler.ts +9 -0
- package/src/server/index.ts +13 -10
- package/src/server/inference/client-encoder-delivery.ts +6 -5
- package/src/server/lifecycle.ts +2 -1
- package/src/server/login-shell-path.ts +146 -0
- package/src/server/management/advisor-routes.ts +191 -0
- package/src/server/management/agent-settings-routes.ts +50 -14
- package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
- package/src/server/management/codex-agent-role-routes.ts +19 -6
- package/src/server/management/codex-prompt-routes.ts +136 -5
- package/src/server/management/codex-role-auto-assign.ts +1 -1
- package/src/server/management/companion-routes.ts +4 -0
- package/src/server/management/config-routes.ts +2 -2
- package/src/server/management/context.ts +4 -0
- package/src/server/management/desktop-compatibility-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
- package/src/server/management/grok-coupon-routes.ts +78 -27
- package/src/server/management/link-enrollment.ts +192 -0
- package/src/server/management/link-route-state.ts +185 -0
- package/src/server/management/link-routes.ts +82 -402
- package/src/server/management/model-routes.ts +2 -2
- package/src/server/management/model-rows.ts +3 -1
- package/src/server/management/protocol-settings-patch.ts +1 -0
- package/src/server/management/provider-overwrite-carry.ts +10 -0
- package/src/server/management/provider-routes.ts +1 -0
- package/src/server/management/route-registry.ts +16 -3
- package/src/server/management/shared.ts +3 -0
- package/src/server/management/sibling-guard.ts +1 -0
- package/src/server/management-api.ts +9 -1
- package/src/server/messages-antigravity-direct.ts +195 -0
- package/src/server/messages-native.ts +73 -43
- package/src/server/relay-eager.ts +2 -0
- package/src/server/request-log-filter.ts +6 -1
- package/src/server/request-log-terminal-status.ts +63 -6
- package/src/server/request-log.ts +7 -0
- package/src/server/responses/adapter-continuation.ts +35 -18
- package/src/server/responses/adapter-delivery.ts +24 -2
- package/src/server/responses/adapter-dispatch.ts +261 -81
- package/src/server/responses/advisor-plan-slot.ts +27 -0
- package/src/server/responses/advisor-slot.ts +336 -0
- package/src/server/responses/code-mode-collab-surface.ts +313 -0
- package/src/server/responses/codex-ws-exchange.ts +147 -6
- package/src/server/responses/codex-ws-wire.ts +203 -0
- package/src/server/responses/collaboration.ts +36 -7
- package/src/server/responses/compact.ts +126 -48
- package/src/server/responses/context-overflow.ts +13 -1
- package/src/server/responses/core-codex-account.ts +105 -1
- package/src/server/responses/core-combo-failure.ts +24 -1
- package/src/server/responses/core-combo.ts +28 -2
- package/src/server/responses/core-errors.ts +29 -1
- package/src/server/responses/core-lifetime.ts +2 -0
- package/src/server/responses/core-normalize.ts +11 -0
- package/src/server/responses/core-options.ts +48 -0
- package/src/server/responses/core-replay.ts +7 -4
- package/src/server/responses/core.ts +1 -1
- package/src/server/responses/deferred-collab-surface.ts +158 -0
- package/src/server/responses/deferred-send-reporter.ts +25 -0
- package/src/server/responses/fetch-helpers.ts +12 -1
- package/src/server/responses/passthrough-delivery.ts +20 -11
- package/src/server/responses/passthrough-dispatch.ts +29 -7
- package/src/server/responses/request-prepare.ts +74 -9
- package/src/server/responses/request-transport.ts +22 -8
- package/src/server/responses/run-turn-execution.ts +70 -8
- package/src/server/responses/sidecar-execution.ts +89 -5
- package/src/server/responses/terminal-guard.ts +2 -1
- package/src/server/responses/ws-upstream.ts +9 -1
- package/src/server/responses-undeclared-tool-guard.ts +30 -9
- package/src/server/startup-action-control.ts +38 -4
- package/src/server/startup-health-cache.ts +5 -0
- package/src/server/system-env-shell.ts +7 -2
- package/src/server/system-env.ts +11 -1
- package/src/service/cli.ts +5 -1
- package/src/service/desktop-startup.ts +161 -34
- package/src/service/desktop-supervision.d.mts +27 -1
- package/src/service/desktop-supervision.mjs +111 -5
- package/src/service/health-sentinel-launch.ts +176 -0
- package/src/service/health-sentinel-main.ts +466 -0
- package/src/service/health-sentinel-policy.ts +241 -0
- package/src/service/health-sentinel-probe.ts +71 -0
- package/src/service/health-sentinel-windows.ts +87 -0
- package/src/service/linux-linger.ts +20 -0
- package/src/service/logging.ts +72 -0
- package/src/tray/windows-tray.ps1 +43 -24
- package/src/types/config.ts +68 -6
- package/src/types/provider.ts +7 -0
- package/src/types/request.ts +15 -0
- package/src/types/tools.ts +2 -0
- package/src/update/async-check.ts +1 -1
- package/src/update/badge.ts +2 -2
- package/src/update/index.ts +88 -15
- package/src/update/job-supervised-restart.ts +107 -0
- package/src/update/job.ts +32 -32
- package/src/update/notify.ts +1 -1
- package/src/update/refresh-scheduler.ts +2 -2
- package/src/update/supervised-recovery.d.mts +27 -0
- package/src/update/supervised-recovery.mjs +157 -0
- package/src/usage/cache-diagnostic.ts +2 -0
- package/src/usage/expected-prices.ts +16 -0
- package/src/usage/log.ts +19 -1
- package/src/web-search/gemini-executor.ts +2 -2
- package/assets/sponsors/packycode.png +0 -0
- package/gui/dist/assets/App-Ba8uG696.js +0 -54
- package/gui/dist/assets/Tray-IImbNPRy.js +0 -1
- package/gui/dist/assets/index-BHRZdJuv.js +0 -94
- package/gui/dist/assets/quota-summary-DiJO6sYY.js +0 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { UNDECLARED_TOOL_CALL_ERROR_CODE } from "../lib/errors";
|
|
1
2
|
import {
|
|
2
3
|
collectAmbiguousDottedAliases,
|
|
3
4
|
dottedAliasIsUnambiguous,
|
|
@@ -78,7 +79,7 @@ const NAMELESS_TOOL_SPEC_TYPES = new Set([
|
|
|
78
79
|
/** An upstream-supplied name reaches the error message; keep it bounded. */
|
|
79
80
|
const MAX_REPORTED_NAME_CHARS = 100;
|
|
80
81
|
|
|
81
|
-
export
|
|
82
|
+
export { UNDECLARED_TOOL_CALL_ERROR_CODE };
|
|
82
83
|
|
|
83
84
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
84
85
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
@@ -370,6 +371,24 @@ export function hasExplicitWireToolCatalog(body: unknown): boolean {
|
|
|
370
371
|
);
|
|
371
372
|
}
|
|
372
373
|
|
|
374
|
+
/**
|
|
375
|
+
* Whether the caller declared an explicitly EMPTY catalog — `tools: []` or an
|
|
376
|
+
* `additional_tools` item with `tools: []`. Unlike an absent or partial catalog this is an
|
|
377
|
+
* unambiguous deny-all: no deferred registry can exist under it, so even deferred wires
|
|
378
|
+
* enforce it. A hosted-only catalog is NOT empty — it still defers like an absent one.
|
|
379
|
+
*/
|
|
380
|
+
export function hasExplicitEmptyToolCatalog(body: unknown): boolean {
|
|
381
|
+
if (!isPlainObject(body)) return false;
|
|
382
|
+
if (Array.isArray(body.tools) && body.tools.length === 0) return true;
|
|
383
|
+
if (!Array.isArray(body.input)) return false;
|
|
384
|
+
return body.input.some(item =>
|
|
385
|
+
isPlainObject(item)
|
|
386
|
+
&& item.type === "additional_tools"
|
|
387
|
+
&& Array.isArray(item.tools)
|
|
388
|
+
&& item.tools.length === 0
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
373
392
|
/**
|
|
374
393
|
* Evaluates whether an individual output item represents an undeclared tool call.
|
|
375
394
|
*
|
|
@@ -703,7 +722,6 @@ function failedBlocks(name: string, newline: string): readonly string[] {
|
|
|
703
722
|
* @param providerExecutedCallTypes - Call types executed by the provider.
|
|
704
723
|
* @param declaredBare - Explicitly declared bare tool names without namespace provenance.
|
|
705
724
|
* @param declaredCustom - Current bare custom declarations eligible for code-mode recovery.
|
|
706
|
-
* @param enforceDeclaredToolNames - Refuse undeclared calls; normalization remains active when false.
|
|
707
725
|
* @returns An SSE block rewrite function.
|
|
708
726
|
*/
|
|
709
727
|
export function createUndeclaredToolCallGuardBlockRewrite(
|
|
@@ -712,8 +730,11 @@ export function createUndeclaredToolCallGuardBlockRewrite(
|
|
|
712
730
|
providerExecutedCallTypes: ProviderExecutedCallTypes = EMPTY_PROVIDER_EXECUTED_CALL_TYPES,
|
|
713
731
|
declaredBare?: ReadonlySet<string>,
|
|
714
732
|
declaredCustom?: ReadonlySet<string>,
|
|
715
|
-
|
|
733
|
+
options?: { refuseUndeclared?: boolean },
|
|
716
734
|
): SseBlockRewrite {
|
|
735
|
+
// Deferred-catalog wires (Chat/Anthropic) keep the default-namespace normalization — the
|
|
736
|
+
// client can only resolve the bare name it declared — while the refusal itself stays off.
|
|
737
|
+
const refuse = options?.refuseUndeclared !== false;
|
|
717
738
|
let tripped = false;
|
|
718
739
|
return (block: string) => {
|
|
719
740
|
if (tripped) return [];
|
|
@@ -725,12 +746,12 @@ export function createUndeclaredToolCallGuardBlockRewrite(
|
|
|
725
746
|
} catch {
|
|
726
747
|
return [block];
|
|
727
748
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
749
|
+
if (refuse) {
|
|
750
|
+
const name = undeclaredToolCallName(parsed, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes, declaredBare, declaredCustom);
|
|
751
|
+
if (name !== undefined) {
|
|
752
|
+
tripped = true;
|
|
753
|
+
return failedBlocks(name, block.includes("\r\n") ? "\r\n" : "\n");
|
|
754
|
+
}
|
|
734
755
|
}
|
|
735
756
|
const normalized = normalizeDefaultNamespaceInPayload(parsed, declared, declaredBare);
|
|
736
757
|
if (normalized.changed) {
|
|
@@ -2,6 +2,9 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { durableBunPath } from "../lib/bun-runtime";
|
|
5
|
+
import { selfLaunchArgv } from "../lib/self-launch-argv";
|
|
6
|
+
import { isDesktopSupervised } from "../lib/system-restart-contract";
|
|
7
|
+
import { withLoginShellPath } from "./login-shell-path";
|
|
5
8
|
import {
|
|
6
9
|
WINDOWS_SCHTASKS_CREATE_ACCESS_DENIED_MARKER,
|
|
7
10
|
isWindowsSchtasksCreateAccessDenied,
|
|
@@ -61,6 +64,20 @@ export function setStartupInstallFinalizeForTests(next: FinalizeFn | null): void
|
|
|
61
64
|
finalizeImpl = next ?? finalizeWindowsSchedulerServiceRegistration;
|
|
62
65
|
}
|
|
63
66
|
|
|
67
|
+
interface StartupInstallLaunch {
|
|
68
|
+
execFile?: typeof execFile;
|
|
69
|
+
/** Overrides Bun's own standalone detection, which a source-mode test cannot flip. */
|
|
70
|
+
isStandaloneExecutable?: boolean;
|
|
71
|
+
isDesktopSupervised?: () => boolean;
|
|
72
|
+
withLoginShellPath?: (env: NodeJS.ProcessEnv) => Promise<NodeJS.ProcessEnv>;
|
|
73
|
+
}
|
|
74
|
+
let installLaunch: StartupInstallLaunch = {};
|
|
75
|
+
|
|
76
|
+
/** Test-only seam: observe the installer child's argv and simulate a compiled `ocx`. */
|
|
77
|
+
export function setStartupInstallLaunchForTests(next: StartupInstallLaunch | null): void {
|
|
78
|
+
installLaunch = next ?? {};
|
|
79
|
+
}
|
|
80
|
+
|
|
64
81
|
/** Snapshot of the install lock for diagnostics and tests. */
|
|
65
82
|
export function getStartupInstallState(): StartupInstallState {
|
|
66
83
|
return installState;
|
|
@@ -156,20 +173,37 @@ export function installFailureDetail(stdout: string, stderr: string, error: Erro
|
|
|
156
173
|
return classifyCliInstallFailure(stdout, stderr, error).detail;
|
|
157
174
|
}
|
|
158
175
|
|
|
159
|
-
|
|
176
|
+
/**
|
|
177
|
+
* Under OpenCodex Desktop the proxy has the GUI launch PATH, where no user-installed Codex is
|
|
178
|
+
* visible. The shim installer gets the user's login-shell PATH instead (#6971).
|
|
179
|
+
*/
|
|
180
|
+
async function installChildEnv(action: StartupInstallAction): Promise<NodeJS.ProcessEnv> {
|
|
181
|
+
if (action !== "install-shim" || !(installLaunch.isDesktopSupervised ?? isDesktopSupervised)()) return process.env;
|
|
182
|
+
return (installLaunch.withLoginShellPath ?? withLoginShellPath)(process.env);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function runCliInstall(
|
|
160
186
|
action: StartupInstallAction,
|
|
161
187
|
options?: { repair?: boolean },
|
|
162
188
|
): Promise<{ stdout: string; stderr: string }> {
|
|
163
189
|
const bun = durableBunPath();
|
|
164
190
|
const cli = join(import.meta.dir, "..", "cli", "index.ts");
|
|
165
|
-
|
|
191
|
+
// The Desktop app and the Linux deb run a compiled `ocx`, where `process.execPath` is the CLI
|
|
192
|
+
// itself: prefixing the source entrypoint made it an unknown command and every dashboard
|
|
193
|
+
// Install/Repair failed (#6963). Same builder as the startup-health probe.
|
|
194
|
+
const argv = selfLaunchArgv(startupInstallArgv(action, options), {
|
|
195
|
+
sourceEntrypoint: cli,
|
|
196
|
+
isStandaloneExecutable: installLaunch.isStandaloneExecutable,
|
|
197
|
+
});
|
|
198
|
+
const run = installLaunch.execFile ?? execFile;
|
|
199
|
+
const env = await installChildEnv(action);
|
|
166
200
|
return new Promise((resolve, reject) => {
|
|
167
201
|
const timeout = process.platform === "win32" && action === "install-service"
|
|
168
202
|
? 0
|
|
169
203
|
: 60_000;
|
|
170
|
-
|
|
204
|
+
run(bun, argv, {
|
|
171
205
|
encoding: "utf8",
|
|
172
|
-
env
|
|
206
|
+
env,
|
|
173
207
|
// A fresh Windows scheduler install now owns its UAC prompt inside this CLI
|
|
174
208
|
// transaction. Killing only the CLI at 60s can orphan its elevated schtasks child,
|
|
175
209
|
// which may register the task after the Dashboard has reported failure. Keep the
|
|
@@ -9,6 +9,7 @@ import { selfLaunchArgv } from "../lib/self-launch-argv";
|
|
|
9
9
|
import { desktopStartupOwnership } from "../service/desktop-startup";
|
|
10
10
|
import type { OcxConfig } from "../types";
|
|
11
11
|
import { truncateRetainedUtf8 } from "../lib/admission";
|
|
12
|
+
import { isPassiveRead } from "../lib/passive-read";
|
|
12
13
|
|
|
13
14
|
const CACHE_TTL_MS = 30_000;
|
|
14
15
|
const PROBE_TIMEOUT_MS = startupHealthProbeBudgetMs();
|
|
@@ -69,6 +70,8 @@ export function getStartupHealthSnapshot(
|
|
|
69
70
|
): StartupHealth {
|
|
70
71
|
const now = deps.now ?? Date.now;
|
|
71
72
|
if (cached && now() - cached.timestamp < CACHE_TTL_MS) return cached.value;
|
|
73
|
+
// A passive read (a Remote Link Child's, #6959) starts no diagnostic process on this machine.
|
|
74
|
+
if (isPassiveRead()) return staleOrFallback(config);
|
|
72
75
|
refreshInBackground(config, deps);
|
|
73
76
|
return staleOrFallback(config);
|
|
74
77
|
}
|
|
@@ -114,6 +117,7 @@ function conservativeFallback(config: Pick<OcxConfig, "codexAutoStart">): Startu
|
|
|
114
117
|
serviceSupported: process.platform === "win32" || process.platform === "darwin" || process.platform === "linux",
|
|
115
118
|
shimInstalled: shim.installed,
|
|
116
119
|
shimHealthy: shim.healthy,
|
|
120
|
+
...(shim.activationPending ? { shimActivationPending: true } : {}),
|
|
117
121
|
platform: process.platform,
|
|
118
122
|
diagnosticStale: true,
|
|
119
123
|
});
|
|
@@ -189,6 +193,7 @@ export async function getCachedStartupHealth(
|
|
|
189
193
|
): Promise<StartupHealth> {
|
|
190
194
|
const now = deps.now ?? Date.now;
|
|
191
195
|
if (cached && now() - cached.timestamp < CACHE_TTL_MS) return cached.value;
|
|
196
|
+
if (isPassiveRead()) return staleOrFallback(config);
|
|
192
197
|
refreshInBackground(config, deps);
|
|
193
198
|
// An expired or empty read is an explicit protection check. Wait for the
|
|
194
199
|
// isolated probe instead of presenting a synthetic failure while that probe
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { accessSync, constants, readFileSync, writeFileSync, unlinkSync, mkdirSync, statSync } from "node:fs";
|
|
2
2
|
import { delimiter, join } from "node:path";
|
|
3
3
|
import { getConfigDir } from "../config";
|
|
4
|
-
import { claudeToolSearchEnv, resolveAutoContext, type AutoContextMode } from "../claude/context-windows";
|
|
4
|
+
import { claudeTierDisplayName, claudeToolSearchEnv, isClaudeTierLabelSlot, resolveAutoContext, type AutoContextMode } from "../claude/context-windows";
|
|
5
5
|
import { PROXY_MARKER, defaultAuthDetectDeps, detectClaudeAuth, ownAdmissionTokens, type AuthDetectDeps } from "../claude/auth-detect";
|
|
6
6
|
import { resolveClaudeAuthMode } from "../claude/auth-mode";
|
|
7
7
|
import { ANTHROPIC_PARENT_ENV_SLOTS, trustedNodeLauncherContext, type AnthropicParentEnvSlot } from "../cli/launcher-context";
|
|
@@ -115,7 +115,12 @@ export function writeShellEnvFile(
|
|
|
115
115
|
}
|
|
116
116
|
for (const [name, value] of Object.entries(modelEnv)) {
|
|
117
117
|
if (name === "ANTHROPIC_MODEL") continue;
|
|
118
|
-
|
|
118
|
+
// An alias tier slot carries its picker label (#6926) in the same branch, so the label is
|
|
119
|
+
// exported only when this file writes the slot and the shell has no label of its own.
|
|
120
|
+
const label = isClaudeTierLabelSlot(name) ? claudeTierDisplayName(value) : undefined;
|
|
121
|
+
lines.push(label === undefined
|
|
122
|
+
? conditional(name, value)
|
|
123
|
+
: `${conditional(name, value)} && [ -z "\${${name}_NAME+x}" ] && export ${name}_NAME=${shellValue(label)}`);
|
|
119
124
|
}
|
|
120
125
|
const maxCtx = config.claudeCode?.maxContextTokens;
|
|
121
126
|
if (typeof maxCtx === "number" && Number.isFinite(maxCtx) && maxCtx > 0) {
|
package/src/server/system-env.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import { readFileSync, writeFileSync, unlinkSync, mkdirSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { getConfigDir } from "../config";
|
|
5
|
-
import { claudeToolSearchEnv, resolveAutoContext, type AutoContextMode } from "../claude/context-windows";
|
|
5
|
+
import { CLAUDE_TIER_LABEL_SLOTS, claudeTierDisplayName, claudeToolSearchEnv, resolveAutoContext, type AutoContextMode } from "../claude/context-windows";
|
|
6
6
|
import { PROXY_MARKER } from "../claude/auth-detect";
|
|
7
7
|
import { isProxyAdmissionSecret } from "./auth-cors";
|
|
8
8
|
import type { OcxConfig } from "../types";
|
|
@@ -30,6 +30,8 @@ const MANAGED_SYSTEM_ENV_NAMES = new Set<string>([
|
|
|
30
30
|
"ANTHROPIC_DEFAULT_FABLE_MODEL",
|
|
31
31
|
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
|
32
32
|
"ANTHROPIC_SMALL_FAST_MODEL",
|
|
33
|
+
// Picker labels paired with an alias tier slot this instance injected (#6926).
|
|
34
|
+
...CLAUDE_TIER_LABEL_SLOTS.map(slot => `${slot}_NAME`),
|
|
33
35
|
"CLAUDE_CODE_MAX_CONTEXT_TOKENS",
|
|
34
36
|
"DISABLE_COMPACT",
|
|
35
37
|
"CLAUDE_CODE_AUTO_COMPACT_WINDOW",
|
|
@@ -315,6 +317,14 @@ export async function injectSystemEnv(
|
|
|
315
317
|
if (name === "ANTHROPIC_MODEL") continue; // legacy slot handled by shell file only (back-compat)
|
|
316
318
|
injectLever(name, value);
|
|
317
319
|
}
|
|
320
|
+
// Picker labels (#6926) only for alias tier slots this instance owns after the loop above: a
|
|
321
|
+
// launchd slot the user set keeps whatever label it has, and a label nothing produces any
|
|
322
|
+
// more is unset by the produced-key sweep below like any other tracked lever.
|
|
323
|
+
for (const slot of CLAUDE_TIER_LABEL_SLOTS) {
|
|
324
|
+
const value = modelEnv[slot];
|
|
325
|
+
const label = value === undefined ? undefined : claudeTierDisplayName(value);
|
|
326
|
+
if (label !== undefined && injectedKeys.includes(slot)) injectLever(`${slot}_NAME`, label);
|
|
327
|
+
}
|
|
318
328
|
const maxCtx = config.claudeCode?.maxContextTokens;
|
|
319
329
|
if (typeof maxCtx === "number" && Number.isFinite(maxCtx) && maxCtx > 0) {
|
|
320
330
|
injectLever("CLAUDE_CODE_MAX_CONTEXT_TOKENS", String(Math.floor(maxCtx)));
|
package/src/service/cli.ts
CHANGED
|
@@ -27,6 +27,8 @@ import { win32 } from "node:path";
|
|
|
27
27
|
import { serviceDiagnosticsSummary } from "./diagnostics";
|
|
28
28
|
import { runServiceClaim } from "./claim";
|
|
29
29
|
import { createSupervisionLatch, desktopServiceCommandRefusal } from "./desktop-command-guard";
|
|
30
|
+
import { linuxServiceBootHint } from "./linux-linger";
|
|
31
|
+
import { diagnoseDesktopStartup, desktopLoginStatusSummary } from "./desktop-startup";
|
|
30
32
|
|
|
31
33
|
/**
|
|
32
34
|
* `restart` is NO LONGER folded into `repair`.
|
|
@@ -336,7 +338,7 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
336
338
|
// and the baked one cannot have diverged yet — unlike `start`, which reads the
|
|
337
339
|
// installed artifact instead.
|
|
338
340
|
await reportServiceServing("installed", { port: resolveServiceListenPort() });
|
|
339
|
-
if (process.platform === "linux") console.log(
|
|
341
|
+
if (process.platform === "linux") console.log(linuxServiceBootHint());
|
|
340
342
|
// Service users never reach the `ocx start` prompt: the proxy they run is the
|
|
341
343
|
// supervised child, which always carries OCX_SERVICE=1. This command, though, is
|
|
342
344
|
// hand-typed in a real terminal, so it is the one interactive moment they get.
|
|
@@ -433,6 +435,8 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
433
435
|
const lease = ownershipMutationLeaseStatusLine(serviceStatePaths());
|
|
434
436
|
if (lease) console.log(lease);
|
|
435
437
|
console.log(`Diagnostics: ${serviceDiagnosticsSummary()}`);
|
|
438
|
+
const desktop = diagnoseDesktopStartup();
|
|
439
|
+
if (desktop) console.log(desktopLoginStatusSummary(desktop));
|
|
436
440
|
break;
|
|
437
441
|
}
|
|
438
442
|
case "uninstall":
|
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { accessSync, constants, readFileSync, realpathSync } from "node:fs";
|
|
2
|
+
import { accessSync, constants, lstatSync, readFileSync, realpathSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
import { basename, dirname, isAbsolute, join, resolve, win32 } 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
|
+
import { inspectDesktopSupervision, processIdentity, procfs, type DesktopSupervisionDeps, type ProcReader } from "./desktop-supervision.mjs";
|
|
8
8
|
|
|
9
9
|
export interface DesktopStartupDiagnostic {
|
|
10
10
|
/** Durable desktop claim; failed supervision does not release ownership. */
|
|
11
11
|
owned: boolean;
|
|
12
12
|
loginEnabled: boolean;
|
|
13
|
+
/** Verified enabled macOS registration; absence means registration is not credited. */
|
|
14
|
+
loginRegistration?: "loaded" | "registered-not-loaded";
|
|
13
15
|
running: boolean;
|
|
14
16
|
viable: boolean;
|
|
15
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Proven live parentage; never converts supervision into ownership. macOS and Linux report it
|
|
19
|
+
* only without a durable claim. Windows also reports it beside a durable Desktop claim, because
|
|
20
|
+
* older Windows runtimes never published a Desktop diagnostic that status could defer to.
|
|
21
|
+
*/
|
|
16
22
|
supervisor?: { supervisorPid: number; runtimePid: number; app: string };
|
|
17
23
|
}
|
|
18
24
|
|
|
@@ -21,6 +27,14 @@ export function deriveDesktopStartup(facts: Omit<DesktopStartupDiagnostic, "viab
|
|
|
21
27
|
return { ...facts, viable: (facts.owned || facts.supervisor !== undefined) && facts.loginEnabled && facts.running };
|
|
22
28
|
}
|
|
23
29
|
|
|
30
|
+
export function desktopLoginStatusSummary(desktop: DesktopStartupDiagnostic): string {
|
|
31
|
+
if (!desktop.loginEnabled) return "Desktop Start at Login: not registered, disabled, or could not be verified.";
|
|
32
|
+
if (desktop.loginRegistration === "registered-not-loaded") {
|
|
33
|
+
return "Desktop Start at Login: registered and enabled for next login; not loaded in this session.";
|
|
34
|
+
}
|
|
35
|
+
return `Desktop Start at Login: enabled${desktop.loginRegistration === "loaded" ? " and loaded in this session" : ""}.`;
|
|
36
|
+
}
|
|
37
|
+
|
|
24
38
|
function run(command: string, args: string[]): string {
|
|
25
39
|
return execFileSync(command, args, { encoding: "utf8", timeout: 750, maxBuffer: 128 * 1024, stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
26
40
|
}
|
|
@@ -35,12 +49,16 @@ interface DesktopStartupDeps {
|
|
|
35
49
|
run?: typeof run;
|
|
36
50
|
env?: NodeJS.ProcessEnv;
|
|
37
51
|
proc?: ProcReader;
|
|
52
|
+
windowsProcesses?: DesktopSupervisionDeps["windowsProcesses"];
|
|
53
|
+
now?: () => number;
|
|
54
|
+
/** The Desktop's own install id (Tauri app_config_dir); null when unreadable. */
|
|
55
|
+
readWindowsInstallId?: () => string | null;
|
|
38
56
|
}
|
|
39
57
|
|
|
40
58
|
/** Cheap ownership-only snapshot: no launchd/process probes on the server request path. */
|
|
41
59
|
export function desktopStartupOwnership(deps: DesktopStartupDeps = {}): DesktopStartupDiagnostic | undefined {
|
|
42
60
|
const platform = deps.platform ?? process.platform;
|
|
43
|
-
if (platform !== "darwin" && platform !== "linux") return undefined;
|
|
61
|
+
if (platform !== "darwin" && platform !== "linux" && platform !== "win32") return undefined;
|
|
44
62
|
const owner = (deps.ownership ?? resolveServiceOwnership)();
|
|
45
63
|
return owner.kind === "owned" && owner.ownership.owner === "desktop"
|
|
46
64
|
? deriveDesktopStartup({ owned: true, loginEnabled: false, running: false }) : undefined;
|
|
@@ -49,33 +67,99 @@ export function desktopStartupOwnership(deps: DesktopStartupDeps = {}): DesktopS
|
|
|
49
67
|
function macLoginRegistration(deps: DesktopStartupDeps) {
|
|
50
68
|
const execute = deps.run ?? run;
|
|
51
69
|
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
70
|
const domain = `gui/${deps.uid ?? process.getuid!()}`;
|
|
63
|
-
const
|
|
64
|
-
|
|
71
|
+
const identity = (file: string) => {
|
|
72
|
+
const stat = lstatSync(file, { bigint: true });
|
|
73
|
+
return [stat.dev, stat.ino, stat.size, stat.mode, stat.mtimeNs, stat.ctimeNs].map(String).join(":");
|
|
74
|
+
};
|
|
75
|
+
const capture = () => {
|
|
76
|
+
const registrationPath = realpathSync(path);
|
|
77
|
+
const plistIdentity = identity(path), targetIdentity = identity(registrationPath);
|
|
78
|
+
const definition = execute("/usr/bin/plutil", ["-convert", "json", "-o", "-", path]);
|
|
79
|
+
const plist = JSON.parse(definition);
|
|
80
|
+
const args = plist.ProgramArguments;
|
|
81
|
+
if (plist.Label !== "OpenCodex" || plist.RunAtLoad !== true || !Array.isArray(args)
|
|
82
|
+
|| args.length !== 2 || args[1] !== "--autostart" || typeof args[0] !== "string"
|
|
83
|
+
|| !args[0].endsWith("/Contents/MacOS/opencodex-desktop")
|
|
84
|
+
|| (plist.Program !== undefined && plist.Program !== args[0])
|
|
85
|
+
|| (plist.Disabled !== undefined && typeof plist.Disabled !== "boolean")) return null;
|
|
86
|
+
const app = realpathSync(args[0]);
|
|
87
|
+
const proxy = realpathSync(join(dirname(app), "ocx"));
|
|
88
|
+
accessSync(app, constants.X_OK);
|
|
89
|
+
accessSync(proxy, constants.X_OK);
|
|
90
|
+
const appIdentity = identity(app), proxyIdentity = identity(proxy);
|
|
91
|
+
const disabled = execute("/bin/launchctl", ["print-disabled", domain]);
|
|
92
|
+
const mentions = disabled.match(/"OpenCodex"/g)?.length ?? 0;
|
|
93
|
+
const override = /"OpenCodex"\s*=>\s*(enabled|false|disabled|true)(?=\s*(?:\r?\n|[,;}]|$))/.exec(disabled)?.[1];
|
|
94
|
+
const loginEnabled = /^\s*disabled services = \{[\s\S]*\}\s*$/.test(disabled)
|
|
95
|
+
&& (mentions === 0 ? plist.Disabled !== true : mentions === 1 && (override === "enabled" || override === "false"));
|
|
96
|
+
if (registrationPath !== realpathSync(path) || plistIdentity !== identity(path) || targetIdentity !== identity(registrationPath)
|
|
97
|
+
|| app !== realpathSync(args[0]) || proxy !== realpathSync(join(dirname(app), "ocx"))
|
|
98
|
+
|| appIdentity !== identity(app) || proxyIdentity !== identity(proxy)) return null;
|
|
99
|
+
return { app, proxy, loginEnabled, registrationPath, plistIdentity, targetIdentity, definition, appIdentity, proxyIdentity, disabled };
|
|
100
|
+
};
|
|
101
|
+
const first = capture();
|
|
102
|
+
if (!first) return null;
|
|
103
|
+
const { app, proxy } = first;
|
|
104
|
+
if (!first.loginEnabled) return { app, proxy, loginEnabled: false };
|
|
105
|
+
let loaded: string;
|
|
106
|
+
try { loaded = execute("/bin/launchctl", ["print", `${domain}/OpenCodex`]); }
|
|
107
|
+
catch (error) {
|
|
108
|
+
// execFileSync's specific service-not-found result is absence, not a failed probe.
|
|
109
|
+
// Timeout, permission denial, missing tools and another domain remain unverified.
|
|
110
|
+
const failure = error as { status?: number; stderr?: unknown; signal?: unknown; code?: unknown } | null;
|
|
111
|
+
const stderr = typeof failure?.stderr === "string" ? failure.stderr
|
|
112
|
+
: Buffer.isBuffer(failure?.stderr) ? failure.stderr.toString("utf8") : "";
|
|
113
|
+
const message = `Could not find service "OpenCodex" in domain for user gui: ${domain.slice(4)}`;
|
|
114
|
+
const missing = failure?.status === 113 && !failure.signal && !failure.code
|
|
115
|
+
&& [message, `Bad request.\n${message}`].includes(stderr.trim());
|
|
116
|
+
if (!missing) throw error;
|
|
117
|
+
// The absent job supplies no durable registration evidence. Re-read the entire
|
|
118
|
+
// registration and enabled state; replacement or ambiguity revokes login credit.
|
|
119
|
+
const final = capture();
|
|
120
|
+
if (!final || JSON.stringify(first) !== JSON.stringify(final)) return { app, proxy, loginEnabled: false };
|
|
121
|
+
return { app, proxy, loginEnabled: true, loginRegistration: "registered-not-loaded" as const };
|
|
122
|
+
}
|
|
65
123
|
const program = /^\s*program = (.+)$/m.exec(loaded)?.[1];
|
|
66
124
|
const loadedPath = /^\s*path = (.+)$/m.exec(loaded)?.[1];
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
125
|
+
// macOS 13+ Background Task Management can take over the user LaunchAgent and resubmit it
|
|
126
|
+
// through smd; launchctl then names the submitter instead of the plist (#7019). The loaded
|
|
127
|
+
// job no longer points at the registration, so credit it like an unloaded job: same program,
|
|
128
|
+
// and the entire registration re-read unchanged.
|
|
129
|
+
if (program !== undefined && loadedPath !== undefined && SMD_SUBMITTED.test(loadedPath)
|
|
130
|
+
&& /^\s*managed_by = (.+)$/m.exec(loaded)?.[1] === SERVICE_MANAGEMENT && realpathSync(program) === app) {
|
|
131
|
+
const final = capture();
|
|
132
|
+
if (!final || JSON.stringify(first) !== JSON.stringify(final)) return { app, proxy, loginEnabled: false };
|
|
133
|
+
return { app, proxy, loginEnabled: true, loginRegistration: "loaded" as const };
|
|
134
|
+
}
|
|
135
|
+
const loginEnabled = program !== undefined && realpathSync(program) === app
|
|
136
|
+
&& loadedPath !== undefined && realpathSync(loadedPath) === first.registrationPath;
|
|
137
|
+
return { app, proxy, loginEnabled, ...(loginEnabled ? { loginRegistration: "loaded" as const } : {}) };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** launchctl's `path` for a job Background Task Management resubmitted through smd. */
|
|
141
|
+
const SMD_SUBMITTED = /^\(submitted by smd\.\d+\)$/;
|
|
142
|
+
const SERVICE_MANAGEMENT = "com.apple.xpc.ServiceManagement";
|
|
143
|
+
|
|
144
|
+
type DesktopLoginFacts = Pick<DesktopStartupDiagnostic, "loginEnabled" | "loginRegistration">;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Start at Login credit for one Desktop app copy. The tray's Start at Login item ports this
|
|
148
|
+
* judgment (desktop/src-tauri/src/login_registration.rs); both run the cases in
|
|
149
|
+
* tests/fixtures/desktop-login-registration-cases.json. Throws when evidence is unreadable.
|
|
150
|
+
*/
|
|
151
|
+
export function macLoginFactsFor(app: string, deps: DesktopStartupDeps): DesktopLoginFacts {
|
|
152
|
+
const login = macLoginRegistration(deps);
|
|
153
|
+
return login !== null && login.app === app
|
|
154
|
+
? { loginEnabled: login.loginEnabled, ...(login.loginRegistration ? { loginRegistration: login.loginRegistration } : {}) }
|
|
155
|
+
: { loginEnabled: false };
|
|
72
156
|
}
|
|
73
157
|
|
|
74
|
-
function diagnoseSupervisedStartup(deps: DesktopStartupDeps, ownerRevision: number, loginFor: (app: string) =>
|
|
158
|
+
function diagnoseSupervisedStartup(deps: DesktopStartupDeps, ownerRevision: number, loginFor: (app: string) => DesktopLoginFacts): DesktopStartupDiagnostic | undefined {
|
|
75
159
|
const evidence = inspectDesktopSupervision(deps);
|
|
76
160
|
if (evidence.kind !== "desktop") return undefined;
|
|
77
|
-
let
|
|
78
|
-
try {
|
|
161
|
+
let login: DesktopLoginFacts = { loginEnabled: false };
|
|
162
|
+
try { login = loginFor(evidence.app); }
|
|
79
163
|
catch { /* Login failure revokes restart viability, not the observed live supervisor. */ }
|
|
80
164
|
if ((deps.ownership ?? resolveServiceOwnership)().kind !== "none") return undefined;
|
|
81
165
|
// Login probes can outlive the runtime or its parent; only the same live chain gets credit.
|
|
@@ -86,7 +170,7 @@ function diagnoseSupervisedStartup(deps: DesktopStartupDeps, ownerRevision: numb
|
|
|
86
170
|
const finalOwner = (deps.ownership ?? resolveServiceOwnership)();
|
|
87
171
|
if (finalOwner.kind !== "none" || finalOwner.revision !== ownerRevision) return undefined;
|
|
88
172
|
const { supervisorPid, runtimePid, app } = evidence;
|
|
89
|
-
return deriveDesktopStartup({ owned: false,
|
|
173
|
+
return deriveDesktopStartup({ owned: false, ...login, running: true, supervisor: { supervisorPid, runtimePid, app } });
|
|
90
174
|
}
|
|
91
175
|
|
|
92
176
|
/** Read-only macOS desktop ownership, login registration and live parent/child checks. */
|
|
@@ -94,12 +178,9 @@ export function diagnoseMacDesktopStartup(deps: DesktopStartupDeps = {}): Deskto
|
|
|
94
178
|
if ((deps.platform ?? process.platform) !== "darwin") return undefined;
|
|
95
179
|
const ownership = deps.ownership ?? resolveServiceOwnership;
|
|
96
180
|
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
|
-
});
|
|
181
|
+
if (owner.kind === "none") return diagnoseSupervisedStartup(deps, owner.revision, app => macLoginFactsFor(app, deps));
|
|
101
182
|
if (owner.kind !== "owned" || owner.ownership.owner !== "desktop") return undefined;
|
|
102
|
-
const facts = { owned: true, loginEnabled: false, running: false };
|
|
183
|
+
const facts: Omit<DesktopStartupDiagnostic, "viable"> = { owned: true, loginEnabled: false, running: false };
|
|
103
184
|
const execute = deps.run ?? run;
|
|
104
185
|
const pidReader = deps.readPid ?? readPid;
|
|
105
186
|
try {
|
|
@@ -110,6 +191,7 @@ export function diagnoseMacDesktopStartup(deps: DesktopStartupDeps = {}): Deskto
|
|
|
110
191
|
if (!login) return deriveDesktopStartup(facts);
|
|
111
192
|
const { app, proxy } = login;
|
|
112
193
|
facts.loginEnabled = login.loginEnabled;
|
|
194
|
+
if (login.loginRegistration) facts.loginRegistration = login.loginRegistration;
|
|
113
195
|
const pid = pidReader();
|
|
114
196
|
if (pid !== null) {
|
|
115
197
|
const child = processIdentity(pid, execute);
|
|
@@ -156,10 +238,10 @@ export function diagnoseLinuxDesktopStartup(deps: DesktopStartupDeps = {}): Desk
|
|
|
156
238
|
const configured = (deps.env ?? process.env).XDG_CONFIG_HOME;
|
|
157
239
|
const config = configured && isAbsolute(configured) ? configured : join(home, ".config");
|
|
158
240
|
// auto-launch's HOME entry must remain inside the session's XDG autostart search path.
|
|
159
|
-
if (resolve(config) !== resolve(home, ".config")) return false;
|
|
241
|
+
if (resolve(config) !== resolve(home, ".config")) return { loginEnabled: false };
|
|
160
242
|
const entry = readFileSync(join(home, ".config", "autostart", "OpenCodex.desktop"), "utf8");
|
|
161
243
|
const login = linuxLoginApp(entry);
|
|
162
|
-
return login !== null && realpathSync(login) === app;
|
|
244
|
+
return { loginEnabled: login !== null && realpathSync(login) === app };
|
|
163
245
|
});
|
|
164
246
|
if (owner.kind !== "owned" || owner.ownership.owner !== "desktop") return undefined;
|
|
165
247
|
const facts = { owned: true, loginEnabled: false, running: false };
|
|
@@ -212,5 +294,50 @@ export function diagnoseLinuxDesktopStartup(deps: DesktopStartupDeps = {}): Desk
|
|
|
212
294
|
|
|
213
295
|
/** Platform dispatch for the full (probing) desktop startup diagnostic. */
|
|
214
296
|
export function diagnoseDesktopStartup(deps: DesktopStartupDeps = {}): DesktopStartupDiagnostic | undefined {
|
|
215
|
-
|
|
297
|
+
const platform = deps.platform ?? process.platform;
|
|
298
|
+
if (platform === "linux") return diagnoseLinuxDesktopStartup(deps);
|
|
299
|
+
if (platform === "win32") return diagnoseWindowsDesktopStartup(deps);
|
|
300
|
+
return diagnoseMacDesktopStartup(deps);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Tauri resolves app_config_dir to the roaming AppData folder plus the bundle identifier. */
|
|
304
|
+
function readWindowsInstallId(env: NodeJS.ProcessEnv): string | null {
|
|
305
|
+
const appData = env.APPDATA;
|
|
306
|
+
if (!appData || !/^[A-Za-z]:[\\/]/.test(appData)) return null;
|
|
307
|
+
try {
|
|
308
|
+
return readFileSync(win32.join(appData, "com.opencodex.desktop", "install-id"), "utf8").trim() || null;
|
|
309
|
+
} catch {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Read-only Windows desktop ownership and live parent/child checks. Windows Start at Login is
|
|
316
|
+
* not verified, so it is never credited and Desktop never reads as startup protection here;
|
|
317
|
+
* the diagnostic still names the supervisor so status and doctor stop recommending service repairs.
|
|
318
|
+
*/
|
|
319
|
+
export function diagnoseWindowsDesktopStartup(deps: DesktopStartupDeps = {}): DesktopStartupDiagnostic | undefined {
|
|
320
|
+
if ((deps.platform ?? process.platform) !== "win32") return undefined;
|
|
321
|
+
const ownership = deps.ownership ?? resolveServiceOwnership;
|
|
322
|
+
const owner = ownership();
|
|
323
|
+
if (owner.kind === "none") return diagnoseSupervisedStartup(deps, owner.revision, () => ({ loginEnabled: false }));
|
|
324
|
+
if (owner.kind !== "owned" || owner.ownership.owner !== "desktop") return undefined;
|
|
325
|
+
const facts = { owned: true, loginEnabled: false, running: false };
|
|
326
|
+
try {
|
|
327
|
+
const installId = deps.readWindowsInstallId ? deps.readWindowsInstallId() : readWindowsInstallId(deps.env ?? process.env);
|
|
328
|
+
if (installId !== owner.ownership.installId) return deriveDesktopStartup(facts);
|
|
329
|
+
const evidence = inspectDesktopSupervision(deps);
|
|
330
|
+
if (evidence.kind !== "desktop") return deriveDesktopStartup(facts);
|
|
331
|
+
// Only the same live chain, read again after the claim, earns running and the supervisor.
|
|
332
|
+
const final = inspectDesktopSupervision({ ...deps, targetPid: evidence.runtimePid });
|
|
333
|
+
const current = ownership();
|
|
334
|
+
if (final.kind !== "desktop" || final.runtimePid !== evidence.runtimePid
|
|
335
|
+
|| final.supervisorPid !== evidence.supervisorPid || final.app !== evidence.app
|
|
336
|
+
|| current.kind === "unknown" || !sameServiceOwnershipSubject(owner, current)) return deriveDesktopStartup(facts);
|
|
337
|
+
const { supervisorPid, runtimePid, app } = final;
|
|
338
|
+
return deriveDesktopStartup({ ...facts, running: true, supervisor: { supervisorPid, runtimePid, app } });
|
|
339
|
+
} catch {
|
|
340
|
+
// Unreadable evidence never grants running or releases the claim.
|
|
341
|
+
return deriveDesktopStartup(facts);
|
|
342
|
+
}
|
|
216
343
|
}
|
|
@@ -9,18 +9,44 @@ export interface ProcReader {
|
|
|
9
9
|
parent(pid: number): number;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/** One Win32_Process row; `created` is UTC ticks as a decimal string (exceeds 2^53). */
|
|
13
|
+
export interface WindowsProcessRow {
|
|
14
|
+
pid: number;
|
|
15
|
+
parent: number;
|
|
16
|
+
exe: string | null;
|
|
17
|
+
session: number;
|
|
18
|
+
created: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface WindowsProcessPair {
|
|
22
|
+
child: WindowsProcessRow | null;
|
|
23
|
+
parent: WindowsProcessRow | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface WindowsProcessReadOptions {
|
|
27
|
+
/** Called with the parent image before row validation, so partial Desktop evidence keeps its veto. */
|
|
28
|
+
onExecutable?: (path: string) => void;
|
|
29
|
+
timeoutMs?: number;
|
|
30
|
+
env?: NodeJS.ProcessEnv;
|
|
31
|
+
}
|
|
32
|
+
|
|
12
33
|
export interface DesktopSupervisionDeps {
|
|
13
34
|
targetPid?: number;
|
|
14
35
|
platform?: NodeJS.Platform;
|
|
15
|
-
run?: (command: string, args: string[]) => string;
|
|
36
|
+
run?: (command: string, args: string[], timeoutMs?: number) => string;
|
|
16
37
|
proc?: ProcReader;
|
|
17
38
|
readPid?: () => number | null;
|
|
18
39
|
readRuntimePortPid?: () => number | null;
|
|
40
|
+
windowsProcesses?: (pid: number, options: Pick<WindowsProcessReadOptions, "onExecutable" | "timeoutMs">) => WindowsProcessPair | null;
|
|
41
|
+
env?: NodeJS.ProcessEnv;
|
|
42
|
+
now?: () => number;
|
|
19
43
|
}
|
|
20
44
|
|
|
21
45
|
export function desktopSupervisionPaths(): { pid: string; runtimePort: string };
|
|
22
46
|
export const procfs: ProcReader;
|
|
23
47
|
export function processIdentity(pid: number, execute?: DesktopSupervisionDeps["run"], onExecutable?: (path: string) => void):
|
|
24
48
|
{ parent: number; executable: string } | null;
|
|
49
|
+
export function windowsPowerShellPath(env?: NodeJS.ProcessEnv): string;
|
|
50
|
+
export function windowsProcessPair(pid: number, execute?: DesktopSupervisionDeps["run"], options?: WindowsProcessReadOptions): WindowsProcessPair;
|
|
25
51
|
export function inspectDesktopSupervision(deps?: DesktopSupervisionDeps): SupervisionEvidence;
|
|
26
52
|
export function createSupervisionLatch(): { observe(evidence: SupervisionEvidence): boolean };
|