@bitkyc08/opencodex 2.59.0 → 2.61.0-preview.20260922
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/AGENTS_INSTALL.md +64 -0
- package/README.md +28 -1
- package/bin/ocx.mjs +382 -209
- package/gui/dist/assets/App-E64Rzjap.js +50 -0
- package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
- package/gui/dist/assets/index-DpdfZWMK.js +86 -0
- package/gui/dist/assets/index-_bpvxJu0.css +1 -0
- package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/stepfun-color.svg +1 -0
- package/package.json +8 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +20 -1
- package/src/adapters/base.ts +23 -0
- package/src/adapters/coding-agent/protocol.ts +36 -6
- package/src/adapters/coding-agent/turn.ts +32 -4
- package/src/adapters/command-code.ts +52 -4
- package/src/adapters/cursor/catalog.ts +51 -7
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/protobuf-request.ts +6 -3
- package/src/adapters/cursor/request-builder.ts +20 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +14 -3
- package/src/adapters/declaration-carrier.ts +45 -0
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
- package/src/adapters/devin.ts +125 -36
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +34 -7
- package/src/adapters/google-errors.ts +7 -1
- package/src/adapters/google-http.ts +49 -10
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +66 -11
- package/src/adapters/image.ts +4 -1
- package/src/adapters/input-media-guard.ts +21 -9
- package/src/adapters/kiro/usage.ts +3 -2
- package/src/adapters/kiro-tool-fallback.ts +1 -1
- package/src/adapters/ollama-native.ts +6 -0
- package/src/adapters/openai-chat/developer-role.ts +61 -0
- package/src/adapters/openai-chat/messages.ts +46 -27
- package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
- package/src/adapters/openai-chat/passthrough.ts +33 -9
- package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +23 -58
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +17 -3
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/adapters/registry.ts +3 -2
- package/src/adapters/run-turn-queue.ts +178 -29
- package/src/adapters/xai-web-search.ts +16 -1
- package/src/bridge/errors.ts +8 -2
- package/src/bridge/response-json.ts +9 -1
- package/src/bridge/sse.ts +13 -151
- package/src/chat/inbound.ts +141 -5
- package/src/claude/desktop-3p.ts +7 -1
- package/src/claude/desktop-first-party.ts +183 -0
- package/src/claude/desktop-gateway-state.ts +41 -0
- package/src/claude/inbound-content-options.ts +6 -0
- package/src/claude/inbound.ts +32 -6
- package/src/claude/intercept/connect-proxy.ts +179 -0
- package/src/claude/intercept/listener.ts +122 -0
- package/src/claude/intercept/local-ca.ts +298 -0
- package/src/claude/intercept/runtime.ts +98 -0
- package/src/claude/intercept/settings.ts +189 -0
- package/src/cli/access.ts +87 -0
- package/src/cli/account-auth.ts +19 -0
- package/src/cli/account-extended.ts +4 -4
- package/src/cli/capabilities.ts +31 -0
- package/src/cli/claude-desktop.ts +206 -16
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/companion.ts +56 -0
- package/src/cli/dispatch.ts +46 -7
- package/src/cli/doctor.ts +28 -9
- package/src/cli/ensure-desired-integrations.ts +43 -5
- package/src/cli/help.ts +7 -9
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +203 -59
- package/src/cli/init.ts +8 -0
- package/src/cli/integrations.ts +7 -1
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/cli/registry.ts +41 -2
- package/src/cli/resolve.ts +230 -0
- package/src/cli/root.ts +24 -1
- package/src/cli/start-ownership-publication.ts +56 -0
- package/src/cli/status-probes.ts +2 -18
- package/src/cli/status.ts +62 -0
- package/src/cli/stop-report.ts +143 -0
- package/src/cli/uninstall-plan.ts +9 -0
- package/src/client/machine-api.ts +2 -2
- package/src/client/machine-listener.ts +4 -7
- package/src/client/runtime.ts +26 -2
- package/src/clients/aside-profiles.ts +4 -0
- package/src/clients/config-export/zcode-store.ts +157 -0
- package/src/clients/config-export.ts +36 -0
- package/src/codex/account-store.ts +65 -0
- package/src/codex/app-server-processes.ts +72 -40
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/login-flow.ts +6 -1
- package/src/codex/auth-api/pool-quota-probe.ts +30 -7
- package/src/codex/autostart-health.ts +28 -0
- package/src/codex/catalog/build-entries.ts +2 -2
- package/src/codex/catalog/effort.ts +3 -3
- package/src/codex/catalog/gather-capture.ts +21 -2
- package/src/codex/catalog/model-hints.ts +29 -28
- package/src/codex/catalog/parsing.ts +7 -0
- package/src/codex/catalog/provider-models.ts +43 -17
- package/src/codex/catalog/retained-sync.ts +24 -28
- package/src/codex/catalog/routed-gather.ts +19 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +2 -2
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +31 -166
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/inject/config-toml.ts +41 -6
- package/src/codex/inject/paginated-openai-compat.ts +90 -0
- package/src/codex/inject.ts +18 -15
- package/src/codex/injected-marker.ts +18 -0
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/main-account.ts +6 -0
- package/src/codex/model-cache.ts +99 -6
- package/src/codex/model-entitlement-admission.ts +59 -0
- package/src/codex/model-entitlements.ts +116 -54
- package/src/codex/native-main-admission.ts +83 -0
- package/src/codex/observed-model-denials.ts +101 -8
- package/src/codex/prompt-text-probe.ts +9 -6
- package/src/codex/routing/health-store.ts +39 -0
- package/src/codex/routing/selection.ts +37 -1
- package/src/codex/routing.ts +12 -42
- package/src/codex/shim-templates.ts +29 -3
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +22 -4
- package/src/combos/failover.ts +3 -0
- package/src/companion/settings.ts +132 -0
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/atomic-write.ts +117 -5
- package/src/config/diagnostics.ts +22 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +52 -7
- package/src/config/process-state.ts +1 -1
- package/src/config/proxy-env.ts +8 -2
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +31 -1
- package/src/config/schema/leaf-validators.ts +59 -0
- package/src/config.ts +3 -3
- package/src/generated/compatibility-version.json +604 -280
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/config-io.ts +44 -10
- package/src/integrations/merge.ts +120 -13
- package/src/integrations/mutation-plan.ts +921 -0
- package/src/integrations/registry.ts +38 -0
- package/src/integrations/state.ts +78 -45
- package/src/integrations/target.ts +208 -0
- package/src/integrations/writer.ts +134 -110
- package/src/lab/conformance/fixture-provider.ts +5 -0
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/browser-launch-notice.ts +59 -0
- package/src/lib/bun-runtime.ts +6 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +63 -0
- package/src/lib/errors.ts +79 -0
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/open-url.ts +51 -7
- package/src/lib/package-tree-integrity.ts +2 -1
- package/src/lib/package-version.ts +8 -0
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-egress.ts +310 -0
- package/src/lib/provider-outbound.ts +109 -16
- package/src/lib/proxy-env.ts +82 -7
- package/src/lib/request-execution-budget.ts +72 -0
- package/src/lib/request-failure-attribution.ts +183 -0
- package/src/lib/request-failure-model.ts +236 -0
- package/src/lib/request-resend-gate.ts +138 -0
- package/src/lib/socks5-fetch.ts +136 -26
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +218 -27
- package/src/lib/standalone.ts +16 -0
- package/src/lib/upstream-retry.ts +167 -16
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/winsw.ts +2 -2
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +1 -0
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +27 -108
- package/src/oauth/login-cli.ts +80 -29
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/providers/api-key-resolve.ts +133 -0
- package/src/providers/api-key-selection.ts +5 -1
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/key-failover.ts +97 -19
- package/src/providers/key-store.ts +34 -110
- package/src/providers/model-rename-fields.ts +147 -0
- package/src/providers/model-rename-migration.ts +179 -38
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/vendor-probes-key.ts +38 -23
- package/src/providers/reasoning-metadata.ts +43 -18
- package/src/providers/registry/entries-core.ts +47 -20
- package/src/providers/registry/entries-extended.ts +63 -4
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +56 -10
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/providers/xai-transport.ts +12 -1
- package/src/reasoning-effort.ts +8 -0
- package/src/responses/apply-patch-envelope.ts +0 -12
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/function-call-compat.ts +38 -1
- package/src/responses/inline-document.ts +65 -0
- package/src/responses/input-media.ts +42 -8
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/muse-tool-name-alias.ts +19 -0
- package/src/responses/parser-content.ts +8 -2
- package/src/responses/parser-tools.ts +3 -0
- package/src/responses/parser.ts +3 -1
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +3 -0
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +108 -117
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/admission-model-scope.ts +219 -0
- package/src/server/audio-live.ts +9 -3
- package/src/server/audio-upstream.ts +18 -0
- package/src/server/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +60 -4
- package/src/server/chat-native.ts +19 -4
- package/src/server/claude-messages.ts +61 -20
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/grok-responses-snapshot-repair.ts +113 -11
- package/src/server/gui-freshness.ts +103 -0
- package/src/server/gui-static.ts +7 -9
- package/src/server/images.ts +59 -6
- package/src/server/index/claude-intercept-lifecycle.ts +49 -0
- package/src/server/index/serve-options.ts +142 -39
- package/src/server/index/spend-ledger-lifecycle.ts +92 -0
- package/src/server/index/startup-warnings.ts +24 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +34 -38
- package/src/server/lifecycle.ts +4 -4
- package/src/server/live-call-bindings.ts +6 -0
- package/src/server/live.ts +88 -3
- package/src/server/management/agent-settings-routes.ts +121 -36
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/companion-routes.ts +77 -0
- package/src/server/management/config-routes.ts +18 -2
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/logs-usage-routes.ts +19 -0
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/native-integration-routes.ts +103 -6
- package/src/server/management/oauth-account-routes.ts +45 -7
- package/src/server/management/route-registry.ts +20 -0
- package/src/server/management/shared.ts +28 -4
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management/usage-timeline-routes.ts +44 -0
- package/src/server/management-api.ts +10 -9
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +75 -0
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +138 -12
- package/src/server/request-log-failure-attribution.ts +99 -0
- package/src/server/request-log.ts +169 -2
- package/src/server/request-metrics.ts +298 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +11 -6
- package/src/server/responses/codex-ws-wire.ts +34 -8
- package/src/server/responses/combo-stream-preflight.ts +168 -6
- package/src/server/responses/compact.ts +43 -10
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +8 -3
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +6 -12
- package/src/server/responses/core-opaque-recovery.ts +91 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/fetch-helpers.ts +124 -8
- package/src/server/responses/input-admission.ts +10 -0
- package/src/server/responses/passthrough-delivery.ts +45 -15
- package/src/server/responses/passthrough-dispatch.ts +215 -44
- package/src/server/responses/passthrough-error.ts +27 -8
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +109 -18
- package/src/server/responses/request-send-budget.ts +17 -1
- package/src/server/responses/request-sidecar-auth.ts +1 -1
- package/src/server/responses/request-transport.ts +26 -6
- package/src/server/responses/reset-replay.ts +108 -0
- package/src/server/responses/run-turn-execution.ts +25 -3
- package/src/server/responses/sidecar-execution.ts +17 -2
- package/src/server/responses/ws-upstream.ts +14 -27
- package/src/server/responses-custom-tool-repair.ts +27 -54
- package/src/server/responses-request-tool-scope.ts +214 -0
- package/src/server/responses-undeclared-tool-guard.ts +35 -2
- package/src/server/search.ts +25 -1
- package/src/server/sse-payload-rewrite.ts +1 -1
- package/src/server/usage-ledger-retention.ts +73 -0
- package/src/service/cli.ts +48 -2
- package/src/service/health.ts +3 -2
- package/src/service/install-state-contract.d.mts +27 -0
- package/src/service/install-state-contract.mjs +34 -0
- package/src/service/launchd.ts +1 -1
- package/src/service/orchestration.ts +2 -4
- package/src/service/ownership-compatibility.ts +164 -0
- package/src/service/ownership-mutation-lease.d.mts +32 -0
- package/src/service/ownership-mutation-lease.mjs +211 -0
- package/src/service/repair.ts +45 -1
- package/src/service/state-lock.ts +269 -0
- package/src/service/state-record.d.mts +36 -0
- package/src/service/state-record.mjs +138 -0
- package/src/service/state.ts +582 -68
- package/src/service/windows-taskxml.ts +11 -10
- package/src/service.ts +7 -3
- package/src/tray/windows-tray.ps1 +156 -4
- package/src/types/config.ts +48 -3
- package/src/types/provider.ts +91 -0
- package/src/types/request.ts +30 -2
- package/src/types/tools.ts +33 -0
- package/src/types.ts +4 -0
- package/src/update/index.ts +207 -63
- package/src/update/job.ts +9 -5
- package/src/update/ownership-transaction.ts +47 -0
- package/src/update/restart-ownership.ts +54 -0
- package/src/update/runtime-ownership.d.mts +40 -0
- package/src/update/runtime-ownership.mjs +122 -0
- package/src/usage/attempt-delivery.ts +198 -0
- package/src/usage/cache-diagnostic.ts +305 -0
- package/src/usage/failure-fingerprint.ts +118 -0
- package/src/usage/failure-projection-cache.ts +174 -0
- package/src/usage/failure-projection.ts +174 -0
- package/src/usage/ledger-retention.ts +165 -0
- package/src/usage/log.ts +126 -79
- package/src/usage/request-outcome.ts +150 -0
- package/src/usage/retention-contract.ts +28 -0
- package/src/usage/summary.ts +2 -2
- package/src/usage/telemetry-contract.ts +237 -0
- package/src/usage/timeline.ts +236 -0
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/alpha-search.ts +21 -1
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/gui/dist/assets/index-C5IebErG.js +0 -136
- package/gui/dist/assets/index-OESInAjC.css +0 -1
|
@@ -29,33 +29,14 @@ export { CODEX_WS_LIVENESS_PING_INTERVAL_MS, CODEX_WS_RESPONSE_PRELUDE_TIMEOUT_M
|
|
|
29
29
|
export const MIN_BOUNDED_CODEX_WS_BUN_VERSION = "1.4.0";
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* Dial URL for a
|
|
33
|
-
*
|
|
34
|
-
* path
|
|
35
|
-
* /v1/responses path get the same fast lane. Plain HTTP remains on SSE because
|
|
36
|
-
* a provider WS handshake would otherwise send credentials and request data
|
|
37
|
-
* without transport encryption.
|
|
32
|
+
* Dial URL for a first-party Responses endpoint. The canonical ChatGPT backend
|
|
33
|
+
* keeps its constant; the api.openai.com Responses endpoint swaps https for wss
|
|
34
|
+
* on the same path. No other upstream may enter the WebSocket lane.
|
|
38
35
|
*/
|
|
39
36
|
function wsUpstreamUrlFor(httpUrl: string): string {
|
|
40
37
|
if (httpUrl === CODEX_RESPONSES_HTTP_URL) return CODEX_RESPONSES_WS_URL;
|
|
41
|
-
return httpUrl.replace(/^http(s?):/, "ws$1:");
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* An operator-opted OpenAI-compatible upstream only joins the WS lane for
|
|
46
|
-
* Responses endpoints: the WebSocket path speaks the Responses event protocol,
|
|
47
|
-
* and every downstream consumer (adapter parsers, usage sniffing, SSE relay)
|
|
48
|
-
* assumes that wire. Other paths (chat completions, images, search) stay HTTP.
|
|
49
|
-
*/
|
|
50
|
-
function isResponsesWebsocketEligibleUrl(url: string): boolean {
|
|
51
|
-
let parsed: URL;
|
|
52
|
-
try {
|
|
53
|
-
parsed = new URL(url);
|
|
54
|
-
} catch {
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
return parsed.protocol === "https:"
|
|
58
|
-
&& parsed.pathname.endsWith("/responses");
|
|
38
|
+
if (httpUrl === OPENAI_API_RESPONSES_URL) return httpUrl.replace(/^http(s?):/, "ws$1:");
|
|
39
|
+
throw new Error("unsupported Codex WebSocket upstream");
|
|
59
40
|
}
|
|
60
41
|
export type BunRuntimeIdentity = {
|
|
61
42
|
version: string;
|
|
@@ -108,8 +89,13 @@ export function shouldUseCodexWsUpstream(
|
|
|
108
89
|
): boolean {
|
|
109
90
|
if (!bunSupportsBoundedCodexWsRelay(runtime)) return false;
|
|
110
91
|
if (socks5ProxyFromEnv()) return false;
|
|
111
|
-
|
|
112
|
-
|
|
92
|
+
// Bun's client WebSocket API delivers only fully assembled messages and has
|
|
93
|
+
// no enforceable inbound payload limit. Keep arbitrary provider endpoints on
|
|
94
|
+
// bounded HTTP/SSE until the client can reject fragmented text and binary
|
|
95
|
+
// messages during ingestion rather than after allocation. The first-party
|
|
96
|
+
// api.openai.com lane still requires the operator opt-in.
|
|
97
|
+
if (url !== CODEX_RESPONSES_HTTP_URL
|
|
98
|
+
&& !(upstreamWebsocketConfigured && url === OPENAI_API_RESPONSES_URL)) return false;
|
|
113
99
|
if ((init?.method ?? "GET").toUpperCase() !== "POST") return false;
|
|
114
100
|
const body = init?.body;
|
|
115
101
|
if (typeof body !== "string") return false;
|
|
@@ -139,7 +125,8 @@ export function codexWsUpstreamFetch(
|
|
|
139
125
|
const prepared = prepareCodexWsRequest(url, init);
|
|
140
126
|
if (!prepared) return sseFallback(url, prepareCodexHttpInit(url, init));
|
|
141
127
|
init = prepared.httpInit;
|
|
142
|
-
if (
|
|
128
|
+
if ((url !== CODEX_RESPONSES_HTTP_URL && url !== OPENAI_API_RESPONSES_URL)
|
|
129
|
+
|| !bunSupportsBoundedCodexWsRelay(runtime)) {
|
|
143
130
|
return sseFallback(url, init);
|
|
144
131
|
}
|
|
145
132
|
const signal = init.signal ?? undefined;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
2
2
|
import { mayBecomePatchEnvelope, normalizeApplyPatchDelimiters } from "../responses/apply-patch-envelope";
|
|
3
3
|
import { compileCodeModeHelperInput, resolveCodeModeHelperName } from "../responses/code-mode-helper-compat";
|
|
4
|
+
import { progressiveFreeformInput } from "../responses/progressive-freeform-input";
|
|
4
5
|
import { declaresCodeModeExec } from "../types/tools";
|
|
5
6
|
import {
|
|
6
7
|
customToolItemId,
|
|
@@ -15,50 +16,10 @@ import {
|
|
|
15
16
|
type SseBlockRewrite,
|
|
16
17
|
} from "./sse-payload-rewrite";
|
|
17
18
|
|
|
18
|
-
/** Exact compact prefix used by our upstream rewriter; progressive matching also
|
|
19
|
-
* tolerates insignificant JSON whitespace via FREEFORM_WRAP_PREFIX_RE. */
|
|
20
|
-
const FREEFORM_WRAP_PREFIX = '{"input":"';
|
|
21
|
-
const FREEFORM_WRAP_PREFIX_RE = /^\s*\{\s*"input"\s*:\s*"/;
|
|
22
|
-
|
|
23
19
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
24
20
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
25
21
|
}
|
|
26
22
|
|
|
27
|
-
/**
|
|
28
|
-
* Progressive decode of the freeform `{ "input": "…" }` wrapper.
|
|
29
|
-
* Returns null when the accumulated text does not (yet) match that wrapper so
|
|
30
|
-
* callers suppress deltas and rely on `response.custom_tool_call_input.done`.
|
|
31
|
-
*/
|
|
32
|
-
function partialCustomToolInput(argumentsText: string): string | null {
|
|
33
|
-
const match = FREEFORM_WRAP_PREFIX_RE.exec(argumentsText);
|
|
34
|
-
if (!match) return null;
|
|
35
|
-
const body = argumentsText.slice(match[0]!.length);
|
|
36
|
-
let output = "";
|
|
37
|
-
for (let index = 0; index < body.length; index++) {
|
|
38
|
-
const char = body[index];
|
|
39
|
-
if (char === '"') break;
|
|
40
|
-
if (char !== "\\") {
|
|
41
|
-
output += char;
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
const escaped = body[index + 1];
|
|
45
|
-
if (escaped === undefined) break;
|
|
46
|
-
index += 1;
|
|
47
|
-
if (escaped === "n") output += "\n";
|
|
48
|
-
else if (escaped === "t") output += "\t";
|
|
49
|
-
else if (escaped === "r") output += "\r";
|
|
50
|
-
else if (escaped === "b") output += "\b";
|
|
51
|
-
else if (escaped === "f") output += "\f";
|
|
52
|
-
else if (escaped === "u") {
|
|
53
|
-
const hex = body.slice(index + 1, index + 5);
|
|
54
|
-
if (hex.length !== 4 || !/^[0-9a-fA-F]{4}$/.test(hex)) break;
|
|
55
|
-
output += String.fromCharCode(Number.parseInt(hex, 16));
|
|
56
|
-
index += 4;
|
|
57
|
-
} else output += escaped;
|
|
58
|
-
}
|
|
59
|
-
return output;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
23
|
function replaceSseEventName(block: string, type: string): string {
|
|
63
24
|
const newline = block.includes("\r\n") ? "\r\n" : "\n";
|
|
64
25
|
const lines = block.split(/\r?\n/);
|
|
@@ -332,21 +293,33 @@ export function createRoutedCustomToolRestoreBlockRewrite(
|
|
|
332
293
|
openCalls.set(upstreamItemId, open);
|
|
333
294
|
// A helper alias will become JavaScript at completion, never raw patch/JSON.
|
|
334
295
|
if (itemNames.get(upstreamItemId)?.aliased) return [];
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const fullInput =
|
|
296
|
+
const itemName = itemNames.get(upstreamItemId);
|
|
297
|
+
const ownsFreeformGrammar = itemName?.namespace === undefined
|
|
298
|
+
|| itemName?.namespace === "functions";
|
|
299
|
+
const fullInput = progressiveFreeformInput(
|
|
300
|
+
open.argumentsText,
|
|
301
|
+
ownsFreeformGrammar ? itemName?.name ?? "" : "",
|
|
302
|
+
);
|
|
339
303
|
if (fullInput === null) return [];
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
304
|
+
// This routed path historically holds every unrecognized JSON object until done.
|
|
305
|
+
// The shared decoder streams ordinary raw input, so retain the stricter routed rule
|
|
306
|
+
// when no recognized wrapper transformed the accumulated object.
|
|
307
|
+
if (fullInput === open.argumentsText && open.argumentsText.trimStart().startsWith("{")) return [];
|
|
308
|
+
// Hold a buffer that could still become a complete patch envelope, for either of the two
|
|
309
|
+
// reasons completion rewrites one. Both are the same rewind this path forbids, and both
|
|
310
|
+
// mirror `src/bridge/sse.ts`.
|
|
311
|
+
//
|
|
312
|
+
// `exec`: the done event recompiles such a body into an apply_patch helper call.
|
|
313
|
+
// `apply_patch`: `normalizeApplyPatchDelimiters` rewrites a decorated
|
|
314
|
+
// `*** Begin Patch ***` envelope at completion, so the decorated markers would be
|
|
315
|
+
// published and then replaced by the normalized ones. Before the shared decoder, this
|
|
316
|
+
// path held every non-canonical shape and so never reached that case; now that ordinary
|
|
317
|
+
// raw input streams, the second reason has to be stated explicitly.
|
|
318
|
+
const mayCompile = declaresCodeModeExec(declaredNames)
|
|
319
|
+
&& itemName?.namespace === undefined
|
|
320
|
+
&& itemName?.name === "exec";
|
|
321
|
+
const mayNormalize = ownsFreeformGrammar && itemName?.name === "apply_patch";
|
|
322
|
+
if ((mayCompile || mayNormalize) && mayBecomePatchEnvelope(fullInput)) return [];
|
|
350
323
|
if (!fullInput.startsWith(open.emittedInput) || fullInput.length === open.emittedInput.length) return [];
|
|
351
324
|
const inputDelta = fullInput.slice(open.emittedInput.length);
|
|
352
325
|
open.emittedInput = fullInput;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tool selection a Responses request actually authorized, read from the final outbound body.
|
|
3
|
+
*
|
|
4
|
+
* The undeclared-tool guard answers whether a NAME was declared. This answers a different
|
|
5
|
+
* question: whether this request still permits a client tool call at all, and which names it
|
|
6
|
+
* permits. `tool_choice: "none"`, a forced selector and an `allowed_tools` allow-list each narrow
|
|
7
|
+
* the catalog without removing a declaration, so a name can be declared and forbidden at the same
|
|
8
|
+
* time — and a repair that rebuilds a terminal from collected items would otherwise hand the
|
|
9
|
+
* client a call the caller ruled out.
|
|
10
|
+
*
|
|
11
|
+
* The scope is read from the OUTBOUND body, after every removal, rename and translation, because
|
|
12
|
+
* that is the request the destination answered. A catalog that ends up empty there authorizes no
|
|
13
|
+
* client call whatever the selector still says.
|
|
14
|
+
*/
|
|
15
|
+
import type { MuseToolNameAliases } from "../responses/muse-tool-name-alias";
|
|
16
|
+
import { museWireNameForOriginal } from "../responses/muse-tool-name-alias";
|
|
17
|
+
import type {
|
|
18
|
+
RoutedNamespaceToolAliases,
|
|
19
|
+
RoutedNamespaceToolIdentity,
|
|
20
|
+
} from "../responses/namespace-tool-compat";
|
|
21
|
+
import {
|
|
22
|
+
CLIENT_EXECUTED_CALL_TYPES,
|
|
23
|
+
collectDeclaredWireToolNames,
|
|
24
|
+
hasExplicitWireToolCatalog,
|
|
25
|
+
} from "./responses-undeclared-tool-guard";
|
|
26
|
+
import { isPlainObject } from "./responses-snapshot-codec";
|
|
27
|
+
|
|
28
|
+
type ToolKind = "function" | "custom";
|
|
29
|
+
const BUILTIN_FUNCTIONS_NAMESPACE = "functions";
|
|
30
|
+
|
|
31
|
+
type ToolIdentity = Readonly<{
|
|
32
|
+
kind: ToolKind;
|
|
33
|
+
name: string;
|
|
34
|
+
namespace?: string;
|
|
35
|
+
}>;
|
|
36
|
+
|
|
37
|
+
export type RequestToolScopeCorrespondence = Readonly<{
|
|
38
|
+
clientToolAuthorizationBody?: unknown;
|
|
39
|
+
routedNamespaceToolAliases?: RoutedNamespaceToolAliases;
|
|
40
|
+
routedMuseToolNameAliases?: MuseToolNameAliases;
|
|
41
|
+
convertedRoutedCustomToolNames?: ReadonlySet<string>;
|
|
42
|
+
}>;
|
|
43
|
+
|
|
44
|
+
function identityKey(identity: ToolIdentity): string {
|
|
45
|
+
return JSON.stringify([identity.kind, identity.namespace ?? null, identity.name]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function clientNamespace(value: unknown): string | undefined {
|
|
49
|
+
return typeof value === "string" && value.length > 0 && value !== BUILTIN_FUNCTIONS_NAMESPACE
|
|
50
|
+
? value
|
|
51
|
+
: undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function selectorIdentity(selector: unknown): ToolIdentity | undefined {
|
|
55
|
+
if (!isPlainObject(selector)) return undefined;
|
|
56
|
+
if (selector.type !== "function" && selector.type !== "custom") return undefined;
|
|
57
|
+
if (typeof selector.name !== "string" || selector.name.length === 0) return undefined;
|
|
58
|
+
if ("namespace" in selector && typeof selector.namespace !== "string") return undefined;
|
|
59
|
+
const namespace = clientNamespace(selector.namespace);
|
|
60
|
+
return { kind: selector.type, name: selector.name, ...(namespace ? { namespace } : {}) };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function callIdentity(item: Record<string, unknown>): ToolIdentity | undefined {
|
|
64
|
+
const kind = item.type === "function_call"
|
|
65
|
+
? "function"
|
|
66
|
+
: item.type === "custom_tool_call"
|
|
67
|
+
? "custom"
|
|
68
|
+
: undefined;
|
|
69
|
+
if (!kind || typeof item.name !== "string" || item.name.length === 0) return undefined;
|
|
70
|
+
const namespace = clientNamespace(item.namespace);
|
|
71
|
+
return { kind, name: item.name, ...(namespace ? { namespace } : {}) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function sameRestoredIdentity(left: ToolIdentity, right: RoutedNamespaceToolIdentity): boolean {
|
|
75
|
+
return left.namespace === right.namespace
|
|
76
|
+
&& left.name === right.name
|
|
77
|
+
&& left.kind === right.kind;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Exact identities this restored call could have used on the final outbound wire.
|
|
82
|
+
*
|
|
83
|
+
* Namespace spellings come only from namespace-tool-compat's request-scoped, ambiguity-checked
|
|
84
|
+
* aliases. Muse aliases then compose over those wire names. A kind change is admitted only when
|
|
85
|
+
* the request recorded that exact custom identity as converted to a function.
|
|
86
|
+
*/
|
|
87
|
+
function outboundCallIdentities(
|
|
88
|
+
item: Record<string, unknown>,
|
|
89
|
+
correspondence: RequestToolScopeCorrespondence,
|
|
90
|
+
): ReadonlySet<string> {
|
|
91
|
+
const restored = callIdentity(item);
|
|
92
|
+
if (!restored) return new Set();
|
|
93
|
+
const keys = new Set<string>([identityKey(restored)]);
|
|
94
|
+
const museAliases = correspondence.routedMuseToolNameAliases ?? new Map();
|
|
95
|
+
const convertedCustom = correspondence.convertedRoutedCustomToolNames ?? new Set();
|
|
96
|
+
const originalSelection = isPlainObject(correspondence.clientToolAuthorizationBody)
|
|
97
|
+
? toolSelection(correspondence.clientToolAuthorizationBody)
|
|
98
|
+
: UNRESTRICTED;
|
|
99
|
+
const originalSelectionAllowsRestored = originalSelection.kind === "allow"
|
|
100
|
+
&& originalSelection.identities.has(identityKey(restored));
|
|
101
|
+
|
|
102
|
+
const addWireIdentity = (
|
|
103
|
+
preMuseName: string,
|
|
104
|
+
clientKind: ToolKind,
|
|
105
|
+
conversionIdentityVerified: boolean,
|
|
106
|
+
): void => {
|
|
107
|
+
const name = museWireNameForOriginal(preMuseName, museAliases);
|
|
108
|
+
if (name === undefined) return;
|
|
109
|
+
const kind = clientKind === "custom"
|
|
110
|
+
&& convertedCustom.has(preMuseName)
|
|
111
|
+
&& conversionIdentityVerified
|
|
112
|
+
? "function"
|
|
113
|
+
: clientKind;
|
|
114
|
+
keys.add(identityKey({ kind, name }));
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (restored.namespace === undefined) {
|
|
118
|
+
addWireIdentity(restored.name, restored.kind, originalSelectionAllowsRestored);
|
|
119
|
+
}
|
|
120
|
+
for (const [wireName, identity] of correspondence.routedNamespaceToolAliases ?? new Map()) {
|
|
121
|
+
// namespace-tool-compat emits only aliases authorized under the outbound selector's kind,
|
|
122
|
+
// then restores `custom` provenance from the request's conversion set. That exact alias edge is
|
|
123
|
+
// already the proof that this custom-to-function transition belongs to this identity.
|
|
124
|
+
if (sameRestoredIdentity(restored, identity)) addWireIdentity(wireName, identity.kind, true);
|
|
125
|
+
}
|
|
126
|
+
return keys;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
type ToolSelection =
|
|
130
|
+
| { readonly kind: "unrestricted" }
|
|
131
|
+
| { readonly kind: "deny_all" }
|
|
132
|
+
| { readonly kind: "allow"; readonly identities: ReadonlySet<string> };
|
|
133
|
+
|
|
134
|
+
const UNRESTRICTED: ToolSelection = { kind: "unrestricted" };
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Read the selector only where it states a client-call boundary.
|
|
138
|
+
*
|
|
139
|
+
* `auto`, `required` and an absent selector restrict nothing. A hosted selector
|
|
140
|
+
* (`{ type: "web_search" }`) forces a tool the PROVIDER runs and does not describe the client
|
|
141
|
+
* calls this turn may contain, so it is left alone rather than read as a deny-all: a false
|
|
142
|
+
* refusal would drop a call the caller could have executed.
|
|
143
|
+
*/
|
|
144
|
+
function toolSelection(body: Record<string, unknown>): ToolSelection {
|
|
145
|
+
const choice = body.tool_choice;
|
|
146
|
+
if (choice === "none") return { kind: "deny_all" };
|
|
147
|
+
if (!isPlainObject(choice)) return UNRESTRICTED;
|
|
148
|
+
if (choice.type === "allowed_tools") {
|
|
149
|
+
const identities = new Set<string>();
|
|
150
|
+
if (!Array.isArray(choice.tools)) return { kind: "allow", identities };
|
|
151
|
+
for (const entry of choice.tools) {
|
|
152
|
+
const identity = selectorIdentity(entry);
|
|
153
|
+
if (identity) identities.add(identityKey(identity));
|
|
154
|
+
}
|
|
155
|
+
// An allow-list carrying no client tool — emptied by normalization, or hosted entries only —
|
|
156
|
+
// still bounds this turn: it allows no client call.
|
|
157
|
+
return { kind: "allow", identities };
|
|
158
|
+
}
|
|
159
|
+
if (choice.type === "function" || choice.type === "custom") {
|
|
160
|
+
const identity = selectorIdentity(choice);
|
|
161
|
+
return {
|
|
162
|
+
kind: "allow",
|
|
163
|
+
identities: new Set(identity ? [identityKey(identity)] : []),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return UNRESTRICTED;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export type RequestToolScope = {
|
|
170
|
+
/**
|
|
171
|
+
* The name a client call is refused under, or undefined when this request permits it.
|
|
172
|
+
* A nameless call type is not answered here: only the declaration guard knows those.
|
|
173
|
+
*/
|
|
174
|
+
forbiddenClientToolCallName(item: Record<string, unknown>): string | undefined;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The client-call boundary this request states, or undefined when it states none.
|
|
179
|
+
*
|
|
180
|
+
* Returning undefined for an unrestricted request keeps every ordinary turn on the path it
|
|
181
|
+
* already had: a caller that selected nothing gets no new refusal.
|
|
182
|
+
*/
|
|
183
|
+
export function requestToolScope(
|
|
184
|
+
body: unknown,
|
|
185
|
+
correspondence: RequestToolScopeCorrespondence = {},
|
|
186
|
+
): RequestToolScope | undefined {
|
|
187
|
+
if (!isPlainObject(body)) return undefined;
|
|
188
|
+
const selection = toolSelection(body);
|
|
189
|
+
// A readable catalog that declares no client-executable name is authoritative, exactly as it is
|
|
190
|
+
// for the declaration guard: an explicit empty list denies every client call. An absent catalog
|
|
191
|
+
// says nothing — a passthrough request may omit `tools` and still receive a call the client
|
|
192
|
+
// understands.
|
|
193
|
+
const catalogDeniesClientCalls = hasExplicitWireToolCatalog(body)
|
|
194
|
+
&& collectDeclaredWireToolNames(body).size === 0;
|
|
195
|
+
if (selection.kind === "unrestricted" && !catalogDeniesClientCalls) return undefined;
|
|
196
|
+
return {
|
|
197
|
+
forbiddenClientToolCallName(item: Record<string, unknown>): string | undefined {
|
|
198
|
+
if (typeof item.type !== "string" || !CLIENT_EXECUTED_CALL_TYPES.has(item.type)) {
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
const restored = callIdentity(item);
|
|
202
|
+
const reported = restored?.name;
|
|
203
|
+
if (reported === undefined) return undefined;
|
|
204
|
+
if (catalogDeniesClientCalls || selection.kind === "deny_all") return reported;
|
|
205
|
+
if (selection.kind === "allow") {
|
|
206
|
+
const candidates = outboundCallIdentities(item, correspondence);
|
|
207
|
+
return [...candidates].some(candidate => selection.identities.has(candidate))
|
|
208
|
+
? undefined
|
|
209
|
+
: reported;
|
|
210
|
+
}
|
|
211
|
+
return undefined;
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
collectAmbiguousDottedAliases,
|
|
3
|
+
dottedAliasIsUnambiguous,
|
|
4
|
+
isSchemaValidResponsesToolName,
|
|
5
|
+
wireToolInnerName,
|
|
6
|
+
} from "../responses/tool-name-aliases";
|
|
2
7
|
import {
|
|
3
8
|
dottedToolName,
|
|
4
9
|
NAMESPACED_BARE_ALIAS_EXCLUDED_NAMES,
|
|
@@ -8,7 +13,10 @@ import {
|
|
|
8
13
|
import { replaceSseDataPayload, sseDataPayload, type SseBlockRewrite } from "./sse-payload-rewrite";
|
|
9
14
|
|
|
10
15
|
/** Item types the client executes through a request-declared wire name. */
|
|
11
|
-
const CLIENT_EXECUTED_CALL_TYPES = new Set([
|
|
16
|
+
export const CLIENT_EXECUTED_CALL_TYPES: ReadonlySet<string> = new Set([
|
|
17
|
+
"function_call",
|
|
18
|
+
"custom_tool_call",
|
|
19
|
+
]);
|
|
12
20
|
/** Codex groups ordinary top-level tools here; unlike an MCP namespace, it has no wire prefix. */
|
|
13
21
|
const BUILTIN_FUNCTIONS_NAMESPACE = "functions";
|
|
14
22
|
|
|
@@ -512,6 +520,31 @@ export function normalizeDefaultNamespaceInItem(
|
|
|
512
520
|
return { value: { ...item, name: bare }, changed: true };
|
|
513
521
|
}
|
|
514
522
|
}
|
|
523
|
+
// Authorization and emission were reading two different resolvers for the same question.
|
|
524
|
+
// `undeclaredNameInItem` resolves an emitted name through `normalizeDeclaredToolName`, which
|
|
525
|
+
// maps a `default.`-prefixed code-mode helper onto the declared `exec` (#4412) as well as a
|
|
526
|
+
// `default.`-prefixed bare tool (#4176); the rewrite above learned only the second case. So a
|
|
527
|
+
// routed Muse turn under a code-mode catalog had `default.view_image` ACCEPTED as `exec` and
|
|
528
|
+
// then relayed under the name the upstream schema rejects. Codex stored
|
|
529
|
+
// `{"type":"function_call","name":"default.view_image"}`, answered "unsupported call", and
|
|
530
|
+
// every later replay of that history — a side chat, a compaction — was refused on
|
|
531
|
+
// `input[N].name` for the lifetime of the conversation (#5095).
|
|
532
|
+
//
|
|
533
|
+
// Emit the name the guard authorized rather than a second, weaker opinion about it. The two
|
|
534
|
+
// must agree: a name good enough to admit is the name the client has to receive.
|
|
535
|
+
//
|
|
536
|
+
// Gated on the name actually being unusable, so this branch cannot touch a name the upstream
|
|
537
|
+
// accepts whatever the resolver would have said about it. The routed-custom-tool and
|
|
538
|
+
// namespace restores run earlier in the same chain and already rewrite the shapes they own,
|
|
539
|
+
// which leaves this as the boundary check for the names no restore claimed.
|
|
540
|
+
if (!isSchemaValidResponsesToolName(name)) {
|
|
541
|
+
const authorized = normalizeDeclaredToolName(name, declared, declaredBare);
|
|
542
|
+
if (
|
|
543
|
+
authorized !== name
|
|
544
|
+
&& declared.has(authorized)
|
|
545
|
+
&& isSchemaValidResponsesToolName(authorized)
|
|
546
|
+
) return { value: { ...item, name: authorized }, changed: true };
|
|
547
|
+
}
|
|
515
548
|
}
|
|
516
549
|
return { value: item, changed: false };
|
|
517
550
|
}
|
package/src/server/search.ts
CHANGED
|
@@ -25,6 +25,9 @@ import { codexAccountNamespaceForModel } from "../codex/account-namespace-match"
|
|
|
25
25
|
import { NATIVE_RESERVE_MODEL } from "../codex/catalog/native-models";
|
|
26
26
|
import { isCodexReserveRequestEligible } from "../codex/loopback-target";
|
|
27
27
|
import type { DataPlaneAdmission } from "./auth-cors";
|
|
28
|
+
import {
|
|
29
|
+
admissionScopeDenial,
|
|
30
|
+
} from "./admission-model-scope";
|
|
28
31
|
import { formatCodexProviderForLog } from "../codex/routing";
|
|
29
32
|
import { signalWithTimeout } from "../lib/abort";
|
|
30
33
|
import { readBoundedResponseBytes } from "../lib/bounded-body";
|
|
@@ -87,6 +90,11 @@ export async function handleSearch(
|
|
|
87
90
|
if (!route.codexAccountId || route.codexAccountNamespace !== accountNamespace) {
|
|
88
91
|
return formatErrorResponse(400, "invalid_request_error", "Invalid Codex account-qualified search model");
|
|
89
92
|
}
|
|
93
|
+
// This branch resolves a model through the router and bills the account it
|
|
94
|
+
// names, so a scoped key is held to the same destination rule it is held
|
|
95
|
+
// to on the inference path.
|
|
96
|
+
const denial = admissionScopeDenial(config, admission, model, route);
|
|
97
|
+
if (denial) return denial;
|
|
90
98
|
exactAccount = { accountId: route.codexAccountId, modelId: route.modelId };
|
|
91
99
|
logCtx.provider = `${route.providerName}-${accountNamespace}`;
|
|
92
100
|
logCtx.routeDecision = route.routeDecision;
|
|
@@ -108,7 +116,7 @@ export async function handleSearch(
|
|
|
108
116
|
}
|
|
109
117
|
const candidates = listOpenAiForwardSidecarCandidates(config);
|
|
110
118
|
if (candidates.length === 0) {
|
|
111
|
-
return handleAlphaSearchSidecarFallback(body, config, req.signal, logCtx);
|
|
119
|
+
return handleAlphaSearchSidecarFallback(body, config, req.signal, logCtx, admission);
|
|
112
120
|
}
|
|
113
121
|
|
|
114
122
|
let upstream: Awaited<ReturnType<typeof resolveFirstUsableOpenAiSidecar>>;
|
|
@@ -148,6 +156,22 @@ export async function handleSearch(
|
|
|
148
156
|
throw err;
|
|
149
157
|
}
|
|
150
158
|
|
|
159
|
+
if (!accountNamespace) {
|
|
160
|
+
// An unqualified search model is never routed: the caller's own string is
|
|
161
|
+
// relayed to whichever ChatGPT account this upstream resolved to, and that
|
|
162
|
+
// account is billed for it. The qualified branch above was already judged
|
|
163
|
+
// against the route it resolved, so it is not judged twice here.
|
|
164
|
+
const searchModel = typeof model === "string" && model.trim() ? model : undefined;
|
|
165
|
+
const denial = admissionScopeDenial(config, admission, searchModel, {
|
|
166
|
+
providerName: upstream.providerName,
|
|
167
|
+
modelId: searchModel,
|
|
168
|
+
});
|
|
169
|
+
if (denial) {
|
|
170
|
+
upstream.releaseProbeLease?.();
|
|
171
|
+
return denial;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
151
175
|
const headers: Record<string, string> = { "content-type": "application/json" };
|
|
152
176
|
if (upstream.provider.headers) Object.assign(headers, upstream.provider.headers);
|
|
153
177
|
for (const [name, value] of upstream.headers) headers[name] = value;
|
|
@@ -230,7 +230,7 @@ export function replaceSseDataPayload(block: string, payload: string): string {
|
|
|
230
230
|
continue;
|
|
231
231
|
}
|
|
232
232
|
if (!replaced) {
|
|
233
|
-
rewritten.push(`data: ${
|
|
233
|
+
rewritten.push(...payload.split(/\r?\n/).map(line => `data: ${line}`));
|
|
234
234
|
replaced = true;
|
|
235
235
|
}
|
|
236
236
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime owner for the opt-in usage-ledger size limit (#5063).
|
|
3
|
+
*
|
|
4
|
+
* The compactor in `src/usage/ledger-retention.ts` knows how to publish a smaller ledger safely.
|
|
5
|
+
* This is the part that decides when, and -- the half #5063 was missing -- what has to be
|
|
6
|
+
* discarded afterwards.
|
|
7
|
+
*
|
|
8
|
+
* Deleting rows from usage.jsonl invalidates three readers that do not watch the file: the
|
|
9
|
+
* 2,000-entry Logs ring, which otherwise keeps serving rows the ledger no longer has; the
|
|
10
|
+
* retained usage aggregate and failure projection, whose checkpoints now point past a boundary
|
|
11
|
+
* that moved; and the request-history index, whose source identity has changed. A compaction
|
|
12
|
+
* that skips any of them makes the dashboard disagree with the ledger, which is the disagreement
|
|
13
|
+
* this batch exists to remove.
|
|
14
|
+
*/
|
|
15
|
+
import { currentUsageLogRevision, setUsageLedgerAppendHook } from "../usage/log";
|
|
16
|
+
import { enforceUsageLedgerSizeLimit } from "../usage/ledger-retention";
|
|
17
|
+
import { discardRetainedFailureProjection } from "../usage/failure-projection-cache";
|
|
18
|
+
import { rehydrateRequestLogsAfterLedgerReplacement } from "./request-log";
|
|
19
|
+
import type { UsageLedgerRetentionStatus } from "../usage/retention-contract";
|
|
20
|
+
|
|
21
|
+
let configuredMaxBytes: number | undefined;
|
|
22
|
+
let enforcing = false;
|
|
23
|
+
|
|
24
|
+
function invalidateLedgerReaders(): void {
|
|
25
|
+
// Ordered cheapest-first, and each guarded on its own: a projection that fails to discard
|
|
26
|
+
// must not stop the ring from being rebuilt, because the ring is the surface an operator is
|
|
27
|
+
// looking at while this happens.
|
|
28
|
+
try { discardRetainedFailureProjection(); } catch { /* rebuildable by construction */ }
|
|
29
|
+
void (async () => {
|
|
30
|
+
try {
|
|
31
|
+
const { discardRetainedUsageAggregate } = await import("./management/usage-aggregate-cache");
|
|
32
|
+
discardRetainedUsageAggregate();
|
|
33
|
+
} catch { /* rebuildable by construction */ }
|
|
34
|
+
try {
|
|
35
|
+
const { closeRequestHistoryIndex } = await import("../routing/history/indexer");
|
|
36
|
+
closeRequestHistoryIndex();
|
|
37
|
+
} catch { /* the index rebuilds from its own source-identity contract */ }
|
|
38
|
+
})();
|
|
39
|
+
try { rehydrateRequestLogsAfterLedgerReplacement(); } catch { /* the ring refills as rows arrive */ }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function enforceNow(): void {
|
|
43
|
+
// Re-entrancy guard, not a lock. The compaction itself runs inside the append call stack, and
|
|
44
|
+
// its own publication writes nothing through appendUsageEntry -- this exists so a future
|
|
45
|
+
// caller on that path cannot start a second pass over a file the first one is replacing.
|
|
46
|
+
if (enforcing) return;
|
|
47
|
+
enforcing = true;
|
|
48
|
+
try {
|
|
49
|
+
const result = enforceUsageLedgerSizeLimit(configuredMaxBytes);
|
|
50
|
+
if (result.kind === "replaced") invalidateLedgerReaders();
|
|
51
|
+
} catch (error) {
|
|
52
|
+
// Never fail a request because history could not be trimmed. The limit is not enforced and
|
|
53
|
+
// says so; the next append tries again from a fresh revision.
|
|
54
|
+
console.warn(
|
|
55
|
+
`[usage-retention] could not enforce the usage ledger size limit: ${error instanceof Error ? error.message : String(error)}`,
|
|
56
|
+
);
|
|
57
|
+
} finally {
|
|
58
|
+
enforcing = false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Install or update the policy. `undefined` removes the hook entirely. */
|
|
63
|
+
export function setUsageLedgerRetention(maxBytes: number | undefined): void {
|
|
64
|
+
configuredMaxBytes = maxBytes;
|
|
65
|
+
setUsageLedgerAppendHook(maxBytes === undefined ? null : enforceNow);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function usageLedgerRetentionStatus(): UsageLedgerRetentionStatus {
|
|
69
|
+
return {
|
|
70
|
+
...(configuredMaxBytes !== undefined ? { maxBytes: configuredMaxBytes } : {}),
|
|
71
|
+
currentBytes: currentUsageLogRevision()?.size ?? 0,
|
|
72
|
+
};
|
|
73
|
+
}
|
package/src/service/cli.ts
CHANGED
|
@@ -13,7 +13,8 @@ import { resolveServiceListenPort, reportServiceServing } from "./health";
|
|
|
13
13
|
import { platformOps, proxyStillLiveAfterStop, stopTrackedProxyForServiceCommand, installServiceSafely, installFreshWindowsSchedulerSafely, removeServiceInstallState, isServiceInstalled } from "./orchestration";
|
|
14
14
|
import { repairService } from "./repair";
|
|
15
15
|
import type { ServiceRepairVerb } from "./repair";
|
|
16
|
-
import { TASK, plistPath, readServiceBackend } from "./state";
|
|
16
|
+
import { TASK, plistPath, readServiceBackend, releaseServiceOwner, resolveServiceOwnership } from "./state";
|
|
17
|
+
import { foreignServiceOwnerRefusal, unknownServiceOwnerRefusal } from "./repair";
|
|
17
18
|
import type { ServiceBackend } from "./state";
|
|
18
19
|
import { unitPath } from "./systemd";
|
|
19
20
|
import { inspectWindowsSchedulerServiceStatus, schtasksErrorDetail, probeWindowsSchedulerTask } from "./windows-scheduler";
|
|
@@ -233,6 +234,15 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
233
234
|
case "install":
|
|
234
235
|
assertServiceEnvironmentMatchesInstall();
|
|
235
236
|
assertServiceAuthEnvironment();
|
|
237
|
+
// The install may advance provenance, but it may take back only the owner it observed
|
|
238
|
+
// before touching the registration. A desktop successor that claims ownership while
|
|
239
|
+
// the install is running must survive the late release below.
|
|
240
|
+
const ownershipBeforeInstall = resolveServiceOwnership();
|
|
241
|
+
if (ownershipBeforeInstall.kind === "unknown") {
|
|
242
|
+
console.error(`❌ ${unknownServiceOwnerRefusal(ownershipBeforeInstall.reason, "install")}`);
|
|
243
|
+
process.exitCode = 1;
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
236
246
|
// A manually started proxy can still own the configured port while the service
|
|
237
247
|
// registration is absent or unloaded. Stop both the registered manager and any
|
|
238
248
|
// tracked standalone listener before loading the freshly written service assets.
|
|
@@ -258,6 +268,23 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
258
268
|
process.exitCode = 1;
|
|
259
269
|
break;
|
|
260
270
|
}
|
|
271
|
+
// ONLY now. `install` is the verb that takes the runtime back, and it does so after the
|
|
272
|
+
// registration exists — releasing first meant a cancelled UAC prompt, a failed
|
|
273
|
+
// registration or an aborted cleanup left the retained npm registration looking
|
|
274
|
+
// CLI-owned, so the next incidental repair would reactivate it.
|
|
275
|
+
//
|
|
276
|
+
// `repair` and `restart` refuse under a foreign owner precisely because they run
|
|
277
|
+
// incidentally — from a tray helper, from `ocx update`, from a doctor suggestion — and
|
|
278
|
+
// undoing a takeover the user consented to must be something the user asked for.
|
|
279
|
+
{
|
|
280
|
+
const released = releaseServiceOwner(ownershipBeforeInstall, { allowRevisionAdvance: true });
|
|
281
|
+
if (released) {
|
|
282
|
+
console.log(
|
|
283
|
+
`ℹ️ The desktop app owned the background runtime (install ${released.installId}, `
|
|
284
|
+
+ `consent generation ${released.consentGeneration}); this install took it back.`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
261
288
|
// The wrapper was written moments ago in this process, so the configured port
|
|
262
289
|
// and the baked one cannot have diverged yet — unlike `start`, which reads the
|
|
263
290
|
// installed artifact instead.
|
|
@@ -269,15 +296,34 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
269
296
|
// Same one-time marker and same guards (TTY, gh auth, agent deferral) apply.
|
|
270
297
|
await maybeShowStarPrompt();
|
|
271
298
|
break;
|
|
272
|
-
case "start":
|
|
299
|
+
case "start": {
|
|
273
300
|
// The installed launcher preserves the recorded CODEX_SQLITE_HOME: a
|
|
274
301
|
// changed sqlite_home/CODEX_SQLITE_HOME/CODEX_HOME would start the service
|
|
275
302
|
// on the recorded database while this shell resolves another, splitting
|
|
276
303
|
// native Codex history between databases. Same guard `stop` already runs.
|
|
277
304
|
assertServiceEnvironmentMatchesInstall();
|
|
305
|
+
// `start` activates the npm registration, so it refuses on the same terms repair does.
|
|
306
|
+
// The Windows tray starts the service automatically, which would otherwise put a second
|
|
307
|
+
// proxy beside the one the desktop app is running without anyone asking for it.
|
|
308
|
+
// `stop` and `uninstall` are deliberately NOT gated: they deactivate.
|
|
309
|
+
//
|
|
310
|
+
// Reported rather than thrown: the tray drives this through `runTrayProxyStart`, which
|
|
311
|
+
// does not catch, and a refusal is a decision rather than a crash.
|
|
312
|
+
const ownership = resolveServiceOwnership();
|
|
313
|
+
const refusal = ownership.kind === "unknown"
|
|
314
|
+
? unknownServiceOwnerRefusal(ownership.reason, "start")
|
|
315
|
+
: ownership.kind === "owned" && ownership.ownership.owner !== "cli"
|
|
316
|
+
? foreignServiceOwnerRefusal(ownership.ownership, "start")
|
|
317
|
+
: null;
|
|
318
|
+
if (refusal) {
|
|
319
|
+
console.error(`❌ ${refusal}`);
|
|
320
|
+
process.exitCode = 1;
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
278
323
|
ops.start();
|
|
279
324
|
await reportServiceServing("started");
|
|
280
325
|
break;
|
|
326
|
+
}
|
|
281
327
|
case "stop": {
|
|
282
328
|
assertServiceEnvironmentMatchesInstall();
|
|
283
329
|
// Only stop what is actually installed. The unguarded call ran a real `launchctl unload`
|