@bitkyc08/opencodex 2.77.0 → 2.78.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/bin/ocx.mjs +11 -10
- package/gui/dist/assets/App-BtfG5id-.js +54 -0
- package/gui/dist/assets/App-lCIslZ1L.css +1 -0
- package/gui/dist/assets/{Tray-gloIYNaW.js → Tray-DiJpSub0.js} +1 -1
- package/gui/dist/assets/index-Btc3Ogw4.js +94 -0
- package/gui/dist/assets/index-DmQB8D_R.css +1 -0
- package/gui/dist/assets/quota-summary-B_a96nXj.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +19 -2
- package/src/adapters/anthropic/client-identity.ts +13 -4
- package/src/adapters/anthropic/native-client-preamble.ts +31 -0
- package/src/adapters/anthropic/passthrough.ts +103 -25
- package/src/adapters/devin/cloud-direct/chat.ts +7 -2
- package/src/adapters/devin/trajectory.ts +49 -0
- package/src/adapters/devin.ts +13 -0
- package/src/adapters/ollama-native.ts +35 -19
- package/src/claude/intercept/client-hello.ts +103 -0
- package/src/claude/intercept/model-bindings.ts +3 -2
- package/src/claude/intercept/picker-listener.ts +279 -66
- package/src/cli/access-audio-input.ts +84 -0
- package/src/cli/access-audio-live.ts +99 -0
- package/src/cli/access-audio.ts +123 -0
- package/src/cli/access-data-client.ts +127 -0
- package/src/cli/access-data-plane.ts +104 -0
- package/src/cli/access-data-response.ts +76 -0
- package/src/cli/access.ts +65 -2
- package/src/cli/account-api.ts +30 -6
- package/src/cli/account-auth.ts +111 -42
- package/src/cli/account-key-quota.ts +118 -0
- package/src/cli/account-main.ts +11 -4
- package/src/cli/account-next-actions.ts +63 -0
- package/src/cli/account-policy-dto.ts +68 -0
- package/src/cli/account-policy.ts +167 -0
- package/src/cli/account-target.ts +8 -0
- package/src/cli/account.ts +34 -8
- package/src/cli/agent-runtime-settings.ts +130 -0
- package/src/cli/agent-settings.ts +101 -0
- package/src/cli/agent.ts +19 -2
- package/src/cli/alias.ts +7 -2
- package/src/cli/aside-profiles.ts +2 -2
- package/src/cli/capabilities-access-remote.ts +147 -0
- package/src/cli/capabilities-accounts.ts +195 -0
- package/src/cli/capabilities-agents-routing.ts +414 -0
- package/src/cli/capabilities-base.ts +783 -0
- package/src/cli/capabilities-command.ts +1 -0
- package/src/cli/capabilities-integrations.ts +264 -0
- package/src/cli/capabilities-lab.ts +214 -0
- package/src/cli/capabilities-observe-system.ts +662 -0
- package/src/cli/capabilities-provider-models.ts +302 -0
- package/src/cli/capabilities.ts +30 -1199
- package/src/cli/capability-types.ts +54 -0
- package/src/cli/catalog-command-result.ts +57 -0
- package/src/cli/chatgpt-command.ts +25 -2
- package/src/cli/claude-agent-startup-sync.ts +17 -0
- package/src/cli/claude-desktop-profile.ts +100 -0
- package/src/cli/claude-desktop.ts +39 -6
- package/src/cli/codex-shim-autorestore.ts +2 -1
- package/src/cli/codex-shim-readiness.ts +23 -3
- package/src/cli/combo-input.ts +88 -0
- package/src/cli/combo-result.ts +52 -0
- package/src/cli/combo-stats.ts +123 -0
- package/src/cli/combo.ts +37 -14
- package/src/cli/companion-timeline.ts +142 -0
- package/src/cli/companion-usage.ts +146 -0
- package/src/cli/companion.ts +12 -3
- package/src/cli/config-command.ts +24 -8
- package/src/cli/dispatch.ts +52 -79
- package/src/cli/doctor.ts +22 -4
- package/src/cli/help-catalog.ts +31 -14
- package/src/cli/help-recovery.ts +2 -2
- package/src/cli/help.ts +25 -11
- package/src/cli/index.ts +38 -27
- package/src/cli/injection-follow.ts +35 -0
- package/src/cli/inspect.ts +5 -0
- package/src/cli/integration-aside-sync.ts +69 -0
- package/src/cli/integration-cursor.ts +106 -0
- package/src/cli/integration-input.ts +60 -0
- package/src/cli/integration-journal.ts +49 -0
- package/src/cli/integration-plan-dto.ts +156 -0
- package/src/cli/integration-preview.ts +215 -0
- package/src/cli/integrations.ts +24 -18
- package/src/cli/json-input.ts +134 -0
- package/src/cli/link.ts +32 -4
- package/src/cli/local-sync-result.ts +41 -0
- package/src/cli/log-follow.ts +84 -0
- package/src/cli/log-view-filter.ts +146 -0
- package/src/cli/logout-command.ts +44 -0
- package/src/cli/model-picker-ordering.ts +137 -0
- package/src/cli/models-custom-input.ts +107 -0
- package/src/cli/models-custom-runtime.ts +112 -0
- package/src/cli/models-order.ts +194 -0
- package/src/cli/models-runtime-subcommands.ts +2 -0
- package/src/cli/models-runtime.ts +7 -0
- package/src/cli/models.ts +66 -81
- package/src/cli/observe-stream.ts +142 -0
- package/src/cli/observe.ts +75 -34
- package/src/cli/provider-batch.ts +77 -0
- package/src/cli/provider-lifecycle-runtime.ts +170 -0
- package/src/cli/provider-result.ts +124 -0
- package/src/cli/provider-runtime.ts +15 -12
- package/src/cli/provider-settings.ts +143 -0
- package/src/cli/provider.ts +112 -34
- package/src/cli/registry.ts +62 -20
- package/src/cli/remote-workspace-hub.ts +83 -0
- package/src/cli/remote-workspace.ts +6 -1
- package/src/cli/restart-failure.ts +27 -0
- package/src/cli/root.ts +12 -0
- package/src/cli/route-policy-write.ts +137 -0
- package/src/cli/route-policy.ts +8 -0
- package/src/cli/runtime-api.ts +56 -79
- package/src/cli/secret-args.ts +149 -0
- package/src/cli/settings-result.ts +30 -0
- package/src/cli/storage.ts +77 -7
- package/src/cli/system-command.ts +60 -2
- package/src/cli/system-restart-client.ts +20 -3
- package/src/cli/system-settings-parity.ts +83 -0
- package/src/cli/update-restart-candidate.ts +21 -0
- package/src/cli/update-restart-child.ts +81 -0
- package/src/cli/update-restart-home.ts +38 -0
- package/src/cli/update-restart-transport.ts +193 -0
- package/src/cli/update-restart.ts +260 -0
- package/src/cli/usage-model-search.ts +58 -0
- package/src/cli/usage-report.ts +13 -5
- package/src/cli/v2-input.ts +66 -0
- package/src/cli/v2-local-output.ts +37 -0
- package/src/cli/v2-runtime.ts +90 -0
- package/src/cli/v2.ts +102 -144
- package/src/clients/config-export/dsh.ts +21 -0
- package/src/clients/config-export.ts +7 -1
- package/src/codex/catalog/build-entries.ts +1 -1
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/heal-observation.ts +66 -0
- package/src/codex/catalog/publication-observer.ts +19 -0
- package/src/codex/catalog/remote.ts +13 -15
- package/src/codex/catalog/restore.ts +19 -6
- package/src/codex/catalog/retained-sync.ts +58 -10
- package/src/codex/catalog/routed-removal.ts +116 -0
- package/src/codex/catalog/write-audit-contract.ts +21 -0
- package/src/codex/catalog/write-audit.ts +171 -0
- package/src/codex/catalog-self-heal.ts +284 -0
- package/src/codex/catalog-write-serialization.ts +71 -7
- package/src/codex/codex-home-owner.ts +124 -0
- package/src/codex/convergence.ts +55 -15
- package/src/codex/features.ts +8 -1
- package/src/codex/inject/restore.ts +46 -4
- package/src/codex/inject.ts +11 -1
- package/src/codex/internal/catalog-writer.ts +120 -13
- package/src/codex/journal.ts +49 -13
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/quota-types.ts +3 -0
- package/src/codex/quota.ts +13 -3
- package/src/codex/refresh.ts +20 -9
- package/src/codex/runtime.ts +7 -1
- package/src/codex/shim-diagnostics.ts +80 -0
- package/src/codex/shim-fingerprint.ts +7 -2
- package/src/codex/shim-migration.ts +153 -0
- package/src/codex/shim-overlay.ts +509 -0
- package/src/codex/shim-path-resolution.ts +14 -2
- package/src/codex/shim-probe.ts +4 -3
- package/src/codex/shim-state-file.ts +93 -7
- package/src/codex/shim.ts +15 -68
- package/src/codex/sync.ts +24 -4
- package/src/combos/failover.ts +19 -0
- package/src/combos/index.ts +1 -0
- package/src/combos/resolve.ts +12 -0
- package/src/config/atomic-write.ts +13 -8
- package/src/config/diagnostics.ts +19 -1
- package/src/config/load-degrade.ts +3 -0
- package/src/config/schema/chatgpt-desktop.ts +22 -0
- package/src/config/schema/config-schema.ts +32 -14
- package/src/config/schema/leaf-validators.ts +3 -2
- package/src/generated/compatibility-version.json +445 -129
- package/src/integrations/merge.ts +13 -5
- package/src/integrations/mutation-plan.ts +46 -3
- package/src/integrations/omp-yaml-source.ts +181 -1
- package/src/integrations/ownership-policy.ts +66 -0
- package/src/integrations/registry.ts +50 -1
- package/src/integrations/state.ts +46 -21
- package/src/integrations/target.ts +25 -2
- package/src/integrations/writer.ts +89 -31
- package/src/lib/package-tree-integrity.ts +42 -1
- package/src/lib/proxy-env.ts +3 -0
- package/src/lib/test-home-guard.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +19 -8
- package/src/oauth/anthropic-routing.ts +17 -12
- package/src/oauth/anthropic.ts +27 -2
- package/src/oauth/index.ts +6 -8
- package/src/oauth/pool-settings-capability.ts +9 -2
- package/src/protocols/plan-snapshot.ts +1 -1
- package/src/protocols/settings.ts +28 -4
- package/src/server/caller-session-identity.ts +32 -0
- package/src/server/claude-messages.ts +11 -4
- package/src/server/index/serve-options.ts +12 -7
- package/src/server/index.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +6 -5
- package/src/server/management/context.ts +8 -1
- package/src/server/management/cursor-integration-routes.ts +3 -3
- package/src/server/management/integration-routes.ts +5 -5
- package/src/server/management/model-routes.ts +3 -3
- package/src/server/management/native-integration-routes.ts +2 -2
- package/src/server/management/oauth-account-routes.ts +96 -30
- package/src/server/management/route-registry.ts +25 -24
- package/src/server/messages-native-eligibility.ts +3 -6
- package/src/server/messages-native-oauth.ts +82 -35
- package/src/server/messages-native.ts +78 -16
- package/src/server/responses/adapter-dispatch.ts +53 -28
- package/src/server/responses/core-auth.ts +10 -4
- package/src/server/responses/core-combo.ts +46 -6
- package/src/service/cli.ts +6 -1
- package/src/service/ownership-mutation-lease.d.mts +26 -0
- package/src/service/ownership-mutation-lease.mjs +76 -1
- package/src/types/config.ts +2 -0
- package/src/types/provider.ts +10 -10
- package/src/update/index.ts +2 -1
- package/src/update/update-failure-guidance.d.mts +6 -0
- package/src/update/update-failure-guidance.mjs +30 -5
- package/gui/dist/assets/App-COdEeqSj.css +0 -1
- package/gui/dist/assets/App-DEpCJEP_.js +0 -54
- package/gui/dist/assets/index-Cd7ittMv.css +0 -1
- package/gui/dist/assets/index-DwJKFJm5.js +0 -94
- package/gui/dist/assets/quota-summary-DGcEJU3h.js +0 -1
package/src/oauth/index.ts
CHANGED
|
@@ -1032,18 +1032,16 @@ export async function refreshAnthropicAccountWithLock(
|
|
|
1032
1032
|
// OAuthLoginRequiredError. One 503 locked the account out of refresh until manual
|
|
1033
1033
|
// re-auth even after upstream recovered.
|
|
1034
1034
|
//
|
|
1035
|
-
//
|
|
1036
|
-
//
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
// Those outcomes keep the intent so the guard still refuses a blind replay.
|
|
1041
|
-
if ((!refreshMayHaveReachedProvider || definitivelyAnswered(error)) && attemptIntent) {
|
|
1035
|
+
// Clear only for proven pre-dispatch failure or an explicit HTTP rejection.
|
|
1036
|
+
// Other transport/body failures may follow rotation and keep the replay guard.
|
|
1037
|
+
const requestNotSent = !refreshMayHaveReachedProvider
|
|
1038
|
+
|| (error instanceof AnthropicTokenError && error.requestNotSent);
|
|
1039
|
+
if ((requestNotSent || definitivelyAnswered(error)) && attemptIntent) {
|
|
1042
1040
|
await clearAnthropicRefreshIntentForKnownFailure(
|
|
1043
1041
|
provider,
|
|
1044
1042
|
accountId,
|
|
1045
1043
|
attemptIntent,
|
|
1046
|
-
|
|
1044
|
+
requestNotSent ? "pre-dispatch" : "definitive-rejection",
|
|
1047
1045
|
error,
|
|
1048
1046
|
);
|
|
1049
1047
|
}
|
|
@@ -50,7 +50,7 @@ export function parseGenericStickyLimit(value: unknown): number | null {
|
|
|
50
50
|
|
|
51
51
|
/** Fields the unified pool-settings contract can carry, per kind. */
|
|
52
52
|
export const POOL_SETTINGS_FIELDS = [
|
|
53
|
-
"enabled", "strategy", "stickyLimit", "autoSwitchThreshold", "quotaWindow", "maxConcurrentPerAccount", "routes",
|
|
53
|
+
"enabled", "strategy", "stickyLimit", "autoSwitchThreshold", "quotaWindow", "maxConcurrentPerAccount", "routes", "nativeMessages",
|
|
54
54
|
] as const;
|
|
55
55
|
export type PoolSettingsField = typeof POOL_SETTINGS_FIELDS[number];
|
|
56
56
|
|
|
@@ -84,8 +84,12 @@ export interface PoolSettingsDto {
|
|
|
84
84
|
quotaWindow: string | null;
|
|
85
85
|
maxConcurrentPerAccount: number | null;
|
|
86
86
|
routes: AnthropicModelRoute[] | null;
|
|
87
|
+
/** Anthropic preference, default true; null for unsupported pool kinds. */
|
|
88
|
+
nativeMessages: boolean | null;
|
|
87
89
|
/** Present only when stored Anthropic routes fail validation on read. */
|
|
88
90
|
routesError?: string;
|
|
91
|
+
/** Saved state confirmed despite a post-publication failure. */
|
|
92
|
+
warning?: "config_bookkeeping_failed";
|
|
89
93
|
}
|
|
90
94
|
|
|
91
95
|
|
|
@@ -134,7 +138,7 @@ export function genericPoolSettingsDto(
|
|
|
134
138
|
/** Which fields each kind actually honours. Declared, never silently omitted. */
|
|
135
139
|
const SUPPORTED_BY_KIND: Record<PoolSettingsKind, PoolSettingsField[]> = {
|
|
136
140
|
codex: ["strategy", "stickyLimit", "autoSwitchThreshold"],
|
|
137
|
-
anthropic: ["enabled", "strategy", "stickyLimit", "autoSwitchThreshold", "quotaWindow", "routes"],
|
|
141
|
+
anthropic: ["enabled", "strategy", "stickyLimit", "autoSwitchThreshold", "quotaWindow", "routes", "nativeMessages"],
|
|
138
142
|
generic: ["enabled", "strategy", "stickyLimit", "autoSwitchThreshold"],
|
|
139
143
|
};
|
|
140
144
|
|
|
@@ -165,6 +169,7 @@ export function unifiedPoolSettingsDto(
|
|
|
165
169
|
quotaWindow: null,
|
|
166
170
|
maxConcurrentPerAccount: null,
|
|
167
171
|
routes: null,
|
|
172
|
+
nativeMessages: null,
|
|
168
173
|
};
|
|
169
174
|
}
|
|
170
175
|
if (kind === "anthropic") {
|
|
@@ -174,6 +179,7 @@ export function unifiedPoolSettingsDto(
|
|
|
174
179
|
...base,
|
|
175
180
|
enabled,
|
|
176
181
|
enabledEffective: enabled === true,
|
|
182
|
+
nativeMessages: !Object.hasOwn(pool, "nativeMessages") || pool.nativeMessages === true,
|
|
177
183
|
strategy: parseGenericPoolStrategy(pool.strategy) ?? "quota",
|
|
178
184
|
stickyLimit: parseGenericStickyLimit(pool.stickyLimit) ?? 1,
|
|
179
185
|
autoSwitchThreshold: parseGenericAutoSwitchThreshold(pool.autoSwitchThreshold) ?? 80,
|
|
@@ -198,5 +204,6 @@ export function unifiedPoolSettingsDto(
|
|
|
198
204
|
quotaWindow: null,
|
|
199
205
|
maxConcurrentPerAccount: provider === "kiro" ? parseKiroAccountCap(failover.maxConcurrentPerAccount) : null,
|
|
200
206
|
routes: null,
|
|
207
|
+
nativeMessages: null,
|
|
201
208
|
};
|
|
202
209
|
}
|
|
@@ -138,7 +138,7 @@ function candidateFor(
|
|
|
138
138
|
const reason = rows.effortRow ? "effort-row" : nativeChatDeclineReason(settled, chatBodyForFeatures(features), config);
|
|
139
139
|
nativeEligible = reason === undefined;
|
|
140
140
|
if (reason) declineReasons = [reason];
|
|
141
|
-
} else if (inbound === "messages" && resolveProtocolSettings(config).rollout.managedMessagesNative) {
|
|
141
|
+
} else if (inbound === "messages" && resolveProtocolSettings(config, settled.providerName).rollout.managedMessagesNative) {
|
|
142
142
|
// The runtime rule itself. With the switch off nothing is judged, so the default preview
|
|
143
143
|
// is exactly what it was before the managed native lane existed.
|
|
144
144
|
// Pinned effort is judged from config and the route; blocked-skill elision and the web-search
|
|
@@ -71,19 +71,29 @@ export interface ProtocolSettings {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/** Resolve protocol policy with conservative defaults for every absent or malformed field. */
|
|
74
|
-
export function resolveProtocolSettings(config: Pick<OcxConfig, "protocols"
|
|
74
|
+
export function resolveProtocolSettings(config: Pick<OcxConfig, "protocols" | "anthropicAccountPool">, providerName?: string): Readonly<ProtocolSettings> {
|
|
75
75
|
const raw: unknown = config.protocols;
|
|
76
76
|
const protocols = isRec(raw) ? raw : {};
|
|
77
77
|
const rollout = isRec(protocols.rollout) ? protocols.rollout : {};
|
|
78
78
|
const on = (key: keyof ProtocolRolloutSettings): boolean => rollout[key] === true;
|
|
79
|
-
|
|
79
|
+
// A present malformed container cannot become an absent/default-on policy.
|
|
80
|
+
const validContainers = (raw === undefined || isRec(raw))
|
|
81
|
+
&& (!Object.hasOwn(protocols, "rollout") || isRec(protocols.rollout));
|
|
82
|
+
const rawPool: unknown = config.anthropicAccountPool;
|
|
83
|
+
const pool = isRec(rawPool) ? rawPool : {};
|
|
84
|
+
const pooled = providerName === "anthropic" && pool.enabled === true;
|
|
85
|
+
const poolPreference = !Object.hasOwn(pool, "nativeMessages") || pool.nativeMessages === true;
|
|
86
|
+
const nativeOn = (key: "managedMessagesNative" | "managedMessagesNativeOAuth") => validContainers
|
|
87
|
+
&& (!pooled || poolPreference)
|
|
88
|
+
&& (Object.hasOwn(rollout, key) ? rollout[key] === true : pooled);
|
|
89
|
+
const managedMessagesNative = nativeOn("managedMessagesNative");
|
|
80
90
|
return Object.freeze({
|
|
81
91
|
unrepresentable: protocols.unrepresentable === "reject" ? "reject" : "legacy",
|
|
82
92
|
rollout: Object.freeze({
|
|
83
93
|
nativeChatCombos: on("nativeChatCombos"),
|
|
84
94
|
managedMessagesNative,
|
|
85
95
|
// OAuth extension is meaningless without the key-auth path it extends.
|
|
86
|
-
managedMessagesNativeOAuth: managedMessagesNative &&
|
|
96
|
+
managedMessagesNativeOAuth: managedMessagesNative && nativeOn("managedMessagesNativeOAuth"),
|
|
87
97
|
directEncoders: on("directEncoders"),
|
|
88
98
|
shadowPlan: on("shadowPlan"),
|
|
89
99
|
}),
|
|
@@ -95,16 +105,30 @@ export function resolveProtocolSettings(config: Pick<OcxConfig, "protocols">): R
|
|
|
95
105
|
* dashboard can tell a preview computed under an older policy from a current one. Not a
|
|
96
106
|
* security boundary; FNV-1a over a canonical JSON projection.
|
|
97
107
|
*/
|
|
98
|
-
export function protocolPolicyRevision(config: Pick<OcxConfig, "apiSurfaces" | "claudeCode" | "protocols">): string {
|
|
108
|
+
export function protocolPolicyRevision(config: Pick<OcxConfig, "apiSurfaces" | "claudeCode" | "protocols" | "anthropicAccountPool">): string {
|
|
99
109
|
const surfaces = resolveApiSurfaceSettings(config);
|
|
100
110
|
const settings = resolveProtocolSettings(config);
|
|
111
|
+
const anthropicSettings = resolveProtocolSettings(config, "anthropic");
|
|
112
|
+
const state = (record: unknown, key: string) => !isRec(record) || !Object.hasOwn(record, key)
|
|
113
|
+
? "absent" : record[key] === true ? "true" : record[key] === false ? "false" : "invalid";
|
|
114
|
+
const container = (value: unknown) => value === undefined ? "absent" : isRec(value) ? "object" : "invalid";
|
|
115
|
+
const rawProtocols: unknown = config.protocols;
|
|
116
|
+
const rawRollout = isRec(rawProtocols) ? rawProtocols.rollout : undefined;
|
|
101
117
|
const canonical = JSON.stringify({
|
|
102
118
|
messages: [surfaces.messages.enabled, surfaces.messages.source],
|
|
103
119
|
unrepresentable: settings.unrepresentable,
|
|
120
|
+
nativeInputs: [
|
|
121
|
+
container(rawProtocols), container(rawRollout),
|
|
122
|
+
state(rawRollout, "managedMessagesNative"), state(rawRollout, "managedMessagesNativeOAuth"),
|
|
123
|
+
container(config.anthropicAccountPool), state(config.anthropicAccountPool, "enabled"),
|
|
124
|
+
state(config.anthropicAccountPool, "nativeMessages"),
|
|
125
|
+
],
|
|
104
126
|
rollout: [
|
|
105
127
|
settings.rollout.nativeChatCombos,
|
|
106
128
|
settings.rollout.managedMessagesNative,
|
|
107
129
|
settings.rollout.managedMessagesNativeOAuth,
|
|
130
|
+
anthropicSettings.rollout.managedMessagesNative,
|
|
131
|
+
anthropicSettings.rollout.managedMessagesNativeOAuth,
|
|
108
132
|
settings.rollout.directEncoders,
|
|
109
133
|
settings.rollout.shadowPlan,
|
|
110
134
|
],
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Promote third-party conversation markers after ingress authentication and origin checks. */
|
|
2
|
+
import { contextPrincipalIdOf, type DataPlaneAdmission } from "./auth-cors";
|
|
3
|
+
|
|
4
|
+
const SESSION_HEADERS = ["session_id", "session-id", "thread-id"] as const;
|
|
5
|
+
const SAFE_CALLER_SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
6
|
+
|
|
7
|
+
export function safeCallerSessionId(raw: string | null): string | undefined {
|
|
8
|
+
const value = raw?.trim();
|
|
9
|
+
return value && SAFE_CALLER_SESSION_ID.test(value) ? value : undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function callerSessionId(headers: Headers): string | undefined {
|
|
13
|
+
if (SESSION_HEADERS.some(name => headers.has(name))) return undefined;
|
|
14
|
+
return safeCallerSessionId(headers.get("x-session-id"));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Authenticated callers share continuity only within their trusted credential principal. */
|
|
18
|
+
export function withCallerSessionIdentity(req: Request, admission: DataPlaneAdmission): Request {
|
|
19
|
+
const conversation = callerSessionId(req.headers);
|
|
20
|
+
if (!conversation) return req;
|
|
21
|
+
let sessionId = conversation;
|
|
22
|
+
if (admission.kind !== "loopback") {
|
|
23
|
+
const principal = contextPrincipalIdOf(admission);
|
|
24
|
+
if (!principal) return req;
|
|
25
|
+
sessionId = new Bun.CryptoHasher("sha256")
|
|
26
|
+
.update(JSON.stringify(["opencodex-caller-session-v1", principal, conversation])).digest("hex");
|
|
27
|
+
}
|
|
28
|
+
const headers = new Headers(req.headers);
|
|
29
|
+
headers.set("session_id", sessionId);
|
|
30
|
+
// Preserve the unread body stream and propagate the original request's abort signal.
|
|
31
|
+
return new Request(req, { headers });
|
|
32
|
+
}
|
|
@@ -80,7 +80,7 @@ import { upstreamWireForAdapter } from "../protocols/contract";
|
|
|
80
80
|
import { createProtocolEnvelope, type ProtocolEnvelope } from "../protocols/envelope";
|
|
81
81
|
import { featuresFromMessagesBody, type ProtocolFeature } from "../protocols/features";
|
|
82
82
|
import { credentialDomainFor, messagesBodyHasOpaqueState } from "../protocols/opaque-state";
|
|
83
|
-
import { hasAnthropicFailoverQuorum } from "../oauth/anthropic-routing";
|
|
83
|
+
import { hasAnthropicFailoverQuorum, anthropicSessionKeyFromParts } from "../oauth/anthropic-routing";
|
|
84
84
|
import { checkRepresentable, unrepresentableMessage } from "../protocols/guard";
|
|
85
85
|
import { requestPathForLane } from "../protocols/path";
|
|
86
86
|
import { resolveApiSurfaceSettings, resolveProtocolSettings } from "../protocols/settings";
|
|
@@ -1208,6 +1208,13 @@ async function handleClaudeMessagesWithBudget(
|
|
|
1208
1208
|
// Compatibility identity is an opaque request-local handle, separate from credentials.
|
|
1209
1209
|
clientIdentity: captureAnthropicClientIdentity(req.headers),
|
|
1210
1210
|
callerAnthropicBeta: req.headers.get("anthropic-beta"),
|
|
1211
|
+
sessionKey: anthropicSessionKeyFromParts({
|
|
1212
|
+
sessionIdHeader: req.headers.get("session_id")?.trim() || req.headers.get("x-claude-code-session-id"),
|
|
1213
|
+
threadIdHeader: req.headers.get("thread_id"),
|
|
1214
|
+
clientThreadId: conversationIdFromClaudeMetadata(isRec(nativeBody.metadata) ? nativeBody.metadata : undefined),
|
|
1215
|
+
promptCacheKey: typeof internalBody.prompt_cache_key === "string" ? internalBody.prompt_cache_key : null,
|
|
1216
|
+
promptCacheKeyIsSharedCohort: cacheKeySource === "system",
|
|
1217
|
+
}),
|
|
1211
1218
|
});
|
|
1212
1219
|
}
|
|
1213
1220
|
|
|
@@ -1685,9 +1692,9 @@ export async function handleClaudeCountTokens(
|
|
|
1685
1692
|
// A thread delta would undercount; refuse it exactly as the translated Messages path does.
|
|
1686
1693
|
if (carriesMessageThread(raw)) return messageThreadUnsupportedResponse();
|
|
1687
1694
|
// PF-08: an eligible managed-key route counts the body the native lane would send.
|
|
1688
|
-
const nativeCountBody =
|
|
1689
|
-
|
|
1690
|
-
|
|
1695
|
+
const nativeCountBody = (await import("./messages-native")).nativeMessagesCountBody(
|
|
1696
|
+
config, cc, raw, { fastRow: countFastRow !== null }, captureAnthropicClientIdentity(req.headers),
|
|
1697
|
+
);
|
|
1691
1698
|
// A count answers for the prompt a real turn from this model would forward, so it projects
|
|
1692
1699
|
// the same unserialized content that turn's `message_start` floor does. Counting the raw
|
|
1693
1700
|
// caller body instead reported replayed thinking this route never sends (#4857 family).
|
|
@@ -203,6 +203,7 @@ import { readyProtocolMetadata } from "../../remote/protocol";
|
|
|
203
203
|
import { modelCapabilityFields } from "../models-capabilities";
|
|
204
204
|
import { createWebsocketHandler } from "./websocket-handler";
|
|
205
205
|
import { withGrokSessionIdentity } from "../../grok/session-identity";
|
|
206
|
+
import { withCallerSessionIdentity } from "../caller-session-identity";
|
|
206
207
|
|
|
207
208
|
export type ServerIngress = "public" | "unauthenticated-loopback" | "hub-management" | "claude-intercept" | "hub-link";
|
|
208
209
|
|
|
@@ -298,9 +299,11 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
298
299
|
const requestMetrics = metricsExportEnabled(config)
|
|
299
300
|
? createRequestMetricsOwner(Date.now() / 1000, cachedKiroQuotaMetricRows) : undefined;
|
|
300
301
|
const requestMetricsLogContext = requestMetrics ? { requestMetricsRecorder: requestMetrics } : {};
|
|
301
|
-
const requestManagementApiDeps: ManagementApiDeps =
|
|
302
|
-
|
|
303
|
-
:
|
|
302
|
+
const requestManagementApiDeps: ManagementApiDeps = {
|
|
303
|
+
...managementApiDeps,
|
|
304
|
+
liveListenPort: () => ctx.boundPort ?? listenPort,
|
|
305
|
+
...(requestMetrics ? { requestMetrics: { snapshot: () => requestMetrics.snapshot() } } : {}),
|
|
306
|
+
};
|
|
304
307
|
const serveOptions = {
|
|
305
308
|
idleTimeout: 255,
|
|
306
309
|
// Bun rejects an oversized body before `fetch` runs, so the listener has to be raised
|
|
@@ -1474,10 +1477,11 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
1474
1477
|
logged = true;
|
|
1475
1478
|
addFinalRequestLog(requestId, start, logCtx, status, meta);
|
|
1476
1479
|
};
|
|
1477
|
-
|
|
1480
|
+
const sessionReq = withCallerSessionIdentity(withGrokSessionIdentity(req), admission);
|
|
1481
|
+
return runAdmittedHttpTurn(sessionReq, policy, async turnAdmissionLease => {
|
|
1478
1482
|
let response: Response;
|
|
1479
1483
|
try {
|
|
1480
|
-
response = await handleResponses(
|
|
1484
|
+
response = await handleResponses(sessionReq, config, logCtx, {
|
|
1481
1485
|
turnAdmissionLease,
|
|
1482
1486
|
admission,
|
|
1483
1487
|
onRequestBodyRead: () => disableResponsesRequestTimeout(req, requestServer),
|
|
@@ -1554,8 +1558,9 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
1554
1558
|
// Logging is finalized inside handleClaudeMessages (Responses-vocab tap on the
|
|
1555
1559
|
// pre-translation stream + native passthrough callbacks) — do not re-wrap the
|
|
1556
1560
|
// translated Anthropic stream here.
|
|
1557
|
-
|
|
1558
|
-
|
|
1561
|
+
const sessionReq = withCallerSessionIdentity(req, admission);
|
|
1562
|
+
return runAdmittedHttpTurn(sessionReq, policy, async turnAdmissionLease => withCors(
|
|
1563
|
+
await handleClaudeMessages(sessionReq, config, logCtx, { requestId, start, turnAdmissionLease, admission }, policy, { claudeIntercept: ingress === "claude-intercept" }),
|
|
1559
1564
|
req,
|
|
1560
1565
|
policy,
|
|
1561
1566
|
), { requestId, start, logCtx });
|
package/src/server/index.ts
CHANGED
|
@@ -281,7 +281,7 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
|
|
|
281
281
|
// with the later startup sync and warns ONCE about stale app-servers; warning
|
|
282
282
|
// here instead would read a catalog mtime the sync is about to move.
|
|
283
283
|
const outcome = withCatalogWriteSerialization(startupCodexHome, permit =>
|
|
284
|
-
invalidateCodexModelsCacheWithPermit(permit, startupCodexHome));
|
|
284
|
+
invalidateCodexModelsCacheWithPermit(permit, startupCodexHome), { intent: "cache", writer: "startup-cache" });
|
|
285
285
|
// A refused permit is not a write; only a completed run that returned true is.
|
|
286
286
|
setStartupCacheInvalidationWrite(outcome.kind === "completed" && outcome.value === true);
|
|
287
287
|
} catch { /* no readable Codex home: nothing to invalidate */ }
|
|
@@ -205,7 +205,7 @@ export function setGrokApplyFlightTestHooks(
|
|
|
205
205
|
grokApplyFlight = null;
|
|
206
206
|
grokApplyHighWaterBytes = 0;
|
|
207
207
|
}
|
|
208
|
-
import type
|
|
208
|
+
import { managementInferencePort, type ManagementContext } from "./context";
|
|
209
209
|
|
|
210
210
|
async function injectionModelOptions(config: OcxConfig, models: readonly CatalogModel[]) {
|
|
211
211
|
const disabled = new Set(config.disabledModels ?? []);
|
|
@@ -262,7 +262,8 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
262
262
|
const writtenProfile = current.claudeCode.desktopProfile;
|
|
263
263
|
const markerBaseline = captureDesktopAppliedMarker(writtenProfile);
|
|
264
264
|
const result = (deps.writeDesktop3pConfig ?? writeDesktop3pConfig)(
|
|
265
|
-
|
|
265
|
+
// Live bound port, not config: a CLI override or ephemeral bind must not strand Desktop (#6598).
|
|
266
|
+
managementInferencePort({ config: current, deps }) ?? 10100,
|
|
266
267
|
[...desktopVisibleNativeSlugs(current)],
|
|
267
268
|
routed,
|
|
268
269
|
current.apiKeys?.[0]?.key,
|
|
@@ -898,7 +899,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
898
899
|
if (url.pathname === "/api/claude-desktop" && req.method === "GET") {
|
|
899
900
|
try {
|
|
900
901
|
const state = await buildClaudeDesktopState(config);
|
|
901
|
-
const runtimePort =
|
|
902
|
+
const runtimePort = managementInferencePort(ctx);
|
|
902
903
|
return jsonResponse({ ...state, port: runtimePort });
|
|
903
904
|
} catch (error) {
|
|
904
905
|
return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
@@ -966,7 +967,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
966
967
|
};
|
|
967
968
|
}
|
|
968
969
|
const saved = await buildClaudeDesktopState(config);
|
|
969
|
-
const runtimePort =
|
|
970
|
+
const runtimePort = managementInferencePort(ctx);
|
|
970
971
|
return jsonResponse({ ok: true, ...saved, port: runtimePort });
|
|
971
972
|
} catch (error) {
|
|
972
973
|
return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
@@ -1131,7 +1132,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1131
1132
|
// Picker mode belongs to first-party: stop terminating claude.ai before the gateway lands.
|
|
1132
1133
|
const pickerOff = await ops.disableLocked({ persist: false });
|
|
1133
1134
|
const result = (deps.writeDesktop3pConfig ?? writeDesktop3pConfig)(
|
|
1134
|
-
|
|
1135
|
+
managementInferencePort({ config: latest, deps }),
|
|
1135
1136
|
[...desktopVisibleNativeSlugs(latest)],
|
|
1136
1137
|
routed,
|
|
1137
1138
|
latest.apiKeys?.[0]?.key,
|
|
@@ -160,7 +160,7 @@ export interface ManagementApiDeps {
|
|
|
160
160
|
issueApiKey?: (config: OcxConfig, name: string) => IssuedApiKey;
|
|
161
161
|
revokeApiKey?: (config: OcxConfig, id: string) => boolean;
|
|
162
162
|
loadLinkCandidates?: () => Array<{ alias: string; source: "ssh_config" | "tailscale" }>;
|
|
163
|
-
/**
|
|
163
|
+
/** Bound public inference port, not the ingress receiving this management request. */
|
|
164
164
|
liveListenPort?: () => number | undefined;
|
|
165
165
|
now?: () => number;
|
|
166
166
|
}
|
|
@@ -191,3 +191,10 @@ export interface ManagementContext {
|
|
|
191
191
|
convergeCodexCatalog: () => Promise<CatalogDisposition>;
|
|
192
192
|
syncClaudeAgentDefsBestEffort: () => Promise<void>;
|
|
193
193
|
}
|
|
194
|
+
|
|
195
|
+
/** A management-only ingress cannot supply the port used by generated inference clients. */
|
|
196
|
+
export function managementInferencePort(ctx: Pick<ManagementContext, "config" | "deps">): number {
|
|
197
|
+
// Lifecycle owns the actual port (including CLI overrides/ephemeral binds); direct route
|
|
198
|
+
// fixtures fall back to config without consulting another runtime's state file.
|
|
199
|
+
return ctx.deps.liveListenPort?.() ?? ctx.config.port;
|
|
200
|
+
}
|
|
@@ -53,9 +53,9 @@ export async function buildCursorIntegrationStatus(
|
|
|
53
53
|
const privateInference = pick(installs, "private-inference");
|
|
54
54
|
const regular = pick(installs, "regular");
|
|
55
55
|
const runtime = (deps.readRuntimePort ?? readRuntimePort)(process.pid);
|
|
56
|
-
// The
|
|
57
|
-
//
|
|
58
|
-
const port =
|
|
56
|
+
// The lifecycle-bound public port wins, then the PID-matched runtime record, then config.
|
|
57
|
+
// A management-only ingress port must never become the Cursor gateway (#6598).
|
|
58
|
+
const port = deps.liveListenPort?.() ?? runtime?.port ?? config.port;
|
|
59
59
|
// Cursor runs on this machine, so the gateway URL it is told to paste is the LOCAL one: the
|
|
60
60
|
// unauthenticated loopback listener when one is enabled, and otherwise the bind address on the
|
|
61
61
|
// public port — 127.0.0.1 for a loopback or wildcard bind exactly as before, and the tailnet
|
|
@@ -46,7 +46,7 @@ import {
|
|
|
46
46
|
} from "../../integrations/mutation-flight";
|
|
47
47
|
import { jsonResponse } from "../auth-cors";
|
|
48
48
|
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
49
|
-
import type
|
|
49
|
+
import { managementInferencePort, type ManagementContext } from "./context";
|
|
50
50
|
import { exportSnapshotIdentity, loadExportModels, previewExportSnapshot } from "./model-rows";
|
|
51
51
|
import {
|
|
52
52
|
previewIntegration,
|
|
@@ -230,7 +230,7 @@ function asideOptions(ctx: ManagementContext): AsideProfileRouteOptions {
|
|
|
230
230
|
return {
|
|
231
231
|
input: () => input ??= {
|
|
232
232
|
config: ctx.config,
|
|
233
|
-
port:
|
|
233
|
+
port: managementInferencePort(ctx),
|
|
234
234
|
models: () => loadExportModels(ctx.config),
|
|
235
235
|
store: integrationStore(),
|
|
236
236
|
...pathOverrides(),
|
|
@@ -252,7 +252,7 @@ async function buildIntegrationWriteInput(
|
|
|
252
252
|
clientId,
|
|
253
253
|
models: await loadExportModels(ctx.config),
|
|
254
254
|
config: ctx.config,
|
|
255
|
-
port:
|
|
255
|
+
port: managementInferencePort(ctx),
|
|
256
256
|
...(droidReasoningDefaults === undefined ? {} : { droidReasoningDefaults: { ...droidReasoningDefaults } }),
|
|
257
257
|
store,
|
|
258
258
|
io: integrationMutationTestHooks?.io,
|
|
@@ -284,7 +284,7 @@ async function buildIntegrationPreviewInput(
|
|
|
284
284
|
clientId,
|
|
285
285
|
models: snapshot.models,
|
|
286
286
|
config: ctx.config,
|
|
287
|
-
port:
|
|
287
|
+
port: managementInferencePort(ctx),
|
|
288
288
|
...(droidReasoningDefaults === undefined ? {} : { droidReasoningDefaults: { ...droidReasoningDefaults } }),
|
|
289
289
|
store,
|
|
290
290
|
io: integrationMutationTestHooks?.io,
|
|
@@ -619,7 +619,7 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
|
|
|
619
619
|
if (url.pathname === "/api/client-integrations" && req.method === "GET") {
|
|
620
620
|
try {
|
|
621
621
|
const models = await loadExportModels(ctx.config);
|
|
622
|
-
const port =
|
|
622
|
+
const port = managementInferencePort(ctx);
|
|
623
623
|
// One store for the whole collection read: without it this route retried
|
|
624
624
|
// maintenance and counted snapshots in the default store even when the
|
|
625
625
|
// caller had bound everything else to a temp root (A-gate round 13).
|
|
@@ -167,7 +167,7 @@ import type {
|
|
|
167
167
|
|
|
168
168
|
import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
|
|
169
169
|
import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
|
|
170
|
-
import type
|
|
170
|
+
import { managementInferencePort, type ManagementContext } from "./context";
|
|
171
171
|
import { effectiveModelReasoningEfforts, inheritedModelReasoningEfforts, listManagementModelRows, loadExportModels } from "./model-rows";
|
|
172
172
|
import { initialModelSelectionPending } from "../../providers/initial-model-selection";
|
|
173
173
|
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
@@ -204,7 +204,7 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
204
204
|
?? (await import("../../integrations/catalog-refresh")).refreshOwnedCatalogIntegrations;
|
|
205
205
|
const clientIntegrations = await refresh({
|
|
206
206
|
config,
|
|
207
|
-
port:
|
|
207
|
+
port: managementInferencePort(ctx),
|
|
208
208
|
models: () => loadExportModels(config),
|
|
209
209
|
});
|
|
210
210
|
return { catalogRefresh, clientIntegrations };
|
|
@@ -562,7 +562,7 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
562
562
|
reason: "non_loopback",
|
|
563
563
|
}, 400, req, config);
|
|
564
564
|
}
|
|
565
|
-
const baseUrl = opencodeProxyBaseUrl(
|
|
565
|
+
const baseUrl = opencodeProxyBaseUrl(managementInferencePort(ctx), config.hostname, config);
|
|
566
566
|
let models: ExportModel[];
|
|
567
567
|
try {
|
|
568
568
|
// The ONE loader every export surface uses. It carries the visibility
|
|
@@ -51,7 +51,7 @@ import type { CodexNativeRestoreResult } from "../../codex/inject";
|
|
|
51
51
|
import type { OcxConfig } from "../../types";
|
|
52
52
|
import { jsonResponse } from "../auth-cors";
|
|
53
53
|
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
54
|
-
import type
|
|
54
|
+
import { managementInferencePort, type ManagementContext } from "./context";
|
|
55
55
|
|
|
56
56
|
export type NativeIntegrationClientId = "claude" | "grok" | "codex" | "claude-desktop";
|
|
57
57
|
|
|
@@ -540,7 +540,7 @@ async function handleGrokToggle(ctx: ManagementContext): Promise<Response> {
|
|
|
540
540
|
* a stale config.hostname picks the wrong loopback policy branch entirely.
|
|
541
541
|
*/
|
|
542
542
|
const runtime = (deps.readRuntimePort ?? readRuntimePort)(process.pid);
|
|
543
|
-
const port = runtime?.port ?? (
|
|
543
|
+
const port = deps.liveListenPort?.() ?? runtime?.port ?? managementInferencePort(ctx);
|
|
544
544
|
const hostname = runtime?.hostname ?? config.hostname;
|
|
545
545
|
|
|
546
546
|
/*
|