@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
package/src/cli/status.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
5
5
|
import { codexAutoStartEnabled, getConfigPath, readConfigDiagnostics } from "../config";
|
|
6
6
|
import { getPidPath, readPid, readRuntimePort, type RuntimePortState } from "../config/process-state";
|
|
7
7
|
import { diagnoseCodexBundledPlugins, type CodexPluginsDiagnostic } from "../codex/plugins-doctor";
|
|
8
|
-
import { findLiveProxy, probeHostname } from "../server/proxy-liveness";
|
|
8
|
+
import { findLiveProxy, probeHostname, type LiveProxy } from "../server/proxy-liveness";
|
|
9
9
|
import type { OcxConfig } from "../types";
|
|
10
10
|
import { diagnoseService, serviceLogPath } from "../service";
|
|
11
11
|
import { inspectDesktopSupervision } from "../service/desktop-supervision.mjs";
|
|
@@ -104,6 +104,16 @@ export type CliRemoteHubStatus = {
|
|
|
104
104
|
truncated: boolean;
|
|
105
105
|
/** The hub's own Claude Code toggle; a client cannot infer it from local config. */
|
|
106
106
|
claudeCodeEnabled: boolean | null;
|
|
107
|
+
/**
|
|
108
|
+
* Present on a Remote Link Child (#6957): the peer is a Home reached over the link, not a hub,
|
|
109
|
+
* and every sentence names it that way. `codexSettings` lists the Home's Codex settings the
|
|
110
|
+
* last sync applied, or why the Child's local values are in effect.
|
|
111
|
+
*/
|
|
112
|
+
link?: {
|
|
113
|
+
linkId: string;
|
|
114
|
+
homePhrase: string;
|
|
115
|
+
codexSettings: { source: "home"; fetchedAt: string; lines: string[] } | { source: "local"; reason: string };
|
|
116
|
+
};
|
|
107
117
|
};
|
|
108
118
|
|
|
109
119
|
export type CliStatusJson = {
|
|
@@ -152,6 +162,7 @@ export type CliStatusJson = {
|
|
|
152
162
|
};
|
|
153
163
|
connection: {
|
|
154
164
|
state: "disconnected" | "connected" | "invalid" | "mismatched";
|
|
165
|
+
transport?: "hub" | "link";
|
|
155
166
|
reason?: string;
|
|
156
167
|
serverUrl?: string;
|
|
157
168
|
managementUrl?: string;
|
|
@@ -321,8 +332,12 @@ export function selectStatusStartupHealth(
|
|
|
321
332
|
if (evidence?.kind === "desktop" && evidence.runtimePid === livePid) {
|
|
322
333
|
const local = fallback();
|
|
323
334
|
const supervisor = local.desktop?.supervisor;
|
|
324
|
-
// The local probe runs later; it must still describe the same
|
|
325
|
-
|
|
335
|
+
// The local probe runs later; it must still describe the same process pair. macOS and Linux
|
|
336
|
+
// accept only an unowned pair. On Windows an older runtime never published a Desktop
|
|
337
|
+
// diagnostic, so a durable Desktop claim beside proven live parentage qualifies too.
|
|
338
|
+
const ownershipEligible = local.desktop?.owned === false
|
|
339
|
+
|| (local.desktop?.owned === true && local.platform === "win32");
|
|
340
|
+
if (ownershipEligible && supervisor
|
|
326
341
|
&& supervisor.runtimePid === evidence.runtimePid && supervisor.supervisorPid === evidence.supervisorPid) {
|
|
327
342
|
return { startup: local, startupSource: "local-supervision-override" };
|
|
328
343
|
}
|
|
@@ -499,8 +514,8 @@ function boundHubStateToken(
|
|
|
499
514
|
* degradation.
|
|
500
515
|
*/
|
|
501
516
|
export async function collectRemoteHubStatus(
|
|
502
|
-
connection: Pick<ClientConnectionStatus, "state" | "serverUrl" | "apiKeyId" | "connectedAt"
|
|
503
|
-
options: { fetchImpl?: typeof fetch; timeoutMs?: number; now?: number } = {},
|
|
517
|
+
connection: Pick<ClientConnectionStatus, "state" | "serverUrl" | "apiKeyId" | "connectedAt"> & { transport?: "hub" | "link" },
|
|
518
|
+
options: { fetchImpl?: typeof fetch; timeoutMs?: number; now?: number; findChild?: () => Promise<LiveProxy | null> } = {},
|
|
504
519
|
): Promise<CliRemoteHubStatus> {
|
|
505
520
|
if (connection.state !== "connected" || !connection.serverUrl || !connection.apiKeyId || !connection.connectedAt) {
|
|
506
521
|
return disconnectedRemoteHubStatus();
|
|
@@ -512,14 +527,50 @@ export async function collectRemoteHubStatus(
|
|
|
512
527
|
connectedAt: connection.connectedAt,
|
|
513
528
|
};
|
|
514
529
|
const bound = boundHubStateToken(readClientConnectionState(), readServiceApiTokenState(), owner);
|
|
530
|
+
// Link credentials never go to the persisted tunnel URL from this CLI. Ask the local Child
|
|
531
|
+
// without a credential; its connection-bound relay authenticates the actual Home connection.
|
|
532
|
+
let fetchImpl = options.fetchImpl;
|
|
533
|
+
let allowNetwork = true;
|
|
534
|
+
let localRelayFetchImpl: typeof fetch | undefined;
|
|
535
|
+
if (connection.transport === "link") {
|
|
536
|
+
const live = await (options.findChild ?? findLiveProxy)();
|
|
537
|
+
const current = readClientConnectionState();
|
|
538
|
+
allowNetwork = live?.role === "client" && current.kind === "connected" && sameClientConnectionOwner(current.value, owner);
|
|
539
|
+
if (allowNetwork && live) {
|
|
540
|
+
const localOrigin = `http://${probeHostname(live.hostname)}:${live.port}`;
|
|
541
|
+
localRelayFetchImpl = ((_, init) => (options.fetchImpl ?? fetch)(localOrigin + "/v1/hub-state", {
|
|
542
|
+
method: "GET", redirect: "manual", signal: init?.signal, headers: { Accept: "application/json" },
|
|
543
|
+
})) as typeof fetch;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
// #6957: the Home's name, the link id and the Codex settings the last sync applied.
|
|
547
|
+
const linkState = readClientConnectionState();
|
|
548
|
+
const linkConnection = connection.transport === "link" && linkState.kind === "connected" && linkState.value.transport === "link" && linkState.value.link
|
|
549
|
+
? linkState.value : null;
|
|
550
|
+
const linkHome = linkConnection ? await import("../client/link-home") : null;
|
|
515
551
|
const resolved = await resolveHubState({
|
|
516
552
|
owner,
|
|
517
|
-
token: bound.token,
|
|
553
|
+
token: connection.transport === "link" ? null : bound.token,
|
|
554
|
+
...(localRelayFetchImpl ? { localRelayFetchImpl } : {}),
|
|
518
555
|
...(bound.withheldReason ? { withheldTokenReason: bound.withheldReason } : {}),
|
|
519
|
-
...(
|
|
556
|
+
...(fetchImpl ? { fetchImpl } : {}),
|
|
557
|
+
allowNetwork,
|
|
558
|
+
...(connection.transport === "link" ? { peer: "Home" as const } : {}),
|
|
520
559
|
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
|
|
521
560
|
...(options.now === undefined ? {} : { now: options.now }),
|
|
522
561
|
});
|
|
562
|
+
let link: CliRemoteHubStatus["link"];
|
|
563
|
+
if (linkConnection && linkHome) {
|
|
564
|
+
const { describeHomeCodexSettings } = await import("../link/codex-settings");
|
|
565
|
+
const record = linkHome.readHomeCodexSettingsRecord(linkConnection.link!.linkId);
|
|
566
|
+
link = {
|
|
567
|
+
linkId: linkConnection.link!.linkId,
|
|
568
|
+
homePhrase: linkHome.linkHomePhrase(linkConnection),
|
|
569
|
+
codexSettings: record
|
|
570
|
+
? { source: "home", fetchedAt: record.fetchedAt, lines: describeHomeCodexSettings(record.document.settings) }
|
|
571
|
+
: { source: "local", reason: "no Home settings recorded yet; run: ocx sync" },
|
|
572
|
+
};
|
|
573
|
+
}
|
|
523
574
|
return {
|
|
524
575
|
connected: true,
|
|
525
576
|
// The hub's own advertised origin when it publishes one; otherwise the URL this client
|
|
@@ -535,6 +586,7 @@ export async function collectRemoteHubStatus(
|
|
|
535
586
|
subagentModels: resolved.state?.subagentModels ?? [],
|
|
536
587
|
truncated: resolved.state?.truncated === true,
|
|
537
588
|
claudeCodeEnabled: resolved.state ? resolved.state.claudeCode.enabled : null,
|
|
589
|
+
...(link ? { link } : {}),
|
|
538
590
|
};
|
|
539
591
|
}
|
|
540
592
|
|
|
@@ -546,8 +598,24 @@ export async function collectRemoteHubStatus(
|
|
|
546
598
|
* A buried `Remote hub: connected (<url>)` line — which is all this report had — does not stop
|
|
547
599
|
* that, as #4236 demonstrated.
|
|
548
600
|
*/
|
|
549
|
-
export function remoteHubBannerLine(remoteHub: CliRemoteHubStatus): string | null {
|
|
601
|
+
export function remoteHubBannerLine(remoteHub: CliRemoteHubStatus, transport?: "hub" | "link"): string | null {
|
|
550
602
|
if (!remoteHub.connected) return null;
|
|
603
|
+
if (remoteHub.link) {
|
|
604
|
+
const peer = `${remoteHub.link.homePhrase} over Remote Link (${remoteHub.link.linkId})`;
|
|
605
|
+
if (remoteHub.stateSource === "unavailable") {
|
|
606
|
+
return `⚠️ ${peer}: state unavailable (${remoteHub.reason ?? "unknown reason"}) — provider and login lines below are LOCAL and do not describe the Home.`;
|
|
607
|
+
}
|
|
608
|
+
const cached = remoteHub.stateSource === "cache"
|
|
609
|
+
? ` — cached ${remoteHub.ageSeconds ?? "?"}s ago (${remoteHub.reason ?? "live read failed"})`
|
|
610
|
+
: "";
|
|
611
|
+
return `🔗 State from ${peer}${cached}: provider credentials, logins and delegable models below are the Home's, not this machine's.`;
|
|
612
|
+
}
|
|
613
|
+
if (transport === "link") {
|
|
614
|
+
const origin = remoteHub.origin ?? "unknown";
|
|
615
|
+
return remoteHub.stateSource === "unavailable"
|
|
616
|
+
? `🔗 Remote Link Home ${origin}: provider credentials and routing come from Home. State unavailable (${remoteHub.reason ?? "unknown reason"}).`
|
|
617
|
+
: `🔗 State from Remote Link Home ${origin}${remoteHub.stateSource === "cache" ? " (cached)" : ""}: provider credentials, logins and models below come from Home.`;
|
|
618
|
+
}
|
|
551
619
|
const origin = remoteHub.origin ?? "the hub";
|
|
552
620
|
if (remoteHub.stateSource === "unavailable") {
|
|
553
621
|
return `⚠️ Hub ${origin}: state unavailable (${remoteHub.reason ?? "unknown reason"}) — provider and login lines below are LOCAL and do not describe the hub.`;
|
|
@@ -562,32 +630,47 @@ export function remoteHubBannerLine(remoteHub: CliRemoteHubStatus): string | nul
|
|
|
562
630
|
* The hub-sourced provider/login/model block, owned here so the sentences are testable without
|
|
563
631
|
* spawning the CLI. Indentation is the caller's. Empty when nothing true is known.
|
|
564
632
|
*/
|
|
565
|
-
export function remoteHubStatusLines(remoteHub: CliRemoteHubStatus): string[] {
|
|
566
|
-
if (!remoteHub.connected
|
|
567
|
-
const
|
|
633
|
+
export function remoteHubStatusLines(remoteHub: CliRemoteHubStatus, transport?: "hub" | "link"): string[] {
|
|
634
|
+
if (!remoteHub.connected) return [];
|
|
635
|
+
const settings = remoteHub.link ? linkCodexSettingsLines(remoteHub.link) : [];
|
|
636
|
+
if (remoteHub.stateSource === "unavailable") return settings;
|
|
637
|
+
const isLink = Boolean(remoteHub.link) || transport === "link";
|
|
638
|
+
const peer = remoteHub.link?.homePhrase ?? `${isLink ? "Home" : "hub"} ${remoteHub.origin ?? (isLink ? "Home" : "hub")}`;
|
|
639
|
+
const subject = isLink ? "the Home" : "the hub";
|
|
568
640
|
const lines = [
|
|
569
|
-
`OAuth logins (
|
|
641
|
+
`OAuth logins (${peer}):`,
|
|
570
642
|
...(remoteHub.oauth.length === 0
|
|
571
|
-
? [
|
|
643
|
+
? [` (${subject} reported no OAuth providers)`]
|
|
572
644
|
: remoteHub.oauth.map(entry => ` ${entry.provider.padEnd(10)} ${entry.loggedIn ? "✓ logged in" : "✗ not logged in"}`)),
|
|
573
|
-
`Providers (
|
|
645
|
+
`Providers (${peer}):`,
|
|
574
646
|
...(remoteHub.providers.length === 0
|
|
575
|
-
? [
|
|
647
|
+
? [` (${subject} reported no providers)`]
|
|
576
648
|
: remoteHub.providers.map(provider => {
|
|
577
649
|
// `authMode` is what keeps "no API key" from reading as "not configured": an `oauth`
|
|
578
650
|
// provider legitimately has no key and is still fully usable.
|
|
579
651
|
const credential = provider.hasCredential ? "credential stored" : `no API key (authMode ${provider.authMode ?? "key"})`;
|
|
580
652
|
return ` ${provider.name.padEnd(10)} ${provider.adapter} — ${credential}${provider.disabled ? ", disabled" : ""}`;
|
|
581
653
|
})),
|
|
582
|
-
`Delegable models (
|
|
654
|
+
`Delegable models (${peer}): ${remoteHub.subagentModels.length === 0 ? "none" : remoteHub.subagentModels.join(", ")}`,
|
|
583
655
|
];
|
|
584
|
-
if (remoteHub.hubVersion) lines.push(
|
|
656
|
+
if (remoteHub.hubVersion) lines.push(`${remoteHub.link ? "Home" : "Hub"} version: ${remoteHub.hubVersion}`);
|
|
585
657
|
// The hub told us its own lists are a prefix. Saying nothing here would make this report claim
|
|
586
658
|
// completeness it does not have — the same shape of confident-and-wrong answer as #4236.
|
|
587
659
|
if (remoteHub.truncated) {
|
|
588
|
-
lines.push(
|
|
660
|
+
lines.push(` (${subject} truncated this state to fit its response caps; some rows are not listed)`);
|
|
589
661
|
}
|
|
590
|
-
return lines;
|
|
662
|
+
return [...lines, ...settings];
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** Which Codex settings the last sync took from the Home (#6957). */
|
|
666
|
+
export function linkCodexSettingsLines(link: NonNullable<CliRemoteHubStatus["link"]>): string[] {
|
|
667
|
+
if (link.codexSettings.source === "local") {
|
|
668
|
+
return [`Codex settings: this machine's local values (${link.codexSettings.reason})`];
|
|
669
|
+
}
|
|
670
|
+
return [
|
|
671
|
+
`Codex settings from ${link.homePhrase} (applied ${link.codexSettings.fetchedAt}):`,
|
|
672
|
+
...link.codexSettings.lines.map(line => ` ${line}`),
|
|
673
|
+
];
|
|
591
674
|
}
|
|
592
675
|
|
|
593
676
|
export function selectListenTarget(
|
|
@@ -760,7 +843,7 @@ export async function collectStatus(options: { mainAccountPolicy?: boolean } = {
|
|
|
760
843
|
// Asked before the local probes below so a connected client's report is hub-sourced from its
|
|
761
844
|
// first line. Bounded and failure-tolerant: an offline hub degrades the remoteHub block, it
|
|
762
845
|
// does not fail `ocx status`.
|
|
763
|
-
const remoteHub = await collectRemoteHubStatus(clientConnection);
|
|
846
|
+
const remoteHub = await collectRemoteHubStatus({ ...clientConnection, transport: config.client?.transport });
|
|
764
847
|
// Prefer identity-verified liveness (runtime-port + /healthz) over ocx.pid alone (#618).
|
|
765
848
|
// Pass the already-resolved diagnostics config so findLiveProxy does not re-load and
|
|
766
849
|
// warn on malformed config.json (status --json must stay stderr-clean).
|
|
@@ -960,6 +1043,7 @@ export async function collectStatus(options: { mainAccountPolicy?: boolean } = {
|
|
|
960
1043
|
},
|
|
961
1044
|
connection: {
|
|
962
1045
|
state: clientConnection.state,
|
|
1046
|
+
...(config.client?.transport ? { transport: config.client.transport } : {}),
|
|
963
1047
|
...(clientConnection.reason ? { reason: clientConnection.reason } : {}),
|
|
964
1048
|
...(clientConnection.serverUrl ? { serverUrl: clientConnection.serverUrl } : {}),
|
|
965
1049
|
...(clientConnection.managementUrl ? { managementUrl: clientConnection.managementUrl } : {}),
|
package/src/client/connect.ts
CHANGED
|
@@ -74,6 +74,16 @@ import {
|
|
|
74
74
|
assertNoClientDisconnectPending, assertClientConnectionUnchanged, sameClientConnectionOwner,
|
|
75
75
|
} from "./state";
|
|
76
76
|
import { assertClientCatalogCompatible, type CatalogCompatibilityDeps } from "./catalog-compatibility";
|
|
77
|
+
import { overlayHomeCodexSettings } from "../link/codex-settings";
|
|
78
|
+
import {
|
|
79
|
+
clearHomeCodexSettingsRecord,
|
|
80
|
+
fetchHomeCodexSettings,
|
|
81
|
+
resolveHomeCodexSettings,
|
|
82
|
+
recordHomeCodexSettings,
|
|
83
|
+
writeHomeCodexSettingsRecord,
|
|
84
|
+
type HomeCodexSettingsSource,
|
|
85
|
+
type LinkHomeDeps,
|
|
86
|
+
} from "./link-home";
|
|
77
87
|
import { hubStateCachePath } from "./hub-state";
|
|
78
88
|
import { ClientLinkStateError, clearClientLinkState, readClientLinkState } from "./link-state";
|
|
79
89
|
import { isLinkPort } from "../link/ports";
|
|
@@ -110,6 +120,8 @@ export interface ClientConnectDeps {
|
|
|
110
120
|
now?: () => Date;
|
|
111
121
|
lifecycleLockDeps?: ClientLifecycleLockDeps;
|
|
112
122
|
catalogCompatibility?: CatalogCompatibilityDeps;
|
|
123
|
+
/** Link mode: how the Home's Codex settings are read (#6957). */
|
|
124
|
+
linkHome?: LinkHomeDeps;
|
|
113
125
|
}
|
|
114
126
|
|
|
115
127
|
export interface RotateClientOptions {
|
|
@@ -687,7 +699,16 @@ export async function connectClient(
|
|
|
687
699
|
|
|
688
700
|
const config = earlyConfig ?? loadConfig();
|
|
689
701
|
const target = routingTarget(serverUrl, linkMode ? config.port : undefined, config);
|
|
690
|
-
|
|
702
|
+
// A link Child writes the Home's OpenCodex-owned Codex settings, not its own (#6957). A read
|
|
703
|
+
// that fails here keeps the local values; the next `ocx sync` applies the Home's.
|
|
704
|
+
// Read only: the record is written after the connection commits, so a rollback leaves none.
|
|
705
|
+
const homeSettings = linkMode && linkMetadata && options.selectedClients.includes("codex")
|
|
706
|
+
? await fetchHomeCodexSettings({ transport: "link", link: linkMetadata, apiKeyId }, admissionToken, deps.linkHome)
|
|
707
|
+
: undefined;
|
|
708
|
+
const injectConfig = {
|
|
709
|
+
...(homeSettings?.kind === "fresh" ? overlayHomeCodexSettings(config, homeSettings.document.settings) : config),
|
|
710
|
+
syncResumeHistory: false,
|
|
711
|
+
};
|
|
691
712
|
const preflight = await injectCodexConfig(config.port, injectConfig, {
|
|
692
713
|
validateOnly: true,
|
|
693
714
|
routingTarget: target,
|
|
@@ -734,6 +755,12 @@ export async function connectClient(
|
|
|
734
755
|
commitClientConnection(connection);
|
|
735
756
|
committed = true;
|
|
736
757
|
}), deps.lifecycleLockDeps);
|
|
758
|
+
// Recorded only when the injection that applied it ran (not with --no-sync).
|
|
759
|
+
if (homeSettings?.kind === "fresh" && linkMetadata && injectionCommitted) {
|
|
760
|
+
try {
|
|
761
|
+
writeHomeCodexSettingsRecord({ linkId: linkMetadata.linkId, fetchedAt: now, document: homeSettings.document });
|
|
762
|
+
} catch { /* the next sync records it; the applied config is already committed */ }
|
|
763
|
+
}
|
|
737
764
|
return connection;
|
|
738
765
|
} catch (error) {
|
|
739
766
|
const rollbackFailures: string[] = [];
|
|
@@ -780,7 +807,7 @@ export async function connectClient(
|
|
|
780
807
|
export async function syncConnectedClient(
|
|
781
808
|
_options: { restartCodex?: boolean } = {},
|
|
782
809
|
deps: ClientConnectDeps = {},
|
|
783
|
-
): Promise<{ catalogWritten: boolean; cacheSynced: boolean; injected: boolean; stale: boolean }> {
|
|
810
|
+
): Promise<{ catalogWritten: boolean; cacheSynced: boolean; injected: boolean; stale: boolean; homeSettings?: HomeCodexSettingsSource }> {
|
|
784
811
|
if (siblingOfLivePort() !== null) throw new Error(siblingSkipMessage());
|
|
785
812
|
const initial = withClientLifecycleSync(() => withConfigMutationLockSync(() => {
|
|
786
813
|
assertNoClientDisconnectPending();
|
|
@@ -825,21 +852,28 @@ export async function syncConnectedClient(
|
|
|
825
852
|
if (next.pendingOperation || token.kind !== "present" || token.fingerprint !== next.tokenFingerprint) throw new Error("client_token_changed");
|
|
826
853
|
};
|
|
827
854
|
let injected = false;
|
|
855
|
+
let homeSettings: HomeCodexSettingsSource | undefined;
|
|
828
856
|
if (next.selectedClients.includes("codex")) {
|
|
829
|
-
|
|
857
|
+
let config = loadConfig();
|
|
858
|
+
// #6957: a link Child applies the Home's OpenCodex-owned Codex settings with the catalog.
|
|
859
|
+
if (isLinkConnection(next)) {
|
|
860
|
+
homeSettings = await resolveHomeCodexSettings(next, initial.token.token, deps.linkHome);
|
|
861
|
+
if (homeSettings.source !== "local") config = overlayHomeCodexSettings(config, homeSettings.document.settings);
|
|
862
|
+
}
|
|
830
863
|
const result = await injectCodexConfig(config.port, { ...config, syncResumeHistory: false }, {
|
|
831
864
|
routingTarget: routingTarget(next.serverUrl, isLinkConnection(next) ? config.port : undefined, config), catalogPath: DEFAULT_CATALOG_PATH,
|
|
832
865
|
journalOwner: { kind: "client", apiKeyId: next.apiKeyId }, beforeClientWrite,
|
|
833
866
|
});
|
|
834
867
|
if (!result.success || result.status === "skipped") throw new Error(result.message);
|
|
835
868
|
injected = true;
|
|
869
|
+
if (homeSettings && next.link) recordHomeCodexSettings(next.link.linkId, homeSettings);
|
|
836
870
|
}
|
|
837
871
|
const cacheSynced = withClientLifecycleSync(() => {
|
|
838
872
|
beforeClientWrite();
|
|
839
873
|
// Cache invalidation acquires K itself (N -> K -> C); never call it while C is held.
|
|
840
874
|
return invalidateCodexModelsCache({ allowWhenDesiredDisabled: true });
|
|
841
875
|
}, deps.lifecycleLockDeps);
|
|
842
|
-
return { catalogWritten: downloaded !== undefined, cacheSynced, injected, stale };
|
|
876
|
+
return { catalogWritten: downloaded !== undefined, cacheSynced, injected, stale, ...(homeSettings ? { homeSettings } : {}) };
|
|
843
877
|
}
|
|
844
878
|
|
|
845
879
|
/**
|
|
@@ -1035,6 +1069,7 @@ export async function disconnectClient(
|
|
|
1035
1069
|
if (!(error instanceof ClientLinkStateError)) throw error;
|
|
1036
1070
|
}
|
|
1037
1071
|
if (sidecarLinkId === connection.link.linkId) clearClientLinkState(connection.link.linkId);
|
|
1072
|
+
try { clearHomeCodexSettingsRecord(); } catch { /* derived data; never fails a disconnect */ }
|
|
1038
1073
|
}
|
|
1039
1074
|
removeHubStateCache();
|
|
1040
1075
|
requireDesktopResult(finishRemoteDesktopCleanup(held, receipt.owner));
|
package/src/client/hub-client.ts
CHANGED
|
@@ -485,7 +485,7 @@ export async function fetchHubUsage(
|
|
|
485
485
|
serverUrl: string,
|
|
486
486
|
admissionToken: string,
|
|
487
487
|
query: URLSearchParams,
|
|
488
|
-
options: { timeoutMs?: number; fetchImpl?: typeof fetch } = {},
|
|
488
|
+
options: { timeoutMs?: number; fetchImpl?: typeof fetch; peer?: "hub" | "Home" } = {},
|
|
489
489
|
): Promise<HubUsageReport> {
|
|
490
490
|
const origin = normalizeHubOrigin(serverUrl);
|
|
491
491
|
if (!isHubTransportPermitted(origin)) {
|
|
@@ -498,9 +498,12 @@ export async function fetchHubUsage(
|
|
|
498
498
|
}, options.timeoutMs);
|
|
499
499
|
if (!response.ok) {
|
|
500
500
|
try { await response.body?.cancel(); } catch { /* best effort */ }
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
501
|
+
// A Remote Link Child reads the same route from its Home (#6957).
|
|
502
|
+
const peer = options.peer ?? "hub";
|
|
503
|
+
const Peer = peer === "Home" ? "Home" : "Hub";
|
|
504
|
+
const message = response.status === 404 ? `${Peer} does not support client usage; upgrade the ${peer}`
|
|
505
|
+
: response.status === 401 || response.status === 403 ? `${Peer} rejected this ${peer === "Home" ? "Child's link key" : "client's usage credential"}`
|
|
506
|
+
: `${Peer} usage request failed (${response.status})`;
|
|
504
507
|
throw new HubClientError(`hub_usage_http_${response.status}`, message, response.status);
|
|
505
508
|
}
|
|
506
509
|
if (!jsonCompatibleContentType(response)) {
|
|
@@ -527,16 +530,20 @@ export async function fetchHubUsage(
|
|
|
527
530
|
*/
|
|
528
531
|
export async function fetchHubState(
|
|
529
532
|
serverUrl: string,
|
|
530
|
-
admissionToken: string,
|
|
533
|
+
admissionToken: string | null,
|
|
531
534
|
options: { timeoutMs?: number; fetchImpl?: typeof fetch } = {},
|
|
532
535
|
): Promise<HubStateDTO> {
|
|
533
536
|
const origin = normalizeHubOrigin(serverUrl);
|
|
534
537
|
const response = await fetchBounded(options.fetchImpl ?? fetch, `${origin}/v1/hub-state`, {
|
|
535
538
|
method: "GET",
|
|
536
|
-
headers: new Headers({ Accept: "application/json", "x-opencodex-api-key": admissionToken }),
|
|
539
|
+
headers: new Headers({ Accept: "application/json", ...(admissionToken === null ? {} : { "x-opencodex-api-key": admissionToken }) }),
|
|
537
540
|
}, options.timeoutMs, "headers");
|
|
538
541
|
if (response.status === 404) {
|
|
539
|
-
|
|
542
|
+
let body: unknown = null;
|
|
543
|
+
try { body = JSON.parse(await boundedText(response, 4096, { inactivityTimeoutMs: safeTimeout(options.timeoutMs) })); } catch { /* legacy/non-JSON 404 stays unsupported */ }
|
|
544
|
+
if (body && typeof body === "object" && !Array.isArray(body) && (body as { error?: { code?: unknown } }).error?.code === "hub_state_not_a_hub") {
|
|
545
|
+
throw new HubClientError("hub_state_not_a_hub", "the remote host does not publish hub state", 404);
|
|
546
|
+
}
|
|
540
547
|
throw new HubClientError("hub_state_unsupported", "Hub does not serve /v1/hub-state; upgrade the hub", 404);
|
|
541
548
|
}
|
|
542
549
|
if (!response.ok) {
|
package/src/client/hub-state.ts
CHANGED
|
@@ -124,37 +124,43 @@ export function writeCachedHubState(owner: HubStateOwner, state: HubStateDTO, fe
|
|
|
124
124
|
* banner reading `state unavailable (hub_state_http_507)` sends the reader looking for a client
|
|
125
125
|
* bug when the hub has in fact answered and said something.
|
|
126
126
|
*/
|
|
127
|
-
export function hubStateFailureReason(error: unknown): string {
|
|
127
|
+
export function hubStateFailureReason(error: unknown, peer: "hub" | "Home" = "hub"): string {
|
|
128
|
+
// A Remote Link Child reads the same route from its Home (#6957); the sentences name the peer
|
|
129
|
+
// the operator actually configured.
|
|
130
|
+
const holder = peer === "Home" ? "this Child's link key" : "this client's data key";
|
|
128
131
|
if (error instanceof HubClientError) {
|
|
129
132
|
switch (error.code) {
|
|
133
|
+
case "hub_state_not_a_hub":
|
|
134
|
+
return "the remote host does not publish hub state";
|
|
130
135
|
case "hub_state_unsupported":
|
|
131
|
-
return
|
|
136
|
+
return `this ${peer} is too old to report its state; upgrade the ${peer}`;
|
|
132
137
|
case "hub_state_unauthorized":
|
|
133
|
-
return
|
|
138
|
+
return `the ${peer} rejected ${holder}`;
|
|
134
139
|
case "hub_state_schema_invalid":
|
|
135
140
|
case "hub_state_invalid":
|
|
136
|
-
return
|
|
141
|
+
return `the ${peer} returned an unreadable hub-state document`;
|
|
137
142
|
case "hub_state_content_type_invalid":
|
|
138
143
|
// Usually a captive portal, a TLS-terminating proxy or an error page in front of the
|
|
139
144
|
// hub: the request reached SOMETHING, and that something is not the hub's API.
|
|
140
|
-
return
|
|
145
|
+
return `the ${peer}'s state response was not JSON`;
|
|
141
146
|
case "body_too_large":
|
|
142
|
-
return
|
|
147
|
+
return `the ${peer}'s state response exceeded the allowed size`;
|
|
143
148
|
case "unreachable":
|
|
144
|
-
return
|
|
149
|
+
return `the ${peer} is unreachable`;
|
|
145
150
|
case "redirect_refused":
|
|
146
|
-
return
|
|
151
|
+
return `the ${peer} redirected the state request`;
|
|
147
152
|
default: {
|
|
148
153
|
const status = error.code.startsWith("hub_state_http_")
|
|
149
154
|
? error.code.slice("hub_state_http_".length)
|
|
150
155
|
: null;
|
|
151
156
|
return status && /^\d+$/.test(status)
|
|
152
|
-
? `the
|
|
157
|
+
? `the ${peer} answered HTTP ${status} to the state request`
|
|
153
158
|
: error.code;
|
|
154
159
|
}
|
|
155
160
|
}
|
|
156
161
|
}
|
|
157
|
-
|
|
162
|
+
if (error instanceof Error && error.name === "LinkRelayAuthenticationError") return error.message;
|
|
163
|
+
return `the ${peer} state could not be read`;
|
|
158
164
|
}
|
|
159
165
|
|
|
160
166
|
export interface ResolveHubStateOptions {
|
|
@@ -167,6 +173,8 @@ export interface ResolveHubStateOptions {
|
|
|
167
173
|
* token for a different connection, and the operator acts on that difference.
|
|
168
174
|
*/
|
|
169
175
|
withheldTokenReason?: string;
|
|
176
|
+
/** Identity-checked local Child transport that strips credentials and owns Home admission. */
|
|
177
|
+
localRelayFetchImpl?: typeof fetch;
|
|
170
178
|
timeoutMs?: number;
|
|
171
179
|
fetchImpl?: typeof fetch;
|
|
172
180
|
now?: number;
|
|
@@ -174,6 +182,8 @@ export interface ResolveHubStateOptions {
|
|
|
174
182
|
allowNetwork?: boolean;
|
|
175
183
|
/** False skips the cache write, for read-only callers. */
|
|
176
184
|
persist?: boolean;
|
|
185
|
+
/** Which peer the sentences name: a hub, or a Remote Link Child's Home. */
|
|
186
|
+
peer?: "hub" | "Home";
|
|
177
187
|
}
|
|
178
188
|
|
|
179
189
|
function withAge(
|
|
@@ -201,20 +211,21 @@ export async function resolveHubState(options: ResolveHubStateOptions): Promise<
|
|
|
201
211
|
? withAge("cache", cached.state, cached.fetchedAt, now, reason)
|
|
202
212
|
: withAge("unavailable", null, undefined, now, reason);
|
|
203
213
|
};
|
|
204
|
-
if (!options.token) {
|
|
214
|
+
if (!options.token && !options.localRelayFetchImpl) {
|
|
205
215
|
return fromCache(options.withheldTokenReason ?? "this client has no usable data-plane token");
|
|
206
216
|
}
|
|
207
217
|
if (options.allowNetwork === false) return fromCache("a live hub read was not attempted");
|
|
208
218
|
let state: HubStateDTO;
|
|
209
219
|
try {
|
|
210
|
-
state = await fetchHubState(options.owner.serverUrl, options.token, {
|
|
220
|
+
state = await fetchHubState(options.owner.serverUrl, options.localRelayFetchImpl ? null : options.token, {
|
|
211
221
|
timeoutMs: options.timeoutMs ?? DEFAULT_HUB_STATE_TIMEOUT_MS,
|
|
212
|
-
...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
|
|
222
|
+
...((options.localRelayFetchImpl ?? options.fetchImpl) ? { fetchImpl: options.localRelayFetchImpl ?? options.fetchImpl } : {}),
|
|
213
223
|
});
|
|
214
224
|
} catch (error) {
|
|
215
225
|
// Deliberately no local-state fallback here. A stale cache is still the HUB's state; the
|
|
216
226
|
// client's own providers and logins are not, at any age.
|
|
217
|
-
return
|
|
227
|
+
if (error instanceof HubClientError && error.code === "hub_state_not_a_hub") return withAge("unavailable", null, undefined, now, hubStateFailureReason(error, options.peer));
|
|
228
|
+
return fromCache(hubStateFailureReason(error, options.peer));
|
|
218
229
|
}
|
|
219
230
|
const fetchedAt = new Date(now).toISOString();
|
|
220
231
|
if (options.persist !== false) writeCachedHubState(options.owner, state, fetchedAt);
|