@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/lib/errors.ts
CHANGED
|
@@ -18,6 +18,13 @@ export const ENCRYPTED_FUNCTION_OUTPUT_REJECTION =
|
|
|
18
18
|
*/
|
|
19
19
|
export const SEND_BUDGET_EXHAUSTED_CODE = "request_send_budget_exhausted";
|
|
20
20
|
export const SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE = "spend_ledger_storage_unavailable";
|
|
21
|
+
/**
|
|
22
|
+
* Wire code for the undeclared-tool guard's deterministic refusal. Endpoint translators
|
|
23
|
+
* read it to keep the refusal non-retryable instead of folding it into the transient
|
|
24
|
+
* upstream-error class. Declared here rather than in the guard module because the shared
|
|
25
|
+
* envelope formatter decides whether the identity survives serialization.
|
|
26
|
+
*/
|
|
27
|
+
export const UNDECLARED_TOOL_CALL_ERROR_CODE = "undeclared_tool_call";
|
|
21
28
|
|
|
22
29
|
/** First nonblank string across the canonical upstream error paths, in priority order. */
|
|
23
30
|
export function upstreamErrorMessageFromPayload(payload: unknown): string | undefined {
|
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
import { TRANSLATOR_MAX_TURN_BYTES, TranslatorBudgetExceededError } from "./translator-budget";
|
|
2
2
|
|
|
3
|
+
/** Count one JSON string formed from parts without joining, serializing or encoding it. */
|
|
4
|
+
export function jsonStringPartsUtf8Bytes(parts: readonly string[], limit = TRANSLATOR_MAX_TURN_BYTES): number {
|
|
5
|
+
let bytes = 0;
|
|
6
|
+
let highSurrogate = false;
|
|
7
|
+
const add = (count: number) => {
|
|
8
|
+
if (count > limit - bytes) throw new TranslatorBudgetExceededError("request_copies", limit);
|
|
9
|
+
bytes += count;
|
|
10
|
+
};
|
|
11
|
+
add(2); // The joined string has one pair of JSON quotes.
|
|
12
|
+
for (const text of parts) {
|
|
13
|
+
if (text.length > limit - bytes) throw new TranslatorBudgetExceededError("request_copies", limit);
|
|
14
|
+
for (let i = 0; i < text.length; i++) {
|
|
15
|
+
const code = text.charCodeAt(i);
|
|
16
|
+
if (highSurrogate) {
|
|
17
|
+
highSurrogate = false;
|
|
18
|
+
if (code >= 0xdc00 && code <= 0xdfff) { add(4); continue; }
|
|
19
|
+
add(6);
|
|
20
|
+
}
|
|
21
|
+
if (code >= 0xd800 && code <= 0xdbff) highSurrogate = true;
|
|
22
|
+
else if (code === 0x22 || code === 0x5c || code === 8 || code === 9 || code === 10 || code === 12 || code === 13) add(2);
|
|
23
|
+
else if (code < 0x20) add(6);
|
|
24
|
+
else if (code < 0x80) add(1);
|
|
25
|
+
else if (code < 0x800) add(2);
|
|
26
|
+
else if (code >= 0xdc00 && code <= 0xdfff) add(6);
|
|
27
|
+
else add(3);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (highSurrogate) add(6);
|
|
31
|
+
return bytes;
|
|
32
|
+
}
|
|
33
|
+
|
|
3
34
|
/** Measure plain JSON data without allocating its serialized string or UTF-8 copy. */
|
|
4
35
|
export function jsonUtf8Bytes(value: unknown, limit = TRANSLATOR_MAX_TURN_BYTES): number {
|
|
5
36
|
let bytes = 0;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-local internal-call capability.
|
|
3
|
+
*
|
|
4
|
+
* Some requests ARE the proxy itself talking to its own data plane: the advisor sidecar's
|
|
5
|
+
* loopback consultation is the first user. Such a request must be recognized as INTERNAL without
|
|
6
|
+
* trusting anything the client controls. A client-supplied header value is not evidence — any
|
|
7
|
+
* external caller can send it — and neither is the peer address: a public listener reached
|
|
8
|
+
* through Docker, WSL, a tunnel, or port forwarding can present as loopback on the last hop, as
|
|
9
|
+
* the local-management attestation notes.
|
|
10
|
+
*
|
|
11
|
+
* The authority is therefore process-owned: a 256-bit random value minted once per process, kept
|
|
12
|
+
* in memory only. It is never written to config or disk, never logged, never returned by the
|
|
13
|
+
* management API, never placed in usage or request metadata, and never forwarded upstream. A new
|
|
14
|
+
* process mints a new value, so a token captured from an older process is worthless. Comparison
|
|
15
|
+
* is timing-safe and shape-checked, reusing the same secret shape as the local attestation
|
|
16
|
+
* module.
|
|
17
|
+
*
|
|
18
|
+
* The same primitive is what the vision-describe fence would need for the same reason; wiring
|
|
19
|
+
* that surface is deliberately left out of this change (recorded as a pre-existing analogous
|
|
20
|
+
* issue) so the change stays scoped.
|
|
21
|
+
*/
|
|
22
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
23
|
+
import { isLocalAttestationSecret } from "./local-management-attestation";
|
|
24
|
+
|
|
25
|
+
/** Header an internal sidecar presents on its own loopback request. */
|
|
26
|
+
export const ADVISOR_INTERNAL_CAPABILITY_HEADER = "x-opencodex-advisor-internal";
|
|
27
|
+
|
|
28
|
+
let processCapability: string | null = null;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The current process's internal-call capability. Minted lazily on first use (one random draw,
|
|
32
|
+
* no I/O), so an install that never enables the advisor never generates one.
|
|
33
|
+
*/
|
|
34
|
+
export function internalCallCapability(): string {
|
|
35
|
+
if (processCapability === null) processCapability = randomBytes(32).toString("base64url");
|
|
36
|
+
return processCapability;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Test seam: install a deterministic value, or `null` to force a fresh mint. */
|
|
40
|
+
export function setInternalCallCapabilityForTests(value: string | null): void {
|
|
41
|
+
processCapability = value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* True only when the supplied header value IS this process's capability. Shape-checked first so
|
|
46
|
+
* a malformed value cannot reach the comparison, then compared in constant time.
|
|
47
|
+
*/
|
|
48
|
+
export function isInternalCallCapability(supplied: string | null | undefined): boolean {
|
|
49
|
+
if (typeof supplied !== "string" || !isLocalAttestationSecret(supplied)) return false;
|
|
50
|
+
if (processCapability === null) return false;
|
|
51
|
+
const expected = processCapability;
|
|
52
|
+
const suppliedBytes = Buffer.from(supplied);
|
|
53
|
+
const expectedBytes = Buffer.from(expected);
|
|
54
|
+
return suppliedBytes.length === expectedBytes.length && timingSafeEqual(suppliedBytes, expectedBytes);
|
|
55
|
+
}
|
|
@@ -70,5 +70,40 @@ export function runOptionalShutdownHooks(): void {
|
|
|
70
70
|
/** Test-only reset so an isolated lifecycle test does not inherit registrations. */
|
|
71
71
|
export function resetOptionalShutdownHooksForTests(): void {
|
|
72
72
|
hooks.clear();
|
|
73
|
+
drainStartHooks.clear();
|
|
74
|
+
drainStartHooksRan = false;
|
|
73
75
|
hooksRan = false;
|
|
74
76
|
}
|
|
77
|
+
|
|
78
|
+
const drainStartHooks = new Map<string, ShutdownHook>();
|
|
79
|
+
let drainStartHooksRan = false;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Register work that must run the moment a drain begins, before in-flight turns are awaited.
|
|
83
|
+
* The service health sentinel (#6671) uses it: a deliberate stop whose drain stalls must stay a
|
|
84
|
+
* stop, so the sentinel is withdrawn before the drain can look unresponsive. Same contract as
|
|
85
|
+
* {@link registerOptionalShutdownHook}: keyed, synchronous, best-effort, detachable.
|
|
86
|
+
*/
|
|
87
|
+
export function registerDrainStartHook(key: string, hook: ShutdownHook): () => void {
|
|
88
|
+
drainStartHooks.set(key, hook);
|
|
89
|
+
return () => {
|
|
90
|
+
if (drainStartHooks.get(key) === hook) drainStartHooks.delete(key);
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Run every drain-start hook. Never throws. */
|
|
95
|
+
export function runDrainStartHooks(): void {
|
|
96
|
+
drainStartHooksRan = true;
|
|
97
|
+
for (const [key, hook] of [...drainStartHooks]) {
|
|
98
|
+
try {
|
|
99
|
+
hook();
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.warn(`[shutdown] drain-start hook "${key}" failed:`, err instanceof Error ? err.message : err);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Whether a drain has begun in this process; work that must not start mid-drain checks this. */
|
|
107
|
+
export function didRunDrainStartHooks(): boolean {
|
|
108
|
+
return drainStartHooksRan;
|
|
109
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-local record of credential values this process has handed to an upstream.
|
|
3
|
+
*
|
|
4
|
+
* Upstream diagnostics (error codes, request ids) are provider-controlled. A gateway can echo a
|
|
5
|
+
* credential it received back into those fields, and a credential with no recognizable format slips
|
|
6
|
+
* past pattern redaction. Configured credentials are read directly at capture time (see
|
|
7
|
+
* server/configured-credentials); this registry adds the values that only exist at send time, such
|
|
8
|
+
* as a client-forwarded bearer or a plugin-added header.
|
|
9
|
+
*
|
|
10
|
+
* Two tiers keep ordinary traffic from disabling diagnostics. Values of credential-named headers are
|
|
11
|
+
* strict: they match at any length, and once one is evicted coverage is incomplete for the rest of
|
|
12
|
+
* the process, so callers fail closed. Every other header value is best effort: it matches only at
|
|
13
|
+
* eight characters or more and its eviction never disables diagnostics, so per-turn metadata cannot
|
|
14
|
+
* exhaust anything that matters. Correlation ids (request ids, trace context) are left out of the
|
|
15
|
+
* best-effort tier because a gateway legitimately echoes them as its request id; a configured header
|
|
16
|
+
* of that name is still covered by the configured set. Values are held in memory only: nothing here
|
|
17
|
+
* is serialized, persisted or exposed.
|
|
18
|
+
*/
|
|
19
|
+
import { SENSITIVE_KEY_PATTERN } from "./redact";
|
|
20
|
+
|
|
21
|
+
/** Containment only for credentials long enough that a diagnostic cannot contain one by chance. */
|
|
22
|
+
export const CREDENTIAL_SUBSTRING_MIN_LENGTH = 8;
|
|
23
|
+
const CREDENTIAL_HEADER_NAME = /api[-_]?key|secret|passw|credential|auth|(?:^|[-_])token$/i;
|
|
24
|
+
const CORRELATION_HEADER_NAME = /^(?:x-(?:client-)?request-id|request-id|traceparent|tracestate)$/i;
|
|
25
|
+
const AUTH_SCHEMES = new Set(["bearer", "basic", "token", "digest", "apikey", "api-key", "key", "sso-key", "negotiate", "dpop"]);
|
|
26
|
+
const TOKEN_CHAR = /[!#$%&'*+.^_`|~0-9A-Za-z-]/;
|
|
27
|
+
const BASIC_BASE64 = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
28
|
+
const PRINTABLE_USER_PASS = /^[\x20-\x7e]*:[\x20-\x7e]*$/;
|
|
29
|
+
|
|
30
|
+
/** True for a header (or config field) name whose value is a credential by its name. */
|
|
31
|
+
export function isCredentialName(name: string): boolean {
|
|
32
|
+
return SENSITIVE_KEY_PATTERN.test(name) || CREDENTIAL_HEADER_NAME.test(name);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Values of RFC 9110 auth-params (token BWS "=" BWS ( token / quoted-string )), cookie pairs and
|
|
37
|
+
* other name=value pairs, in one linear pass: every character is visited a bounded number of times,
|
|
38
|
+
* so a long header value cannot stall the event loop.
|
|
39
|
+
*/
|
|
40
|
+
function parameterValues(value: string): string[] {
|
|
41
|
+
const out: string[] = [];
|
|
42
|
+
const n = value.length;
|
|
43
|
+
let unclosedFrom = n; // no closing quote exists for any opening quote at or after this index
|
|
44
|
+
let i = 0;
|
|
45
|
+
while (i < n) {
|
|
46
|
+
if (!TOKEN_CHAR.test(value[i]!)) { i++; continue; }
|
|
47
|
+
while (i < n && TOKEN_CHAR.test(value[i]!)) i++;
|
|
48
|
+
let j = i;
|
|
49
|
+
while (j < n && (value[j] === " " || value[j] === "\t")) j++;
|
|
50
|
+
if (value[j] !== "=") { i = j; continue; }
|
|
51
|
+
j++;
|
|
52
|
+
while (j < n && (value[j] === " " || value[j] === "\t")) j++;
|
|
53
|
+
if (value[j] === "\"") {
|
|
54
|
+
let k = j + 1;
|
|
55
|
+
let text = "";
|
|
56
|
+
let closed = false;
|
|
57
|
+
if (j < unclosedFrom) {
|
|
58
|
+
while (k < n) {
|
|
59
|
+
const char = value[k]!;
|
|
60
|
+
if (char === "\\" && k + 1 < n) { text += value[k + 1]; k += 2; continue; }
|
|
61
|
+
if (char === "\"") { closed = true; break; }
|
|
62
|
+
text += char;
|
|
63
|
+
k++;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (closed) {
|
|
67
|
+
if (text) out.push(text);
|
|
68
|
+
i = k + 1;
|
|
69
|
+
} else {
|
|
70
|
+
unclosedFrom = Math.min(unclosedFrom, j);
|
|
71
|
+
i = j + 1;
|
|
72
|
+
}
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const start = j;
|
|
76
|
+
while (j < n && !/[\s,;"]/.test(value[j]!)) j++;
|
|
77
|
+
const raw = value.slice(start, j);
|
|
78
|
+
if (raw && !/^=+$/.test(raw)) out.push(raw);
|
|
79
|
+
i = j;
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The whole value plus every part a server could authenticate with on its own: tokens split on
|
|
86
|
+
* whitespace, commas and semicolons with auth scheme words dropped, the value of each name=value
|
|
87
|
+
* parameter (cookies, digest; whitespace around "=" and quoted strings with escapes are parsed
|
|
88
|
+
* first, before any splitting), and both halves of a standard Basic credential.
|
|
89
|
+
*/
|
|
90
|
+
export function credentialComponents(value: string): string[] {
|
|
91
|
+
const whole = value.trim();
|
|
92
|
+
if (!whole) return [];
|
|
93
|
+
const out = new Set([whole]);
|
|
94
|
+
for (const param of parameterValues(whole)) out.add(param);
|
|
95
|
+
let previous: string | undefined;
|
|
96
|
+
for (const part of whole.split(/[\s,;]+/)) {
|
|
97
|
+
if (!part) continue;
|
|
98
|
+
const lower = part.toLowerCase();
|
|
99
|
+
if (AUTH_SCHEMES.has(lower)) { previous = lower; continue; }
|
|
100
|
+
out.add(part);
|
|
101
|
+
if (previous === "basic" && BASIC_BASE64.test(part)) {
|
|
102
|
+
const decoded = Buffer.from(part, "base64").toString("utf8");
|
|
103
|
+
if (PRINTABLE_USER_PASS.test(decoded) && Buffer.from(decoded, "utf8").toString("base64").replace(/=+$/, "") === part.replace(/=+$/, "")) {
|
|
104
|
+
const colon = decoded.indexOf(":");
|
|
105
|
+
out.add(decoded.slice(0, colon));
|
|
106
|
+
out.add(decoded.slice(colon + 1));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
previous = undefined;
|
|
110
|
+
}
|
|
111
|
+
out.delete("");
|
|
112
|
+
return [...out];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Exact equality at any length at or above minLength; containment for values of 8+ characters. */
|
|
116
|
+
export function credentialSetMatches(values: Iterable<string>, has: (value: string) => boolean, value: string, minLength = 1): boolean {
|
|
117
|
+
if (value.length >= minLength && has(value)) return true;
|
|
118
|
+
for (const secret of values) {
|
|
119
|
+
if (secret.length >= Math.max(minLength, CREDENTIAL_SUBSTRING_MIN_LENGTH) && value.includes(secret)) return true;
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface OutboundCredentialRegistryOptions {
|
|
125
|
+
maxEntries?: number;
|
|
126
|
+
maxBytes?: number;
|
|
127
|
+
maxValueBytes?: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Bounded LRU of values; eviction marks it incomplete. */
|
|
131
|
+
class BoundedValueSet {
|
|
132
|
+
readonly values = new Map<string, number>();
|
|
133
|
+
private totalBytes = 0;
|
|
134
|
+
complete = true;
|
|
135
|
+
constructor(private readonly maxEntries: number, private readonly maxBytes: number, private readonly maxValueBytes: number) {}
|
|
136
|
+
|
|
137
|
+
add(value: string): void {
|
|
138
|
+
if (!value) return;
|
|
139
|
+
const bytes = Buffer.byteLength(value, "utf8");
|
|
140
|
+
if (bytes > this.maxValueBytes) {
|
|
141
|
+
this.complete = false;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (this.values.has(value)) {
|
|
145
|
+
this.values.delete(value);
|
|
146
|
+
this.values.set(value, bytes);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
this.values.set(value, bytes);
|
|
150
|
+
this.totalBytes += bytes;
|
|
151
|
+
while (this.values.size > this.maxEntries || this.totalBytes > this.maxBytes) {
|
|
152
|
+
const oldest = this.values.keys().next().value as string;
|
|
153
|
+
this.totalBytes -= this.values.get(oldest)!;
|
|
154
|
+
this.values.delete(oldest);
|
|
155
|
+
this.complete = false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
matches(value: string, minLength: number): boolean {
|
|
160
|
+
return credentialSetMatches(this.values.keys(), candidate => this.values.has(candidate), value, minLength);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export class OutboundCredentialRegistry {
|
|
165
|
+
private readonly strict: BoundedValueSet;
|
|
166
|
+
private readonly observed: BoundedValueSet;
|
|
167
|
+
|
|
168
|
+
constructor(options: OutboundCredentialRegistryOptions = {}) {
|
|
169
|
+
const entries = options.maxEntries ?? 4096;
|
|
170
|
+
const bytes = options.maxBytes ?? 1 << 20;
|
|
171
|
+
const valueBytes = options.maxValueBytes ?? 16_384;
|
|
172
|
+
this.strict = new BoundedValueSet(entries, bytes, valueBytes);
|
|
173
|
+
this.observed = new BoundedValueSet(entries, bytes, valueBytes);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Record the components of every value in headers that are about to be sent. */
|
|
177
|
+
remember(headers: HeadersInit | Headers | undefined): void {
|
|
178
|
+
if (!headers) return;
|
|
179
|
+
new Headers(headers).forEach((value, name) => {
|
|
180
|
+
const strict = isCredentialName(name);
|
|
181
|
+
if (!strict && CORRELATION_HEADER_NAME.test(name)) return;
|
|
182
|
+
const tier = strict ? this.strict : this.observed;
|
|
183
|
+
for (const part of credentialComponents(value)) tier.add(part);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** True when value repeats a recorded credential (strict at any length, best effort at 8+). */
|
|
188
|
+
matches(value: string): boolean {
|
|
189
|
+
return this.strict.matches(value, 1) || this.observed.matches(value, CREDENTIAL_SUBSTRING_MIN_LENGTH);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** False once a credential-named value was dropped; callers must then fail closed. */
|
|
193
|
+
complete(): boolean {
|
|
194
|
+
return this.strict.complete;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let active = new OutboundCredentialRegistry();
|
|
199
|
+
|
|
200
|
+
/** The registry fed by the HTTP executor boundary and the Codex WebSocket dial. */
|
|
201
|
+
export function outboundCredentials(): OutboundCredentialRegistry {
|
|
202
|
+
return active;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Swap the active registry for sequential in-file tests; returns the restore function. */
|
|
206
|
+
export function setOutboundCredentialRegistryForTests(registry: OutboundCredentialRegistry): () => void {
|
|
207
|
+
const previous = active;
|
|
208
|
+
active = registry;
|
|
209
|
+
return () => { active = previous; };
|
|
210
|
+
}
|
|
@@ -68,7 +68,7 @@ export interface PackageTreeIntegrityOptions {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
export type ObservePackageTree = () => PackageTreeObservation | null;
|
|
71
|
-
export type PackageTreeRuntimeInstall = "bun" | "mise" | "npm" | "pnpm" | "source";
|
|
71
|
+
export type PackageTreeRuntimeInstall = "bun" | "mise" | "npm" | "pnpm" | "source" | "standalone";
|
|
72
72
|
|
|
73
73
|
const packageManifestUrl = new URL("../../package.json", import.meta.url);
|
|
74
74
|
|
|
@@ -323,7 +323,7 @@ export function createRuntimePackageTreeIntegrityGuard(
|
|
|
323
323
|
now: () => number = Date.now,
|
|
324
324
|
options: PackageTreeIntegrityOptions = {},
|
|
325
325
|
): PackageTreeIntegrityGuard {
|
|
326
|
-
if (installer === "source" || isStandaloneBinary()) {
|
|
326
|
+
if (installer === "source" || installer === "standalone" || isStandaloneBinary()) {
|
|
327
327
|
return { status: () => ({ ok: true }), dispose: () => {} };
|
|
328
328
|
}
|
|
329
329
|
// The restart replacement spawns process.execPath, the bundled Bun in an npm install. Read it
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A passive management read: it shows what this process already knows and does not make it act on
|
|
3
|
+
* providers, accounts, processes or files (#6959).
|
|
4
|
+
*
|
|
5
|
+
* The Remote Link management relay runs every Child read inside this scope
|
|
6
|
+
* (`src/server/index/link-management.ts`), so a Child can show its Home's state without changing
|
|
7
|
+
* it or making it act. Inside the scope:
|
|
8
|
+
*
|
|
9
|
+
* - model discovery yields no routed models (`fetchAllModels`, the management model rows): it would
|
|
10
|
+
* reach provider upstreams, refresh Codex entitlements and persist a pending provider's initial
|
|
11
|
+
* model selection;
|
|
12
|
+
* - provider quota and the Codex account list are served from what the Home has observed, with no
|
|
13
|
+
* probe, token refresh, native-main claim or publication into routing evidence;
|
|
14
|
+
* - the startup-health diagnostic, the Codex runtime version probes and the bundled-catalog refresh
|
|
15
|
+
* are not started, so no process runs and no runtime record is written;
|
|
16
|
+
* - a native-main fence raised for unknown service ownership is reported, not re-inspected;
|
|
17
|
+
* - config-directory and credential-file hardening and malformed-file backups are skipped;
|
|
18
|
+
* - memory-budget enforcement demotes only stores registered as `passiveEvictable` (eviction that
|
|
19
|
+
* only releases memory: the usage caches), never a continuation spill or a replay snapshot.
|
|
20
|
+
*
|
|
21
|
+
* The Home's own dashboard and traffic keep those caches current. A passive read may still join
|
|
22
|
+
* work the Home already started; that work keeps the Home's scope and its writes are the Home's.
|
|
23
|
+
*/
|
|
24
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
25
|
+
|
|
26
|
+
const passive = new AsyncLocalStorage<true>();
|
|
27
|
+
|
|
28
|
+
/** Run a management read passively, including everything it awaits or schedules. */
|
|
29
|
+
export function runPassiveRead<T>(run: () => Promise<T>): Promise<T> {
|
|
30
|
+
return passive.run(true, run);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isPassiveRead(): boolean {
|
|
34
|
+
return passive.getStore() === true;
|
|
35
|
+
}
|
package/src/lib/shadow-call.ts
CHANGED
|
@@ -81,19 +81,58 @@ export function shadowCallTargetsIntersect(
|
|
|
81
81
|
&& target.modelId.startsWith(source.modelId);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Preserve explicit user turns and spawned children before either intercept site.
|
|
86
|
+
* HTTP provenance is `x-codex-turn-metadata` or `x-openai-subagent: collab_spawn`.
|
|
87
|
+
* `x-openai-subagent` is connection-level on both transports: a child session opens
|
|
88
|
+
* its own connection, so a `collab_spawn` upgrade header still names every frame.
|
|
89
|
+
* Turn metadata is per-frame on WebSocket (`CODEX_WS_FRAME_HEADERS`): only the frame's
|
|
90
|
+
* `client_metadata["x-codex-turn-metadata"]` describes this turn, because the
|
|
91
|
+
* upgrade's copy can describe an earlier user or helper turn.
|
|
92
|
+
* `request_kind: "turn"` alone is not user provenance (#1684). Maintenance
|
|
93
|
+
* markers such as `x-openai-subagent: compact`/`review` and metadata naming
|
|
94
|
+
* `memory_consolidation` remain eligible helpers, subject to memory routing.
|
|
95
|
+
* Missing/malformed metadata retains the legacy opt-in source-prefix behavior.
|
|
96
|
+
*/
|
|
97
|
+
export function shadowCallPreservesSelectedModel(
|
|
98
|
+
body: unknown,
|
|
99
|
+
headers: Headers,
|
|
100
|
+
transport?: "websocket",
|
|
101
|
+
): boolean {
|
|
102
|
+
if (headers.get("x-openai-subagent") === "collab_spawn") return true;
|
|
103
|
+
let metadata: unknown = headers.get("x-codex-turn-metadata");
|
|
104
|
+
if (transport === "websocket") {
|
|
105
|
+
const client = body && typeof body === "object" && !Array.isArray(body)
|
|
106
|
+
? (body as Record<string, unknown>).client_metadata : undefined;
|
|
107
|
+
metadata = client && typeof client === "object" && !Array.isArray(client)
|
|
108
|
+
&& Object.hasOwn(client, "x-codex-turn-metadata")
|
|
109
|
+
? (client as Record<string, unknown>)["x-codex-turn-metadata"] : undefined;
|
|
110
|
+
}
|
|
111
|
+
if (typeof metadata !== "string") return false;
|
|
112
|
+
try {
|
|
113
|
+
const parsed: unknown = JSON.parse(metadata);
|
|
114
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return false;
|
|
115
|
+
const turn = parsed as Record<string, unknown>;
|
|
116
|
+
return turn.subagent_kind === "thread_spawn"
|
|
117
|
+
|| (turn.request_kind === "turn" && turn.thread_source === "user");
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
84
123
|
/**
|
|
85
124
|
* Decide whether a matching source model should use the opt-in intercept.
|
|
86
125
|
*
|
|
87
126
|
* Before Codex 0.147.0 this checked x-codex-turn-metadata and exempted
|
|
88
127
|
* request_kind "turn". Codex 0.147.0 can label background helper calls as
|
|
89
|
-
* "turn", causing them to bypass the intercept (#1684).
|
|
90
|
-
*
|
|
128
|
+
* "turn", causing them to bypass the intercept (#1684). Request kind alone
|
|
129
|
+
* cannot exempt a configured source model: callers first preserve explicit
|
|
130
|
+
* user/child provenance with `shadowCallPreservesSelectedModel` (#6921).
|
|
91
131
|
* A replacement intersecting the same provider+model source set remains a
|
|
92
132
|
* no-op because rewriting it would only create self-interception (#2706).
|
|
93
133
|
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* sub-agent model, and an explicitly spawned child must keep the model it chose.
|
|
134
|
+
* `gpt-6-luna` is both the helper slug and a selectable user/sub-agent model;
|
|
135
|
+
* explicit user turns and spawned children must keep the model they chose.
|
|
97
136
|
*/
|
|
98
137
|
export function shouldInterceptShadowCall(
|
|
99
138
|
modelId: string,
|
package/src/lib/socks5-fetch.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import net, { type Socket } from "node:net";
|
|
2
2
|
import tls, { type TLSSocket } from "node:tls";
|
|
3
3
|
import { classifyContentCoding, isNullBodyStatus } from "./http-response-semantics";
|
|
4
|
+
import { socks5Credentials, socks5Handshake, Socks5HandshakeError } from "./socks5-handshake";
|
|
4
5
|
|
|
5
6
|
const DEFAULT_SOCKS5_PORT = 1080;
|
|
6
7
|
const SOCKS5_CONNECT_TIMEOUT_MS = 30_000;
|
|
@@ -10,11 +11,6 @@ const MAX_BODY_SLICE_BYTES = 64 * 1024;
|
|
|
10
11
|
const MAX_DECODED_BODY_BYTES = 32 * 1024 * 1024;
|
|
11
12
|
const MAX_STREAM_DECODE_EXPANSION_RATIO = 128;
|
|
12
13
|
const SOCKS5_VERSION = 0x05;
|
|
13
|
-
const SOCKS5_NO_AUTH = 0x00;
|
|
14
|
-
const SOCKS5_USER_PASS = 0x02;
|
|
15
|
-
const SOCKS5_CONNECT = 0x01;
|
|
16
|
-
const SOCKS5_DOMAIN = 0x03;
|
|
17
|
-
const SOCKS5_SUCCESS = 0x00;
|
|
18
14
|
const CRLF = Buffer.from("\r\n");
|
|
19
15
|
const HEADER_END = Buffer.from("\r\n\r\n");
|
|
20
16
|
|
|
@@ -22,24 +18,6 @@ export class Socks5FetchError extends Error {
|
|
|
22
18
|
override readonly name = "Socks5FetchError";
|
|
23
19
|
}
|
|
24
20
|
|
|
25
|
-
function proxyCredentials(proxy: URL): { username?: Uint8Array; password?: Uint8Array } {
|
|
26
|
-
if (!proxy.username && !proxy.password) return {};
|
|
27
|
-
let username: string;
|
|
28
|
-
let password: string;
|
|
29
|
-
try {
|
|
30
|
-
username = decodeURIComponent(proxy.username);
|
|
31
|
-
password = decodeURIComponent(proxy.password);
|
|
32
|
-
} catch {
|
|
33
|
-
throw new Socks5FetchError("SOCKS5 proxy credentials contain invalid percent encoding");
|
|
34
|
-
}
|
|
35
|
-
const usernameBytes = new TextEncoder().encode(username);
|
|
36
|
-
const passwordBytes = new TextEncoder().encode(password);
|
|
37
|
-
if (usernameBytes.byteLength > 255 || passwordBytes.byteLength > 255) {
|
|
38
|
-
throw new Socks5FetchError("SOCKS5 proxy credentials must each fit in 255 UTF-8 bytes");
|
|
39
|
-
}
|
|
40
|
-
return { username: usernameBytes, password: passwordBytes };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
21
|
function validateProxy(proxy: string): URL {
|
|
44
22
|
let parsed: URL;
|
|
45
23
|
try {
|
|
@@ -55,7 +33,10 @@ function validateProxy(proxy: string): URL {
|
|
|
55
33
|
throw new Socks5FetchError("SOCKS5 proxy port is invalid");
|
|
56
34
|
}
|
|
57
35
|
if (parsed.search || parsed.hash) throw new Socks5FetchError("SOCKS5 proxy URL must not contain a query or fragment");
|
|
58
|
-
|
|
36
|
+
try { socks5Credentials(parsed); } catch (error) {
|
|
37
|
+
if (error instanceof Socks5HandshakeError) throw new Socks5FetchError(error.message);
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
59
40
|
return parsed;
|
|
60
41
|
}
|
|
61
42
|
|
|
@@ -130,6 +111,14 @@ class SocketReader {
|
|
|
130
111
|
socket.resume();
|
|
131
112
|
}
|
|
132
113
|
|
|
114
|
+
write(bytes: Uint8Array): void {
|
|
115
|
+
this.socket.write(bytes);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
readExact(bytes: number, signal?: AbortSignal): Promise<Buffer> {
|
|
119
|
+
return this.read(bytes, signal);
|
|
120
|
+
}
|
|
121
|
+
|
|
133
122
|
private readonly onData = (chunk: Buffer | string): void => {
|
|
134
123
|
const value = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
135
124
|
if (this.anyWaiters.length > 0) {
|
|
@@ -263,7 +252,7 @@ class SocketReader {
|
|
|
263
252
|
|
|
264
253
|
async function socks5Connect(proxy: string, target: URL, signal?: AbortSignal): Promise<Socket> {
|
|
265
254
|
const parsedProxy = validateProxy(proxy);
|
|
266
|
-
const credentials =
|
|
255
|
+
const credentials = socks5Credentials(parsedProxy);
|
|
267
256
|
const proxyHost = parsedProxy.hostname.replace(/^\[|\]$/g, "");
|
|
268
257
|
const socket = await connectSocket(proxyHost, Number(parsedProxy.port) || DEFAULT_SOCKS5_PORT, signal);
|
|
269
258
|
socket.setTimeout(SOCKS5_CONNECT_TIMEOUT_MS, () => {
|
|
@@ -271,49 +260,12 @@ async function socks5Connect(proxy: string, target: URL, signal?: AbortSignal):
|
|
|
271
260
|
});
|
|
272
261
|
const reader = new SocketReader(socket);
|
|
273
262
|
try {
|
|
274
|
-
|
|
275
|
-
socket.write(Buffer.from([SOCKS5_VERSION, methods.byteLength, ...methods]));
|
|
276
|
-
const greeting = await reader.read(2, signal);
|
|
277
|
-
if (greeting[0] !== SOCKS5_VERSION) throw new Socks5FetchError("SOCKS5 proxy returned an invalid greeting");
|
|
278
|
-
if (greeting[1] === SOCKS5_USER_PASS && credentials.username && credentials.password) {
|
|
279
|
-
socket.write(Buffer.from([
|
|
280
|
-
0x01,
|
|
281
|
-
credentials.username.byteLength,
|
|
282
|
-
...credentials.username,
|
|
283
|
-
credentials.password.byteLength,
|
|
284
|
-
...credentials.password,
|
|
285
|
-
]));
|
|
286
|
-
const auth = await reader.read(2, signal);
|
|
287
|
-
if (auth[0] !== 0x01 || auth[1] !== 0x00) throw new Socks5FetchError("SOCKS5 proxy authentication failed");
|
|
288
|
-
} else if (greeting[1] !== SOCKS5_NO_AUTH) {
|
|
289
|
-
throw new Socks5FetchError("SOCKS5 proxy does not accept an offered authentication method");
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const hostname = new TextEncoder().encode(target.hostname);
|
|
293
|
-
if (hostname.byteLength > 255) throw new Socks5FetchError("SOCKS5 target hostname is too long");
|
|
294
|
-
const port = targetPort(target);
|
|
295
|
-
socket.write(Buffer.from([
|
|
296
|
-
SOCKS5_VERSION,
|
|
297
|
-
SOCKS5_CONNECT,
|
|
298
|
-
0x00,
|
|
299
|
-
SOCKS5_DOMAIN,
|
|
300
|
-
hostname.byteLength,
|
|
301
|
-
...hostname,
|
|
302
|
-
port >> 8,
|
|
303
|
-
port & 0xff,
|
|
304
|
-
]));
|
|
305
|
-
const reply = await reader.read(4, signal);
|
|
306
|
-
if (reply[0] !== SOCKS5_VERSION) throw new Socks5FetchError("SOCKS5 proxy returned an invalid connect response");
|
|
307
|
-
if (reply[1] !== SOCKS5_SUCCESS) throw new Socks5FetchError(`SOCKS5 proxy refused the connection (code ${reply[1]})`);
|
|
308
|
-
if (reply[2] !== 0x00 || ![0x01, SOCKS5_DOMAIN, 0x04].includes(reply[3]!)) {
|
|
309
|
-
throw new Socks5FetchError("SOCKS5 proxy returned an invalid address type or reserved byte");
|
|
310
|
-
}
|
|
311
|
-
const addressLength = reply[3] === 0x01 ? 4 : reply[3] === SOCKS5_DOMAIN ? (await reader.read(1, signal))[0]! : 16;
|
|
312
|
-
await reader.read(addressLength + 2, signal);
|
|
263
|
+
await socks5Handshake(reader, { host: target.hostname, port: targetPort(target) }, credentials, signal);
|
|
313
264
|
socket.setTimeout(0);
|
|
314
265
|
return socket;
|
|
315
266
|
} catch (error) {
|
|
316
267
|
socket.destroy();
|
|
268
|
+
if (error instanceof Socks5HandshakeError) throw new Socks5FetchError(error.message);
|
|
317
269
|
throw error;
|
|
318
270
|
} finally {
|
|
319
271
|
reader.dispose();
|
|
@@ -341,25 +293,50 @@ function requestHeaders(request: Request, target: URL): { text: string; chunked:
|
|
|
341
293
|
|
|
342
294
|
async function secureSocket(socket: Socket, target: URL, signal: AbortSignal): Promise<TLSSocket> {
|
|
343
295
|
return new Promise((resolve, reject) => {
|
|
296
|
+
if (signal.aborted) {
|
|
297
|
+
socket.destroy();
|
|
298
|
+
reject(signal.reason instanceof Error ? signal.reason : new Error("The operation was aborted"));
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
344
301
|
const tlsSocket = tls.connect({
|
|
345
302
|
socket,
|
|
346
303
|
servername: target.hostname,
|
|
347
304
|
rejectUnauthorized: true,
|
|
348
305
|
});
|
|
349
|
-
|
|
306
|
+
let settled = false;
|
|
307
|
+
// Absolute deadline: a peer sending occasional TLS bytes must not extend the handshake.
|
|
308
|
+
const timer = setTimeout(() => {
|
|
309
|
+
fail(new Socks5FetchError("SOCKS5 TLS handshake timed out"));
|
|
310
|
+
}, SOCKS5_CONNECT_TIMEOUT_MS);
|
|
311
|
+
const cleanup = () => {
|
|
312
|
+
clearTimeout(timer);
|
|
313
|
+
signal.removeEventListener("abort", onAbort);
|
|
314
|
+
tlsSocket.removeListener("secureConnect", onSecureConnect);
|
|
315
|
+
tlsSocket.removeListener("error", onError);
|
|
316
|
+
tlsSocket.removeListener("close", onClose);
|
|
317
|
+
};
|
|
318
|
+
const fail = (error: Error) => {
|
|
319
|
+
if (settled) return;
|
|
320
|
+
settled = true;
|
|
321
|
+
cleanup();
|
|
322
|
+
// No error argument: pending event observers must not receive a second error.
|
|
350
323
|
tlsSocket.destroy();
|
|
351
|
-
reject(
|
|
324
|
+
reject(error);
|
|
352
325
|
};
|
|
326
|
+
const onAbort = () => fail(signal.reason instanceof Error ? signal.reason : new Error("The operation was aborted"));
|
|
327
|
+
const onError = (error: Error) => fail(error);
|
|
328
|
+
const onClose = () => fail(new Socks5FetchError("SOCKS5 upstream closed during TLS handshake"));
|
|
353
329
|
const onSecureConnect = () => {
|
|
354
|
-
|
|
330
|
+
if (settled) return;
|
|
331
|
+
settled = true;
|
|
332
|
+
cleanup();
|
|
355
333
|
resolve(tlsSocket);
|
|
356
334
|
};
|
|
357
335
|
tlsSocket.once("secureConnect", onSecureConnect);
|
|
358
|
-
tlsSocket.once("error",
|
|
359
|
-
|
|
360
|
-
reject(error);
|
|
361
|
-
});
|
|
336
|
+
tlsSocket.once("error", onError);
|
|
337
|
+
tlsSocket.once("close", onClose);
|
|
362
338
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
339
|
+
if (signal.aborted) onAbort();
|
|
363
340
|
});
|
|
364
341
|
}
|
|
365
342
|
|