@bitkyc08/opencodex 2.79.0 → 2.81.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 +3 -3
- package/gui/dist/assets/App-7yhoys8a.js +54 -0
- package/gui/dist/assets/Tray-EwAOsP0S.js +1 -0
- package/gui/dist/assets/index-RsoaAxXY.js +94 -0
- package/gui/dist/assets/quota-summary-D2lN8tDm.js +1 -0
- package/gui/dist/index.html +1 -1
- package/gui/dist/provider-icons/claude-green.svg +1 -0
- package/package.json +3 -2
- package/src/adapters/anthropic-model-contract.ts +8 -2
- package/src/adapters/anthropic.ts +7 -2
- package/src/adapters/codebuddy/mcp-server.ts +2 -25
- package/src/adapters/codebuddy/tool-bridge.ts +1 -7
- package/src/adapters/coding-agent/mcp-server.ts +33 -0
- package/src/adapters/coding-agent/protocol.ts +122 -17
- package/src/adapters/coding-agent/tool-catalog.ts +75 -0
- package/src/adapters/coding-agent/turn.ts +101 -44
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +2 -0
- package/src/adapters/cursor/live-transport.ts +5 -3
- package/src/adapters/cursor/native-exec-fs.ts +12 -9
- package/src/adapters/cursor/native-exec.ts +13 -5
- package/src/adapters/cursor/protobuf-request.ts +13 -7
- package/src/adapters/cursor/request-builder.ts +23 -10
- package/src/adapters/cursor/tool-definitions.ts +9 -6
- package/src/adapters/cursor/tool-guidance.ts +29 -8
- package/src/adapters/cursor/tool-naming.ts +40 -7
- package/src/adapters/cursor/tool-wording.ts +46 -0
- package/src/adapters/cursor.ts +3 -0
- package/src/adapters/devin/live-models.ts +2 -0
- package/src/adapters/devin.ts +25 -11
- package/src/adapters/kiro-retry.ts +9 -4
- package/src/adapters/openai-chat/messages.ts +7 -4
- package/src/adapters/physical-send.ts +8 -5
- package/src/adapters/qoder/adapter.ts +41 -6
- package/src/adapters/qoder/scaffold-guard.ts +2 -3
- package/src/adapters/zed.ts +16 -1
- package/src/claude/inbound-cache-stabilize.ts +4 -9
- package/src/claude/inbound.ts +10 -0
- package/src/claude/intercept/model-bindings.ts +1 -0
- package/src/claude/intercept/picker-listener.ts +71 -4
- package/src/cli/account-anthropic-threshold.ts +14 -5
- package/src/cli/account-api.ts +3 -1
- package/src/cli/account-extended.ts +13 -9
- package/src/cli/account-policy-dto.ts +1 -1
- package/src/cli/account-policy.ts +13 -4
- package/src/cli/account.ts +3 -3
- package/src/cli/capabilities-accounts.ts +2 -2
- package/src/cli/capabilities-agents-routing.ts +24 -0
- package/src/cli/capabilities-integrations.ts +16 -0
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/dispatch.ts +10 -1
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +11 -5
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/integration-preview.ts +8 -1
- package/src/cli/integrations.ts +32 -3
- package/src/cli/message-args.ts +34 -0
- package/src/cli/message-command.ts +43 -0
- package/src/cli/message-runtime.ts +6 -0
- package/src/cli/observe.ts +3 -1
- package/src/cli/provider.ts +29 -6
- package/src/cli/registry.ts +24 -2
- package/src/clients/config-export/commandcode.ts +131 -0
- package/src/clients/config-export/contracts.ts +12 -0
- package/src/clients/config-export.ts +64 -1
- package/src/codex/auth-api/account-list.ts +11 -3
- package/src/codex/auth-api/main-account-probe.ts +25 -8
- package/src/codex/auth-api/pool-mode-gate.ts +33 -9
- package/src/codex/auth-api/pool-quota-probe.ts +15 -1
- package/src/codex/auth-api/routes.ts +1 -0
- package/src/codex/autostart-health.ts +3 -3
- package/src/codex/catalog/auto-review.ts +13 -6
- package/src/codex/catalog/build-entries.ts +15 -7
- package/src/codex/catalog/control-plane.ts +45 -0
- package/src/codex/catalog/effort.ts +11 -1
- package/src/codex/catalog/gather-capture.ts +5 -3
- package/src/codex/catalog/parsing.ts +2 -1
- package/src/codex/catalog/provider-models.ts +15 -9
- package/src/codex/control-plane-models.ts +6 -0
- package/src/codex/journal.ts +36 -5
- package/src/codex/shim-overlay.ts +9 -1
- package/src/combos/jev-decision-contract.ts +25 -2
- package/src/combos/jev.ts +6 -3
- package/src/combos/types.ts +1 -1
- package/src/config/diagnostics.ts +27 -16
- package/src/config/load-degrade.ts +34 -0
- package/src/config/schema/anthropic-account-pool.ts +143 -0
- package/src/config/schema/config-schema.ts +7 -1
- package/src/config/schema/leaf-validators.ts +4 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +347 -199
- package/src/generated/model-metadata.ts +5 -4
- package/src/images/loop.ts +2 -1
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +7 -0
- package/src/integrations/registry.ts +8 -0
- package/src/integrations/state.ts +9 -2
- package/src/integrations/writer.ts +8 -2
- package/src/lib/lab-live-route-production.ts +9 -1
- package/src/lib/request-execution-budget.ts +236 -58
- package/src/lib/spend-pool-alias-validation.ts +79 -0
- package/src/lib/spend-pool-continuity.ts +34 -0
- package/src/lib/spend-reservation-ledger.ts +412 -50
- package/src/lib/state-store-registrations.ts +20 -3
- package/src/lib/upstream-retry.ts +17 -5
- package/src/lib/workflow-budget.ts +53 -11
- package/src/messaging/budget.ts +45 -0
- package/src/messaging/discovery.ts +57 -0
- package/src/messaging/envelope.ts +49 -0
- package/src/messaging/input.ts +37 -0
- package/src/messaging/rpc.ts +196 -0
- package/src/messaging/send.ts +58 -0
- package/src/messaging/socket.ts +72 -0
- package/src/messaging/types.ts +29 -0
- package/src/oauth/anthropic-account-refusal.ts +51 -17
- package/src/oauth/anthropic-account-threshold.ts +8 -2
- package/src/oauth/anthropic-continuity.ts +15 -1
- package/src/oauth/anthropic-model-quota.ts +120 -48
- package/src/oauth/anthropic-model-routes.ts +11 -3
- package/src/oauth/anthropic-oauth-definitions.ts +98 -0
- package/src/oauth/anthropic-pool-config.ts +30 -0
- package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
- package/src/oauth/anthropic-routing.ts +1202 -866
- package/src/oauth/anthropic-send-ownership.ts +41 -0
- package/src/oauth/anthropic.ts +4 -1
- package/src/oauth/generic-account-failover.ts +3 -1
- package/src/oauth/health.ts +8 -6
- package/src/oauth/index.ts +90 -173
- package/src/oauth/kiro.ts +22 -9
- package/src/oauth/login-cli.ts +2 -0
- package/src/oauth/model-discovery-auth.ts +43 -0
- package/src/oauth/pool-kernel.ts +11 -5
- package/src/oauth/pool-settings-capability.ts +5 -3
- package/src/oauth/store-anthropic-instance.ts +87 -0
- package/src/oauth/store.ts +67 -9
- package/src/oauth/token-guardian.ts +2 -0
- package/src/oauth/types.ts +2 -0
- package/src/protocols/plan-snapshot.ts +5 -0
- package/src/protocols/settings.ts +20 -5
- package/src/providers/anthropic-instance-id.ts +47 -0
- package/src/providers/anthropic-instance.ts +48 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
- package/src/providers/derive.ts +2 -0
- package/src/providers/kiro-models.ts +3 -0
- package/src/providers/label.ts +1 -1
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/quota/account-cache.ts +37 -11
- package/src/providers/quota/anthropic-account-quota.ts +93 -0
- package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
- package/src/providers/quota/report-cache.ts +24 -1
- package/src/providers/quota/vendor-probes-oauth.ts +46 -14
- package/src/providers/quota.ts +22 -32
- package/src/providers/registry/entries-core.ts +35 -30
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/model-seeds.ts +7 -4
- package/src/providers/registry/types.ts +3 -0
- package/src/providers/registry.ts +3 -3
- package/src/responses/spill-store.ts +5 -1
- package/src/responses/state/metrics.ts +87 -0
- package/src/responses/state/snapshot-codec.ts +6 -0
- package/src/responses/state/spill-queue.ts +1 -0
- package/src/responses/state/unforced-store-false.ts +55 -0
- package/src/responses/state.ts +50 -66
- package/src/router.ts +28 -4
- package/src/routing/compatibility/assemble.ts +1 -1
- package/src/routing/quota.ts +7 -3
- package/src/server/auth-cors.ts +5 -0
- package/src/server/chat-completions.ts +6 -1
- package/src/server/chat-native.ts +45 -25
- package/src/server/claude-messages.ts +16 -2
- package/src/server/gui-static.ts +38 -4
- package/src/server/index/spend-ledger-lifecycle.ts +10 -4
- package/src/server/index.ts +16 -16
- package/src/server/inference/context.ts +1 -1
- package/src/server/lifecycle.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +30 -6
- package/src/server/management/anthropic-account-threshold.ts +12 -9
- package/src/server/management/anthropic-pool-settings.ts +72 -0
- package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
- package/src/server/management/config-routes.ts +27 -7
- package/src/server/management/decision-routes.ts +2 -1
- package/src/server/management/integration-routes.ts +25 -2
- package/src/server/management/oauth-account-routes.ts +79 -98
- package/src/server/management/provider-instance-ownership.ts +44 -0
- package/src/server/management/provider-routes.ts +21 -28
- package/src/server/management/route-registry.ts +3 -1
- package/src/server/management/routing-profile-routes.ts +4 -4
- package/src/server/management/system-restart.ts +44 -14
- package/src/server/management/system-routes.ts +24 -2
- package/src/server/management/vision-sidecar-options.ts +9 -6
- package/src/server/management/web-search-sidecar-options.ts +51 -2
- package/src/server/management-auth.ts +6 -0
- package/src/server/messages-native-eligibility.ts +3 -2
- package/src/server/messages-native-oauth.ts +86 -41
- package/src/server/messages-native-scope.ts +4 -1
- package/src/server/messages-native-selector.ts +39 -0
- package/src/server/messages-native.ts +84 -25
- package/src/server/request-log.ts +5 -0
- package/src/server/responses/adapter-continuation.ts +29 -17
- package/src/server/responses/adapter-dispatch.ts +47 -17
- package/src/server/responses/antigravity-validation-refusal.ts +20 -6
- package/src/server/responses/combo-requested-effort.ts +14 -0
- package/src/server/responses/compact.ts +25 -8
- package/src/server/responses/compaction-recovery.ts +1 -1
- package/src/server/responses/core-combo-native.ts +4 -1
- package/src/server/responses/core-combo.ts +21 -18
- package/src/server/responses/core-normalize.ts +1 -0
- package/src/server/responses/core-options.ts +3 -1
- package/src/server/responses/core-replay.ts +5 -0
- package/src/server/responses/passthrough-delivery.ts +7 -3
- package/src/server/responses/passthrough-dispatch.ts +21 -8
- package/src/server/responses/request-prepare.ts +37 -5
- package/src/server/responses/request-send-budget.ts +74 -37
- package/src/server/responses/request-spend.ts +325 -33
- package/src/server/responses/request-transport.ts +132 -52
- package/src/server/responses/run-turn-execution.ts +11 -3
- package/src/server/responses/sidecar-execution.ts +96 -14
- package/src/server/responses-undeclared-tool-guard.ts +5 -1
- package/src/server/workflow-refusal.ts +30 -0
- package/src/service/desktop-startup.ts +100 -3
- package/src/service/ownership-mutation-lease.d.mts +1 -1
- package/src/service/ownership-mutation-lease.mjs +5 -7
- package/src/sidecar/anthropic-binding.ts +79 -0
- package/src/sidecar/auth.ts +76 -2
- package/src/types/anthropic-account-pool.ts +33 -0
- package/src/types/config.ts +12 -28
- package/src/types/provider.ts +5 -0
- package/src/types/tools.ts +7 -0
- package/src/types.ts +4 -0
- package/src/usage/cost.ts +3 -2
- package/src/usage/expected-prices.ts +66 -14
- package/src/vision/anthropic-describe.ts +16 -3
- package/src/vision/index.ts +3 -0
- package/src/vision/plan.ts +19 -6
- package/src/web-search/alpha-search.ts +7 -1
- package/src/web-search/anthropic-executor.ts +16 -4
- package/src/web-search/header-deadline.ts +67 -0
- package/src/web-search/index.ts +7 -3
- package/src/web-search/loop.ts +32 -10
- package/src/web-search/passthrough-bridge.ts +4 -1
- package/src/web-search/sidecar-providers.ts +4 -3
- package/gui/dist/assets/App-BOUa-ek3.js +0 -54
- package/gui/dist/assets/Tray-DUtzB4zF.js +0 -1
- package/gui/dist/assets/index-DwHNDF4s.js +0 -94
- package/gui/dist/assets/quota-summary-BuL4WIBo.js +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { rotateAnthropicAccountOnResponseForInstance } from "../../oauth/anthropic-account-refusal";
|
|
2
2
|
import type { ResponsesRequestContext } from "./core-options";
|
|
3
3
|
import type { PreparedResponsesRequest } from "./request-prepare";
|
|
4
4
|
import type { ResponsesTransport } from "./request-transport";
|
|
@@ -24,20 +24,22 @@ import {
|
|
|
24
24
|
isGenericOAuthFailoverEnabled,
|
|
25
25
|
rotateGenericOAuthAccountOn429,
|
|
26
26
|
rotateGenericOAuthAccountOnRefusal,
|
|
27
|
+
rotateAntigravityAccountOnAuthRefusal,
|
|
27
28
|
quarantineKiroSuspendedAccount,
|
|
28
29
|
failoverAccountSnapshot,
|
|
29
30
|
} from "../../oauth/generic-account-failover";
|
|
30
31
|
import { classifyKiroRefusal } from "../../adapters/kiro-refusal";
|
|
32
|
+
import { isNonReplayableResponse } from "../../lib/upstream-retry";
|
|
31
33
|
import { noteKiroMonthlyRefusal, noteKiroServedSuccess } from "../../providers/kiro-usage";
|
|
32
34
|
import { persistKiroAccountState } from "../../providers/kiro-account-state-disk";
|
|
33
35
|
import { readDisplaySafeErrorText } from "./core-errors";
|
|
34
36
|
import {
|
|
35
37
|
ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
|
|
36
|
-
|
|
38
|
+
anthropicRoutingFor,
|
|
37
39
|
formatAnthropicProviderForLog,
|
|
38
40
|
} from "../../oauth/anthropic-routing";
|
|
39
41
|
import { resolveWireProtocolOverride } from "../adapter-resolve";
|
|
40
|
-
import { bindRouteReasoningReplayScope, adapterNeedsForcedContinuation } from "./core-replay";
|
|
42
|
+
import { bindRouteReasoningReplayScope, adapterNeedsForcedContinuation, adapterNeedsToolCallContinuation } from "./core-replay";
|
|
41
43
|
import { namespacedToolName } from "../../types";
|
|
42
44
|
import { providerFetch } from "./fetch-helpers";
|
|
43
45
|
import type { AttemptRecoveryKind } from "../../usage/log";
|
|
@@ -45,6 +47,7 @@ import { recordAdapterReasoning, recordAdapterTier } from "../request-log";
|
|
|
45
47
|
import { normalizeLogConversationId } from "../request-log-conversation";
|
|
46
48
|
import { rememberResponseState } from "../../responses/state";
|
|
47
49
|
import { trackStreamLifetime } from "../lifecycle";
|
|
50
|
+
import { isAntigravityRawValidationRefusal } from "./antigravity-validation-refusal";
|
|
48
51
|
|
|
49
52
|
/** One responsibility of the Responses request pipeline; state owners are explicit. */
|
|
50
53
|
export async function executeResponsesSidecars(
|
|
@@ -67,6 +70,8 @@ export async function executeResponsesSidecars(
|
|
|
67
70
|
| "genericFailoverLimit"
|
|
68
71
|
| "replayOAuthCredentialSnapshot"
|
|
69
72
|
| "applyFailoverSnapshot"
|
|
73
|
+
| "anthropicInstance"
|
|
74
|
+
| "currentAnthropicRouteDecision"
|
|
70
75
|
| "anthropicRouteDecision"
|
|
71
76
|
| "anthropicPoolAccountId"
|
|
72
77
|
| "anthropicPoolFailovers"
|
|
@@ -85,11 +90,15 @@ export async function executeResponsesSidecars(
|
|
|
85
90
|
| "notifyResponseComplete"
|
|
86
91
|
| "cancelResponseCompletion"
|
|
87
92
|
>,
|
|
88
|
-
sendBudgetState: Pick<ResponsesSendBudget, "reserveCredentialHop">,
|
|
93
|
+
sendBudgetState: Pick<ResponsesSendBudget, "reserveCredentialHop" | "refundableAdapterDispatchBudget" | "pendingHopPermit" | "noteAdapterPhysicalSend">,
|
|
89
94
|
) {
|
|
90
95
|
const { config, options, logCtx } = requestContext;
|
|
96
|
+
const antigravityPoolActivated = requestState.route.providerName === "google-antigravity"
|
|
97
|
+
&& isGenericOAuthFailoverEnabled(config, requestState.route.providerName);
|
|
91
98
|
const {
|
|
92
99
|
applyFailoverSnapshot,
|
|
100
|
+
anthropicInstance,
|
|
101
|
+
currentAnthropicRouteDecision,
|
|
93
102
|
anthropicSessionKey,
|
|
94
103
|
commitResolvedOAuthSelection,
|
|
95
104
|
resolveSelectionAdapter,
|
|
@@ -182,13 +191,29 @@ export async function executeResponsesSidecars(
|
|
|
182
191
|
|| (logCtx.activeAttempt?.deliverySummary?.semanticBytes ?? 0) > 0
|
|
183
192
|
|| (logCtx.activeAttempt?.deliverySummary?.sideEffectEvents ?? 0) > 0;
|
|
184
193
|
const noteSidecarOutput = () => { sidecarOutputStarted = true; options.onFirstOutput?.(); };
|
|
194
|
+
// A single request may rotate once for a verification refusal. Keep this separate from the
|
|
195
|
+
// broader OAuth failover count: a sidecar turn can also spend that count on rate-limit recovery.
|
|
196
|
+
let antigravityValidationRotationAttempted = false;
|
|
185
197
|
const rotateSidecarProviderOn429 = async (
|
|
186
198
|
retryAfter: string | null,
|
|
187
199
|
responseHeaders?: Headers,
|
|
188
200
|
retryParsed?: OcxParsedRequest,
|
|
189
201
|
originalResponse?: Response,
|
|
190
202
|
): Promise<{ adapter: ProviderAdapter; recoveryKind: AttemptRecoveryKind } | null> => {
|
|
191
|
-
|
|
203
|
+
const antigravityValidationResponse = canRunWebSearch && route.providerName === "google-antigravity"
|
|
204
|
+
&& route.provider.authMode === "oauth" && originalResponse?.status === 403;
|
|
205
|
+
if (route.providerName !== "kiro" && !(anthropicInstance && originalResponse?.status === 403)
|
|
206
|
+
&& !antigravityValidationResponse && originalResponse && originalResponse.status !== 429) return null;
|
|
207
|
+
let antigravityVerification = false;
|
|
208
|
+
if (antigravityValidationResponse) {
|
|
209
|
+
if (transportState.adapter.name !== "google" || !antigravityPoolActivated
|
|
210
|
+
|| options.abortSignal?.aborted || isNonReplayableResponse(originalResponse)
|
|
211
|
+
|| antigravityValidationRotationAttempted
|
|
212
|
+
|| transportState.genericFailovers >= transportState.genericFailoverLimit
|
|
213
|
+
|| sidecarHasCommittedOutput()) return null;
|
|
214
|
+
antigravityVerification = await isAntigravityRawValidationRefusal(originalResponse, options.abortSignal);
|
|
215
|
+
if (!antigravityVerification) return null;
|
|
216
|
+
}
|
|
192
217
|
const refusal = route.providerName === "kiro" && originalResponse
|
|
193
218
|
? classifyKiroRefusal(originalResponse.status,
|
|
194
219
|
await readDisplaySafeErrorText(originalResponse.clone(), options.abortSignal ?? new AbortController().signal, "")).kind
|
|
@@ -205,6 +230,42 @@ export async function executeResponsesSidecars(
|
|
|
205
230
|
// sidecar loops used to flatten all three to `key-429`, so an account rotation read as a key
|
|
206
231
|
// rotation in the attempt row and in the Logs UI.
|
|
207
232
|
let recoveryKind: AttemptRecoveryKind = "key-429";
|
|
233
|
+
if (antigravityVerification) {
|
|
234
|
+
const sent = transportState.replayOAuthCredentialSnapshot;
|
|
235
|
+
const failedAccountId = transportState.genericFailoverAccountId;
|
|
236
|
+
if (!sent || !failedAccountId || sent.accountId !== failedAccountId) return null;
|
|
237
|
+
if (options.abortSignal?.aborted || sidecarHasCommittedOutput()) return null;
|
|
238
|
+
|
|
239
|
+
const hop = reserveCredentialHop(
|
|
240
|
+
"auth-recovery",
|
|
241
|
+
`${route.providerName}|${route.modelId}|sidecar-oauth-verify`,
|
|
242
|
+
);
|
|
243
|
+
if (!hop.allowed) return null;
|
|
244
|
+
antigravityValidationRotationAttempted = true;
|
|
245
|
+
const nextAccountId = rotateAntigravityAccountOnAuthRefusal(
|
|
246
|
+
antigravityPoolActivated, sent.accountId, sent.generation, route.modelId,
|
|
247
|
+
);
|
|
248
|
+
if (!nextAccountId) {
|
|
249
|
+
hop.permit?.release();
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
// Keep the sibling's bearer and its account-matched project together; never inherit the
|
|
254
|
+
// failed account's identity or project.
|
|
255
|
+
const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
|
|
256
|
+
if (options.abortSignal?.aborted || !await applyFailoverSnapshot(snapshot, retryParsed)
|
|
257
|
+
|| transportState.replayOAuthCredentialSnapshot?.accountId !== nextAccountId) {
|
|
258
|
+
hop.permit?.release();
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
transportState.genericFailovers += 1;
|
|
262
|
+
} catch {
|
|
263
|
+
hop.permit?.release();
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
recoveryKind = "oauth-account-403";
|
|
267
|
+
sendBudgetState.pendingHopPermit = hop.permit;
|
|
268
|
+
}
|
|
208
269
|
const rotated = !originalResponse || originalResponse.status === 429
|
|
209
270
|
? rotateProviderTransportOn429(config, route.providerName, route.provider, {
|
|
210
271
|
retryAfter,
|
|
@@ -212,7 +273,9 @@ export async function executeResponsesSidecars(
|
|
|
212
273
|
attemptedKey: route.provider.apiKey,
|
|
213
274
|
promptCacheKey: parsed.options.promptCacheKey,
|
|
214
275
|
}) : null;
|
|
215
|
-
if (
|
|
276
|
+
if (antigravityVerification) {
|
|
277
|
+
// `applyFailoverSnapshot` has already rebound the route to the sibling identity/project.
|
|
278
|
+
} else if (rotated) {
|
|
216
279
|
route.provider = rotated;
|
|
217
280
|
} else if (
|
|
218
281
|
// A POSITIVE gate, not an early return. An early `return null` here made every later arm
|
|
@@ -225,7 +288,8 @@ export async function executeResponsesSidecars(
|
|
|
225
288
|
) {
|
|
226
289
|
// Intersection with the request's shared budget. The sidecar replay is dispatched by the
|
|
227
290
|
// web-search/image loop and never reaches `onSendsConsumed`, so this reservation is the
|
|
228
|
-
// charge
|
|
291
|
+
// charge for other sidecars. Antigravity web search hands its reservation to the adapter
|
|
292
|
+
// physical-send owner instead; a refusal preserves the real 429.
|
|
229
293
|
const hop = reserveCredentialHop(
|
|
230
294
|
"auth-recovery",
|
|
231
295
|
`${route.providerName}|${route.modelId}|sidecar-oauth-429`,
|
|
@@ -259,12 +323,13 @@ export async function executeResponsesSidecars(
|
|
|
259
323
|
return null;
|
|
260
324
|
}
|
|
261
325
|
recoveryKind = "oauth-account-429";
|
|
262
|
-
hop.permit
|
|
326
|
+
if (canRunWebSearch && route.providerName === "google-antigravity") sendBudgetState.pendingHopPermit = hop.permit;
|
|
327
|
+
else hop.permit?.use();
|
|
263
328
|
} else if (
|
|
264
329
|
// Anthropic's pool is excluded from generic failover, so without this arm a 429 inside a
|
|
265
330
|
// web-search or image-bridge turn was terminal even with the pool fully enabled -- while
|
|
266
331
|
// the very same 429 on the main response path rotated.
|
|
267
|
-
transportState.anthropicPoolAccountId
|
|
332
|
+
anthropicInstance && transportState.anthropicPoolAccountId
|
|
268
333
|
) {
|
|
269
334
|
// Same intersection for the Anthropic roster: its own per-request bound still applies,
|
|
270
335
|
// and the shared budget decides whether this request may spend another send at all.
|
|
@@ -272,10 +337,10 @@ export async function executeResponsesSidecars(
|
|
|
272
337
|
"auth-recovery",
|
|
273
338
|
`${route.providerName}|${route.modelId}|sidecar-anthropic-429`,
|
|
274
339
|
);
|
|
275
|
-
const nextAccountId = await
|
|
340
|
+
const nextAccountId = await rotateAnthropicAccountOnResponseForInstance(anthropicInstance,
|
|
276
341
|
originalResponse ?? new Response(null, { status: 429, headers: responseHeaders ?? (retryAfter ? { "retry-after": retryAfter } : undefined) }), {
|
|
277
342
|
config, accountId: transportState.anthropicPoolAccountId, sessionKey: anthropicSessionKey,
|
|
278
|
-
model: route.modelId, requestKey: transportState, decision: transportState.anthropicRouteDecision, signal: options.abortSignal,
|
|
343
|
+
model: route.modelId, requestKey: transportState, decision: transportState.anthropicRouteDecision, currentDecision: currentAnthropicRouteDecision, signal: options.abortSignal,
|
|
279
344
|
allow429Recovery: !sidecarHasCommittedOutput(), allowAccountRefusal: !sidecarHasCommittedOutput(),
|
|
280
345
|
canRetry: hop.allowed && transportState.anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
|
|
281
346
|
});
|
|
@@ -289,12 +354,12 @@ export async function executeResponsesSidecars(
|
|
|
289
354
|
// carries none, and getAnthropicPoolAccessToken is what enforces its fail-closed
|
|
290
355
|
// local-cli credential rule. Both existing Anthropic rotation sites apply the token the
|
|
291
356
|
// same way.
|
|
292
|
-
const admitted = await commitResolvedOAuthSelection(await getAnthropicPoolAccessSnapshot(nextAccountId));
|
|
357
|
+
const admitted = await commitResolvedOAuthSelection(await anthropicRoutingFor(anthropicInstance).getAnthropicPoolAccessSnapshot(nextAccountId));
|
|
293
358
|
if (!admitted) throw new Error("OAuth selection changed during recovery");
|
|
294
359
|
transportState.anthropicPoolAccountId = admitted.accountId;
|
|
295
360
|
transportState.anthropicPoolFailovers += 1;
|
|
296
361
|
route.provider = { ...route.provider, apiKey: admitted.accessToken };
|
|
297
|
-
logCtx.provider = formatAnthropicProviderForLog(
|
|
362
|
+
logCtx.provider = formatAnthropicProviderForLog(anthropicInstance, admitted.accountId, config);
|
|
298
363
|
} catch {
|
|
299
364
|
hop.permit?.release();
|
|
300
365
|
return null;
|
|
@@ -321,6 +386,7 @@ export async function executeResponsesSidecars(
|
|
|
321
386
|
providerName: route.providerName,
|
|
322
387
|
provider: route.provider,
|
|
323
388
|
adapterName: rotatedAdapter.name,
|
|
389
|
+
...(antigravityVerification ? { oauthCredentialSnapshot: transportState.replayOAuthCredentialSnapshot } : {}),
|
|
324
390
|
});
|
|
325
391
|
}
|
|
326
392
|
bindRouteReasoningReplayScope({
|
|
@@ -328,6 +394,7 @@ export async function executeResponsesSidecars(
|
|
|
328
394
|
providerName: route.providerName,
|
|
329
395
|
provider: route.provider,
|
|
330
396
|
adapterName: rotatedAdapter.name,
|
|
397
|
+
...(antigravityVerification ? { oauthCredentialSnapshot: transportState.replayOAuthCredentialSnapshot } : {}),
|
|
331
398
|
});
|
|
332
399
|
return { adapter: rotatedAdapter, recoveryKind };
|
|
333
400
|
};
|
|
@@ -440,7 +507,10 @@ export async function executeResponsesSidecars(
|
|
|
440
507
|
parsed._rawBody,
|
|
441
508
|
response,
|
|
442
509
|
continuationStateForResponse(providerState),
|
|
443
|
-
|
|
510
|
+
{
|
|
511
|
+
...responseStateOptions(adapterNeedsForcedContinuation(transportState.adapter.name)),
|
|
512
|
+
retainForToolContinuation: adapterNeedsToolCallContinuation(transportState.adapter.name),
|
|
513
|
+
},
|
|
444
514
|
);
|
|
445
515
|
notifyResponseComplete(response);
|
|
446
516
|
},
|
|
@@ -484,6 +554,11 @@ export async function executeResponsesSidecars(
|
|
|
484
554
|
abortSignal: options.abortSignal,
|
|
485
555
|
translatorBudget,
|
|
486
556
|
providerFetch: routedProviderFetch,
|
|
557
|
+
...(route.providerName === "google-antigravity" ? {
|
|
558
|
+
sendBudget: sendBudgetState.refundableAdapterDispatchBudget,
|
|
559
|
+
onPhysicalSend: (send: { ordinal: number; recovery?: AttemptRecoveryKind }) =>
|
|
560
|
+
sendBudgetState.noteAdapterPhysicalSend(logCtx.usageLogInputTokens, send),
|
|
561
|
+
} : {}),
|
|
487
562
|
},
|
|
488
563
|
backend: wsPlan.backend,
|
|
489
564
|
forwardProvider: wsPlan.forwardSidecar?.provider,
|
|
@@ -516,6 +591,13 @@ export async function executeResponsesSidecars(
|
|
|
516
591
|
stallTimeoutSec: wsPlan.stallTimeoutSec,
|
|
517
592
|
streamRoutedModelOutput: wsPlan.streamRoutedModelOutput,
|
|
518
593
|
on429: rotateSidecarProviderOn429,
|
|
594
|
+
...(route.providerName === "google-antigravity" ? { onIterationEnd: () => {
|
|
595
|
+
// The next Antigravity adapter dispatch claims this permit. If request construction,
|
|
596
|
+
// admission or cancellation stopped it first, return the unused send allowance.
|
|
597
|
+
const permit = sendBudgetState.pendingHopPermit;
|
|
598
|
+
sendBudgetState.pendingHopPermit = undefined;
|
|
599
|
+
permit?.release();
|
|
600
|
+
} } : {}),
|
|
519
601
|
retryOn429Policy: route.providerName === "kiro" && isGenericOAuthFailoverEnabled(config, "kiro")
|
|
520
602
|
? null : rateLimitRetryPolicyFor(route.provider),
|
|
521
603
|
onCompletedResponse: response => {
|
|
@@ -703,6 +703,7 @@ function failedBlocks(name: string, newline: string): readonly string[] {
|
|
|
703
703
|
* @param providerExecutedCallTypes - Call types executed by the provider.
|
|
704
704
|
* @param declaredBare - Explicitly declared bare tool names without namespace provenance.
|
|
705
705
|
* @param declaredCustom - Current bare custom declarations eligible for code-mode recovery.
|
|
706
|
+
* @param enforceDeclaredToolNames - Refuse undeclared calls; normalization remains active when false.
|
|
706
707
|
* @returns An SSE block rewrite function.
|
|
707
708
|
*/
|
|
708
709
|
export function createUndeclaredToolCallGuardBlockRewrite(
|
|
@@ -711,6 +712,7 @@ export function createUndeclaredToolCallGuardBlockRewrite(
|
|
|
711
712
|
providerExecutedCallTypes: ProviderExecutedCallTypes = EMPTY_PROVIDER_EXECUTED_CALL_TYPES,
|
|
712
713
|
declaredBare?: ReadonlySet<string>,
|
|
713
714
|
declaredCustom?: ReadonlySet<string>,
|
|
715
|
+
enforceDeclaredToolNames = true,
|
|
714
716
|
): SseBlockRewrite {
|
|
715
717
|
let tripped = false;
|
|
716
718
|
return (block: string) => {
|
|
@@ -723,7 +725,9 @@ export function createUndeclaredToolCallGuardBlockRewrite(
|
|
|
723
725
|
} catch {
|
|
724
726
|
return [block];
|
|
725
727
|
}
|
|
726
|
-
const name =
|
|
728
|
+
const name = enforceDeclaredToolNames
|
|
729
|
+
? undeclaredToolCallName(parsed, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes, declaredBare, declaredCustom)
|
|
730
|
+
: undefined;
|
|
727
731
|
if (name !== undefined) {
|
|
728
732
|
tripped = true;
|
|
729
733
|
return failedBlocks(name, block.includes("\r\n") ? "\r\n" : "\n");
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sharedPoolContinuityDenial } from "../lib/spend-reservation-ledger";
|
|
1
2
|
/**
|
|
2
3
|
* The one place that knows how this proxy refuses a turn on its own workflow budget.
|
|
3
4
|
*
|
|
@@ -96,6 +97,22 @@ export function workflowRefusalResponse(
|
|
|
96
97
|
return refusal;
|
|
97
98
|
}
|
|
98
99
|
|
|
100
|
+
/** Render the approved conservative-overlay explanation after a dispatch reservation refuses. */
|
|
101
|
+
export function unboundPoolSpendRefusalMessage(
|
|
102
|
+
logCtx: Pick<RequestLogContext, "spendRefusalDetail">,
|
|
103
|
+
): string | undefined {
|
|
104
|
+
const detail = logCtx.spendRefusalDetail;
|
|
105
|
+
if (!detail?.includesUnboundPoolHistory) return undefined;
|
|
106
|
+
return workflowDenialSummary("workflow-spend-exhausted", detail).message;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** HTTP form for pre-commit send-budget catches; the tracker already recorded the event. */
|
|
110
|
+
export function unboundPoolSpendRefusalResponse(logCtx: RequestLogContext): Response | undefined {
|
|
111
|
+
const detail = logCtx.spendRefusalDetail;
|
|
112
|
+
if (!detail?.includesUnboundPoolHistory) return undefined;
|
|
113
|
+
return workflowRefusalResponse("workflow-spend-exhausted", logCtx, undefined, undefined, detail);
|
|
114
|
+
}
|
|
115
|
+
|
|
99
116
|
/**
|
|
100
117
|
* Admit one HTTP turn against its root workflow budget.
|
|
101
118
|
*
|
|
@@ -104,8 +121,20 @@ export function workflowRefusalResponse(
|
|
|
104
121
|
* children. A request that names a parent is treated as that fan-out; a top-level request is
|
|
105
122
|
* the conversation and may use the reserved slots.
|
|
106
123
|
*/
|
|
124
|
+
/** Read-only alias validation: keep replay corruption distinct from conflicting pool owners. */
|
|
125
|
+
export function poolContinuityRefusalReason(): WorkflowDenial | undefined {
|
|
126
|
+
const denial = sharedPoolContinuityDenial();
|
|
127
|
+
if (!denial) return undefined;
|
|
128
|
+
return denial.reason === "pool-history-unresolved" ? "workflow-pool-history-unresolved" : "workflow-spend-undurable";
|
|
129
|
+
}
|
|
130
|
+
|
|
107
131
|
export function admitHttpWorkflowTurn(headers: Headers): WorkflowDecision | undefined {
|
|
108
132
|
const rootId = headers.get("x-codex-parent-thread-id")?.trim() || undefined;
|
|
133
|
+
const continuityRefusal = poolContinuityRefusalReason();
|
|
134
|
+
if (continuityRefusal) {
|
|
135
|
+
recordWorkflowRefusalEvent(rootId, continuityRefusal);
|
|
136
|
+
return { admitted: false, reason: continuityRefusal, rootId: rootId ?? "" };
|
|
137
|
+
}
|
|
109
138
|
const threadId = headers.get("thread-id")?.trim() || undefined;
|
|
110
139
|
const lane: WorkflowLane = rootId !== undefined && threadId !== undefined && threadId !== rootId
|
|
111
140
|
? "worker"
|
|
@@ -132,6 +161,7 @@ export function workflowDecisionRefusalResponse(
|
|
|
132
161
|
scope: decision.spendScope,
|
|
133
162
|
limit: decision.spendLimit,
|
|
134
163
|
...(decision.spendProjected !== undefined ? { projected: decision.spendProjected } : {}),
|
|
164
|
+
...(decision.spendIncludesUnboundPoolHistory ? { includesUnboundPoolHistory: true } : {}),
|
|
135
165
|
}
|
|
136
166
|
: undefined;
|
|
137
167
|
return workflowRefusalResponse(decision.reason, logCtx, refusalLog, undefined, spend);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { accessSync, constants, readFileSync, realpathSync } from "node:fs";
|
|
2
|
+
import { accessSync, constants, readFileSync, readlinkSync, realpathSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
4
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
5
|
import { readPid } from "../config/process-state";
|
|
6
6
|
import { resolveServiceOwnership, sameServiceOwnershipSubject, type ServiceOwnershipResolution } from "./state";
|
|
7
7
|
|
|
@@ -29,11 +29,28 @@ interface DesktopStartupDeps {
|
|
|
29
29
|
ownership?: () => ServiceOwnershipResolution;
|
|
30
30
|
readPid?: () => number | null;
|
|
31
31
|
run?: typeof run;
|
|
32
|
+
env?: NodeJS.ProcessEnv;
|
|
33
|
+
proc?: ProcReader;
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
/** Linux process identity from procfs: executable target and parent pid. */
|
|
37
|
+
interface ProcReader {
|
|
38
|
+
exe(pid: number): string;
|
|
39
|
+
parent(pid: number): number;
|
|
40
|
+
}
|
|
41
|
+
const procfs: ProcReader = {
|
|
42
|
+
exe: pid => readlinkSync(`/proc/${pid}/exe`),
|
|
43
|
+
// comm may contain spaces or parentheses, so fields are counted after the last ')'.
|
|
44
|
+
parent: pid => {
|
|
45
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
46
|
+
return Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
34
50
|
/** Cheap ownership-only snapshot: no launchd/process probes on the server request path. */
|
|
35
51
|
export function desktopStartupOwnership(deps: DesktopStartupDeps = {}): DesktopStartupDiagnostic | undefined {
|
|
36
|
-
|
|
52
|
+
const platform = deps.platform ?? process.platform;
|
|
53
|
+
if (platform !== "darwin" && platform !== "linux") return undefined;
|
|
37
54
|
const owner = (deps.ownership ?? resolveServiceOwnership)();
|
|
38
55
|
return owner.kind === "owned" && owner.ownership.owner === "desktop"
|
|
39
56
|
? deriveDesktopStartup({ owned: true, loginEnabled: false, running: false }) : undefined;
|
|
@@ -91,3 +108,83 @@ export function diagnoseMacDesktopStartup(deps: DesktopStartupDeps = {}): Deskto
|
|
|
91
108
|
return deriveDesktopStartup({ ...facts, running: false });
|
|
92
109
|
}
|
|
93
110
|
}
|
|
111
|
+
|
|
112
|
+
/** HOME-based autostart entry written by the desktop app's login item (`tauri-plugin-autostart`). */
|
|
113
|
+
function linuxLoginApp(entry: string): string | null {
|
|
114
|
+
const fields = new Map<string, string>();
|
|
115
|
+
let section = "";
|
|
116
|
+
for (const raw of entry.split(/\r?\n/)) {
|
|
117
|
+
const line = raw.trim();
|
|
118
|
+
if (!line || line.startsWith("#")) continue;
|
|
119
|
+
if (line.startsWith("[")) { section = line; continue; }
|
|
120
|
+
const at = line.indexOf("=");
|
|
121
|
+
if (section === "[Desktop Entry]" && at > 0 && !fields.has(line.slice(0, at).trim())) {
|
|
122
|
+
fields.set(line.slice(0, at).trim(), line.slice(at + 1).trim());
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (fields.get("Type") !== "Application" || fields.get("Hidden") === "true"
|
|
126
|
+
|| fields.get("X-GNOME-Autostart-enabled") === "false"
|
|
127
|
+
|| ["OnlyShowIn", "NotShowIn", "TryExec"].some(key => fields.has(key))) return null;
|
|
128
|
+
// Credit only the backend's simple Exec form; desktop quoting/escaping/field codes are unsupported.
|
|
129
|
+
const app = /^([^\s"'\\%`><~|&;$*?#()]+) --autostart$/.exec(fields.get("Exec") ?? "")?.[1];
|
|
130
|
+
return app && isAbsolute(app) && basename(app) === "opencodex-desktop" ? app : null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Read-only Linux desktop ownership, HOME login entry and live parent/child checks via procfs. */
|
|
134
|
+
export function diagnoseLinuxDesktopStartup(deps: DesktopStartupDeps = {}): DesktopStartupDiagnostic | undefined {
|
|
135
|
+
if ((deps.platform ?? process.platform) !== "linux") return undefined;
|
|
136
|
+
const ownership = deps.ownership ?? resolveServiceOwnership;
|
|
137
|
+
const owner = ownership();
|
|
138
|
+
if (owner.kind !== "owned" || owner.ownership.owner !== "desktop") return undefined;
|
|
139
|
+
const facts = { owned: true, loginEnabled: false, running: false };
|
|
140
|
+
const proc = deps.proc ?? procfs;
|
|
141
|
+
const pidReader = deps.readPid ?? readPid;
|
|
142
|
+
try {
|
|
143
|
+
const env = deps.env ?? process.env;
|
|
144
|
+
const home = deps.home ?? homedir();
|
|
145
|
+
const configured = env.XDG_CONFIG_HOME;
|
|
146
|
+
const config = configured && isAbsolute(configured) ? configured : join(home, ".config");
|
|
147
|
+
// auto-launch 0.5.0 writes autostart under HOME; Tauri app_config_dir uses XDG for install-id.
|
|
148
|
+
// A relocated XDG_CONFIG_HOME puts that entry outside the session's autostart search path.
|
|
149
|
+
if (resolve(config) !== resolve(home, ".config")) return deriveDesktopStartup(facts);
|
|
150
|
+
const capture = () => {
|
|
151
|
+
const id = readFileSync(join(config, "com.opencodex.desktop", "install-id"), "utf8");
|
|
152
|
+
if (id.trim() !== owner.ownership.installId) return null;
|
|
153
|
+
const entry = readFileSync(join(home, ".config", "autostart", "OpenCodex.desktop"), "utf8");
|
|
154
|
+
const login = linuxLoginApp(entry);
|
|
155
|
+
if (!login) return null;
|
|
156
|
+
const app = realpathSync(login);
|
|
157
|
+
if (basename(app) !== "opencodex-desktop") return null;
|
|
158
|
+
const proxy = realpathSync(join(dirname(app), "ocx"));
|
|
159
|
+
if (dirname(proxy) !== dirname(app) || basename(proxy) !== "ocx") return null;
|
|
160
|
+
accessSync(app, constants.X_OK);
|
|
161
|
+
accessSync(proxy, constants.X_OK);
|
|
162
|
+
const pid = pidReader();
|
|
163
|
+
const parent = pid !== null ? proc.parent(pid) : null;
|
|
164
|
+
const child = pid !== null ? realpathSync(proc.exe(pid)) : null;
|
|
165
|
+
const parentApp = parent !== null && parent > 1 ? realpathSync(proc.exe(parent)) : null;
|
|
166
|
+
const running = pid !== null && child === proxy && parentApp === app && pidReader() === pid;
|
|
167
|
+
const currentOwner = ownership();
|
|
168
|
+
if (currentOwner.kind === "unknown" || !sameServiceOwnershipSubject(owner, currentOwner)) return null;
|
|
169
|
+
return { id, entry, login, app, proxy, pid, parent, child, parentApp, running };
|
|
170
|
+
};
|
|
171
|
+
const first = capture();
|
|
172
|
+
if (!first) return deriveDesktopStartup(facts);
|
|
173
|
+
const final = capture();
|
|
174
|
+
// Re-read the entire evidence chain, including files, targets and the exact parent PID.
|
|
175
|
+
// A changed or unreadable snapshot cannot retain protection from the first read.
|
|
176
|
+
if (final && JSON.stringify(first) === JSON.stringify(final)) {
|
|
177
|
+
facts.loginEnabled = true;
|
|
178
|
+
facts.running = final.running;
|
|
179
|
+
}
|
|
180
|
+
return deriveDesktopStartup(facts);
|
|
181
|
+
} catch {
|
|
182
|
+
// Unreadable login or procfs evidence never grants protection or releases the claim.
|
|
183
|
+
return deriveDesktopStartup({ ...facts, running: false });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Platform dispatch for the full (probing) desktop startup diagnostic. */
|
|
188
|
+
export function diagnoseDesktopStartup(deps: DesktopStartupDeps = {}): DesktopStartupDiagnostic | undefined {
|
|
189
|
+
return (deps.platform ?? process.platform) === "linux" ? diagnoseLinuxDesktopStartup(deps) : diagnoseMacDesktopStartup(deps);
|
|
190
|
+
}
|
|
@@ -5,7 +5,7 @@ export interface OwnershipMutationLeaseOptions {
|
|
|
5
5
|
readonly processAlive?: (pid: number) => boolean;
|
|
6
6
|
readonly beforeRelease?: (lockPath: string) => void;
|
|
7
7
|
readonly joinToken?: string;
|
|
8
|
-
/** Names the current executable at the recorded PID, identity unverified;
|
|
8
|
+
/** Names the current executable at the recorded PID, identity unverified; uses `ps` on POSIX and omits the default image lookup on Windows. */
|
|
9
9
|
readonly processImage?: (pid: number) => string | null;
|
|
10
10
|
}
|
|
11
11
|
|
|
@@ -112,13 +112,11 @@ function readIncompleteOwner(path) {
|
|
|
112
112
|
/** The current executable at the recorded PID, identity unverified; null if unreadable within a second. */
|
|
113
113
|
function processImage(pid) {
|
|
114
114
|
try {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return listed.status === 0 && match && Number(match[2]) === pid ? match[1] : null;
|
|
121
|
-
}
|
|
115
|
+
// This module also runs in the Node npm/pnpm launcher, before Bun is available.
|
|
116
|
+
// Without a kernel-backed system-directory resolver, omit the optional Windows
|
|
117
|
+
// image label rather than searching the caller's directory/PATH or trusting env roots.
|
|
118
|
+
// PID, liveness, age and lease ownership decisions do not depend on this label.
|
|
119
|
+
if (process.platform === "win32") return null;
|
|
122
120
|
const listed = spawnSync("ps", ["-o", "comm=", "-p", String(pid)], { encoding: "utf8", timeout: 1_000 });
|
|
123
121
|
const command = (listed.stdout ?? "").trim();
|
|
124
122
|
return listed.status === 0 && command ? basename(command) : null;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { OcxConfig } from "../types";
|
|
2
|
+
import type { AnthropicInstanceId } from "../providers/anthropic-instance-id";
|
|
3
|
+
import { configuredAnthropicInstance } from "../providers/anthropic-instance";
|
|
4
|
+
import { anthropicRoutingFor } from "../oauth/anthropic-routing";
|
|
5
|
+
import { resolveAnthropicModelRouteForInstance, routeCandidates } from "../oauth/anthropic-model-routes";
|
|
6
|
+
import type { OAuthAccessSnapshot } from "../oauth";
|
|
7
|
+
import { AnthropicHelperUnavailableError } from "./auth";
|
|
8
|
+
import { captureAnthropicPhysicalSendOwnership, anthropicPhysicalSendOwnershipIsCurrent } from "../oauth/anthropic-send-ownership";
|
|
9
|
+
import { recordAnthropicAccountQuotaFromHeadersForInstance } from "../providers/quota/account-cache";
|
|
10
|
+
import { captureConfigGeneration } from "../lib/state-store-sweeper";
|
|
11
|
+
import { getAccountCredentialWithStatus } from "../oauth/store";
|
|
12
|
+
import { bindAnthropicRefusalCredentialForSend } from "../oauth/anthropic-account-refusal";
|
|
13
|
+
|
|
14
|
+
/** The one Messages URL builder shared by helper executors and the physical-send fence. */
|
|
15
|
+
export function anthropicHelperMessagesUrl(baseUrl: string): string {
|
|
16
|
+
return `${baseUrl.replace(/\/+$/, "").replace(/\/v1$/, "")}/v1/messages`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Select the helper's account the way the legacy sidecar token path does: a read of the pool's
|
|
21
|
+
* selection for this model route, with no promotion. A helper never moves the active pointer and
|
|
22
|
+
* never consumes an operator's one-dispatch manual choice; the physical send is still fenced by
|
|
23
|
+
* the captured target/config, the account's credential generation and its send ownership.
|
|
24
|
+
*/
|
|
25
|
+
export async function resolveAnthropicHelperSnapshot(config: OcxConfig, instance: AnthropicInstanceId, model: string): Promise<OAuthAccessSnapshot> {
|
|
26
|
+
const target = config.providers[instance]?.baseUrl;
|
|
27
|
+
const assertConfigured = () => {
|
|
28
|
+
const provider = config.providers[instance];
|
|
29
|
+
if (!provider || provider.disabled || provider.authMode !== "oauth" || provider.adapter !== "anthropic"
|
|
30
|
+
|| configuredAnthropicInstance(config, instance) !== instance || provider.baseUrl !== target) {
|
|
31
|
+
throw new AnthropicHelperUnavailableError(instance);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
assertConfigured();
|
|
35
|
+
const routing = anthropicRoutingFor(instance);
|
|
36
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
37
|
+
const route = resolveAnthropicModelRouteForInstance(instance, config, model);
|
|
38
|
+
if (route.error) throw new AnthropicHelperUnavailableError(instance);
|
|
39
|
+
const selection = routing.resolveAnthropicAccountForSession(null, config, Date.now(), route.decision, model);
|
|
40
|
+
if (!selection.accountId) throw new AnthropicHelperUnavailableError(instance);
|
|
41
|
+
const snapshot = await routing.getAnthropicPoolAccessSnapshot(selection.accountId);
|
|
42
|
+
assertConfigured();
|
|
43
|
+
const current = resolveAnthropicModelRouteForInstance(instance, config, model);
|
|
44
|
+
if (current.error || JSON.stringify(current.decision) !== JSON.stringify(route.decision)) continue;
|
|
45
|
+
if (snapshot.provider === instance && snapshot.accountId === selection.accountId) return snapshot;
|
|
46
|
+
}
|
|
47
|
+
throw new AnthropicHelperUnavailableError(instance);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** One physical helper send; reset replays call this boundary again with the same snapshot. */
|
|
51
|
+
export async function fetchAnthropicHelper(
|
|
52
|
+
config: OcxConfig, snapshot: OAuthAccessSnapshot, model: string,
|
|
53
|
+
capturedTarget: string, url: string, init: RequestInit,
|
|
54
|
+
): Promise<Response> {
|
|
55
|
+
const instance = snapshot.provider;
|
|
56
|
+
if (instance !== "anthropic" && instance !== "anthropic2") throw new Error("Invalid Anthropic helper instance");
|
|
57
|
+
const row = config.providers[instance];
|
|
58
|
+
const live = getAccountCredentialWithStatus(instance, snapshot.accountId);
|
|
59
|
+
if (!row || row.disabled || row.authMode !== "oauth" || row.adapter !== "anthropic"
|
|
60
|
+
|| configuredAnthropicInstance(config, instance) !== instance || row.baseUrl !== capturedTarget
|
|
61
|
+
|| url !== anthropicHelperMessagesUrl(capturedTarget) || !live || live.paused || live.needsReauth
|
|
62
|
+
|| new Headers(init.headers).get("authorization") !== `Bearer ${snapshot.accessToken}`) {
|
|
63
|
+
throw new AnthropicHelperUnavailableError(instance);
|
|
64
|
+
}
|
|
65
|
+
const route = resolveAnthropicModelRouteForInstance(instance, config, model);
|
|
66
|
+
const routing = anthropicRoutingFor(instance);
|
|
67
|
+
if (route.error || !routeCandidates(routing.getEligibleAnthropicAccounts(Date.now(), model), route.decision).includes(snapshot.accountId)) {
|
|
68
|
+
throw new AnthropicHelperUnavailableError(instance);
|
|
69
|
+
}
|
|
70
|
+
const owner = captureAnthropicPhysicalSendOwnership(snapshot);
|
|
71
|
+
if (!owner) throw new AnthropicHelperUnavailableError(instance);
|
|
72
|
+
const writer = captureConfigGeneration();
|
|
73
|
+
const response = await fetch(url, init);
|
|
74
|
+
if (anthropicPhysicalSendOwnershipIsCurrent(owner)) {
|
|
75
|
+
recordAnthropicAccountQuotaFromHeadersForInstance(instance, snapshot.accountId, response.headers, writer, response.status, model);
|
|
76
|
+
bindAnthropicRefusalCredentialForSend(response, owner);
|
|
77
|
+
}
|
|
78
|
+
return response;
|
|
79
|
+
}
|