@bitkyc08/opencodex 2.86.0 → 2.87.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/gui/dist/assets/App-ZfYbLHkB.js +58 -0
- package/gui/dist/assets/App-wT3mXqFz.css +1 -0
- package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-DCd-A1az.js} +1 -1
- package/gui/dist/assets/index-CfYDMj34.js +94 -0
- package/gui/dist/assets/index-DPeB9HpV.css +1 -0
- package/gui/dist/assets/quota-summary-ChvmKGOk.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +9 -8
- package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
- package/src/adapters/anthropic/passthrough.ts +7 -23
- package/src/adapters/anthropic-progress.ts +84 -0
- package/src/adapters/anthropic.ts +108 -21
- package/src/adapters/base.ts +2 -0
- package/src/adapters/claude-cli/adapter.ts +111 -23
- package/src/adapters/claude-cli/profiles.ts +2 -1
- package/src/adapters/claude-cli/stable-replay.ts +108 -0
- package/src/adapters/claude-cli/usage-admission.ts +230 -0
- package/src/adapters/coding-agent/protocol.ts +1 -1
- package/src/adapters/coding-agent/turn.ts +18 -2
- package/src/adapters/dsh-account.ts +97 -0
- package/src/adapters/google.ts +35 -8
- package/src/adapters/openai-chat/messages.ts +4 -3
- package/src/adapters/openai-chat/passthrough.ts +8 -2
- package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
- package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
- package/src/adapters/openai-responses/passthrough.ts +57 -125
- package/src/adapters/openai-responses/request-strips.ts +25 -3
- package/src/adapters/openai-responses/transport.ts +81 -0
- package/src/adapters/qoder/adapter.ts +4 -2
- package/src/adapters/registry.ts +5 -0
- package/src/adapters/zed.ts +26 -1
- package/src/advisor/activity.ts +55 -0
- package/src/advisor/budget.ts +62 -0
- package/src/advisor/consult.ts +4 -1
- package/src/advisor/context.ts +16 -4
- package/src/advisor/evidence-tool.ts +21 -0
- package/src/advisor/evidence.ts +117 -0
- package/src/advisor/runtime.ts +79 -31
- package/src/advisor/settings.ts +11 -2
- package/src/bridge/errors.ts +19 -1
- package/src/bridge/internal.ts +3 -1
- package/src/bridge/response-json.ts +18 -3
- package/src/bridge/sse.ts +58 -8
- package/src/bridge/terminal-refusal.ts +59 -0
- package/src/claude/desktop-3p-library.ts +32 -7
- package/src/claude/desktop-3p.ts +55 -13
- package/src/claude/desktop-first-party.ts +1 -1
- package/src/claude/desktop-picker-platform.ts +4 -0
- package/src/claude/desktop-picker.ts +15 -8
- package/src/claude/desktop-system-proxy.ts +50 -4
- package/src/claude/inbound-content-options.ts +11 -0
- package/src/claude/inbound.ts +33 -0
- package/src/claude/intercept/picker-ca-startup.ts +2 -1
- package/src/claude/intercept/picker-runtime.ts +6 -5
- package/src/claude/intercept/picker-trust-windows.ts +338 -0
- package/src/claude/intercept/picker-trust.ts +19 -0
- package/src/claude/intercept/port-policy.ts +30 -0
- package/src/claude/intercept/runtime.ts +97 -94
- package/src/claude/outbound.ts +38 -13
- package/src/cli/account-api.ts +1 -0
- package/src/cli/account-policy-dto.ts +6 -0
- package/src/cli/account-policy.ts +34 -5
- package/src/cli/account-preservation.ts +44 -0
- package/src/cli/account.ts +8 -2
- package/src/cli/advisor.ts +17 -3
- package/src/cli/capabilities-accounts.ts +20 -1
- package/src/cli/capabilities-agents-routing.ts +5 -2
- package/src/cli/capabilities-base.ts +38 -10
- package/src/cli/capabilities-observe-system.ts +63 -3
- package/src/cli/capabilities-provider-models.ts +11 -0
- package/src/cli/codex-cli-update.ts +151 -41
- package/src/cli/codex-provider.ts +44 -0
- package/src/cli/combo-input.ts +10 -1
- package/src/cli/combo-result.ts +1 -1
- package/src/cli/combo.ts +2 -1
- package/src/cli/cross-home-owner.ts +17 -14
- package/src/cli/dispatch.ts +25 -4
- package/src/cli/doctor.ts +6 -1
- package/src/cli/ensure-desktop-cold-start.ts +60 -7
- package/src/cli/help.ts +3 -2
- package/src/cli/index.ts +1 -1
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/log-history.ts +172 -0
- package/src/cli/models-runtime-subcommands.ts +1 -0
- package/src/cli/models-runtime.ts +72 -8
- package/src/cli/observe-stream.ts +2 -0
- package/src/cli/observe.ts +16 -4
- package/src/cli/provider.ts +27 -5
- package/src/cli/registry.ts +28 -8
- package/src/cli/runtime-api.ts +5 -3
- package/src/cli/system-command.ts +40 -8
- package/src/cli/uninstall-client-state.ts +32 -0
- package/src/cli/usage-compare.ts +309 -0
- package/src/cli/usage-model-search.ts +4 -4
- package/src/cli/usage-report.ts +2 -2
- package/src/cli/v2.ts +1 -1
- package/src/client/machine-listener.ts +1 -3
- package/src/client/runtime.ts +1 -0
- package/src/clients/client-path.ts +51 -0
- package/src/clients/config-export/contracts.ts +2 -1
- package/src/clients/config-export.ts +18 -44
- package/src/clients/gentle-shell-path.ts +87 -0
- package/src/codex/account-auto-switch.ts +2 -0
- package/src/codex/account-identity.ts +29 -0
- package/src/codex/account-label.ts +1 -1
- package/src/codex/account-lifecycle.ts +21 -5
- package/src/codex/account-preservation.ts +28 -0
- package/src/codex/account-use-remaining.ts +23 -0
- package/src/codex/app-server-processes.ts +97 -7
- package/src/codex/app-server-restart-service.ts +55 -4
- package/src/codex/auth-api/account-list.ts +4 -0
- package/src/codex/auth-api/account-pause-group.ts +6 -50
- package/src/codex/auth-api/account-preservation.ts +57 -0
- package/src/codex/auth-api/routes.ts +8 -0
- package/src/codex/auth-api/use-remaining-route.ts +36 -0
- package/src/codex/auth-api.ts +0 -1
- package/src/codex/auth-context.ts +2 -2
- package/src/codex/catalog/auto-review.ts +20 -3
- package/src/codex/catalog/build-entries.ts +9 -1
- package/src/codex/catalog/combo-member.ts +14 -5
- package/src/codex/catalog/configured-tiers.ts +1 -1
- package/src/codex/catalog/gather-capture.ts +2 -0
- package/src/codex/catalog/metadata.ts +11 -9
- package/src/codex/catalog/model-hints.ts +21 -16
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/parsing.ts +30 -86
- package/src/codex/catalog/provider-models.ts +36 -34
- package/src/codex/catalog/routed-gather.ts +12 -13
- package/src/codex/cli-install-provenance.ts +105 -17
- package/src/codex/cli-update-plan.ts +545 -0
- package/src/codex/config-write-lock.ts +23 -2
- package/src/codex/desktop-app/picker-freshness.ts +96 -0
- package/src/codex/desktop-app-restart.ts +21 -0
- package/src/codex/desktop-compatibility/installed-build.ts +24 -6
- package/src/codex/desktop-compatibility/routing-preflight.ts +1 -1
- package/src/codex/desktop-compatibility/runtime.ts +12 -3
- package/src/codex/desktop-compatibility/windows-key-protection.ts +115 -9
- package/src/codex/features.ts +85 -45
- package/src/codex/inject/history-failure.ts +27 -0
- package/src/codex/inject/restore.ts +24 -2
- package/src/codex/inject/routing-classify.ts +4 -3
- package/src/codex/inject.ts +18 -27
- package/src/codex/main-account-hard-lock.ts +30 -1
- package/src/codex/native-profile-startup.ts +1 -1
- package/src/codex/ocx-compaction-history.ts +12 -10
- package/src/codex/paths.ts +86 -1
- package/src/codex/provider-routing.ts +174 -0
- package/src/codex/refresh.ts +2 -2
- package/src/codex/routing/health-store.ts +1 -1
- package/src/codex/routing/selection.ts +4 -3
- package/src/codex/routing/thread-affinity.ts +1 -1
- package/src/codex/signed-in-identity.ts +43 -0
- package/src/codex/subagent-model-fallback.ts +1 -1
- package/src/codex/sync.ts +6 -1
- package/src/codex/user-identity.ts +16 -5
- package/src/codex/windows-local-appdata.ts +84 -0
- package/src/combos/failover.ts +6 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/resolve.ts +46 -10
- package/src/combos/types.ts +15 -0
- package/src/config/diagnostics.ts +33 -6
- package/src/config/live-reconcile.ts +38 -2
- package/src/config/load-degrade.ts +17 -0
- package/src/config/opencode-wire-upgrade.ts +54 -0
- package/src/config/opencode-zen-upgrade.ts +52 -0
- package/src/config/owner-registry.ts +22 -17
- package/src/config/provider-validation.ts +45 -2
- package/src/config/schema/config-schema.ts +22 -0
- package/src/config/schema/leaf-validators.ts +38 -9
- package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
- package/src/config/schema/native-reasoning-retention.ts +26 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +608 -284
- package/src/grok/sync.ts +1 -1
- package/src/images/loop.ts +46 -8
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +1 -0
- package/src/integrations/registry.ts +8 -0
- package/src/lib/codex-restart-contract.ts +15 -0
- package/src/lib/desktop-upstream-tunnel.ts +18 -53
- package/src/lib/empty-tool-catalog.ts +62 -0
- package/src/lib/errors.ts +19 -3
- package/src/lib/gcp-adc.ts +96 -9
- package/src/lib/gcp-credential-input.ts +12 -0
- package/src/lib/lab-live-route-production.ts +5 -0
- package/src/lib/opaque-proxy-tunnel.ts +217 -0
- package/src/lib/redact.ts +2 -2
- package/src/oauth/account-quota-rank.ts +69 -2
- package/src/oauth/anthropic-account-refusal.ts +20 -2
- package/src/oauth/anthropic-oauth-definitions.ts +3 -1
- package/src/oauth/anthropic-routing.ts +1 -21
- package/src/oauth/anthropic.ts +38 -7
- package/src/oauth/callback-server.ts +27 -1
- package/src/oauth/chatgpt-device.ts +1 -1
- package/src/oauth/chatgpt-shared.ts +83 -0
- package/src/oauth/chatgpt.ts +8 -72
- package/src/oauth/dsh.ts +203 -0
- package/src/oauth/generic-account-failover.ts +33 -7
- package/src/oauth/generic-session-affinity.ts +125 -0
- package/src/oauth/index.ts +184 -109
- package/src/oauth/local-token-detect.ts +21 -1
- package/src/oauth/login-cli-errors.ts +44 -0
- package/src/oauth/login-cli.ts +69 -7
- package/src/oauth/login-flow-state.ts +371 -23
- package/src/oauth/meta-muse-errors.ts +41 -0
- package/src/oauth/meta-muse-keychain.ts +52 -0
- package/src/oauth/meta-muse.ts +51 -84
- package/src/oauth/model-discovery-auth.ts +3 -0
- package/src/oauth/pool-settings-capability.ts +4 -2
- package/src/oauth/refresh-flight-retirement.ts +48 -0
- package/src/oauth/session-affinity-key.ts +23 -0
- package/src/oauth/store-anthropic-instance.ts +9 -2
- package/src/oauth/store.ts +12 -5
- package/src/protocols/encoders/adapter-events.ts +5 -4
- package/src/protocols/encoders/messages.ts +9 -6
- package/src/protocols/provider-summary.ts +2 -1
- package/src/providers/api-key-cooldowns.ts +49 -0
- package/src/providers/api-key-resolve.ts +11 -6
- package/src/providers/api-key-response-ownership.ts +57 -0
- package/src/providers/api-key-selection.ts +31 -6
- package/src/providers/api-keys.ts +214 -3
- package/src/providers/claude-cli-usage.ts +96 -0
- package/src/providers/dsh-account-destination.ts +31 -0
- package/src/providers/dsh-account-models.ts +41 -0
- package/src/providers/gcp-credential-cleanup.ts +59 -0
- package/src/providers/key-failover.ts +54 -50
- package/src/providers/key-store.ts +7 -2
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/model-rename-migration.ts +2 -2
- package/src/providers/openai-tiers-destination.ts +11 -2
- package/src/providers/openai-tiers.ts +5 -12
- package/src/providers/opencode-go-transport.ts +3 -1
- package/src/providers/quota/account-cache.ts +6 -5
- package/src/providers/quota/account-row.ts +34 -0
- package/src/providers/quota/antigravity.ts +28 -7
- package/src/providers/quota/dsh-account.ts +70 -0
- package/src/providers/quota/vendor-probes-oauth.ts +1 -93
- package/src/providers/quota-types.ts +2 -0
- package/src/providers/quota.ts +14 -20
- package/src/providers/registry/entries-core.ts +37 -8
- package/src/providers/registry/types.ts +1 -1
- package/src/providers/request-pacing.ts +77 -19
- package/src/providers/routed-config.ts +339 -0
- package/src/providers/service-tier.ts +2 -1
- package/src/quota/window-mapping.ts +5 -2
- package/src/responses/anthropic-progress.ts +34 -0
- package/src/responses/assistant-replay.ts +35 -0
- package/src/responses/compaction-history-framing.ts +65 -0
- package/src/responses/compaction.ts +6 -1
- package/src/responses/incomplete-reason.ts +31 -0
- package/src/responses/parser.ts +50 -5
- package/src/responses/reasoning-envelope.ts +4 -0
- package/src/responses/reasoning-replay-cache.ts +24 -2
- package/src/responses/reasoning-retention.ts +173 -0
- package/src/responses/retained-compaction.ts +46 -0
- package/src/responses/schema.ts +23 -0
- package/src/router.ts +8 -321
- package/src/routing/health.ts +37 -22
- package/src/routing/history/indexer.ts +6 -5
- package/src/routing/history/ingest.ts +6 -2
- package/src/routing/profile.ts +1 -1
- package/src/server/admission-model-scope.ts +17 -0
- package/src/server/auth-cors.ts +11 -0
- package/src/server/chat-completions.ts +7 -1
- package/src/server/chat-native-reasoning.ts +123 -0
- package/src/server/chat-native-sse.ts +110 -7
- package/src/server/chat-native.ts +76 -17
- package/src/server/claude-messages.ts +55 -414
- package/src/server/effort-policy.ts +1 -1
- package/src/server/images.ts +15 -2
- package/src/server/index/model-alias-id.ts +36 -0
- package/src/server/index/optional-listeners.ts +2 -0
- package/src/server/index/serve-options.ts +9 -1
- package/src/server/index/startup-warnings.ts +2 -0
- package/src/server/index.ts +1 -1
- package/src/server/inference/advisor-diagnostics.ts +31 -0
- package/src/server/management/advisor-routes.ts +15 -4
- package/src/server/management/claude-desktop-picker-routes.ts +1 -1
- package/src/server/management/combo-routes.ts +176 -50
- package/src/server/management/companion-routes.ts +1 -1
- package/src/server/management/config-routes.ts +75 -4
- package/src/server/management/cursor-integration-routes.ts +1 -1
- package/src/server/management/model-routes.ts +11 -6
- package/src/server/management/model-rows.ts +14 -0
- package/src/server/management/native-integration-routes.ts +2 -7
- package/src/server/management/native-reasoning-retention-routes.ts +41 -0
- package/src/server/management/oauth-account-routes.ts +87 -14
- package/src/server/management/provider-gcp-credential-routes.ts +29 -0
- package/src/server/management/provider-key-pacing-routes.ts +31 -0
- package/src/server/management/provider-overwrite-carry.ts +7 -0
- package/src/server/management/provider-routes.ts +25 -30
- package/src/server/management/route-registry.ts +10 -1
- package/src/server/management-api.ts +3 -1
- package/src/server/messages-native-tool-guard.ts +185 -0
- package/src/server/messages-native.ts +101 -21
- package/src/server/messages-shared.ts +431 -0
- package/src/server/relay.ts +1 -1
- package/src/server/request-log-terminal-status.ts +1 -1
- package/src/server/request-log.ts +6 -1
- package/src/server/responses/account-change-state.ts +8 -2
- package/src/server/responses/adapter-continuation.ts +13 -1
- package/src/server/responses/adapter-delivery.ts +19 -6
- package/src/server/responses/adapter-dispatch.ts +19 -2
- package/src/server/responses/advisor-retention.ts +117 -0
- package/src/server/responses/advisor-slot.ts +73 -41
- package/src/server/responses/codex-ws-wire.ts +15 -8
- package/src/server/responses/compact.ts +303 -125
- package/src/server/responses/compaction-recovery.ts +18 -10
- package/src/server/responses/compaction-retention.ts +61 -0
- package/src/server/responses/context-overflow.ts +11 -6
- package/src/server/responses/core-auth.ts +6 -0
- package/src/server/responses/core-codex-account.ts +11 -1
- package/src/server/responses/core-combo-failure.ts +2 -0
- package/src/server/responses/core-combo.ts +120 -36
- package/src/server/responses/core-errors.ts +3 -3
- package/src/server/responses/core-lifetime.ts +5 -2
- package/src/server/responses/core-opaque-recovery.ts +35 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-pipeline.ts +141 -0
- package/src/server/responses/core-replay.ts +18 -2
- package/src/server/responses/core.ts +2 -115
- package/src/server/responses/fetch-helpers.ts +73 -19
- package/src/server/responses/main-hard-lock-routing.ts +109 -0
- package/src/server/responses/native-response-control.ts +4 -2
- package/src/server/responses/passthrough-delivery.ts +3 -1
- package/src/server/responses/passthrough-dispatch.ts +15 -13
- package/src/server/responses/request-prepare.ts +76 -12
- package/src/server/responses/request-sidecar-auth.ts +2 -2
- package/src/server/responses/request-transport.ts +104 -27
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +96 -30
- package/src/server/responses/sidecar-execution.ts +20 -2
- package/src/server/responses/terminal-guard.ts +2 -16
- package/src/service/auth-preflight.ts +71 -0
- package/src/service/cli.ts +4 -2
- package/src/service/definition.ts +97 -0
- package/src/service/guards.ts +1 -61
- package/src/service/health-sentinel-linux.ts +112 -0
- package/src/service/health-sentinel-main.ts +76 -57
- package/src/service/health.ts +5 -89
- package/src/service/launchd.ts +27 -7
- package/src/service/logging.ts +72 -9
- package/src/service/repair.ts +2 -1
- package/src/service/state.ts +7 -8
- package/src/service/systemd.ts +1 -1
- package/src/service/windows-guarded-manager.ts +2 -1
- package/src/service/windows-ops.ts +1 -1
- package/src/service/windows-scheduler.ts +2 -7
- package/src/service/windows-taskxml.ts +2 -2
- package/src/service/windows-wscript.ts +14 -0
- package/src/service.ts +4 -2
- package/src/sidecar/auth.ts +2 -0
- package/src/types/config.ts +64 -3
- package/src/types/opencode-go.ts +61 -0
- package/src/types/opencode-zen.ts +29 -0
- package/src/types/provider.ts +32 -6
- package/src/types/request.ts +23 -2
- package/src/types/wire.ts +29 -37
- package/src/types.ts +1 -0
- package/src/update/npm-invocation.mjs +12 -2
- package/src/usage/cost.ts +48 -12
- package/src/usage/expected-prices.ts +3 -0
- package/src/usage/ledger-retention.ts +2 -1
- package/src/usage/ledger-scanner.ts +6 -5
- package/src/usage/log.ts +17 -8
- package/src/usage/summary.ts +22 -9
- package/src/usage/user-cost-overlays.ts +14 -19
- package/src/usage/user-cost-validation.ts +38 -0
- package/src/vision/eligibility.ts +13 -0
- package/src/web-search/loop.ts +55 -5
- package/src/web-search/progress-stream.ts +4 -1
- package/src/web-search/run-turn-loop.ts +9 -1
- package/gui/dist/assets/App-BjlttVLy.js +0 -58
- package/gui/dist/assets/App-CSJvmjNs.css +0 -1
- package/gui/dist/assets/index-BB6uK6tU.css +0 -1
- package/gui/dist/assets/index-BMssP4k3.js +0 -94
- package/gui/dist/assets/quota-summary-D8M1bc_R.js +0 -1
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
clampImageMaxRounds,
|
|
22
22
|
} from "../../images";
|
|
23
23
|
import type { ProviderAdapter } from "../../adapters/base";
|
|
24
|
-
import type { OcxParsedRequest } from "../../types";
|
|
24
|
+
import type { OcxParsedRequest, OcxUsage } from "../../types";
|
|
25
25
|
import { rotateProviderTransportOn429, rateLimitRetryPolicyFor } from "../../providers/key-failover";
|
|
26
26
|
import {
|
|
27
27
|
isGenericOAuthFailoverEnabled,
|
|
@@ -50,6 +50,7 @@ import { providerFetch } from "./fetch-helpers";
|
|
|
50
50
|
import type { AttemptRecoveryKind } from "../../usage/log";
|
|
51
51
|
import { recordAdapterReasoning, recordAdapterTier } from "../request-log";
|
|
52
52
|
import { normalizeLogConversationId } from "../request-log-conversation";
|
|
53
|
+
import { attemptAccountChanged } from "../request-log-account-rotation";
|
|
53
54
|
import { rememberResponseState } from "../../responses/state";
|
|
54
55
|
import { trackStreamLifetime } from "../lifecycle";
|
|
55
56
|
import { isAntigravityRawValidationRefusal } from "./antigravity-validation-refusal";
|
|
@@ -65,6 +66,7 @@ export async function executeResponsesSidecars(
|
|
|
65
66
|
| "inboundWire"
|
|
66
67
|
| "selectedForwardHeaders"
|
|
67
68
|
| "translatorBudget"
|
|
69
|
+
| "clientToolCatalogRestriction"
|
|
68
70
|
| "toolBridgeMaps"
|
|
69
71
|
| "rememberKiroDeliveredFinalAnswer"
|
|
70
72
|
| "responseStateOptions"
|
|
@@ -417,7 +419,8 @@ export async function executeResponsesSidecars(
|
|
|
417
419
|
hop.permit?.release();
|
|
418
420
|
return null;
|
|
419
421
|
}
|
|
420
|
-
recoveryKind = originalResponse?.status === 401 ? "oauth-401"
|
|
422
|
+
recoveryKind = originalResponse?.status === 401 ? "oauth-401"
|
|
423
|
+
: originalResponse?.status === 403 ? "oauth-account-403" : "anthropic-oauth-429";
|
|
421
424
|
sidecarBudget.ownCredentialHop(hop.permit);
|
|
422
425
|
} else {
|
|
423
426
|
// No key pool, no generic OAuth roster, no Anthropic pool could produce a replacement
|
|
@@ -451,6 +454,15 @@ export async function executeResponsesSidecars(
|
|
|
451
454
|
});
|
|
452
455
|
return { adapter: rotatedAdapter, recoveryKind };
|
|
453
456
|
};
|
|
457
|
+
// A loop's hidden rounds before a rotation were spent by the account it left. Bill them to that
|
|
458
|
+
// account's attempt while it is still open; the next send seals it. Declined when that send
|
|
459
|
+
// keeps the account (no new attempt), so the loop's final total still carries them.
|
|
460
|
+
const billRotatedUsage = (usage: OcxUsage): boolean => {
|
|
461
|
+
const attempt = logCtx.activeAttempt;
|
|
462
|
+
if (!attempt || !attemptAccountChanged(attempt.accountLogLabel, logCtx.accountLogLabel, attempt.provider, logCtx.provider)) return false;
|
|
463
|
+
transportState.bindKeyUsageFromBridge(usage);
|
|
464
|
+
return true;
|
|
465
|
+
};
|
|
454
466
|
if ((imgPlan || vidPlan) && wsPlan) {
|
|
455
467
|
// Web search takes priority when both are active — the media bridge cannot run
|
|
456
468
|
// alongside runWithWebSearch. Surface a runtime signal so the user knows their
|
|
@@ -508,6 +520,7 @@ export async function executeResponsesSidecars(
|
|
|
508
520
|
);
|
|
509
521
|
const imgResponse = await runWithImageBridge({
|
|
510
522
|
parsed, adapter: transportState.adapter,
|
|
523
|
+
clientToolCatalogRestriction: route.provider.authMode === "forward" ? undefined : requestState.clientToolCatalogRestriction,
|
|
511
524
|
incomingMeta: { headers: requestState.selectedForwardHeaders, providerName: route.providerName, abortSignal: options.abortSignal, translatorBudget, ...(options.comboInitialSend ? { providerFetch: imageProviderFetch } : {}), ...sidecarBudget.incomingMeta },
|
|
512
525
|
onProducerStart: sidecarBudget.takeProducerOwnership,
|
|
513
526
|
onProducerEnd: sidecarBudget.release,
|
|
@@ -549,10 +562,12 @@ export async function executeResponsesSidecars(
|
|
|
549
562
|
transportState.bindKeyUsageFromBridge(usage);
|
|
550
563
|
},
|
|
551
564
|
on429: rotateSidecarProviderOn429,
|
|
565
|
+
onRotatedUsage: billRotatedUsage,
|
|
552
566
|
retryOn429Policy: route.providerName === "kiro" && isGenericOAuthFailoverEnabled(config, "kiro")
|
|
553
567
|
? null : rateLimitRetryPolicyFor(route.provider),
|
|
554
568
|
onFirstOutput: noteSidecarOutput,
|
|
555
569
|
...(options.forceEmptyResponseId ? { forceEmptyResponseId: true } : {}),
|
|
570
|
+
terminalRefusalAsFailed: inboundWire !== "chat" && inboundWire !== "anthropic",
|
|
556
571
|
onCompletedResponse: (response, providerState) => {
|
|
557
572
|
const served = transportState.replayOAuthCredentialSnapshot;
|
|
558
573
|
if (route.providerName === "kiro" && response.status === "completed" && served
|
|
@@ -603,6 +618,7 @@ export async function executeResponsesSidecars(
|
|
|
603
618
|
})(input, init)) as typeof globalThis.fetch;
|
|
604
619
|
const loopResponse = await runWithWebSearch({
|
|
605
620
|
parsed, adapter: transportState.adapter,
|
|
621
|
+
clientToolCatalogRestriction: route.provider.authMode === "forward" ? undefined : requestState.clientToolCatalogRestriction,
|
|
606
622
|
onIterationEnd: sidecarBudget.releaseUnsentHop,
|
|
607
623
|
fetchForRequest: (request, iterParsed, adapter) => providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
608
624
|
dispatchOverride: oauthDispatch(request, iterParsed),
|
|
@@ -633,6 +649,7 @@ export async function executeResponsesSidecars(
|
|
|
633
649
|
settings: wsPlan.settings,
|
|
634
650
|
maxSearches: wsPlan.maxSearches,
|
|
635
651
|
forceEmptyResponseId: true,
|
|
652
|
+
terminalRefusalAsFailed: inboundWire !== "chat" && inboundWire !== "anthropic",
|
|
636
653
|
abortSignal: options.abortSignal,
|
|
637
654
|
onFirstOutput: noteSidecarOutput,
|
|
638
655
|
onRequestBuilt: request => {
|
|
@@ -651,6 +668,7 @@ export async function executeResponsesSidecars(
|
|
|
651
668
|
stallTimeoutSec: wsPlan.stallTimeoutSec,
|
|
652
669
|
streamRoutedModelOutput: wsPlan.streamRoutedModelOutput,
|
|
653
670
|
on429: rotateSidecarProviderOn429,
|
|
671
|
+
onRotatedUsage: billRotatedUsage,
|
|
654
672
|
...(!options.comboInitialSend && route.providerName === "google-antigravity" ? { onIterationEnd: () => {
|
|
655
673
|
// The next Antigravity adapter dispatch claims this permit. If request construction,
|
|
656
674
|
// admission or cancellation stopped it first, return the unused send allowance.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { assistantReplayContent } from "../../responses/assistant-replay";
|
|
1
2
|
import type {
|
|
2
3
|
AdapterEvent,
|
|
3
|
-
OcxAssistantContentPart,
|
|
4
4
|
OcxAssistantMessage,
|
|
5
5
|
OcxParsedRequest,
|
|
6
6
|
OcxUsage,
|
|
@@ -121,21 +121,7 @@ export function analyzeTerminalTurn(parsed: OcxParsedRequest, events: readonly A
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
function assistantMessageFromEvents(events: readonly AdapterEvent[], timestamp: number): OcxAssistantMessage | undefined {
|
|
124
|
-
|
|
125
|
-
let thinking = "";
|
|
126
|
-
let signature: string | undefined;
|
|
127
|
-
const redacted: string[] = [];
|
|
128
|
-
for (const event of events) {
|
|
129
|
-
if (event.type === "text_delta") text += event.text;
|
|
130
|
-
else if (event.type === "thinking_delta") thinking += event.thinking;
|
|
131
|
-
else if (event.type === "thinking_signature") signature = event.signature;
|
|
132
|
-
else if (event.type === "redacted_thinking") redacted.push(event.data);
|
|
133
|
-
}
|
|
134
|
-
const content: OcxAssistantContentPart[] = [];
|
|
135
|
-
if (thinking || signature || redacted.length > 0) {
|
|
136
|
-
content.push({ type: "thinking", thinking, ...(signature ? { signature } : {}), ...(redacted.length > 0 ? { redacted } : {}) });
|
|
137
|
-
}
|
|
138
|
-
if (text) content.push({ type: "text", text });
|
|
124
|
+
const content = assistantReplayContent(events);
|
|
139
125
|
if (content.length === 0) return undefined;
|
|
140
126
|
return { role: "assistant", content, timestamp };
|
|
141
127
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The install/repair preflight over the data-plane credential, plus the retry command it
|
|
3
|
+
* names. Split out of `guards.ts` so the guard module stops importing `diagnostics`.
|
|
4
|
+
*
|
|
5
|
+
* Direction of the remaining edge is deliberate: this module asks `guards` for
|
|
6
|
+
* `assertNotAdminToken` and `diagnostics` for the install verdict, and neither imports it back.
|
|
7
|
+
*/
|
|
8
|
+
import { loadConfig } from "../config";
|
|
9
|
+
import { isLoopbackHostname } from "../codex/loopback-target";
|
|
10
|
+
import { readServiceApiTokenState, serviceApiTokenFilePath } from "../lib/service-secrets";
|
|
11
|
+
import { diagnoseService } from "./diagnostics";
|
|
12
|
+
import type { ServiceDiagnostic } from "./diagnostics";
|
|
13
|
+
import { assertNotAdminToken } from "./guards";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The `ocx` command a user should rerun for the service state they actually have.
|
|
17
|
+
*
|
|
18
|
+
* `installed` alone is not enough: `repairService()` refuses a Task-Scheduler-plus-WinSW
|
|
19
|
+
* conflict outright, so recommending repair there names a command guaranteed to fail.
|
|
20
|
+
* Install IS the valid conflict recovery, because `installWindows` removes the native
|
|
21
|
+
* backend first. Exported so the guard tests the real selector rather than a copy of it.
|
|
22
|
+
*/
|
|
23
|
+
export function serviceRetryCommand(
|
|
24
|
+
diag: Pick<ServiceDiagnostic, "installed" | "conflict"> = diagnoseService(),
|
|
25
|
+
): string {
|
|
26
|
+
return diag.installed && !diag.conflict ? "ocx service repair" : "ocx service install";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Preflight for `service install` / `service repair` on the data-plane credential.
|
|
31
|
+
*
|
|
32
|
+
* It used to DEMAND `OPENCODEX_API_AUTH_TOKEN` for a non-loopback hostname, and it threw
|
|
33
|
+
* even when `~/.opencodex/service-api-token` already held a perfectly good token. That is
|
|
34
|
+
* the defect behind the incident this unit exists to close (#4236): an operator exported the
|
|
35
|
+
* ADMIN token as OPENCODEX_API_AUTH_TOKEN because `install` asked for a token, the hub then
|
|
36
|
+
* crash-looped on `assertNotAdminToken`, and `service repair` asked for the same env var
|
|
37
|
+
* again — so the only remembered way to make the command proceed was the thing that broke it.
|
|
38
|
+
*
|
|
39
|
+
* Nobody should have to export a token by hand to run a hub. {@link writeServiceApiTokenFile}
|
|
40
|
+
* provisions one, so the only conditions left that install cannot fix are an admin-token
|
|
41
|
+
* collision in the environment and a token file that exists but cannot be used.
|
|
42
|
+
*/
|
|
43
|
+
export function assertServiceAuthEnvironment(): void {
|
|
44
|
+
const config = loadConfig();
|
|
45
|
+
// Both collision checks come BEFORE the loopback short-circuit, because the launch wrapper
|
|
46
|
+
// exports the token file unconditionally (`buildServiceShellCommand` cats it whenever it
|
|
47
|
+
// exists, whatever the hostname): a management token in either source fences the whole
|
|
48
|
+
// management plane closed at boot, even on a loopback install that needs no admission
|
|
49
|
+
// secret. Returning early is what let that broken state through.
|
|
50
|
+
const present = process.env.OPENCODEX_API_AUTH_TOKEN?.trim();
|
|
51
|
+
if (present) assertNotAdminToken(present);
|
|
52
|
+
const state = readServiceApiTokenState();
|
|
53
|
+
// An existing FILE holding the admin token is the incident shape itself, and the first round
|
|
54
|
+
// only checked the env var — so install/repair reused it and the hub crash-looped at boot.
|
|
55
|
+
// On a machine connected to a hub this same file holds that hub's issued client key, which
|
|
56
|
+
// is never a management token, so the check is a no-op there.
|
|
57
|
+
if (state.kind === "present") assertNotAdminToken(state.token, process.env, "file");
|
|
58
|
+
if (isLoopbackHostname(config.hostname)) return;
|
|
59
|
+
if (present) return;
|
|
60
|
+
// Absent is fine — install/repair generates one below. `unsafe` is not: the writer refuses
|
|
61
|
+
// to replace a path it cannot vouch for, so say so here, where the operator can still act,
|
|
62
|
+
// instead of failing mid-install. Reached from `service repair` as well as `install`, so
|
|
63
|
+
// name a command that can actually succeed (see serviceRetryCommand).
|
|
64
|
+
if (state.kind !== "unsafe") return;
|
|
65
|
+
const diag = diagnoseService();
|
|
66
|
+
throw new Error(
|
|
67
|
+
`The data-plane token file cannot be used (${state.reason}): ${serviceApiTokenFilePath()}. `
|
|
68
|
+
+ `Move it aside, then rerun \`${serviceRetryCommand(diag)}\`; the service provisions a `
|
|
69
|
+
+ "fresh owner-only token and needs nothing from the environment.",
|
|
70
|
+
);
|
|
71
|
+
}
|
package/src/service/cli.ts
CHANGED
|
@@ -11,8 +11,10 @@ import { statusWinswRaw, type WinswStatus } from "../lib/winsw";
|
|
|
11
11
|
import { withWindowsServiceMutationLock } from "../lib/windows-service-mutation-lock";
|
|
12
12
|
import { maybeShowStarPrompt } from "../cli/star-prompt";
|
|
13
13
|
import { serviceStatusReport } from "./diagnostics";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
14
|
+
import { assertServiceAuthEnvironment } from "./auth-preflight";
|
|
15
|
+
import { assertServiceEnvironmentMatchesInstall } from "./guards";
|
|
16
|
+
import { resolveServiceListenPort } from "./definition";
|
|
17
|
+
import { reportServiceServing } from "./health";
|
|
16
18
|
import { platformOps, proxyStillLiveAfterStop, stopTrackedProxyForServiceCommand, installServiceSafely, installFreshWindowsSchedulerSafely, removeServiceInstallState, isServiceInstalled } from "./orchestration";
|
|
17
19
|
import { repairService } from "./repair";
|
|
18
20
|
import type { ServiceRepairVerb } from "./repair";
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape of a service definition: the shell command both Unix backends bake into
|
|
3
|
+
* their unit/plist, the port they pin into it, and the proxy environment it carries.
|
|
4
|
+
*
|
|
5
|
+
* Leaf module. It reads config and the token-file path, and asks nothing of the
|
|
6
|
+
* backends that consume it — importing it from `launchd.ts`/`systemd.ts` cannot
|
|
7
|
+
* route back through them (see `installedServiceListenPort` in `health.ts`, which
|
|
8
|
+
* intentionally stays behind because it asks every backend).
|
|
9
|
+
*/
|
|
10
|
+
import { loadConfig } from "../config";
|
|
11
|
+
import { serviceApiTokenFilePath } from "../lib/service-secrets";
|
|
12
|
+
import { PROXY_ENV_KEYS } from "../lib/proxy-env";
|
|
13
|
+
|
|
14
|
+
export function shellQuote(value: string): string {
|
|
15
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Listen port baked into service wrappers / WinSW XML.
|
|
20
|
+
* Priority: explicit override → OCX_BAKE_PORT (update restart) → config.port → 10100.
|
|
21
|
+
* `config.port === 0` means ephemeral for interactive start; services need a stable pin,
|
|
22
|
+
* so treat 0 / invalid like unset (default 10100) instead of baking `--port 0`.
|
|
23
|
+
*/
|
|
24
|
+
export function resolveServiceListenPort(override?: number): number {
|
|
25
|
+
if (typeof override === "number" && Number.isFinite(override) && override > 0 && override <= 65535) {
|
|
26
|
+
return Math.trunc(override);
|
|
27
|
+
}
|
|
28
|
+
const baked = process.env.OCX_BAKE_PORT?.trim();
|
|
29
|
+
if (baked && /^\d+$/.test(baked)) {
|
|
30
|
+
const n = Number(baked);
|
|
31
|
+
if (n > 0 && n <= 65535) return n;
|
|
32
|
+
}
|
|
33
|
+
const configured = loadConfig().port;
|
|
34
|
+
if (typeof configured === "number" && configured > 0 && configured <= 65535) return configured;
|
|
35
|
+
return 10100;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function buildServiceShellCommand(bun: string, cli: string | null, port = resolveServiceListenPort()): string {
|
|
39
|
+
const tokenFile = serviceApiTokenFilePath();
|
|
40
|
+
const args = cli ? `${shellQuote(cli)} start` : "start";
|
|
41
|
+
return `if [ -f ${shellQuote(tokenFile)} ]; then OPENCODEX_API_AUTH_TOKEN="$(cat ${shellQuote(tokenFile)})"; export OPENCODEX_API_AUTH_TOKEN; fi; exec ${shellQuote(bun)} ${args} --port ${port}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The same command shape, launched through a stable `ocx` executable instead of an
|
|
46
|
+
* explicit Bun + CLI pair. The token-file preamble is identical and deliberately shared
|
|
47
|
+
* in form: the service still reads the token from disk at start and never carries it in
|
|
48
|
+
* the unit.
|
|
49
|
+
*/
|
|
50
|
+
export function buildServiceLauncherShellCommand(launcher: string, port = resolveServiceListenPort()): string {
|
|
51
|
+
const tokenFile = serviceApiTokenFilePath();
|
|
52
|
+
return `if [ -f ${shellQuote(tokenFile)} ]; then OPENCODEX_API_AUTH_TOKEN="$(cat ${shellQuote(tokenFile)})"; export OPENCODEX_API_AUTH_TOKEN; fi; exec ${shellQuote(launcher)} start --port ${port}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Shared tail parser for the baked `--port <n>`.
|
|
57
|
+
*
|
|
58
|
+
* Terminators cover all three artifact shapes: whitespace (batch wrapper, systemd
|
|
59
|
+
* unit), `"` (systemd's quoted ExecStart), `<` (WinSW's `</arguments>`), and `&` (an
|
|
60
|
+
* XML-escaped quote). Matched LAST because every artifact carries the Bun and CLI
|
|
61
|
+
* paths ahead of the argument, and a path containing the literal must not shadow it.
|
|
62
|
+
*/
|
|
63
|
+
export function parseBakedListenPort(read: () => string): number | null {
|
|
64
|
+
try {
|
|
65
|
+
const last = [...read().matchAll(/start --port (\d{1,5})(?:\s|"|&|<|$)/gm)].at(-1);
|
|
66
|
+
if (!last) return null;
|
|
67
|
+
const n = Number(last[1]);
|
|
68
|
+
return n > 0 && n <= 65535 ? n : null;
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Outbound proxy settings the installing shell had, resolved for baking into a service
|
|
76
|
+
* definition.
|
|
77
|
+
*
|
|
78
|
+
* A service manager does not inherit the environment of the shell that installed it, and
|
|
79
|
+
* `ExecStart=/bin/sh -lc` is dash on Ubuntu/WSL — login dash reads `.profile`, not
|
|
80
|
+
* `.bashrc`, which is where proxy exports usually live. So a user who needs a proxy to
|
|
81
|
+
* reach the upstream got a service that dialed direct: the socket was reset, the retry
|
|
82
|
+
* budget drained, and the request surfaced as `502 Provider unreachable` (#2107). The
|
|
83
|
+
* same install driven through `ocx codex-shim` worked, because that path spawns with
|
|
84
|
+
* `{ ...process.env }`.
|
|
85
|
+
*
|
|
86
|
+
* Lower-case variants are honored because curl-style tooling sets them and the runtime's
|
|
87
|
+
* own `applyProxyEnv` already treats both cases as equivalent. Only the canonical
|
|
88
|
+
* upper-case name is baked, so a definition never carries two spellings of one setting.
|
|
89
|
+
*/
|
|
90
|
+
export function resolvedProxyEnv(env: NodeJS.ProcessEnv = process.env): { name: string; value: string }[] {
|
|
91
|
+
const resolved: { name: string; value: string }[] = [];
|
|
92
|
+
for (const key of PROXY_ENV_KEYS) {
|
|
93
|
+
const value = env[key]?.trim() || env[key.toLowerCase()]?.trim();
|
|
94
|
+
if (value) resolved.push({ name: key, value });
|
|
95
|
+
}
|
|
96
|
+
return resolved;
|
|
97
|
+
}
|
package/src/service/guards.ts
CHANGED
|
@@ -2,14 +2,12 @@ import { execSync } from "node:child_process";
|
|
|
2
2
|
import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { getConfigDir, loadConfig } from "../config";
|
|
4
4
|
import { withConfigMutationLockSync } from "../config/mutation-lock";
|
|
5
|
-
import { hardenReusedServiceApiToken,
|
|
5
|
+
import { hardenReusedServiceApiToken, serviceApiTokenFilePath } from "../lib/service-secrets";
|
|
6
6
|
import { tokenCollidesWithAdmin } from "../lib/admin-secrets";
|
|
7
7
|
import { randomBytes } from "node:crypto";
|
|
8
8
|
import { hardenSecretDir, hardenSecretPath } from "../lib/windows-secret-acl";
|
|
9
9
|
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
10
10
|
import { isTestHomeGuardArmed } from "../lib/test-home-guard";
|
|
11
|
-
import { diagnoseService } from "./diagnostics";
|
|
12
|
-
import type { ServiceDiagnostic } from "./diagnostics";
|
|
13
11
|
import { currentCodexHome, currentOpenCodexHome, resolveServiceState, serviceCodexHomeMatchesInstall, servicePathMatchesInstall } from "./state";
|
|
14
12
|
import { resolveCodexSqliteHome } from "../codex/paths";
|
|
15
13
|
import type { CodexHomeDeps } from "../codex/home";
|
|
@@ -76,20 +74,6 @@ export function assertServiceEnvironmentMatchesInstall(deps: CodexHomeDeps = {})
|
|
|
76
74
|
}
|
|
77
75
|
}
|
|
78
76
|
|
|
79
|
-
/**
|
|
80
|
-
* The `ocx` command a user should rerun for the service state they actually have.
|
|
81
|
-
*
|
|
82
|
-
* `installed` alone is not enough: `repairService()` refuses a Task-Scheduler-plus-WinSW
|
|
83
|
-
* conflict outright, so recommending repair there names a command guaranteed to fail.
|
|
84
|
-
* Install IS the valid conflict recovery, because `installWindows` removes the native
|
|
85
|
-
* backend first. Exported so the guard tests the real selector rather than a copy of it.
|
|
86
|
-
*/
|
|
87
|
-
export function serviceRetryCommand(
|
|
88
|
-
diag: Pick<ServiceDiagnostic, "installed" | "conflict"> = diagnoseService(),
|
|
89
|
-
): string {
|
|
90
|
-
return diag.installed && !diag.conflict ? "ocx service repair" : "ocx service install";
|
|
91
|
-
}
|
|
92
|
-
|
|
93
77
|
/**
|
|
94
78
|
* Refuse a management (admin) token as the data-plane secret.
|
|
95
79
|
*
|
|
@@ -139,50 +123,6 @@ export function assertNotAdminToken(
|
|
|
139
123
|
);
|
|
140
124
|
}
|
|
141
125
|
|
|
142
|
-
/**
|
|
143
|
-
* Preflight for `service install` / `service repair` on the data-plane credential.
|
|
144
|
-
*
|
|
145
|
-
* It used to DEMAND `OPENCODEX_API_AUTH_TOKEN` for a non-loopback hostname, and it threw
|
|
146
|
-
* even when `~/.opencodex/service-api-token` already held a perfectly good token. That is
|
|
147
|
-
* the defect behind the incident this unit exists to close (#4236): an operator exported the
|
|
148
|
-
* ADMIN token as OPENCODEX_API_AUTH_TOKEN because `install` asked for a token, the hub then
|
|
149
|
-
* crash-looped on `assertNotAdminToken`, and `service repair` asked for the same env var
|
|
150
|
-
* again — so the only remembered way to make the command proceed was the thing that broke it.
|
|
151
|
-
*
|
|
152
|
-
* Nobody should have to export a token by hand to run a hub. {@link writeServiceApiTokenFile}
|
|
153
|
-
* provisions one, so the only conditions left that install cannot fix are an admin-token
|
|
154
|
-
* collision in the environment and a token file that exists but cannot be used.
|
|
155
|
-
*/
|
|
156
|
-
export function assertServiceAuthEnvironment(): void {
|
|
157
|
-
const config = loadConfig();
|
|
158
|
-
// Both collision checks come BEFORE the loopback short-circuit, because the launch wrapper
|
|
159
|
-
// exports the token file unconditionally (`buildServiceShellCommand` cats it whenever it
|
|
160
|
-
// exists, whatever the hostname): a management token in either source fences the whole
|
|
161
|
-
// management plane closed at boot, even on a loopback install that needs no admission
|
|
162
|
-
// secret. Returning early is what let that broken state through.
|
|
163
|
-
const present = process.env.OPENCODEX_API_AUTH_TOKEN?.trim();
|
|
164
|
-
if (present) assertNotAdminToken(present);
|
|
165
|
-
const state = readServiceApiTokenState();
|
|
166
|
-
// An existing FILE holding the admin token is the incident shape itself, and the first round
|
|
167
|
-
// only checked the env var — so install/repair reused it and the hub crash-looped at boot.
|
|
168
|
-
// On a machine connected to a hub this same file holds that hub's issued client key, which
|
|
169
|
-
// is never a management token, so the check is a no-op there.
|
|
170
|
-
if (state.kind === "present") assertNotAdminToken(state.token, process.env, "file");
|
|
171
|
-
if (isLoopbackHostname(config.hostname)) return;
|
|
172
|
-
if (present) return;
|
|
173
|
-
// Absent is fine — install/repair generates one below. `unsafe` is not: the writer refuses
|
|
174
|
-
// to replace a path it cannot vouch for, so say so here, where the operator can still act,
|
|
175
|
-
// instead of failing mid-install. Reached from `service repair` as well as `install`, so
|
|
176
|
-
// name a command that can actually succeed (see serviceRetryCommand).
|
|
177
|
-
if (state.kind !== "unsafe") return;
|
|
178
|
-
const diag = diagnoseService();
|
|
179
|
-
throw new Error(
|
|
180
|
-
`The data-plane token file cannot be used (${state.reason}): ${serviceApiTokenFilePath()}. `
|
|
181
|
-
+ `Move it aside, then rerun \`${serviceRetryCommand(diag)}\`; the service provisions a `
|
|
182
|
-
+ "fresh owner-only token and needs nothing from the environment.",
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
126
|
/** How the data-plane token the service will export was obtained. */
|
|
187
127
|
export type ServiceApiTokenOrigin = "env" | "file" | "generated";
|
|
188
128
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/** Linux pidfd ownership for the opt-in sentinel. Loaded only on Linux. */
|
|
2
|
+
import { dlopen, FFIType, read } from "bun:ffi";
|
|
3
|
+
import type { TerminateResult } from "./health-sentinel-main";
|
|
4
|
+
|
|
5
|
+
const ESRCH = 3;
|
|
6
|
+
const ENOSYS = 38;
|
|
7
|
+
// Syscalls from 424 onward share one number on every Linux architecture. Binding is limited to
|
|
8
|
+
// x64/arm64 because the FFI signature below assumes their LP64 integer-register calling convention.
|
|
9
|
+
const SYS_PIDFD_OPEN = 434;
|
|
10
|
+
const SYS_PIDFD_SEND_SIGNAL = 424;
|
|
11
|
+
|
|
12
|
+
type CallResult = { value: number; errno: number };
|
|
13
|
+
export type PidfdSyscalls = {
|
|
14
|
+
open(pid: number): CallResult;
|
|
15
|
+
signal(fd: number, signal: number): CallResult;
|
|
16
|
+
close(fd: number): void;
|
|
17
|
+
dispose(): void;
|
|
18
|
+
};
|
|
19
|
+
export type ParentPidfd = { terminate(): TerminateResult; close(): void };
|
|
20
|
+
export type ParentPidfdResult =
|
|
21
|
+
| { kind: "bound"; handle: ParentPidfd }
|
|
22
|
+
| { kind: "unsupported"; reason: string }
|
|
23
|
+
| { kind: "refused"; reason: string };
|
|
24
|
+
|
|
25
|
+
function nativeSyscalls(): PidfdSyscalls {
|
|
26
|
+
const symbols = {
|
|
27
|
+
// syscall is variadic, but these syscalls use integer/pointer arguments only. All supplied
|
|
28
|
+
// slots are machine-word integers (including zero for siginfo and flags), on these LP64 ABIs.
|
|
29
|
+
syscall: { args: [FFIType.i64, FFIType.i64, FFIType.i64, FFIType.i64, FFIType.i64], returns: FFIType.i64 },
|
|
30
|
+
__errno_location: { args: [], returns: FFIType.ptr },
|
|
31
|
+
close: { args: [FFIType.i32], returns: FFIType.i32 },
|
|
32
|
+
} as const;
|
|
33
|
+
const load = (path: string) => dlopen(path, symbols);
|
|
34
|
+
let libc: ReturnType<typeof load>;
|
|
35
|
+
try {
|
|
36
|
+
libc = load("libc.so.6");
|
|
37
|
+
} catch {
|
|
38
|
+
// musl exports libc symbols through its loader; it has no glibc soname or pidfd wrappers.
|
|
39
|
+
libc = load(`/lib/ld-musl-${process.arch === "x64" ? "x86_64" : "aarch64"}.so.1`);
|
|
40
|
+
}
|
|
41
|
+
const call = (nr: number, a: number, b: number, c = 0, d = 0): CallResult => {
|
|
42
|
+
const value = Number(libc.symbols.syscall(nr, a, b, c, d));
|
|
43
|
+
return { value, errno: value === -1 ? read.i32(libc.symbols.__errno_location()!) : 0 };
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
open: pid => call(SYS_PIDFD_OPEN, pid, 0),
|
|
47
|
+
signal: (fd, signal) => call(SYS_PIDFD_SEND_SIGNAL, fd, signal),
|
|
48
|
+
// Never retry close on Linux: even an EINTR has released the descriptor.
|
|
49
|
+
close: fd => { libc.symbols.close(fd); },
|
|
50
|
+
dispose: () => libc.close(),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Acquire before arming; reparenting during acquisition invalidates even a successfully opened fd. */
|
|
55
|
+
export function openParentPidfd(
|
|
56
|
+
pid: number,
|
|
57
|
+
isParent: () => boolean,
|
|
58
|
+
options: { arch?: string; load?: () => PidfdSyscalls } = {},
|
|
59
|
+
): ParentPidfdResult {
|
|
60
|
+
if (!isParent()) return { kind: "refused", reason: "the sentinel is not the watched process's child" };
|
|
61
|
+
const arch = options.arch ?? process.arch;
|
|
62
|
+
if (arch !== "x64" && arch !== "arm64") return { kind: "unsupported", reason: `pidfd syscall ABI is unsupported on ${arch}` };
|
|
63
|
+
let api: PidfdSyscalls;
|
|
64
|
+
try { api = (options.load ?? nativeSyscalls)(); }
|
|
65
|
+
catch { return { kind: "refused", reason: "Linux pidfd libc binding is unavailable" }; }
|
|
66
|
+
let fd: number | null = null;
|
|
67
|
+
let closed = false;
|
|
68
|
+
let signalled = false;
|
|
69
|
+
const close = () => {
|
|
70
|
+
if (closed) return;
|
|
71
|
+
closed = true;
|
|
72
|
+
try { if (fd !== null) api.close(fd); } finally { api.dispose(); }
|
|
73
|
+
};
|
|
74
|
+
const refuse = (reason: string): ParentPidfdResult => { close(); return { kind: "refused", reason }; };
|
|
75
|
+
try {
|
|
76
|
+
const opened = api.open(pid);
|
|
77
|
+
if (opened.value < 0) {
|
|
78
|
+
close();
|
|
79
|
+
return opened.errno === ENOSYS
|
|
80
|
+
? { kind: "unsupported", reason: "pidfd_open is unsupported by this kernel" }
|
|
81
|
+
: { kind: "refused", reason: `pidfd_open failed (errno ${opened.errno})` };
|
|
82
|
+
}
|
|
83
|
+
fd = opened.value;
|
|
84
|
+
if (!isParent()) return refuse("the watched parent exited during pidfd acquisition");
|
|
85
|
+
// Check both syscall support and permission before arming, without sending a signal.
|
|
86
|
+
const probe = api.signal(fd, 0);
|
|
87
|
+
if (probe.value < 0) {
|
|
88
|
+
close();
|
|
89
|
+
return probe.errno === ENOSYS
|
|
90
|
+
? { kind: "unsupported", reason: "pidfd_send_signal is unsupported by this kernel" }
|
|
91
|
+
: { kind: "refused", reason: `pidfd signal check failed (errno ${probe.errno})` };
|
|
92
|
+
}
|
|
93
|
+
if (!isParent()) return refuse("the watched parent exited during pidfd verification");
|
|
94
|
+
return {
|
|
95
|
+
kind: "bound",
|
|
96
|
+
handle: {
|
|
97
|
+
close,
|
|
98
|
+
terminate: () => {
|
|
99
|
+
if (closed || signalled) return "not_target";
|
|
100
|
+
// A throwing binding must settle the recorded intent as "failed", like process.kill does.
|
|
101
|
+
let sent: CallResult;
|
|
102
|
+
try { sent = api.signal(fd!, 9); } catch { return "failed"; }
|
|
103
|
+
if (sent.value < 0) return sent.errno === ESRCH ? "not_target" : "failed";
|
|
104
|
+
signalled = true;
|
|
105
|
+
return "terminated";
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
} catch {
|
|
110
|
+
return refuse("Linux pidfd binding failed");
|
|
111
|
+
}
|
|
112
|
+
}
|