@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
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The user's login-shell PATH, for installers the dashboard runs under OpenCodex Desktop (#6971).
|
|
3
|
+
*
|
|
4
|
+
* A Finder or login launch gives the Desktop app, and so its proxy sidecar, only the system PATH
|
|
5
|
+
* (`/usr/bin:/bin:/usr/sbin:/sbin`). `ocx codex-shim install` scans PATH for the native Codex
|
|
6
|
+
* launcher, so from the dashboard it reported "Native Codex launcher not found" for Homebrew, npm,
|
|
7
|
+
* bun, nvm, fnm, volta and mise installs, and an npm launcher had no `node` for its probe.
|
|
8
|
+
*
|
|
9
|
+
* This asks the user's shell once, as an interactive login shell would start, and keeps only PATH:
|
|
10
|
+
* the value is framed by markers so startup-file output cannot leak into it. The read has a
|
|
11
|
+
* deadline, runs in its own process group so a hung startup file is killed with all of its
|
|
12
|
+
* children, and its result, including failure, is cached so repeated dashboard actions do not
|
|
13
|
+
* spawn shells. It is scoped to the shim installer's child environment; the service definition's
|
|
14
|
+
* PATH has its own policy.
|
|
15
|
+
*/
|
|
16
|
+
import { spawn as nodeSpawn, type ChildProcess } from "node:child_process";
|
|
17
|
+
import { userInfo } from "node:os";
|
|
18
|
+
import { basename, isAbsolute } from "node:path";
|
|
19
|
+
|
|
20
|
+
const BEGIN = "__OCX_LOGIN_PATH_BEGIN__";
|
|
21
|
+
const END = "__OCX_LOGIN_PATH_END__";
|
|
22
|
+
export const LOGIN_SHELL_PATH_TIMEOUT_MS = 5_000;
|
|
23
|
+
export const LOGIN_SHELL_PATH_CACHE_MS = 5 * 60_000;
|
|
24
|
+
const MAX_OUTPUT_BYTES = 64 * 1024;
|
|
25
|
+
/** After the shell exits, how long its stdout may stay open before a background job is assumed to hold it. */
|
|
26
|
+
const STDOUT_DRAIN_MS = 500;
|
|
27
|
+
|
|
28
|
+
export function loginShellPathArgs(shell: string): string[] {
|
|
29
|
+
const print = basename(shell) === "fish"
|
|
30
|
+
? `printf '%s%s%s\\n' ${BEGIN} (string join : $PATH) ${END}`
|
|
31
|
+
: `printf '%s%s%s\\n' ${BEGIN} "$PATH" ${END}`;
|
|
32
|
+
return ["-l", "-i", "-c", print];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The framed PATH, only when every entry is absolute. */
|
|
36
|
+
export function parseLoginShellPath(output: string): string | null {
|
|
37
|
+
const start = output.lastIndexOf(BEGIN);
|
|
38
|
+
if (start < 0) return null;
|
|
39
|
+
const end = output.indexOf(END, start + BEGIN.length);
|
|
40
|
+
if (end < 0) return null;
|
|
41
|
+
const value = output.slice(start + BEGIN.length, end);
|
|
42
|
+
if (!value || /[\r\n\0]/.test(value)) return null;
|
|
43
|
+
const entries = value.split(":").filter(Boolean);
|
|
44
|
+
return entries.length > 0 && entries.every(entry => isAbsolute(entry)) ? entries.join(":") : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Ordered union: the shell's entries first, then any the process had that the shell did not. */
|
|
48
|
+
export function mergePathLists(preferred: string, fallback: string | undefined): string {
|
|
49
|
+
// POSIX only: the login shell runs on macOS and Linux, where PATH is colon-separated.
|
|
50
|
+
return [...new Set([...preferred.split(":"), ...(fallback ?? "").split(":")].filter(Boolean))].join(":");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface LoginShellPathDeps {
|
|
54
|
+
platform?: NodeJS.Platform;
|
|
55
|
+
shell?: string | null;
|
|
56
|
+
env?: NodeJS.ProcessEnv;
|
|
57
|
+
spawn?: (command: string, args: string[], options: Parameters<typeof nodeSpawn>[2]) => ChildProcess;
|
|
58
|
+
killGroup?: (pid: number) => void;
|
|
59
|
+
timeoutMs?: number;
|
|
60
|
+
now?: () => number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let cached: { at: number; value: string | null } | null = null;
|
|
64
|
+
|
|
65
|
+
export function resetLoginShellPathCacheForTests(): void {
|
|
66
|
+
cached = null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function defaultShell(env: NodeJS.ProcessEnv): string | null {
|
|
70
|
+
const fromEnv = env.SHELL?.trim();
|
|
71
|
+
if (fromEnv && isAbsolute(fromEnv)) return fromEnv;
|
|
72
|
+
try {
|
|
73
|
+
const shell = userInfo().shell;
|
|
74
|
+
return shell && isAbsolute(shell) ? shell : null;
|
|
75
|
+
} catch { return null; }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function killProcessGroup(pid: number): void {
|
|
79
|
+
try { process.kill(-pid, "SIGKILL"); } catch { /* already gone */ }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function readLoginShellPath(deps: LoginShellPathDeps = {}): Promise<string | null> {
|
|
83
|
+
const platform = deps.platform ?? process.platform;
|
|
84
|
+
if (platform === "win32") return null;
|
|
85
|
+
const now = deps.now ?? Date.now;
|
|
86
|
+
if (cached && now() - cached.at < LOGIN_SHELL_PATH_CACHE_MS) return cached.value;
|
|
87
|
+
const env = deps.env ?? process.env;
|
|
88
|
+
const shell = deps.shell === undefined ? defaultShell(env) : deps.shell;
|
|
89
|
+
const value = shell ? await probe(shell, env, deps) : null;
|
|
90
|
+
cached = { at: now(), value };
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function probe(shell: string, env: NodeJS.ProcessEnv, deps: LoginShellPathDeps): Promise<string | null> {
|
|
95
|
+
const spawn = deps.spawn ?? nodeSpawn;
|
|
96
|
+
const killGroup = deps.killGroup ?? killProcessGroup;
|
|
97
|
+
return new Promise(resolve => {
|
|
98
|
+
let child: ChildProcess;
|
|
99
|
+
try {
|
|
100
|
+
child = spawn(shell, loginShellPathArgs(shell), {
|
|
101
|
+
detached: true,
|
|
102
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
103
|
+
// Only what a login shell needs to find its startup files; no proxy or credential state.
|
|
104
|
+
env: Object.fromEntries(Object.entries({
|
|
105
|
+
HOME: env.HOME, USER: env.USER, LOGNAME: env.LOGNAME, SHELL: shell, LANG: env.LANG,
|
|
106
|
+
PATH: env.PATH, TERM: "dumb",
|
|
107
|
+
}).filter(([, value]) => value !== undefined)) as NodeJS.ProcessEnv,
|
|
108
|
+
});
|
|
109
|
+
} catch { resolve(null); return; }
|
|
110
|
+
let output = "";
|
|
111
|
+
let settled = false;
|
|
112
|
+
let exitCode: number | null = null;
|
|
113
|
+
let drain: ReturnType<typeof setTimeout> | undefined;
|
|
114
|
+
const finish = (value: string | null, killGroupMembers: boolean) => {
|
|
115
|
+
if (settled) return;
|
|
116
|
+
settled = true;
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
clearTimeout(drain);
|
|
119
|
+
// A hung startup file, or a background job still holding the pipe, is killed with its group.
|
|
120
|
+
// A shell that exited and closed its output is left alone: its group id may already be free.
|
|
121
|
+
if (killGroupMembers && child.pid) killGroup(child.pid);
|
|
122
|
+
resolve(value);
|
|
123
|
+
};
|
|
124
|
+
const timer = setTimeout(() => finish(null, true), deps.timeoutMs ?? LOGIN_SHELL_PATH_TIMEOUT_MS);
|
|
125
|
+
child.stdout?.setEncoding("utf8");
|
|
126
|
+
child.stdout?.on("data", (chunk: string) => {
|
|
127
|
+
output += chunk;
|
|
128
|
+
if (output.length > MAX_OUTPUT_BYTES) finish(null, true);
|
|
129
|
+
});
|
|
130
|
+
const parsed = () => exitCode === 0 ? parseLoginShellPath(output) : null;
|
|
131
|
+
child.once("error", () => finish(null, true));
|
|
132
|
+
// Parse only after stdout closes: `exit` can arrive before the last chunk.
|
|
133
|
+
child.once("close", code => { exitCode ??= code; finish(parsed(), false); });
|
|
134
|
+
child.once("exit", code => {
|
|
135
|
+
exitCode = code;
|
|
136
|
+
drain = setTimeout(() => finish(parsed(), true), STDOUT_DRAIN_MS);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** `env` with the login-shell PATH in front, or unchanged when it cannot be read. */
|
|
142
|
+
export async function withLoginShellPath(env: NodeJS.ProcessEnv, deps: LoginShellPathDeps = {}): Promise<NodeJS.ProcessEnv> {
|
|
143
|
+
const loginPath = await readLoginShellPath({ ...deps, env });
|
|
144
|
+
return loginPath ? { ...env, PATH: mergePathLists(loginPath, env.PATH) } : env;
|
|
145
|
+
}
|
|
146
|
+
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET / PUT /api/advisor/settings — the advisor sidecar's management surface.
|
|
3
|
+
*
|
|
4
|
+
* GET answers the RESOLVED settings (defaults applied, sources marked) plus availability, so the
|
|
5
|
+
* GUI/CLI show real runtime state rather than stored fields. PUT is a strict partial patch:
|
|
6
|
+
* unknown keys and wrong types are refused, values are validated against the same resolver the
|
|
7
|
+
* runtime reads, and the patch persists through the locked config writer with the snapshot-
|
|
8
|
+
* restore discipline used by PATCH /api/protocols/settings — a refused or failed write never
|
|
9
|
+
* leaves the live config serving a state the file does not hold.
|
|
10
|
+
*/
|
|
11
|
+
import { activateAdvisor } from "../../lib/advisor-activation";
|
|
12
|
+
import { jsonResponse } from "../auth-cors";
|
|
13
|
+
import { readManagementJsonBodyOr } from "./body";
|
|
14
|
+
import type { ManagementContext } from "./context";
|
|
15
|
+
import {
|
|
16
|
+
ADVISOR_CONTEXT_SHARING_CONSENT_VERSION,
|
|
17
|
+
ADVISOR_EFFORTS,
|
|
18
|
+
advisorContextSharingBlocked,
|
|
19
|
+
advisorRunnable,
|
|
20
|
+
isCurrentAdvisorContextSharingConsent,
|
|
21
|
+
isValidAdvisorEffort,
|
|
22
|
+
isValidAdvisorPolicy,
|
|
23
|
+
resolveAdvisorSettings,
|
|
24
|
+
type AdvisorEffort,
|
|
25
|
+
type AdvisorPolicy,
|
|
26
|
+
} from "../../advisor/settings";
|
|
27
|
+
|
|
28
|
+
const INVALID_BODY = Symbol("invalid-body");
|
|
29
|
+
|
|
30
|
+
interface AdvisorPatch {
|
|
31
|
+
enabled?: boolean;
|
|
32
|
+
model?: string;
|
|
33
|
+
effort?: AdvisorEffort;
|
|
34
|
+
policy?: AdvisorPolicy;
|
|
35
|
+
timeoutMs?: number;
|
|
36
|
+
/** "v1" records current consent. null removes it. Enabling does not imply this field. */
|
|
37
|
+
contextSharingConsent?: typeof ADVISOR_CONTEXT_SHARING_CONSENT_VERSION | null;
|
|
38
|
+
reset?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type ParsedPatch = { ok: true; patch: AdvisorPatch } | { ok: false; code: string; message: string };
|
|
42
|
+
|
|
43
|
+
const PATCH_KEYS = new Set(["enabled", "model", "effort", "policy", "timeoutMs", "contextSharingConsent", "reset"]);
|
|
44
|
+
|
|
45
|
+
type Rec = Record<string, unknown>;
|
|
46
|
+
function isRec(value: unknown): value is Rec {
|
|
47
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Strict: unknown keys and wrong types are refused; messages name the field, never the value. */
|
|
51
|
+
export function parseAdvisorSettingsPatch(body: unknown): ParsedPatch {
|
|
52
|
+
if (!isRec(body)) return { ok: false, code: "invalid_body", message: "body must be a JSON object" };
|
|
53
|
+
if (Object.keys(body).length === 0) return { ok: false, code: "empty_body", message: "body must set at least one of enabled, model, effort, policy, timeoutMs, contextSharingConsent or reset" };
|
|
54
|
+
for (const key of Object.keys(body)) {
|
|
55
|
+
if (!PATCH_KEYS.has(key)) return { ok: false, code: "unknown_field", message: "body accepts only enabled, model, effort, policy, timeoutMs, contextSharingConsent and reset" };
|
|
56
|
+
}
|
|
57
|
+
const patch: AdvisorPatch = {};
|
|
58
|
+
if (body.reset !== undefined) {
|
|
59
|
+
if (body.reset !== true) return { ok: false, code: "invalid_reset", message: "reset must be true when present" };
|
|
60
|
+
if (Object.keys(body).length > 1) return { ok: false, code: "reset_with_fields", message: "reset must be the only field when present" };
|
|
61
|
+
patch.reset = true;
|
|
62
|
+
return { ok: true, patch };
|
|
63
|
+
}
|
|
64
|
+
if (body.enabled !== undefined) {
|
|
65
|
+
if (typeof body.enabled !== "boolean") return { ok: false, code: "invalid_enabled", message: "enabled must be a boolean" };
|
|
66
|
+
patch.enabled = body.enabled;
|
|
67
|
+
}
|
|
68
|
+
if (body.model !== undefined) {
|
|
69
|
+
// An empty or whitespace-only value is the supported "clear the model" state, not an error:
|
|
70
|
+
// the resolver defaults to an empty model and `advisorRunnable` requires a non-blank one. The
|
|
71
|
+
// only structural rules are the string type and the trimmed length bound.
|
|
72
|
+
if (typeof body.model !== "string" || body.model.trim().length > 200) {
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
code: "invalid_model",
|
|
76
|
+
message: "model must be a string whose trimmed value is at most 200 characters; an empty value clears the advisor model",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
patch.model = body.model.trim();
|
|
80
|
+
}
|
|
81
|
+
if (body.effort !== undefined) {
|
|
82
|
+
if (!isValidAdvisorEffort(body.effort)) {
|
|
83
|
+
return { ok: false, code: "invalid_effort", message: `effort must be one of ${ADVISOR_EFFORTS.join(", ")}` };
|
|
84
|
+
}
|
|
85
|
+
patch.effort = body.effort;
|
|
86
|
+
}
|
|
87
|
+
if (body.policy !== undefined) {
|
|
88
|
+
if (!isValidAdvisorPolicy(body.policy)) {
|
|
89
|
+
return { ok: false, code: "invalid_policy", message: 'policy must be "manual" or "preflight"' };
|
|
90
|
+
}
|
|
91
|
+
patch.policy = body.policy;
|
|
92
|
+
}
|
|
93
|
+
if (body.timeoutMs !== undefined) {
|
|
94
|
+
if (typeof body.timeoutMs !== "number" || !Number.isFinite(body.timeoutMs) || body.timeoutMs < 1_000 || body.timeoutMs > 600_000) {
|
|
95
|
+
return { ok: false, code: "invalid_timeout", message: "timeoutMs must be a number between 1000 and 600000" };
|
|
96
|
+
}
|
|
97
|
+
patch.timeoutMs = Math.floor(body.timeoutMs);
|
|
98
|
+
}
|
|
99
|
+
if (body.contextSharingConsent !== undefined) {
|
|
100
|
+
if (body.contextSharingConsent === null) {
|
|
101
|
+
patch.contextSharingConsent = null;
|
|
102
|
+
} else if (isCurrentAdvisorContextSharingConsent(body.contextSharingConsent)) {
|
|
103
|
+
patch.contextSharingConsent = body.contextSharingConsent;
|
|
104
|
+
} else {
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
code: "invalid_context_sharing_consent",
|
|
108
|
+
message: `contextSharingConsent must be "${ADVISOR_CONTEXT_SHARING_CONSENT_VERSION}" or null`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { ok: true, patch };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function advisorInfo(config: ManagementContext["config"]): Record<string, unknown> {
|
|
116
|
+
const settings = resolveAdvisorSettings(config);
|
|
117
|
+
const warning = settings.enabled && settings.model.trim() === ""
|
|
118
|
+
? "advisor_enabled_without_model"
|
|
119
|
+
: advisorContextSharingBlocked(settings)
|
|
120
|
+
? "advisor_context_sharing_consent_required"
|
|
121
|
+
: undefined;
|
|
122
|
+
return {
|
|
123
|
+
settings,
|
|
124
|
+
runnable: advisorRunnable(settings),
|
|
125
|
+
// Enabled without a model cannot consult. Enabled with a model but without current
|
|
126
|
+
// consent must not send task context; the warning names that block.
|
|
127
|
+
...(warning ? { warning } : {}),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function applyPatchInMemory(config: ManagementContext["config"], patch: AdvisorPatch): void {
|
|
132
|
+
if (patch.reset) {
|
|
133
|
+
delete config.advisor;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const current: Rec = isRec(config.advisor) ? { ...config.advisor } : {};
|
|
137
|
+
if (patch.enabled !== undefined) current.enabled = patch.enabled;
|
|
138
|
+
if (patch.model !== undefined) current.model = patch.model;
|
|
139
|
+
if (patch.effort !== undefined) current.effort = patch.effort;
|
|
140
|
+
if (patch.policy !== undefined) current.policy = patch.policy;
|
|
141
|
+
if (patch.timeoutMs !== undefined) current.timeoutMs = patch.timeoutMs;
|
|
142
|
+
if (patch.contextSharingConsent === null) delete current.contextSharingConsent;
|
|
143
|
+
else if (patch.contextSharingConsent !== undefined) current.contextSharingConsent = patch.contextSharingConsent;
|
|
144
|
+
config.advisor = current as ManagementContext["config"]["advisor"];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function isConfigLockContention(error: unknown): boolean {
|
|
148
|
+
if (!error || typeof error !== "object") return false;
|
|
149
|
+
if ((error as { code?: unknown }).code !== "CONFIG_MUTATION_LOCK_UNAVAILABLE") return false;
|
|
150
|
+
return (error as { cause?: { code?: unknown } }).cause?.code === "SQLITE_BUSY";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function handleAdvisorRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
154
|
+
const { url, req, config } = ctx;
|
|
155
|
+
if (url.pathname !== "/api/advisor/settings") return null;
|
|
156
|
+
|
|
157
|
+
if (req.method === "GET") return jsonResponse(advisorInfo(config), 200, req, config);
|
|
158
|
+
if (req.method === "PUT") return putAdvisorSettings(ctx);
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function putAdvisorSettings(ctx: ManagementContext): Promise<Response> {
|
|
163
|
+
const { req, config } = ctx;
|
|
164
|
+
|
|
165
|
+
const body = await readManagementJsonBodyOr(req, INVALID_BODY);
|
|
166
|
+
const parsed = body === INVALID_BODY
|
|
167
|
+
? { ok: false as const, code: "invalid_json", message: "body must be valid JSON" }
|
|
168
|
+
: parseAdvisorSettingsPatch(body);
|
|
169
|
+
if (!parsed.ok) return jsonResponse({ error: { code: parsed.code, message: parsed.message } }, 400, req, config);
|
|
170
|
+
|
|
171
|
+
// Resolve the writer BEFORE touching the live config: a resolution failure must leave the
|
|
172
|
+
// in-memory config exactly as it was, not rely on the snapshot restore below.
|
|
173
|
+
// `deps.` first: route tests with an in-memory fixture must never write the real config.
|
|
174
|
+
const persist = ctx.deps.saveConfigPreservingClaudeCode
|
|
175
|
+
?? (await import("../../config")).saveConfigPreservingClaudeCode;
|
|
176
|
+
const snapshot = isRec(config.advisor) ? { ...config.advisor } : undefined;
|
|
177
|
+
applyPatchInMemory(config, parsed.patch);
|
|
178
|
+
try {
|
|
179
|
+
persist(config);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
// Undo in memory too: a live config that serves a state the file does not hold would
|
|
182
|
+
// mislead every GET until the next restart.
|
|
183
|
+
if (snapshot === undefined) delete config.advisor;
|
|
184
|
+
else config.advisor = snapshot as ManagementContext["config"]["advisor"];
|
|
185
|
+
return isConfigLockContention(error)
|
|
186
|
+
? jsonResponse({ error: { code: "config_busy", message: "Another process is saving the configuration. Try again in a moment." } }, 409, req, config)
|
|
187
|
+
: jsonResponse({ error: { code: "write_failed", message: "The configuration could not be saved." } }, 500, req, config);
|
|
188
|
+
}
|
|
189
|
+
activateAdvisor(config);
|
|
190
|
+
return jsonResponse(advisorInfo(config), 200, req, config);
|
|
191
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ensureManagementClaudeIntercept, interceptStartRefusal, interceptStatus } from "./claude-intercept-routes";
|
|
2
2
|
import { persistCommittedDesktopGateway } from "../../claude/desktop-gateway-state";
|
|
3
3
|
import { captureDesktopAppliedMarker, commitDesktopAppliedMarker } from "../../claude/desktop-applied-marker";
|
|
4
|
+
import { isPassiveRead } from "../../lib/passive-read";
|
|
4
5
|
import { randomUUID } from "node:crypto";
|
|
5
6
|
import { readFileSync } from "node:fs";
|
|
6
7
|
import { dirname } from "node:path";
|
|
@@ -230,6 +231,32 @@ async function injectionModelOptions(config: OcxConfig, models: readonly Catalog
|
|
|
230
231
|
return [...nativeModels, ...routedModels];
|
|
231
232
|
}
|
|
232
233
|
|
|
234
|
+
/**
|
|
235
|
+
* The GET /api/injection-model body. `routed` is the discovered routed catalog, or `null` in a
|
|
236
|
+
* passive read (a Remote Link Child's, #6959). That body lists the native
|
|
237
|
+
* options and the current pick, which is all a read-only Child shows.
|
|
238
|
+
*/
|
|
239
|
+
export async function injectionModelView(config: OcxConfig, routed: readonly CatalogModel[] | null) {
|
|
240
|
+
const { CODEX_REASONING_LEVELS } = await import("../../reasoning-effort");
|
|
241
|
+
const available = await injectionModelOptions(config, routed ?? []);
|
|
242
|
+
const picked = config.injectionModel;
|
|
243
|
+
if (routed === null && picked && !available.some(option => option.namespaced === picked)) {
|
|
244
|
+
const slash = picked.indexOf("/");
|
|
245
|
+
available.push(slash > 0
|
|
246
|
+
? { provider: picked.slice(0, slash), model: picked.slice(slash + 1), namespaced: picked }
|
|
247
|
+
: { provider: "openai", model: picked, namespaced: picked });
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
multiAgentGuidanceEnabled: multiAgentGuidanceEnabled(config),
|
|
251
|
+
syncCodexSubagentDefaults: subagentDefaultSyncEffective(config),
|
|
252
|
+
model: config.injectionModel ?? null,
|
|
253
|
+
effort: config.injectionEffort ?? null,
|
|
254
|
+
prompt: config.injectionPrompt ?? null,
|
|
255
|
+
efforts: CODEX_REASONING_LEVELS.map(l => l.effort),
|
|
256
|
+
available,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
233
260
|
export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
234
261
|
const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
|
|
235
262
|
|
|
@@ -620,16 +647,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
620
647
|
// effort the prompt tells the agent to pass to spawn_agent. GET returns the current
|
|
621
648
|
// picks + available models/efforts; PUT sets or clears them.
|
|
622
649
|
if (url.pathname === "/api/injection-model" && req.method === "GET") {
|
|
623
|
-
|
|
624
|
-
return jsonResponse({
|
|
625
|
-
multiAgentGuidanceEnabled: multiAgentGuidanceEnabled(config),
|
|
626
|
-
syncCodexSubagentDefaults: subagentDefaultSyncEffective(config),
|
|
627
|
-
model: config.injectionModel ?? null,
|
|
628
|
-
effort: config.injectionEffort ?? null,
|
|
629
|
-
prompt: config.injectionPrompt ?? null,
|
|
630
|
-
efforts: CODEX_REASONING_LEVELS.map(l => l.effort),
|
|
631
|
-
available: await injectionModelOptions(config, await fetchAllModels(config)),
|
|
632
|
-
});
|
|
650
|
+
return jsonResponse(await injectionModelView(config, isPassiveRead() ? null : await fetchAllModels(config)));
|
|
633
651
|
}
|
|
634
652
|
// Read-only: sizes the described delegated work and proposes a model and effort from the
|
|
635
653
|
// same options GET offers. The page applies an accepted proposal through the PUT below.
|
|
@@ -1379,6 +1397,24 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1379
1397
|
}
|
|
1380
1398
|
|
|
1381
1399
|
// Claude Code inbound settings (GUI "Claude ON" toggle + Claude page).
|
|
1400
|
+
if (url.pathname === "/api/claude-code/model-defaults" && (req.method === "GET" || req.method === "POST")) {
|
|
1401
|
+
const { previewClaudeModelDefaults, applyClaudeModelDefaults, ModelDefaultsRecoveryError } = await import("../../claude/model-default-settings");
|
|
1402
|
+
try {
|
|
1403
|
+
if (req.method === "GET") return jsonResponse(previewClaudeModelDefaults(config.claudeCode?.tierModels));
|
|
1404
|
+
let body: unknown;
|
|
1405
|
+
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
1406
|
+
if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).length !== 1 ||
|
|
1407
|
+
!("fingerprint" in body) || typeof body.fingerprint !== "string" || !/^[a-f0-9]{64}$/.test(body.fingerprint)) {
|
|
1408
|
+
return jsonResponse({ error: "fingerprint is required" }, 400);
|
|
1409
|
+
}
|
|
1410
|
+
return jsonResponse(applyClaudeModelDefaults(config.claudeCode?.tierModels, body.fingerprint));
|
|
1411
|
+
} catch (error) {
|
|
1412
|
+
rethrowManagementBodyTooLarge(error);
|
|
1413
|
+
if (error instanceof ModelDefaultsRecoveryError) return jsonResponse({ error: error.message, code: error.message, backupPath: error.backupPath }, 500);
|
|
1414
|
+
const code = error instanceof Error && error.message === "settings_changed" ? "settings_changed" : "settings_unavailable";
|
|
1415
|
+
return jsonResponse({ error: code, code }, code === "settings_changed" ? 409 : 500);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1382
1418
|
if (url.pathname === "/api/claude-code" && req.method === "GET") {
|
|
1383
1419
|
const models = await fetchAllModels(config);
|
|
1384
1420
|
const { listCatalogNativeSlugs } = await import("../../codex/catalog");
|
|
@@ -1495,10 +1531,10 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1495
1531
|
});
|
|
1496
1532
|
}
|
|
1497
1533
|
if (url.pathname === "/api/claude-code" && req.method === "PUT") {
|
|
1498
|
-
// NOTE: model /
|
|
1534
|
+
// NOTE: model / maxContextTokens / alwaysEnableEffort are
|
|
1499
1535
|
// CONFIG-ONLY back-compat fields — the GUI no longer offers controls for them
|
|
1500
|
-
// (default model is owned by Claude Code's /model picker;
|
|
1501
|
-
//
|
|
1536
|
+
// (default model is owned by Claude Code's /model picker;
|
|
1537
|
+
// auto-context supersedes the max-context pair; effort rides
|
|
1502
1538
|
// regardless on 2.1.207). PUT keeps validating them so hand-written configs
|
|
1503
1539
|
// and older GUIs stay safe; GUI saves omit them and the spread preserves them.
|
|
1504
1540
|
let parsedBody: unknown;
|
|
@@ -1817,7 +1853,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1817
1853
|
}
|
|
1818
1854
|
}
|
|
1819
1855
|
if (body.tierModels !== undefined) {
|
|
1820
|
-
//
|
|
1856
|
+
// Family defaults also serve third-party callers that request native tier aliases.
|
|
1821
1857
|
if (body.tierModels === null) {
|
|
1822
1858
|
delete next.tierModels;
|
|
1823
1859
|
} else if (!isPlainObject(body.tierModels)) {
|
|
@@ -19,6 +19,7 @@ import { captureAnthropicPhysicalSendOwnership, anthropicPhysicalSendOwnershipIs
|
|
|
19
19
|
import { anthropicCooldownRecoveryFor } from "../../providers/quota/anthropic-cooldown-recovery";
|
|
20
20
|
import { captureProviderAccountQuotaEpoch } from "../../providers/quota/account-cache";
|
|
21
21
|
import { captureOAuthAccountSelection, getAccountSet, getAccountCredentialWithStatus, listAccounts } from "../../oauth/store";
|
|
22
|
+
import { admitAnthropicAccountAfterReset, type AnthropicResetAdmissionEvidence } from "../../providers/anthropic-reset-admission";
|
|
22
23
|
import {
|
|
23
24
|
ANTHROPIC_RESET_GRANT_ID_RE,
|
|
24
25
|
AnthropicResetGrantError,
|
|
@@ -74,6 +75,8 @@ export interface AnthropicResetGrantRouteDeps {
|
|
|
74
75
|
listAccountIds: () => string[];
|
|
75
76
|
activeAccountId: () => string | null;
|
|
76
77
|
accessTokenFor: (accountId: string) => Promise<string>;
|
|
78
|
+
/** Lift local admission after a journaled reset, bounded by its journal evidence. Tests omit it. */
|
|
79
|
+
admitReset?: (accountId: string, evidence: AnthropicResetAdmissionEvidence) => Promise<void>;
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
function defaultDepsFor(ctx: ManagementContext, provider: AnthropicInstanceId): AnthropicResetGrantRouteDeps | null {
|
|
@@ -109,6 +112,7 @@ function defaultDepsFor(ctx: ManagementContext, provider: AnthropicInstanceId):
|
|
|
109
112
|
if (!targetCurrent() || !owner) throw new Error("reset owner changed");
|
|
110
113
|
return snapshot.accessToken;
|
|
111
114
|
},
|
|
115
|
+
admitReset: (accountId, evidence) => admitAnthropicAccountAfterReset(provider, accountId, evidence),
|
|
112
116
|
};
|
|
113
117
|
}
|
|
114
118
|
|
|
@@ -209,6 +213,16 @@ async function handleConsume(ctx: ManagementContext, injected?: AnthropicResetGr
|
|
|
209
213
|
}
|
|
210
214
|
if (deps.isCurrent?.() === false) return scopedFail(401, "auth_failed");
|
|
211
215
|
const ledger = { journalPath: deps.journalPath, now: deps.now?.() };
|
|
216
|
+
// Retry-after cooldowns survive a usage probe. A journaled reset, including a
|
|
217
|
+
// replay after the process died before admission, has to lift this account,
|
|
218
|
+
// but only refusals recorded before the claim was journaled and only for the
|
|
219
|
+
// windows the journal says were cleared. A failed reread must not turn a
|
|
220
|
+
// spent grant into an error.
|
|
221
|
+
const admitJournaledReset = async (code: string, evidence: AnthropicResetAdmissionEvidence): Promise<void> => {
|
|
222
|
+
if (code !== "reset" || deps.isCurrent?.() === false) return;
|
|
223
|
+
try { await deps.admitReset?.(accountId, evidence); }
|
|
224
|
+
catch { /* The grant is already spent. */ }
|
|
225
|
+
};
|
|
212
226
|
|
|
213
227
|
// A new operation must pass the spend gate on a fresh read. A same-id retry of
|
|
214
228
|
// an open record skips it: that attempt was already gated, and upstream
|
|
@@ -242,6 +256,7 @@ async function handleConsume(ctx: ManagementContext, injected?: AnthropicResetGr
|
|
|
242
256
|
}
|
|
243
257
|
switch (begin.kind) {
|
|
244
258
|
case "replay":
|
|
259
|
+
await admitJournaledReset(begin.code, { claimedAt: begin.claimedAt, cleared: begin.cleared });
|
|
245
260
|
return jsonResponse(
|
|
246
261
|
{ provider, code: begin.code, replayed: true, resetsLeft: begin.resetsLeft, accountId, grantId, operationId },
|
|
247
262
|
200, ctx.req, ctx.config,
|
|
@@ -268,7 +283,7 @@ async function handleConsume(ctx: ManagementContext, injected?: AnthropicResetGr
|
|
|
268
283
|
let settled;
|
|
269
284
|
try {
|
|
270
285
|
settled = settleAnthropicResetOperation(
|
|
271
|
-
{ operationId, code: answer.code, resetsLeft: answer.resetsLeft },
|
|
286
|
+
{ operationId, code: answer.code, resetsLeft: answer.resetsLeft, cleared: answer.cleared },
|
|
272
287
|
{ journalPath: deps.journalPath, now: deps.now?.() },
|
|
273
288
|
);
|
|
274
289
|
} catch {
|
|
@@ -278,6 +293,7 @@ async function handleConsume(ctx: ManagementContext, injected?: AnthropicResetGr
|
|
|
278
293
|
// Report what the journal holds. An overlapping same-id attempt may have
|
|
279
294
|
// settled first; its answer is the canonical one.
|
|
280
295
|
const replayed = settled.code !== answer.code || settled.resetsLeft !== answer.resetsLeft;
|
|
296
|
+
await admitJournaledReset(settled.code, { claimedAt: settled.claimedAt, cleared: settled.cleared });
|
|
281
297
|
return jsonResponse(
|
|
282
298
|
{
|
|
283
299
|
provider,
|
|
@@ -96,12 +96,18 @@ export async function handleCodexAgentRoleRoutes(ctx: ManagementContext): Promis
|
|
|
96
96
|
return jsonResponse({ error: "invalid JSON body" }, 400, req, config);
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
// An omitted model leaves the role's model pin (or its inheritance) alone, an omitted effort
|
|
100
|
+
// keeps the effort, and an effort of null clears it. A body with neither changes nothing, which
|
|
101
|
+
// is a client mistake rather than a save, so it is refused before any role file is read.
|
|
102
|
+
if (body?.model === undefined && body?.effort === undefined) {
|
|
103
|
+
return jsonResponse({ error: "send a model, an effort, or both", code: "empty_update" }, 400, req, config);
|
|
104
|
+
}
|
|
105
|
+
let model: string | undefined;
|
|
106
|
+
let effort: string | null | undefined;
|
|
101
107
|
let toml: { status: "written" | "unchanged" };
|
|
102
108
|
try {
|
|
103
|
-
model = roles.validateAgentRoleModel(body
|
|
104
|
-
effort = body?.effort === undefined
|
|
109
|
+
model = body?.model === undefined ? undefined : roles.validateAgentRoleModel(body.model);
|
|
110
|
+
effort = body?.effort === undefined ? undefined : body.effort === null ? null : roles.validateAgentRoleEffort(body.effort);
|
|
105
111
|
toml = roles.writeCodexAgentRoleModel(role, model, codexHome, effort);
|
|
106
112
|
} catch (error) {
|
|
107
113
|
if (error instanceof roles.AgentRoleModelError) {
|
|
@@ -115,10 +121,17 @@ export async function handleCodexAgentRoleRoutes(ctx: ManagementContext): Promis
|
|
|
115
121
|
// The role TOML is what Codex obeys, so its write stands even when the omo mirror cannot follow.
|
|
116
122
|
let omoStatus: ReturnType<typeof omo.writeOmoRoleModel> | "write_failed";
|
|
117
123
|
try {
|
|
118
|
-
const reasoning = effort === undefined ? undefined : omo.omoReasoningFor(effort);
|
|
124
|
+
const reasoning = effort === undefined ? undefined : effort === null ? null : omo.omoReasoningFor(effort);
|
|
119
125
|
omoStatus = omo.writeOmoRoleModel(role, model, omo.omoJsoncPath(), reasoning);
|
|
120
126
|
} catch {
|
|
121
127
|
omoStatus = "write_failed";
|
|
122
128
|
}
|
|
123
|
-
return jsonResponse({
|
|
129
|
+
return jsonResponse({
|
|
130
|
+
ok: true,
|
|
131
|
+
role,
|
|
132
|
+
...(model !== undefined ? { model } : {}),
|
|
133
|
+
...(effort !== undefined ? { effort } : {}),
|
|
134
|
+
toml,
|
|
135
|
+
omoJsonc: { status: omoStatus },
|
|
136
|
+
}, 200, req, config);
|
|
124
137
|
}
|