@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,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dashboard memory-card drain-and-restart (#563).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* then respawns. Never runs restoreNativeCodex / stripGrokConfig — this is a
|
|
4
|
+
* Waits up to 60s for active turns unless a manual API caller explicitly opts
|
|
5
|
+
* into a shorter grace, with the existing 60s cleanup watchdog, then respawns. Never runs restoreNativeCodex / stripGrokConfig — this is a
|
|
6
6
|
* recycle to reclaim RSS, not a teardown.
|
|
7
7
|
*
|
|
8
8
|
* Respawn policy (matches real supervisor configs in src/service.ts):
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
* Checked before the service rule: that app, not a service manager, is the parent.
|
|
38
38
|
*/
|
|
39
39
|
import {
|
|
40
|
+
abortAndReleaseAllTurns,
|
|
40
41
|
acquireTemporaryDrain,
|
|
41
42
|
beginShutdownDrain,
|
|
42
43
|
drainAndShutdown,
|
|
@@ -64,8 +65,11 @@ import { spawnReplacementStart } from "../restart-replacement";
|
|
|
64
65
|
export { MEMORY_DRAIN_RESTART_MS, REPLACEMENT_READY_TIMEOUT_MS } from "../../lib/system-restart-contract";
|
|
65
66
|
export { waitForReplacementReady, type ReplacementReadinessIo } from "../restart-replacement";
|
|
66
67
|
export const DEADLINE_LISTENER_STOP_TIMEOUT_MS = 5_000;
|
|
68
|
+
const RESTART_RESPONSE_FLUSH_DELAY_MS = 200;
|
|
67
69
|
|
|
68
70
|
export interface SystemRestartIo {
|
|
71
|
+
/** Cancel and release admitted turns without stopping the listener or process. */
|
|
72
|
+
abortActiveTurns?: () => void;
|
|
69
73
|
acquireTemporaryDrain?: () => { release(): void } | null;
|
|
70
74
|
drainAndShutdown?: typeof drainAndShutdown;
|
|
71
75
|
/** True when a background service can actually respawn this process after exit(1). */
|
|
@@ -92,6 +96,11 @@ export interface SystemRestartIo {
|
|
|
92
96
|
now?: () => number;
|
|
93
97
|
}
|
|
94
98
|
|
|
99
|
+
export interface SystemRestartOptions {
|
|
100
|
+
/** Explicit API opt-in; omission preserves the existing 60s drain. */
|
|
101
|
+
drainGraceMs?: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
95
104
|
export interface SystemRestartAdmission {
|
|
96
105
|
/** Only the caller that created this pending restart receives its veto. */
|
|
97
106
|
onAccepted?: (veto: () => void) => void;
|
|
@@ -100,8 +109,9 @@ export interface SystemRestartAdmission {
|
|
|
100
109
|
}
|
|
101
110
|
|
|
102
111
|
let restartIo: SystemRestartIo = {};
|
|
103
|
-
/** Prevents double-scheduling
|
|
112
|
+
/** Prevents double-scheduling during the 200ms response-flush window. */
|
|
104
113
|
let restartAccepted = false;
|
|
114
|
+
let acceptedDrainGraceMs = MEMORY_DRAIN_RESTART_MS;
|
|
105
115
|
|
|
106
116
|
type RestartDrainOutcome = "completed" | "failed" | "rejected" | "deadline";
|
|
107
117
|
type BoundedSettlementOutcome = "completed" | "rejected" | "deadline";
|
|
@@ -173,6 +183,7 @@ export function noteExplicitShutdownRequested(): void {
|
|
|
173
183
|
export function setSystemRestartIoForTests(io: SystemRestartIo = {}): void {
|
|
174
184
|
restartIo = io;
|
|
175
185
|
restartAccepted = false;
|
|
186
|
+
acceptedDrainGraceMs = MEMORY_DRAIN_RESTART_MS;
|
|
176
187
|
explicitShutdownRequested = false;
|
|
177
188
|
}
|
|
178
189
|
|
|
@@ -324,10 +335,10 @@ async function completeDeadlineRestartHandoff(
|
|
|
324
335
|
|
|
325
336
|
/**
|
|
326
337
|
* Accept a drain-and-restart request. Returns immediately; the drain +
|
|
327
|
-
* respawn
|
|
338
|
+
* respawn waits 200ms for the HTTP response to flush; shorter grace cuts turns separately.
|
|
328
339
|
* Idempotent while already draining: returns the accepted shape again.
|
|
329
340
|
*/
|
|
330
|
-
export function acceptSystemRestart(io: SystemRestartIo = restartIo, admission: SystemRestartAdmission = {}): {
|
|
341
|
+
export function acceptSystemRestart(io: SystemRestartIo = restartIo, admission: SystemRestartAdmission = {}, options: SystemRestartOptions = {}): {
|
|
331
342
|
accepted: true;
|
|
332
343
|
alreadyDraining: boolean;
|
|
333
344
|
activeTurnCount: number;
|
|
@@ -337,16 +348,22 @@ export function acceptSystemRestart(io: SystemRestartIo = restartIo, admission:
|
|
|
337
348
|
?? io.isDraining
|
|
338
349
|
?? isShutdownDraining;
|
|
339
350
|
const alreadyDraining = restartAccepted || shutdownActive();
|
|
351
|
+
const drainGraceMs = alreadyDraining
|
|
352
|
+
? (restartAccepted ? acceptedDrainGraceMs : MEMORY_DRAIN_RESTART_MS)
|
|
353
|
+
: (options.drainGraceMs ?? MEMORY_DRAIN_RESTART_MS);
|
|
340
354
|
const activeTurnCount = (io.getActiveTurnCount ?? getActiveTurnCount)();
|
|
341
355
|
const schedule = io.schedule ?? ((fn, ms) => { setTimeout(() => { void fn(); }, ms); });
|
|
342
356
|
|
|
343
357
|
if (!alreadyDraining) {
|
|
344
358
|
restartAccepted = true;
|
|
359
|
+
acceptedDrainGraceMs = drainGraceMs;
|
|
345
360
|
const automatic = Boolean(admission.onAccepted);
|
|
346
361
|
const temporaryDrain = automatic
|
|
347
362
|
? (io.acquireTemporaryDrain ?? (() => acquireTemporaryDrain("automatic-restart")))()
|
|
348
363
|
: null;
|
|
364
|
+
let cancelEarlyCut: (() => void) | undefined;
|
|
349
365
|
const releasePending = () => {
|
|
366
|
+
cancelEarlyCut?.();
|
|
350
367
|
temporaryDrain?.release();
|
|
351
368
|
restartAccepted = false;
|
|
352
369
|
};
|
|
@@ -363,13 +380,27 @@ export function acceptSystemRestart(io: SystemRestartIo = restartIo, admission:
|
|
|
363
380
|
releasePending();
|
|
364
381
|
});
|
|
365
382
|
const now = io.now ?? Date.now;
|
|
366
|
-
const
|
|
383
|
+
const acceptedAtMs = now();
|
|
384
|
+
const restartDeadlineMs = acceptedAtMs + MEMORY_DRAIN_RESTART_MS;
|
|
385
|
+
const drainDeadlineMs = acceptedAtMs + drainGraceMs;
|
|
386
|
+
const scheduleDeadline = io.scheduleDeadline ?? ((fn, ms) => {
|
|
387
|
+
const timer = setTimeout(fn, ms);
|
|
388
|
+
return () => clearTimeout(timer);
|
|
389
|
+
});
|
|
367
390
|
// Reject new data-plane traffic immediately (503), before the 200ms response-flush delay.
|
|
368
391
|
if (!automatic) {
|
|
369
392
|
if (io.beginShutdownDrain) io.beginShutdownDrain();
|
|
370
393
|
else if (io.setDraining) io.setDraining(true);
|
|
371
394
|
else beginShutdownDrain();
|
|
372
395
|
}
|
|
396
|
+
// A shorter grace cuts admitted turns at acceptance + grace, but listener stop
|
|
397
|
+
// and process exit must still wait for the full response-flush window.
|
|
398
|
+
if (!vetoed && drainGraceMs < RESTART_RESPONSE_FLUSH_DELAY_MS) {
|
|
399
|
+
cancelEarlyCut = scheduleDeadline(() => {
|
|
400
|
+
if (vetoed) return;
|
|
401
|
+
(io.abortActiveTurns ?? (() => abortAndReleaseAllTurns(new Error("server shutdown"))))();
|
|
402
|
+
}, drainGraceMs);
|
|
403
|
+
}
|
|
373
404
|
schedule(async () => {
|
|
374
405
|
if (vetoed) return;
|
|
375
406
|
pending = false;
|
|
@@ -390,12 +421,11 @@ export function acceptSystemRestart(io: SystemRestartIo = restartIo, admission:
|
|
|
390
421
|
// closes the listener and makes both the server ref and runtime metadata stale.
|
|
391
422
|
const restartPort = (io.listenPort ?? resolveListenPort)();
|
|
392
423
|
const drain = io.drainAndShutdown ?? drainAndShutdown;
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
});
|
|
424
|
+
// Bound active/scoped-drain waiting independently of cleanup and readiness.
|
|
425
|
+
// A cut turn may already have executed upstream; never replay it automatically.
|
|
426
|
+
const drainPromise = Promise.resolve().then(
|
|
427
|
+
() => drain(undefined, Math.max(0, drainDeadlineMs - now())),
|
|
428
|
+
);
|
|
399
429
|
const drainOutcome = await waitForRestartDrain(drainPromise, restartDeadlineMs, now, scheduleDeadline);
|
|
400
430
|
if (!canHandoff()) return;
|
|
401
431
|
const exitProcess = io.exitProcess ?? ((code: number) => { process.exit(code); });
|
|
@@ -451,13 +481,13 @@ export function acceptSystemRestart(io: SystemRestartIo = restartIo, admission:
|
|
|
451
481
|
}
|
|
452
482
|
(io.markRecycling ?? markRecyclingForExit)();
|
|
453
483
|
exitProcess(drainOutcome === "failed" || drainOutcome === "rejected" ? 1 : 0);
|
|
454
|
-
},
|
|
484
|
+
}, RESTART_RESPONSE_FLUSH_DELAY_MS);
|
|
455
485
|
}
|
|
456
486
|
|
|
457
487
|
return {
|
|
458
488
|
accepted: true,
|
|
459
489
|
alreadyDraining,
|
|
460
490
|
activeTurnCount,
|
|
461
|
-
drainTimeoutMs:
|
|
491
|
+
drainTimeoutMs: drainGraceMs,
|
|
462
492
|
};
|
|
463
493
|
}
|
|
@@ -30,6 +30,7 @@ import { inspectResponseSpillStorage, responseStateMetrics, type ResponseSpillDi
|
|
|
30
30
|
import { appOwnedBytesSnapshot } from "../../lib/app-owned-memory";
|
|
31
31
|
import { readWindowsReplaceRetryCounters } from "../../lib/windows-atomic-replace";
|
|
32
32
|
import {
|
|
33
|
+
MEMORY_DRAIN_RESTART_MS,
|
|
33
34
|
SYSTEM_RESTART_EXPECTED_PID_HEADER,
|
|
34
35
|
parseExpectedSystemRestartPid,
|
|
35
36
|
} from "../../lib/system-restart-contract";
|
|
@@ -46,6 +47,7 @@ import type {
|
|
|
46
47
|
} from "../../codex/app-server-restart-service";
|
|
47
48
|
import type { ManagementContext } from "./context";
|
|
48
49
|
import { acceptSystemRestart } from "./system-restart";
|
|
50
|
+
import { readOptionalManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
49
51
|
|
|
50
52
|
const ENDPOINT_SAMPLE_LIMIT = 60;
|
|
51
53
|
// The spill report reads the snapshot and walks the spill directory synchronously,
|
|
@@ -179,8 +181,28 @@ export async function handleSystemRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
179
181
|
}, 409, req, config);
|
|
180
182
|
}
|
|
181
183
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
+
let body: unknown;
|
|
185
|
+
try { body = await readOptionalManagementJsonBody(req); }
|
|
186
|
+
catch (error) {
|
|
187
|
+
rethrowManagementBodyTooLarge(error);
|
|
188
|
+
return jsonResponse({ success: false, error: "Invalid restart JSON body." }, 400, req, config);
|
|
189
|
+
}
|
|
190
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
191
|
+
return jsonResponse({ success: false, error: "Invalid restart JSON body." }, 400, req, config);
|
|
192
|
+
}
|
|
193
|
+
const drainGraceMs = (body as Record<string, unknown>).drainGraceMs;
|
|
194
|
+
if (drainGraceMs !== undefined && (
|
|
195
|
+
typeof drainGraceMs !== "number" || !Number.isSafeInteger(drainGraceMs)
|
|
196
|
+
|| drainGraceMs < 1 || drainGraceMs > MEMORY_DRAIN_RESTART_MS
|
|
197
|
+
)) {
|
|
198
|
+
return jsonResponse({ success: false, error: "drainGraceMs must be an integer from 1 to 60000." }, 400, req, config);
|
|
199
|
+
}
|
|
200
|
+
// The process capability signs the restart target, not this new body option.
|
|
201
|
+
if (drainGraceMs !== undefined && ctx.principal === "system-restart-capability") {
|
|
202
|
+
return jsonResponse({ success: false, error: "Short grace requires management authentication." }, 403, req, config);
|
|
203
|
+
}
|
|
204
|
+
// Omission retains the informed 60s drain; only an explicit API request shortens it.
|
|
205
|
+
const result = acceptSystemRestart(undefined, {}, { drainGraceMs });
|
|
184
206
|
return jsonResponse({
|
|
185
207
|
success: true,
|
|
186
208
|
message: result.alreadyDraining
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* the same as no gate at all.
|
|
9
9
|
*/
|
|
10
10
|
import type { OcxConfig } from "../../types";
|
|
11
|
-
import {
|
|
11
|
+
import { type AnthropicVisionProvider } from "../../vision";
|
|
12
12
|
import { VISION_BACKENDS } from "../../vision/backends";
|
|
13
13
|
import {
|
|
14
14
|
modelAcceptsImageInput,
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
type VisionSidecarBackend,
|
|
19
19
|
} from "../../vision/eligibility";
|
|
20
20
|
import { pickerVisibleSidecarCandidates } from "../../sidecar/candidates";
|
|
21
|
-
import {
|
|
21
|
+
import { sidecarOptionsAuth } from "./web-search-sidecar-options";
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Backends whose executor could actually run (#2188 roadmap 170): openai
|
|
@@ -38,7 +38,7 @@ export function enabledVisionBackends(
|
|
|
38
38
|
config: OcxConfig,
|
|
39
39
|
anthropicSidecar: AnthropicVisionProvider | undefined,
|
|
40
40
|
): VisionSidecarBackend[] {
|
|
41
|
-
const auth =
|
|
41
|
+
const auth = sidecarOptionsAuth(config, config.visionSidecar?.anthropicInstance);
|
|
42
42
|
// Preserve the caller's resolution for the anthropic side: the descriptor
|
|
43
43
|
// reads the shared auth module, but a caller that already resolved "no
|
|
44
44
|
// executor" must not see anthropic options it cannot dispatch. The filter
|
|
@@ -52,7 +52,7 @@ export function enabledVisionBackends(
|
|
|
52
52
|
// the UNIVERSAL sides: when neither resolves, both are offered so the picker
|
|
53
53
|
// stays populated (permissive-unknown rule; test 6 pins it).
|
|
54
54
|
if (!active.includes("openai") && !active.includes("anthropic")) {
|
|
55
|
-
return ["openai", "anthropic", ...active];
|
|
55
|
+
return ["openai", ...(config.visionSidecar?.anthropicInstance ? [] : ["anthropic" as const]), ...active];
|
|
56
56
|
}
|
|
57
57
|
return active;
|
|
58
58
|
}
|
|
@@ -67,7 +67,7 @@ export function enabledVisionBackends(
|
|
|
67
67
|
* pre-filtering here would deaden the gate (review F1: reject → allow flip).
|
|
68
68
|
*/
|
|
69
69
|
export async function visionCandidateRows(config: OcxConfig): Promise<VisionCandidateModel[]> {
|
|
70
|
-
const auth =
|
|
70
|
+
const auth = sidecarOptionsAuth(config, config.visionSidecar?.anthropicInstance);
|
|
71
71
|
const all = await pickerVisibleSidecarCandidates(config, auth);
|
|
72
72
|
return all.map(candidate => ({
|
|
73
73
|
provider: candidate.provider,
|
|
@@ -158,10 +158,13 @@ export function visionDescriberRejection(
|
|
|
158
158
|
config: OcxConfig,
|
|
159
159
|
candidates: readonly VisionCandidateModel[],
|
|
160
160
|
): { error: string; allowed: string[] } {
|
|
161
|
+
const auth = sidecarOptionsAuth(config, config.visionSidecar?.anthropicInstance);
|
|
162
|
+
const anthropic = auth.anthropicProviderName && auth.anthropicProvider
|
|
163
|
+
? { providerName: auth.anthropicProviderName, provider: auth.anthropicProvider, config } : undefined;
|
|
161
164
|
return {
|
|
162
165
|
error: `${field} "${requested}" cannot describe images: it has no image input support, or it is a model the vision sidecar describes FOR.`,
|
|
163
166
|
// The rejection path is not hot, so it resolves the executor itself rather than
|
|
164
167
|
// making every 400 caller thread one through.
|
|
165
|
-
allowed: visionModelOptionsFrom(config, candidates,
|
|
168
|
+
allowed: visionModelOptionsFrom(config, candidates, anthropic).map(option => option.value),
|
|
166
169
|
};
|
|
167
170
|
}
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
* id outside (candidates ∪ auth slots) can never run and is refused.
|
|
15
15
|
*/
|
|
16
16
|
import type { OcxConfig } from "../../types";
|
|
17
|
-
import { AUTH_SLOT_MODELS, resolveSidecarAuth } from "../../sidecar/auth";
|
|
17
|
+
import { AUTH_SLOT_MODELS, resolveSidecarAuth, resolveAnthropicSidecarAuth, resolveAnthropicHelperInstance, AnthropicHelperUnavailableError, type SidecarAuthState } from "../../sidecar/auth";
|
|
18
|
+
import { ANTHROPIC_INSTANCE_IDS, isAnthropicInstanceId, type AnthropicInstanceId } from "../../providers/anthropic-instance-id";
|
|
19
|
+
import { configuredAnthropicInstance } from "../../providers/anthropic-instance";
|
|
18
20
|
import { pickerVisibleSidecarCandidates, type SidecarCandidate } from "../../sidecar/candidates";
|
|
19
21
|
import { WEB_SEARCH_BACKENDS, webSearchSidecarCandidates } from "../../web-search/backends";
|
|
20
22
|
import type { WebSearchBackendId } from "../../web-search/index";
|
|
@@ -24,6 +26,53 @@ import type { WebSearchBackendId } from "../../web-search/index";
|
|
|
24
26
|
// simply never produce rows.
|
|
25
27
|
export type WebSearchBackend = WebSearchBackendId;
|
|
26
28
|
|
|
29
|
+
export function sidecarAnthropicPoolOptions(config: OcxConfig,
|
|
30
|
+
settings: { backend?: string; anthropicInstance?: AnthropicInstanceId }, parentProviderName?: string): {
|
|
31
|
+
backend?: string; parent?: AnthropicInstanceId; selected?: AnthropicInstanceId;
|
|
32
|
+
resolved?: AnthropicInstanceId; mixed: boolean; available: AnthropicInstanceId[];
|
|
33
|
+
code?: "anthropic_helper_unavailable";
|
|
34
|
+
} {
|
|
35
|
+
const parent = configuredAnthropicInstance(config, parentProviderName);
|
|
36
|
+
const available = ANTHROPIC_INSTANCE_IDS.filter(instance => resolveAnthropicSidecarAuth(config, instance) !== undefined);
|
|
37
|
+
const base = { ...(settings.backend ? { backend: settings.backend } : {}), ...(parent ? { parent } : {}), ...(settings.anthropicInstance ? { selected: settings.anthropicInstance } : {}),
|
|
38
|
+
mixed: !!parent && !!settings.anthropicInstance && parent !== settings.anthropicInstance, available };
|
|
39
|
+
if (settings.backend !== "anthropic") return base;
|
|
40
|
+
try {
|
|
41
|
+
const resolved = resolveAnthropicHelperInstance(config, { backendFamily: "anthropic",
|
|
42
|
+
anthropicInstance: settings.anthropicInstance, parentProviderName });
|
|
43
|
+
const legacy = resolved === undefined ? resolveSidecarAuth(config).anthropicProviderName : resolved;
|
|
44
|
+
return { ...base, ...(isAnthropicInstanceId(legacy) ? { resolved: legacy } : {}) };
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (!(error instanceof AnthropicHelperUnavailableError)) throw error;
|
|
47
|
+
return { ...base, code: error.code };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** An unavailable explicit pool removes its executor; it never resumes legacy discovery. */
|
|
52
|
+
export function sidecarOptionsAuth(config: OcxConfig, instance?: AnthropicInstanceId): SidecarAuthState {
|
|
53
|
+
try { return resolveSidecarAuth(config, instance); }
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (!(error instanceof AnthropicHelperUnavailableError)) throw error;
|
|
56
|
+
// Preserve other backend defaults while removing all Anthropic executor authority.
|
|
57
|
+
const otherBackends = { ...config, providers: Object.fromEntries(Object.entries(config.providers)
|
|
58
|
+
.filter(([, provider]) => provider.adapter !== "anthropic")) };
|
|
59
|
+
return { isCodexAuth: resolveSidecarAuth(otherBackends).isCodexAuth, isAnthropicAuth: false };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Apply only the helper identity fields to a typed, isolated validation snapshot. */
|
|
64
|
+
export function sidecarSettingsAfterPatch<T extends { backend?: string; model?: string; anthropicInstance?: AnthropicInstanceId }>(
|
|
65
|
+
stored: T | undefined, patch: Record<string, unknown>,
|
|
66
|
+
): T {
|
|
67
|
+
const next = { ...stored } as T;
|
|
68
|
+
for (const key of ["backend", "model", "anthropicInstance"] as const) {
|
|
69
|
+
const value = patch[key];
|
|
70
|
+
if (value === null || (key === "model" && value === "")) delete next[key];
|
|
71
|
+
else if (value !== undefined) Object.assign(next, { [key]: value });
|
|
72
|
+
}
|
|
73
|
+
return next;
|
|
74
|
+
}
|
|
75
|
+
|
|
27
76
|
export interface WebSearchCandidateRow extends SidecarCandidate {
|
|
28
77
|
/** Executor backend that admitted this exact candidate row. */
|
|
29
78
|
backend: WebSearchBackend;
|
|
@@ -41,7 +90,7 @@ export interface WebSearchModelOption {
|
|
|
41
90
|
|
|
42
91
|
/** The candidate rows the web-search executors can actually run right now. */
|
|
43
92
|
export async function webSearchCandidateRows(config: OcxConfig): Promise<WebSearchCandidateRow[]> {
|
|
44
|
-
const auth =
|
|
93
|
+
const auth = sidecarOptionsAuth(config, config.webSearchSidecar?.anthropicInstance);
|
|
45
94
|
const all = await pickerVisibleSidecarCandidates(config, auth);
|
|
46
95
|
return webSearchSidecarCandidates(config, auth, all).flatMap(candidate => {
|
|
47
96
|
const descriptor = WEB_SEARCH_BACKENDS.find(entry =>
|
|
@@ -279,6 +279,8 @@ export interface ManagementSessionControl {
|
|
|
279
279
|
isCurrent(req: Request, config: OcxConfig): boolean;
|
|
280
280
|
/** Prove that the current browser session came from the operator-mediated pairing flow. */
|
|
281
281
|
isPaired(req: Request, config: OcxConfig): boolean;
|
|
282
|
+
/** Revalidate independent session authority before disclosing stored data keys. */
|
|
283
|
+
canRevealDataKeys?(req: Request, config: OcxConfig): boolean;
|
|
282
284
|
}
|
|
283
285
|
|
|
284
286
|
export function createManagementSessionControl(state: ManagementAuthState): ManagementSessionControl {
|
|
@@ -307,6 +309,10 @@ export function createManagementSessionControl(state: ManagementAuthState): Mana
|
|
|
307
309
|
isPaired(req: Request, config: OcxConfig): boolean {
|
|
308
310
|
return currentSession(req, config)?.issuance === "pairing";
|
|
309
311
|
},
|
|
312
|
+
canRevealDataKeys(req: Request, config: OcxConfig): boolean {
|
|
313
|
+
const issuance = currentSession(req, config)?.issuance;
|
|
314
|
+
return issuance === "pairing" || issuance === "tailscale-identity";
|
|
315
|
+
},
|
|
310
316
|
revokeCurrent(req: Request): boolean {
|
|
311
317
|
if (!state.available) return false;
|
|
312
318
|
const credential = requestManagementCredential(req);
|
|
@@ -13,6 +13,7 @@ import { credentialDomainFor } from "../protocols/opaque-state";
|
|
|
13
13
|
import { resolveProtocolSettings } from "../protocols/settings";
|
|
14
14
|
import type { RouteResult } from "../router";
|
|
15
15
|
import type { OcxConfig } from "../types";
|
|
16
|
+
import { configuredAnthropicInstance } from "../providers/anthropic-instance";
|
|
16
17
|
import { requiresVisionPreprocessing } from "../vision";
|
|
17
18
|
import { resolvePinnedEffort } from "./effort-policy";
|
|
18
19
|
|
|
@@ -94,7 +95,7 @@ function bridgeOnlyPolicyApplies(
|
|
|
94
95
|
/**
|
|
95
96
|
* The credential rule. A proxy-managed key is native since PF-08. An Anthropic OAuth account
|
|
96
97
|
* is native only with `managedMessagesNativeOAuth` on (itself effective only with
|
|
97
|
-
* `managedMessagesNative`), only for
|
|
98
|
+
* `managedMessagesNative`), only for a configured Anthropic OAuth instance, and only
|
|
98
99
|
* to `api.anthropic.com`. Pool selection and recovery are resolved at native dispatch. `forward`
|
|
99
100
|
* belongs to the caller.
|
|
100
101
|
*/
|
|
@@ -107,7 +108,7 @@ function credentialDeclineReason(
|
|
|
107
108
|
if (provider.authMode === undefined || provider.authMode === "key") return undefined;
|
|
108
109
|
if (provider.authMode !== "oauth") return "auth-mode-not-native";
|
|
109
110
|
if (!resolveProtocolSettings(config, route.providerName).rollout.managedMessagesNativeOAuth) return "auth-mode-not-native";
|
|
110
|
-
if (route.providerName
|
|
111
|
+
if (!configuredAnthropicInstance(config, route.providerName)) return "auth-mode-not-native";
|
|
111
112
|
if (!credentialDomainFor(provider)?.firstPartyAnthropic) return "auth-mode-not-native";
|
|
112
113
|
return undefined;
|
|
113
114
|
}
|
|
@@ -19,17 +19,11 @@
|
|
|
19
19
|
* No token, account id or body content is logged or returned in an error message.
|
|
20
20
|
*/
|
|
21
21
|
import type { OAuthAccessSnapshot } from "../oauth";
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
resolveAnthropicDispatchAccountId,
|
|
28
|
-
resolveAnthropicAccountForSession,
|
|
29
|
-
getEligibleAnthropicAccounts,
|
|
30
|
-
isAnthropicAccountPoolEnabled,
|
|
31
|
-
} from "../oauth/anthropic-routing";
|
|
32
|
-
import { resolveAnthropicModelRoute, routeCandidates, type AnthropicRouteDecision } from "../oauth/anthropic-model-routes";
|
|
22
|
+
import { anthropicRoutingFor } from "../oauth/anthropic-routing";
|
|
23
|
+
import { resolveAnthropicModelRouteForInstance, routeCandidates, type AnthropicRouteDecision } from "../oauth/anthropic-model-routes";
|
|
24
|
+
import { configuredAnthropicInstance, type AnthropicInstanceId } from "../providers/anthropic-instance";
|
|
25
|
+
import { resolveAnthropicMessagesUrl } from "../adapters/anthropic";
|
|
26
|
+
import { routedProviderConfig } from "../router";
|
|
33
27
|
import {
|
|
34
28
|
captureOAuthAccountSelection,
|
|
35
29
|
commitOAuthAccountSelection,
|
|
@@ -40,7 +34,6 @@ import type { TranslatorBudget } from "../lib/translator-budget";
|
|
|
40
34
|
import type { OcxConfig } from "../types";
|
|
41
35
|
import { relaySseWithPayloadRewrite } from "./sse-payload-rewrite";
|
|
42
36
|
|
|
43
|
-
const PROVIDER = "anthropic";
|
|
44
37
|
const MAX_SELECTION_ATTEMPTS = 3;
|
|
45
38
|
|
|
46
39
|
type Selection = NonNullable<ReturnType<typeof captureOAuthAccountSelection>>;
|
|
@@ -52,6 +45,9 @@ function isRec(value: unknown): value is Rec {
|
|
|
52
45
|
|
|
53
46
|
/** The committed selection and the credential snapshot one native request is served with. */
|
|
54
47
|
export interface NativeOAuthBinding {
|
|
48
|
+
readonly instance: AnthropicInstanceId;
|
|
49
|
+
/** Config-authorized physical destination, captured before any credential await. */
|
|
50
|
+
readonly routeTarget: string;
|
|
55
51
|
readonly selection: Selection;
|
|
56
52
|
readonly snapshot: OAuthAccessSnapshot;
|
|
57
53
|
/** Anthropic account UUID from the same stored generation, distinct from snapshot.accountId. */
|
|
@@ -74,17 +70,43 @@ export class NativeOAuthSelectionChangedError extends Error {
|
|
|
74
70
|
/** Invalid operator route configuration remains a request refusal, not an auth failure. */
|
|
75
71
|
export class NativeOAuthModelRouteError extends Error {}
|
|
76
72
|
|
|
77
|
-
|
|
78
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Current configured ownership and target; a revoked B never resolves orphan credentials.
|
|
75
|
+
*
|
|
76
|
+
* Adapter, auth mode and destination are read from the routed provider, the same normalization the
|
|
77
|
+
* settled route used to compute its target. Routing canonicalizes the primary row's adapter and auth
|
|
78
|
+
* mode from the registry, so a raw-field comparison would refuse an existing Pool 1 row the route
|
|
79
|
+
* already accepted. B's own raw-row shape is still enforced through `configuredAnthropicInstance`.
|
|
80
|
+
*/
|
|
81
|
+
function nativeOAuthRouteTarget(instance: AnthropicInstanceId, config: OcxConfig): string {
|
|
82
|
+
const provider = config.providers[instance];
|
|
83
|
+
if (configuredAnthropicInstance(config, instance) !== instance || !provider || provider.disabled === true) {
|
|
84
|
+
throw new NativeOAuthSelectionChangedError();
|
|
85
|
+
}
|
|
86
|
+
let routed: ReturnType<typeof routedProviderConfig>;
|
|
87
|
+
try {
|
|
88
|
+
routed = routedProviderConfig(instance, provider);
|
|
89
|
+
} catch {
|
|
90
|
+
// A destination the router now refuses is a moved route, not a different failure class.
|
|
91
|
+
throw new NativeOAuthSelectionChangedError();
|
|
92
|
+
}
|
|
93
|
+
if (routed.authMode !== "oauth" || routed.adapter !== "anthropic") throw new NativeOAuthSelectionChangedError();
|
|
94
|
+
return resolveAnthropicMessagesUrl(routed);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function currentRoute(instance: AnthropicInstanceId, config: OcxConfig, model?: string): AnthropicRouteDecision | null {
|
|
98
|
+
const resolved = model ? resolveAnthropicModelRouteForInstance(instance, config, model) : { decision: null };
|
|
79
99
|
if (resolved.error) throw new NativeOAuthModelRouteError(`Invalid Anthropic model routes: ${resolved.error}`);
|
|
80
100
|
return resolved.decision ? { ...resolved.decision, accounts: [...resolved.decision.accounts] } : null;
|
|
81
101
|
}
|
|
82
102
|
|
|
83
|
-
function routeIsCurrent(config: OcxConfig, model: string | undefined, decision: AnthropicRouteDecision | null): boolean {
|
|
84
|
-
return JSON.stringify(currentRoute(config, model)) === JSON.stringify(decision);
|
|
103
|
+
function routeIsCurrent(instance: AnthropicInstanceId, config: OcxConfig, model: string | undefined, decision: AnthropicRouteDecision | null): boolean {
|
|
104
|
+
return JSON.stringify(currentRoute(instance, config, model)) === JSON.stringify(decision);
|
|
85
105
|
}
|
|
86
106
|
|
|
87
107
|
export interface NativeOAuthBindingOptions {
|
|
108
|
+
/** Settled route destination; retries may never authorize a replacement destination. */
|
|
109
|
+
routeTarget?: string;
|
|
88
110
|
sessionKey?: string | null;
|
|
89
111
|
model?: string;
|
|
90
112
|
/** An account proposed by the shared pre-output refusal recovery policy. */
|
|
@@ -94,21 +116,34 @@ export interface NativeOAuthBindingOptions {
|
|
|
94
116
|
expectedRecoveryRouteDecision?: AnthropicRouteDecision | null;
|
|
95
117
|
}
|
|
96
118
|
|
|
119
|
+
/** Compatibility entrypoint: the original instance only. */
|
|
120
|
+
export function resolveNativeOAuthBinding(config: OcxConfig, options: NativeOAuthBindingOptions = {}): Promise<NativeOAuthBinding> {
|
|
121
|
+
return resolveNativeOAuthBindingForInstance("anthropic", config, options);
|
|
122
|
+
}
|
|
123
|
+
|
|
97
124
|
/** Resolve the shared pool selector and commit its exact credential generation for dispatch. */
|
|
98
|
-
export async function
|
|
125
|
+
export async function resolveNativeOAuthBindingForInstance(
|
|
126
|
+
instance: AnthropicInstanceId,
|
|
99
127
|
config: OcxConfig,
|
|
100
128
|
options: NativeOAuthBindingOptions = {},
|
|
101
129
|
): Promise<NativeOAuthBinding> {
|
|
130
|
+
const routeTarget = options.routeTarget ?? nativeOAuthRouteTarget(instance, config);
|
|
131
|
+
const routing = anthropicRoutingFor(instance);
|
|
132
|
+
const assertOwner = () => {
|
|
133
|
+
if (nativeOAuthRouteTarget(instance, config) !== routeTarget) throw new NativeOAuthSelectionChangedError();
|
|
134
|
+
};
|
|
102
135
|
const sessionKey = options.sessionKey ?? null;
|
|
103
136
|
const model = options.model;
|
|
104
137
|
for (let attempt = 0; attempt < MAX_SELECTION_ATTEMPTS; attempt++) {
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
const
|
|
138
|
+
assertOwner();
|
|
139
|
+
const routeDecision = currentRoute(instance, config, model);
|
|
140
|
+
const manualSelectionGeneration = routing.captureAnthropicManualSelectionGeneration();
|
|
141
|
+
const selection = captureOAuthAccountSelection(instance);
|
|
108
142
|
// Always ask admission first so pause/login/cooldown retains its typed local refusal.
|
|
109
|
-
const admittedId = await resolveAnthropicDispatchAccountId(config, sessionKey, routeDecision, model);
|
|
110
|
-
|
|
111
|
-
|
|
143
|
+
const admittedId = await routing.resolveAnthropicDispatchAccountId(config, sessionKey, routeDecision, model);
|
|
144
|
+
assertOwner();
|
|
145
|
+
if (!routeIsCurrent(instance, config, model, routeDecision)) continue;
|
|
146
|
+
const proposed = routing.resolveAnthropicAccountForSession(sessionKey, config, Date.now(), routeDecision, model);
|
|
112
147
|
// A lost selection commit discards a recovery proposal: a newer manual/policy choice wins.
|
|
113
148
|
const recoverySelectionMatches = !!options.expectedRecoverySelection
|
|
114
149
|
&& selection?.accountId === options.expectedRecoverySelection.accountId
|
|
@@ -118,27 +153,30 @@ export async function resolveNativeOAuthBinding(
|
|
|
118
153
|
const accountId = attempt === 0 && options.candidateAccountId && recoverySelectionMatches && recoveryRouteMatches
|
|
119
154
|
? options.candidateAccountId : admittedId;
|
|
120
155
|
if (!selection) continue;
|
|
121
|
-
if (!routeCandidates(getEligibleAnthropicAccounts(Date.now(), model), routeDecision).includes(accountId)) {
|
|
156
|
+
if (!routeCandidates(routing.getEligibleAnthropicAccounts(Date.now(), model), routeDecision).includes(accountId)) {
|
|
122
157
|
throw new NativeOAuthSelectionChangedError();
|
|
123
158
|
}
|
|
124
|
-
const candidate = await getAnthropicPoolAccessSnapshot(accountId);
|
|
125
|
-
|
|
126
|
-
|
|
159
|
+
const candidate = await routing.getAnthropicPoolAccessSnapshot(accountId);
|
|
160
|
+
assertOwner();
|
|
161
|
+
if (candidate.provider !== instance) throw new NativeOAuthSelectionChangedError();
|
|
162
|
+
if (!routeIsCurrent(instance, config, model, routeDecision)) continue;
|
|
163
|
+
const committed = await commitOAuthAccountSelection(instance, candidate.accountId, {
|
|
127
164
|
expectedSelection: selection,
|
|
128
165
|
expectedCredentialGeneration: candidate.generation,
|
|
129
166
|
requireUsableAccount: true,
|
|
130
167
|
});
|
|
168
|
+
assertOwner();
|
|
131
169
|
if (committed) {
|
|
132
|
-
if (!routeIsCurrent(config, model, routeDecision)
|
|
133
|
-
|| manualSelectionGeneration !== captureAnthropicManualSelectionGeneration()) continue;
|
|
134
|
-
if (!commitAnthropicSelectionRouting(candidate.accountId, selection, committed, {
|
|
170
|
+
if (!routeIsCurrent(instance, config, model, routeDecision)
|
|
171
|
+
|| manualSelectionGeneration !== routing.captureAnthropicManualSelectionGeneration()) continue;
|
|
172
|
+
if (!routing.commitAnthropicSelectionRouting(candidate.accountId, selection, committed, {
|
|
135
173
|
config, sessionKey, model, routeDecision,
|
|
136
174
|
reason: accountId === proposed.accountId ? proposed.reason : undefined,
|
|
137
175
|
expectedCredentialGeneration: candidate.generation,
|
|
138
176
|
})) continue;
|
|
139
|
-
const row = getAccountCredentialWithStatus(
|
|
140
|
-
if (!row || credentialGeneration(row.credential) !== candidate.generation) continue;
|
|
141
|
-
const binding = { selection: committed, snapshot: candidate, providerAccountUuid: row.credential.accountId, routeDecision, sessionKey, model, config, manualSelectionGeneration };
|
|
177
|
+
const row = getAccountCredentialWithStatus(instance, candidate.accountId);
|
|
178
|
+
if (!row || row.credential.access !== candidate.accessToken || credentialGeneration(row.credential) !== candidate.generation) continue;
|
|
179
|
+
const binding: NativeOAuthBinding = { instance, routeTarget, selection: committed, snapshot: candidate, providerAccountUuid: row.credential.accountId, routeDecision, sessionKey, model, config, manualSelectionGeneration };
|
|
142
180
|
if (nativeOAuthBindingIsCurrent(binding)) return binding;
|
|
143
181
|
}
|
|
144
182
|
}
|
|
@@ -150,12 +188,18 @@ export async function resolveNativeOAuthBinding(
|
|
|
150
188
|
* selection, plus the same usable credential generation. Checked before every physical send.
|
|
151
189
|
*/
|
|
152
190
|
export function nativeOAuthBindingIsCurrent(binding: NativeOAuthBinding): boolean {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
191
|
+
const { instance } = binding;
|
|
192
|
+
const routing = anthropicRoutingFor(instance);
|
|
193
|
+
try {
|
|
194
|
+
if (nativeOAuthRouteTarget(instance, binding.config) !== binding.routeTarget) return false;
|
|
195
|
+
} catch { return false; }
|
|
196
|
+
if (binding.snapshot.provider !== instance) return false;
|
|
197
|
+
if (binding.manualSelectionGeneration !== routing.captureAnthropicManualSelectionGeneration()
|
|
198
|
+
|| !routeIsCurrent(instance, binding.config, binding.model, binding.routeDecision)) return false;
|
|
199
|
+
const selected = captureOAuthAccountSelection(instance);
|
|
200
|
+
const row = getAccountCredentialWithStatus(instance, binding.snapshot.accountId);
|
|
201
|
+
const sessionRoute = routing.isAnthropicAccountPoolEnabled(binding.config) && binding.sessionKey
|
|
202
|
+
? routing.resolveAnthropicAccountForSession(binding.sessionKey, binding.config, Date.now(), binding.routeDecision, binding.model) : null;
|
|
159
203
|
// Another conversation may move the automatic active pointer without revoking this
|
|
160
204
|
// session's affinity. Manual selection clears affinity and precedes it in the selector.
|
|
161
205
|
const selectionCurrent = sessionRoute?.reason === "affinity"
|
|
@@ -163,8 +207,9 @@ export function nativeOAuthBindingIsCurrent(binding: NativeOAuthBinding): boolea
|
|
|
163
207
|
|| (selected?.accountId === binding.selection.accountId && selected?.revision === binding.selection.revision);
|
|
164
208
|
return selectionCurrent
|
|
165
209
|
&& !!row && !row.paused && !row.needsReauth && row.credential.expires > Date.now()
|
|
166
|
-
&& !getAnthropicAccountHealthSnapshot(binding.snapshot.accountId)
|
|
167
|
-
&& routeCandidates(getEligibleAnthropicAccounts(Date.now(), binding.model), binding.routeDecision).includes(binding.snapshot.accountId)
|
|
210
|
+
&& !routing.getAnthropicAccountHealthSnapshot(binding.snapshot.accountId)
|
|
211
|
+
&& routeCandidates(routing.getEligibleAnthropicAccounts(Date.now(), binding.model), binding.routeDecision).includes(binding.snapshot.accountId)
|
|
212
|
+
&& row.credential.access === binding.snapshot.accessToken
|
|
168
213
|
&& credentialGeneration(row.credential) === binding.snapshot.generation
|
|
169
214
|
&& row.credential.accountId === binding.providerAccountUuid;
|
|
170
215
|
}
|
|
@@ -36,7 +36,10 @@ export function nativeMessagesToolScopeDenial(
|
|
|
36
36
|
const definition = tool.definition;
|
|
37
37
|
if (typeof definition.name !== "string") { anonymous.push(definition); continue; }
|
|
38
38
|
// A temporary replacement keeps the declaration it shadows in scope as well.
|
|
39
|
-
|
|
39
|
+
// These arrays belong to this check, not to the caller's declarations.
|
|
40
|
+
const declarations = permanent ? [] : definitions.get(definition.name) ?? [];
|
|
41
|
+
declarations.push(definition);
|
|
42
|
+
definitions.set(definition.name, declarations);
|
|
40
43
|
withdrawn.delete(definition.name);
|
|
41
44
|
} else if (tool.type === "tool_reference" && typeof tool.name === "string") {
|
|
42
45
|
if (block.type === "tool_removal" && permanent) withdrawn.add(tool.name);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Pure caller-forward exclusion shared by Messages ingress and protocol preview. */
|
|
2
|
+
import { resolveInboundModel } from "../claude/inbound-model-options";
|
|
3
|
+
import { routeConcreteModel } from "../router";
|
|
4
|
+
import { configuredAnthropicInstance } from "../providers/anthropic-instance";
|
|
5
|
+
import { hasOwnProvider } from "../config/provider-name";
|
|
6
|
+
import type { OcxConfig } from "../types";
|
|
7
|
+
|
|
8
|
+
export function messagesSelectorTargetsSecondaryInstance(
|
|
9
|
+
config: OcxConfig,
|
|
10
|
+
model: string,
|
|
11
|
+
cc: OcxConfig["claudeCode"] = config.claudeCode,
|
|
12
|
+
): boolean {
|
|
13
|
+
const selector = resolveInboundModel(model, cc);
|
|
14
|
+
const slash = selector.indexOf("/");
|
|
15
|
+
if (slash <= 0) return false;
|
|
16
|
+
const qualifier = selector.slice(0, slash);
|
|
17
|
+
// An orphan or unmarked B selector must also never forward a caller's credential.
|
|
18
|
+
if (qualifier === "anthropic2") return true;
|
|
19
|
+
// Provider keys are case-sensitive and outrank aliases, including when disabled.
|
|
20
|
+
if (hasOwnProvider(config.providers, qualifier)) return false;
|
|
21
|
+
try {
|
|
22
|
+
// Concrete resolution handles configured provider aliases without picking a combo/account.
|
|
23
|
+
return routeConcreteModel(config, selector).providerName === "anthropic2";
|
|
24
|
+
} catch {
|
|
25
|
+
return config.providers.anthropic2?.alias?.trim().toLowerCase() === qualifier.toLowerCase();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A missing B row must not fall through the router's generic default-provider path. */
|
|
30
|
+
export function messagesSecondaryInstanceUnavailable(
|
|
31
|
+
config: OcxConfig,
|
|
32
|
+
model: string,
|
|
33
|
+
cc: OcxConfig["claudeCode"] = config.claudeCode,
|
|
34
|
+
): boolean {
|
|
35
|
+
if (!messagesSelectorTargetsSecondaryInstance(config, model, cc)) return false;
|
|
36
|
+
const provider = config.providers.anthropic2;
|
|
37
|
+
return !provider || provider.disabled === true
|
|
38
|
+
|| provider.authMode === "oauth" && configuredAnthropicInstance(config, "anthropic2") !== "anthropic2";
|
|
39
|
+
}
|