@bitkyc08/opencodex 2.90.0 → 2.92.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 +37 -3
- package/gui/dist/assets/App-DMps4OLk.css +1 -0
- package/gui/dist/assets/{App-DgxkE6Md.js → App-sUf_8afJ.js} +18 -18
- package/gui/dist/assets/{Tray-DbTNc_88.js → Tray-BSsxznDm.js} +1 -1
- package/gui/dist/assets/index-BlFE_VB_.js +94 -0
- package/gui/dist/assets/quota-summary-BRlbXgw0.js +1 -0
- package/gui/dist/index.html +1 -1
- package/gui/dist/provider-icons/workbuddy-color.svg +29 -0
- package/package.json +4 -3
- package/src/adapters/anthropic.ts +2 -5
- package/src/adapters/claude-cli/usage-admission.ts +14 -0
- package/src/adapters/google-vertex-endpoint.ts +9 -1
- package/src/adapters/google-vertex-key-project.ts +223 -0
- package/src/adapters/google.ts +13 -3
- package/src/adapters/openai-chat/tool-name-registry.ts +3 -1
- package/src/adapters/openai-chat.ts +2 -2
- package/src/adapters/openai-responses/passthrough.ts +2 -1
- package/src/adapters/openai-responses/request-strips.ts +92 -0
- package/src/adapters/openai-responses/transport.ts +6 -0
- package/src/adapters/registry.ts +5 -0
- package/src/adapters/workbuddy-schema.ts +61 -0
- package/src/adapters/workbuddy.ts +68 -0
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +1 -1
- package/src/chat/inbound.ts +20 -2
- package/src/chat/outbound.ts +47 -8
- package/src/chat/reasoning-details.ts +137 -0
- package/src/cli/capabilities-agents-routing.ts +53 -3
- package/src/cli/capabilities-base.ts +15 -2
- package/src/cli/capabilities-observe-system.ts +4 -4
- package/src/cli/codex-cli-update.ts +8 -0
- package/src/cli/codex-shim-autorestore.ts +5 -0
- package/src/cli/combo-result.ts +6 -0
- package/src/cli/combo-stats.ts +3 -0
- package/src/cli/combo.ts +66 -42
- package/src/cli/dispatch.ts +30 -0
- package/src/cli/fallback.ts +127 -0
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +1 -2
- package/src/cli/launcher-context.ts +49 -2
- package/src/cli/link-apply-restart.ts +72 -0
- package/src/cli/link-child.ts +6 -2
- package/src/cli/link-cleanup.ts +95 -0
- package/src/cli/link-enrollment.ts +4 -0
- package/src/cli/link.ts +123 -35
- package/src/cli/provider.ts +46 -3
- package/src/cli/registry.ts +13 -1
- package/src/cli/restart-failure.ts +4 -0
- package/src/cli/runtime-api.ts +1 -1
- package/src/cli/status-codex-settings.ts +56 -0
- package/src/cli/status-fallback.ts +29 -0
- package/src/cli/status-install.ts +52 -0
- package/src/cli/status.ts +20 -1
- package/src/cli/system-restart-client.ts +32 -1
- package/src/cli/update-restart-child.ts +4 -3
- package/src/cli/update-restart-home.ts +20 -8
- package/src/cli/usage-compare.ts +68 -12
- package/src/client/connect.ts +82 -6
- package/src/client/home-unreachable.ts +16 -0
- package/src/client/hub-relay.ts +8 -2
- package/src/client/link-home.ts +119 -6
- package/src/client/link-ingress.ts +1 -1
- package/src/client/link-parity-reporter.ts +107 -0
- package/src/client/link-readiness.ts +97 -0
- package/src/client/link-relay-transport.ts +84 -9
- package/src/client/link-relay.ts +98 -15
- package/src/client/link-tunnel.ts +45 -18
- package/src/client/machine-listener.ts +103 -3
- package/src/client/runtime-restart.ts +127 -0
- package/src/client/runtime.ts +155 -25
- package/src/codex/account-store.ts +17 -3
- package/src/codex/app-server-restart-service.ts +6 -5
- package/src/codex/auth-context.ts +10 -6
- package/src/codex/catalog/effort.ts +9 -2
- package/src/codex/catalog/gather-capture.ts +4 -2
- package/src/codex/catalog/parsing.ts +4 -0
- package/src/codex/catalog/provider-models.ts +14 -8
- package/src/codex/chatgpt-refresh-failure.ts +6 -2
- package/src/codex/cli-install-provenance.ts +418 -0
- package/src/codex/cli-installation-targets.ts +5 -1
- package/src/codex/cli-update-plan.ts +43 -16
- package/src/codex/desktop-app/picker-freshness.ts +10 -8
- package/src/codex/desktop-compatibility/usage-activation.ts +2 -1
- package/src/codex/desktop-compatibility/usage-controller.ts +7 -1
- package/src/codex/desktop-compatibility/windows-key-protection.ts +18 -6
- package/src/codex/pool-refresh-backoff.ts +3 -1
- package/src/codex/quota-history.ts +7 -0
- package/src/codex/quota.ts +10 -0
- package/src/codex/subagent-model-fallback.ts +24 -1
- package/src/codex/user-identity.ts +35 -6
- package/src/codex/windows-current-sid.ts +162 -0
- package/src/combos/index.ts +1 -0
- package/src/combos/jev-quota-activation.ts +5 -1
- package/src/combos/jev-quota-route.ts +1 -1
- package/src/combos/jev-quota.ts +9 -1
- package/src/combos/jev-session-pin-config.ts +51 -0
- package/src/combos/jev.ts +6 -0
- package/src/combos/resolve.ts +12 -0
- package/src/combos/types.ts +19 -0
- package/src/config/diagnostics.ts +5 -0
- package/src/config/load-degrade.ts +2 -0
- package/src/config/provider-validation.ts +7 -0
- package/src/config/schema/config-schema.ts +3 -0
- package/src/config/schema/fallback-policy.ts +40 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +375 -195
- package/src/lib/authorization-scheme.ts +95 -0
- package/src/lib/decision-quota-sink.ts +10 -0
- package/src/lib/provider-client-headers.ts +1 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/system-restart-contract.ts +24 -0
- package/src/lib/test-home-guard.ts +78 -11
- package/src/lib/translator-budget.ts +4 -4
- package/src/lib/upstream-retry.ts +4 -1
- package/src/link/child-parity.ts +124 -0
- package/src/link/codex-settings.ts +37 -0
- package/src/link/compatibility.ts +119 -0
- package/src/link/diagnostics.ts +193 -0
- package/src/link/management-routes.ts +2 -0
- package/src/link/peer-report.ts +100 -0
- package/src/link/relay-auth.ts +6 -0
- package/src/link/remote-platform.ts +78 -0
- package/src/link/ssh-argv.ts +19 -0
- package/src/link/status-projection.ts +63 -2
- package/src/link/supervisor.ts +45 -12
- package/src/link/tunnel-history.ts +109 -0
- package/src/link/tunnel-state.ts +80 -17
- package/src/messaging/remote-files.ts +24 -1
- package/src/oauth/generic-account-failover.ts +91 -7
- package/src/oauth/index.ts +16 -1
- package/src/oauth/kimi.ts +2 -0
- package/src/oauth/model-discovery-auth.ts +5 -0
- package/src/oauth/nous.ts +4 -1
- package/src/oauth/store.ts +6 -2
- package/src/oauth/token-guardian.ts +7 -0
- package/src/oauth/types.ts +9 -0
- package/src/oauth/workbuddy.ts +139 -0
- package/src/protocols/codecs/chat.ts +3 -3
- package/src/protocols/encoders/adapter-events.ts +26 -2
- package/src/protocols/encoders/chat.ts +55 -3
- package/src/providers/antigravity-quota-identity.ts +37 -0
- package/src/providers/base-url-choices.ts +8 -5
- package/src/providers/derive.ts +23 -3
- package/src/providers/mistral-transport.ts +37 -4
- package/src/providers/model-discovery.ts +2 -0
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/quota/account-cache.ts +3 -2
- package/src/providers/quota/antigravity.ts +27 -22
- package/src/providers/quota/report-cache.ts +13 -4
- package/src/providers/quota/vendor-probes-key.ts +60 -0
- package/src/providers/quota/workbuddy.ts +83 -0
- package/src/providers/quota-decision-antigravity.ts +64 -0
- package/src/providers/quota-decision-publication.ts +11 -0
- package/src/providers/quota-decision-snapshot.ts +21 -4
- package/src/providers/quota-types.ts +2 -0
- package/src/providers/quota.ts +3 -1
- package/src/providers/registry/entries-core.ts +9 -0
- package/src/providers/registry/entries-extended.ts +20 -7
- package/src/providers/registry/model-ids.ts +3 -0
- package/src/providers/registry/model-seeds.ts +27 -28
- package/src/providers/registry/types.ts +19 -1
- package/src/providers/resolved-model-policy.ts +4 -4
- package/src/providers/routed-config.ts +15 -0
- package/src/providers/stale-model-roster-migration.ts +83 -15
- package/src/providers/workbuddy-credit-balance.ts +81 -0
- package/src/providers/workbuddy-models.ts +37 -0
- package/src/providers/workbuddy.ts +60 -0
- package/src/quota-pool/assemble.ts +326 -0
- package/src/quota-pool/contract.ts +58 -0
- package/src/quota-pool/identity.ts +203 -0
- package/src/remote/features.ts +74 -0
- package/src/remote/protocol.ts +2 -0
- package/src/remote/served-by.ts +57 -0
- package/src/responses/parser.ts +7 -0
- package/src/responses/reasoning-envelope.ts +12 -0
- package/src/responses/reasoning-replay-cache.ts +18 -14
- package/src/router.ts +2 -0
- package/src/routing/analytics.ts +2 -3
- package/src/routing/fallback-policy.ts +290 -0
- package/src/routing/fallback-state.ts +179 -0
- package/src/server/audio-client.ts +2 -1
- package/src/server/audio-dictation.ts +2 -1
- package/src/server/audio-upstream.ts +2 -1
- package/src/server/auth-cors.ts +7 -5
- package/src/server/background-lifecycle.ts +4 -0
- package/src/server/chat-completions.ts +22 -2
- package/src/server/chat-native-reasoning.ts +10 -3
- package/src/server/chat-native.ts +13 -2
- package/src/server/claude-messages.ts +3 -0
- package/src/server/gui-session.ts +2 -1
- package/src/server/index/link-listener.ts +11 -1
- package/src/server/index/link-management.ts +47 -0
- package/src/server/index/link-relay-sessions.ts +20 -7
- package/src/server/index/optional-listeners.ts +11 -2
- package/src/server/index/serve-options.ts +19 -2
- package/src/server/live.ts +5 -0
- package/src/server/local-management-read-client.ts +3 -1
- package/src/server/management/combo-routes.ts +10 -4
- package/src/server/management/context.ts +1 -1
- package/src/server/management/fallback-routes.ts +75 -0
- package/src/server/management/link-enrollment.ts +21 -3
- package/src/server/management/link-route-state.ts +2 -0
- package/src/server/management/link-routes.ts +16 -2
- package/src/server/management/provider-routes.ts +1 -1
- package/src/server/management/quota-pool-routes.ts +24 -0
- package/src/server/management/route-registry.ts +3 -0
- package/src/server/management-api.ts +16 -0
- package/src/server/management-auth.ts +2 -1
- package/src/server/port-reclaim.ts +51 -9
- package/src/server/request-log-conversation.ts +49 -0
- package/src/server/request-log.ts +2 -0
- package/src/server/responses/agent-task-recovery.ts +1 -1
- package/src/server/responses/anthropic-thinking-recovery.ts +1 -0
- package/src/server/responses/compact.ts +12 -4
- package/src/server/responses/core-auth.ts +11 -1
- package/src/server/responses/core-combo.ts +125 -32
- package/src/server/responses/core-lifetime.ts +2 -2
- package/src/server/responses/core-normalize.ts +6 -3
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-replay.ts +2 -1
- package/src/server/responses/encrypted-payload.ts +3 -3
- package/src/server/responses/jev-combo-choice.ts +39 -0
- package/src/server/responses/jev-session-pin.ts +203 -0
- package/src/server/responses/main-hard-lock-routing.ts +54 -2
- package/src/server/responses/passthrough-delivery.ts +4 -0
- package/src/server/responses/policy-fallback.ts +44 -1
- package/src/server/responses/request-prepare.ts +19 -6
- package/src/server/responses/request-transport.ts +25 -6
- package/src/server/responses/run-turn-execution.ts +2 -4
- package/src/server/responses/sidecar-execution.ts +2 -4
- package/src/server/responses-phase-inference.ts +267 -0
- package/src/server/restart-replacement.ts +50 -2
- package/src/server/sse-payload-rewrite.ts +14 -1
- package/src/server/ws-bridge.ts +5 -0
- package/src/service/cli.ts +25 -2
- package/src/service/definition.ts +20 -8
- package/src/service/diagnostics.ts +20 -6
- package/src/service/status-json.ts +116 -0
- package/src/types/config.ts +13 -0
- package/src/types/fallback.ts +38 -0
- package/src/types/provider.ts +9 -1
- package/src/types/request.ts +1 -0
- package/src/types/wire.ts +1 -0
- package/src/update/codex-cli-update-launch-policy.d.mts +9 -0
- package/src/update/codex-cli-update-launch-policy.mjs +30 -0
- package/src/usage/jev-stats.ts +54 -14
- package/gui/dist/assets/App-dnkJ85te.css +0 -1
- package/gui/dist/assets/index-TmaHa54u.js +0 -94
- package/gui/dist/assets/quota-summary-B16x5DZZ.js +0 -1
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `GET|PUT /api/fallback`: the OCX-11 fallback state (`ocx.fallback/1`) and its one scoped write.
|
|
3
|
+
*
|
|
4
|
+
* GET is a passive read. A Remote Link Child reaches it through the read-only link relay
|
|
5
|
+
* (`LINK_MANAGEMENT_READ_PATHS`); there it drops link aliases (`buildFallbackState` passive). PUT
|
|
6
|
+
* runs on the Home only: a Child's relay and CLI refuse writes before sending.
|
|
7
|
+
*/
|
|
8
|
+
import type { ManagementContext } from "./context";
|
|
9
|
+
import { jsonResponse } from "../auth-cors";
|
|
10
|
+
import { mutatePersistedConfig } from "../../config";
|
|
11
|
+
import { isPassiveRead } from "../../lib/passive-read";
|
|
12
|
+
import { readLinkStore, type LinkRecord } from "../../link/store";
|
|
13
|
+
import { linkStorePath } from "../../link/paths";
|
|
14
|
+
import { applyFallbackWrite, buildFallbackState, parseFallbackWrites } from "../../routing/fallback-state";
|
|
15
|
+
import { managementBodyTooLargeResponse, readManagementJsonBody } from "./body";
|
|
16
|
+
|
|
17
|
+
function links(ctx: ManagementContext): LinkRecord[] {
|
|
18
|
+
try {
|
|
19
|
+
return (ctx.deps.readLinkStore?.() ?? readLinkStore(linkStorePath())).links;
|
|
20
|
+
} catch {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function view(ctx: ManagementContext): Response {
|
|
26
|
+
const response = jsonResponse(buildFallbackState(ctx.config, { links: links(ctx), passive: isPassiveRead() }), 200, ctx.req, ctx.config);
|
|
27
|
+
response.headers.set("cache-control", "no-store");
|
|
28
|
+
return response;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function handleFallbackRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
32
|
+
const { req, url, config } = ctx;
|
|
33
|
+
if (url.pathname === "/api/fallback" && req.method === "GET") return view(ctx);
|
|
34
|
+
if (url.pathname === "/api/fallback" && req.method === "PUT") return await writeFallback(ctx);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function writeFallback(ctx: ManagementContext): Promise<Response> {
|
|
39
|
+
const { req, config } = ctx;
|
|
40
|
+
let body: unknown;
|
|
41
|
+
try {
|
|
42
|
+
body = await readManagementJsonBody(req);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return managementBodyTooLargeResponse(error, req, config)
|
|
45
|
+
?? jsonResponse({ error: "invalid JSON body", code: "invalid_request" }, 400, req, config);
|
|
46
|
+
}
|
|
47
|
+
const parsed = parseFallbackWrites(body);
|
|
48
|
+
if (!parsed.ok) return jsonResponse({ error: parsed.error, code: "invalid_request" }, 400, req, config);
|
|
49
|
+
const known = links(ctx);
|
|
50
|
+
for (const write of parsed.writes) {
|
|
51
|
+
if (write.scope !== "device") continue;
|
|
52
|
+
// A link alias names its link; otherwise the key must be a known link id or data key id.
|
|
53
|
+
const byAlias = known.find(link => link.alias === write.key);
|
|
54
|
+
if (byAlias) write.key = byAlias.id;
|
|
55
|
+
// Clearing stays possible for a device that is gone (a revoked link, a deleted data key):
|
|
56
|
+
// its entry still lists in the state, and clearing a missing entry is a no-op.
|
|
57
|
+
if (write.value === null) continue;
|
|
58
|
+
const exists = known.some(link => link.id === write.key)
|
|
59
|
+
|| (config.apiKeys ?? []).some(entry => entry.id === write.key);
|
|
60
|
+
if (!exists) {
|
|
61
|
+
return jsonResponse({ error: "unknown device: use a link id or alias from ocx link status, or a Hub data key id", code: "unknown_device" }, 404, req, config);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const outcome = mutatePersistedConfig(persisted => {
|
|
65
|
+
let changed = false;
|
|
66
|
+
for (const write of parsed.writes) changed = applyFallbackWrite(persisted, write) || changed;
|
|
67
|
+
return { changed, value: persisted.fallback === undefined ? undefined : structuredClone(persisted.fallback) };
|
|
68
|
+
});
|
|
69
|
+
if (outcome.status === "unavailable") {
|
|
70
|
+
return jsonResponse({ error: "config.json is " + outcome.reason + "; fallback settings were not saved", code: "config_unavailable" }, 409, req, config);
|
|
71
|
+
}
|
|
72
|
+
if (outcome.value === undefined) delete config.fallback;
|
|
73
|
+
else config.fallback = outcome.value;
|
|
74
|
+
return view(ctx);
|
|
75
|
+
}
|
|
@@ -3,6 +3,8 @@ import { tmpdir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { assertSshAlias, buildExecArgv, buildFingerprintArgv, buildProbeArgv, REMOTE_COMMAND_NOT_FOUND, REMOTE_PLATFORM_UNSUPPORTED, remoteOcxArgv } from "../../link/ssh-argv";
|
|
5
5
|
import { parseFingerprintLine } from "../../link/fingerprint";
|
|
6
|
+
import { detectRemotePlatform, isWindowsShellRefusal, WINDOWS_CHILD_REFUSAL } from "../../link/remote-platform";
|
|
7
|
+
import type { SshRunner } from "../../link/ssh-runner";
|
|
6
8
|
import { LINK_CATALOG_MAX_BYTES, LINK_RELAY_VERIFY_MS, isChildHealth, linkCommandBudget, linkRestartBudget, parseChildHttp, remoteChildHttpArgv, REMOTE_CURL_PREFLIGHT } from "../../link/remote-probe";
|
|
7
9
|
import { newLinkId, type LinkStore } from "../../link/store";
|
|
8
10
|
import { SshRunnerError, boundHint, sshFailureHint, sshRunnerErrorHint, type SshRunResult } from "../../link/ssh-runner";
|
|
@@ -24,11 +26,23 @@ async function serialized(state: LinkRouteState, alias: string, action: () => Pr
|
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
function remoteFailure(result: SshRunResult, code: string, message: string): Response | null {
|
|
27
|
-
if (result.code === REMOTE_PLATFORM_UNSUPPORTED
|
|
29
|
+
if (result.code === REMOTE_PLATFORM_UNSUPPORTED || (result.code !== 0 && isWindowsShellRefusal(result.stderr))) return windowsRefusal();
|
|
28
30
|
if (result.code === REMOTE_COMMAND_NOT_FOUND) return fail("remote_ocx_missing", "ocx was not found on the remote host.", 502, sshFailureHint(result.stderr));
|
|
29
31
|
return result.code === 0 ? null : fail(code, message, 502, sshFailureHint(result.stderr));
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
function windowsRefusal(): Response {
|
|
35
|
+
return fail("remote_platform_unsupported", WINDOWS_CHILD_REFUSAL, 409);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Pre-issuance Windows gate for a failed remote command: exit 125, the English cmd/PowerShell
|
|
40
|
+
* refusal, or (for localized shells) the fixed %OS% probe against the same host key.
|
|
41
|
+
*/
|
|
42
|
+
async function windowsChild(runner: SshRunner, alias: string, knownHostsFile: string, result: SshRunResult): Promise<boolean> {
|
|
43
|
+
return result.code !== 0 && await detectRemotePlatform(runner, alias, knownHostsFile, result) === "windows";
|
|
44
|
+
}
|
|
45
|
+
|
|
32
46
|
export async function probe(ctx: ManagementContext, state: LinkRouteState): Promise<Response> {
|
|
33
47
|
const value = await readManagementJsonBodyOr(ctx.req, null);
|
|
34
48
|
const body = exactBody(value, ["alias"]) ?? exactBody(value, ["alias", "includeVersion"]);
|
|
@@ -44,7 +58,8 @@ export async function probe(ctx: ManagementContext, state: LinkRouteState): Prom
|
|
|
44
58
|
const started = now();
|
|
45
59
|
const result = await runner.run(buildProbeArgv({ alias, tempKnownHostsFile: path }), { timeoutMs: 30_000 });
|
|
46
60
|
const latency = Math.max(0, now() - started);
|
|
47
|
-
|
|
61
|
+
// cmd.exe and PowerShell refuse the POSIX `true` the probe runs; say Windows, not "probe failed".
|
|
62
|
+
if (result.code !== 0) return await windowsChild(runner, alias, path, result) ? windowsRefusal() : fail("probe_failed", "SSH host probing failed.", 502, sshFailureHint(result.stderr));
|
|
48
63
|
const fp = await runner.run(buildFingerprintArgv(path), { timeoutMs: 10_000 });
|
|
49
64
|
if (fp.code !== 0) return fail("fingerprint_failed", "The SSH host fingerprint could not be read.", 502, sshFailureHint(fp.stderr));
|
|
50
65
|
const parsed = parseFingerprintLine(fp.stdout.trim());
|
|
@@ -53,6 +68,7 @@ export async function probe(ctx: ManagementContext, state: LinkRouteState): Prom
|
|
|
53
68
|
let ocxVersion: string | undefined;
|
|
54
69
|
if (body.includeVersion === true) {
|
|
55
70
|
const versionResult = await runner.run(buildExecArgv({ alias, argv: remoteOcxArgv(["--version"]), knownHostsFile: path }), { timeoutMs: 30_000 });
|
|
71
|
+
if (await windowsChild(runner, alias, path, versionResult)) return windowsRefusal();
|
|
56
72
|
const denied = remoteFailure(versionResult, "version_probe_failed", "The remote ocx version could not be read.");
|
|
57
73
|
if (denied) return denied;
|
|
58
74
|
const parsedVersion = parseVersion(versionResult.stdout);
|
|
@@ -84,7 +100,9 @@ export async function confirmHost(ctx: ManagementContext, state: LinkRouteState)
|
|
|
84
100
|
writeFileSync(path, pending.knownHostLine + "\n", { mode: 0o600 });
|
|
85
101
|
try {
|
|
86
102
|
const started = now();
|
|
87
|
-
const
|
|
103
|
+
const runner = runnerFor(ctx);
|
|
104
|
+
const result = await runner.run(buildExecArgv({ alias, argv: remoteOcxArgv(["--version"]), knownHostsFile: path }), { timeoutMs: 30_000 });
|
|
105
|
+
if (await windowsChild(runner, alias, path, result)) return windowsRefusal();
|
|
88
106
|
const denied = remoteFailure(result, "version_probe_failed", "The remote ocx version could not be confirmed.");
|
|
89
107
|
if (denied) return denied;
|
|
90
108
|
const version = parseVersion(result.stdout);
|
|
@@ -27,6 +27,8 @@ export interface LinkRouteListener {
|
|
|
27
27
|
status(): { state: "off" | "listening" | "failed"; port: number | null; reason: string | null };
|
|
28
28
|
close(): Promise<void>;
|
|
29
29
|
onAuthenticatedCatalog(listener: (apiKeyId: string) => void): () => void;
|
|
30
|
+
/** Optional so route fakes stay small; without it every link reports an unknown peer. */
|
|
31
|
+
peerObservation?(linkId: string): import("../../link/peer-report").LinkPeerEntry;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export interface LinkRouteState {
|
|
@@ -6,7 +6,11 @@ import { clearCompensationFailed, compensationPath, readCompensation } from "../
|
|
|
6
6
|
import { loadHostCandidates } from "../../link/ssh-config";
|
|
7
7
|
import { newLinkId, type LinkStore } from "../../link/store";
|
|
8
8
|
import { sshFailureHint, sshRunnerErrorHint } from "../../link/ssh-runner";
|
|
9
|
-
import { projectLinkStatus, type LinkStatusDto } from "../../link/status-projection";
|
|
9
|
+
import { attachLinkPeers, attachLinkTunnelDiagnostics, projectLinkStatus, type LinkStatusDto } from "../../link/status-projection";
|
|
10
|
+
import { homeLinkChildParity } from "../../link/child-parity";
|
|
11
|
+
import { localLinkPeer } from "../../link/compatibility";
|
|
12
|
+
import { packageVersion } from "../../lib/package-version";
|
|
13
|
+
import { buildPeerFeatures } from "../../remote/features";
|
|
10
14
|
import { isLinkPort } from "../../link/ports";
|
|
11
15
|
import { joinHome, type ClientLinkJoinDeps } from "../../client/link-join";
|
|
12
16
|
import { clientLinkStatusIdentity } from "../../client/link-status";
|
|
@@ -264,10 +268,12 @@ export async function handleLinkRoutes(ctx: ManagementContext, suppliedState?: L
|
|
|
264
268
|
if (denied) return denied;
|
|
265
269
|
const store = readStoreFor(ctx);
|
|
266
270
|
const listenerStatus = state.listener.status();
|
|
271
|
+
const supervisorStates = state.supervisor.status();
|
|
267
272
|
const dto: LinkStatusDto = projectLinkStatus(
|
|
268
|
-
store,
|
|
273
|
+
store, supervisorStates, listenerStatus, ctx.config, readCompensation(),
|
|
269
274
|
ctx.config.runtimeRole === "client" ? clientLinkTunnelStatus() : null,
|
|
270
275
|
ctx.config.runtimeRole === "client" && ctx.config.client?.link ? clientLinkStatusIdentity(ctx.config.client.link.linkId) : null,
|
|
276
|
+
linkId => homeLinkChildParity.get(linkId),
|
|
271
277
|
);
|
|
272
278
|
for (const link of dto.links) {
|
|
273
279
|
const failure = state.compensationFailures?.get(link.id);
|
|
@@ -277,6 +283,14 @@ export async function handleLinkRoutes(ctx: ManagementContext, suppliedState?: L
|
|
|
277
283
|
const failure = state.compensationFailures?.get(store.links[0].id);
|
|
278
284
|
if (failure) Object.assign(dto.child, { state: "failed" as const, since: failure.since, reason: failure.reason });
|
|
279
285
|
}
|
|
286
|
+
// Opt-in: an older `ocx link status` validates the document key by key and would reject the
|
|
287
|
+
// added fields, so only a caller that asks for compatibility gets `local` and `peer`.
|
|
288
|
+
if (url.searchParams.get("compatibility") === "1") {
|
|
289
|
+
attachLinkPeers(dto, localLinkPeer(packageVersion("0.0.0"), buildPeerFeatures()),
|
|
290
|
+
linkId => state.listener.peerObservation?.(linkId) ?? { observed: null, refused: null });
|
|
291
|
+
}
|
|
292
|
+
// The same opt-in for each Home-initiated tunnel's retry progress and last failure cause.
|
|
293
|
+
if (url.searchParams.get("diagnostics") === "1") attachLinkTunnelDiagnostics(dto, supervisorStates);
|
|
280
294
|
// A dashboard session also learns why it cannot join as a Child. The admin-token answer stays
|
|
281
295
|
// the exact K16 document that `ocx link status` validates key by key.
|
|
282
296
|
const denial = ctx.principal === "gui-session" ? joinDenied(ctx) : null;
|
|
@@ -1726,7 +1726,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
1726
1726
|
return jsonResponse({ ok: false, latencyMs: 0, error: "Antigravity project unavailable — re-run `ocx login google-antigravity`" });
|
|
1727
1727
|
}
|
|
1728
1728
|
const { method, url: modelsUrl, headers } = buildModelsRequest(prov, apiKey, name, {
|
|
1729
|
-
oauthApiBaseUrl: snapshot?.apiBaseUrl,
|
|
1729
|
+
oauthApiBaseUrl: snapshot?.apiBaseUrl, workbuddy: snapshot?.workbuddy,
|
|
1730
1730
|
});
|
|
1731
1731
|
const discovery = resolveProviderModelDiscovery(name, prov);
|
|
1732
1732
|
const started = Date.now();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `GET /api/quota-pool`: the `ocx.quota-pool/1` read GOAT consumes (see src/quota-pool/).
|
|
3
|
+
*
|
|
4
|
+
* Same management authorization as the neighbouring reads; a Remote Link Child reaches it through
|
|
5
|
+
* the read-only link relay (`LINK_MANAGEMENT_READ_PATHS`) and a Hub client through its hub relay.
|
|
6
|
+
* The read is always passive: there is no refresh form, so nothing here can make the Home probe.
|
|
7
|
+
*/
|
|
8
|
+
import type { ManagementContext } from "./context";
|
|
9
|
+
import { jsonResponse } from "../auth-cors";
|
|
10
|
+
|
|
11
|
+
export async function handleQuotaPoolRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
12
|
+
const { req, url, config } = ctx;
|
|
13
|
+
// Any query (including a stray refresh=1) is ignored: the read has no active form.
|
|
14
|
+
if (url.pathname === "/api/quota-pool" && req.method === "GET") {
|
|
15
|
+
const { readQuotaPool } = await import("../../quota-pool/assemble");
|
|
16
|
+
const result = await readQuotaPool(config);
|
|
17
|
+
const response = result.ok
|
|
18
|
+
? jsonResponse(result.pool, 200, req, config)
|
|
19
|
+
: jsonResponse({ error: result.error }, 503, req, config);
|
|
20
|
+
response.headers.set("cache-control", "no-store");
|
|
21
|
+
return response;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
@@ -370,6 +370,9 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
370
370
|
{ method: "GET", path: "/api/provider-context-caps", module: "server/management/provider-routes", mutates: false },
|
|
371
371
|
{ method: "GET", path: "/api/provider-presets", module: "server/management/provider-routes", mutates: false },
|
|
372
372
|
{ method: "GET", path: "/api/provider-quotas", module: "server/management/provider-routes", mutates: false },
|
|
373
|
+
{ method: "GET", path: "/api/quota-pool", module: "server/management/quota-pool-routes", mutates: false, exempt: { reason: "deferred-verb", why: "The ocx.quota-pool/1 read is a machine contract GOAT consumes over management and the link relay; the CLI shows the same caches through ocx provider quota and ocx account.", owner: "ocx-1 quota pool", ownerDoc: "structure/quota-pool.md" } },
|
|
374
|
+
{ method: "GET", path: "/api/fallback", module: "server/management/fallback-routes", mutates: false },
|
|
375
|
+
{ method: "PUT", path: "/api/fallback", module: "server/management/fallback-routes", mutates: true },
|
|
373
376
|
{ method: "GET", path: "/api/provider-request-pacing", module: "server/management/provider-key-pacing-routes", mutates: false },
|
|
374
377
|
{ method: "GET", path: "/api/providers", module: "server/management/provider-routes", mutates: false },
|
|
375
378
|
{ method: "PATCH", path: "/api/providers", module: "server/management/provider-routes", mutates: true },
|
|
@@ -161,6 +161,20 @@ async function handleLowQuotaRoutesOnDemand(ctx: ManagementContext): Promise<Res
|
|
|
161
161
|
return handleLowQuotaRoutes(ctx);
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
/** Lazy for the same reason: the quota-pool assembler pulls every quota cache reader. */
|
|
165
|
+
async function handleQuotaPoolRoutesOnDemand(ctx: ManagementContext): Promise<Response | null> {
|
|
166
|
+
if (!pathInManagementNamespace(ctx.url.pathname, "/api/quota-pool", false)) return null;
|
|
167
|
+
const { handleQuotaPoolRoutes } = await import("./management/quota-pool-routes");
|
|
168
|
+
return handleQuotaPoolRoutes(ctx);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Lazy: the OCX-11 fallback state view and its write live off the dashboard's common path. */
|
|
172
|
+
async function handleFallbackRoutesOnDemand(ctx: ManagementContext): Promise<Response | null> {
|
|
173
|
+
if (!pathInManagementNamespace(ctx.url.pathname, "/api/fallback", false)) return null;
|
|
174
|
+
const { handleFallbackRoutes } = await import("./management/fallback-routes");
|
|
175
|
+
return handleFallbackRoutes(ctx);
|
|
176
|
+
}
|
|
177
|
+
|
|
164
178
|
/**
|
|
165
179
|
* Lazy like the Lab and routing-profile handlers, and for the same recorded reason: this file is
|
|
166
180
|
* mounted for every dashboard request, so a static import would put the workflow-budget ledger
|
|
@@ -351,6 +365,8 @@ export async function handleManagementAPI(
|
|
|
351
365
|
?? (await handleRequestHistoryRoutes(ctx))
|
|
352
366
|
?? (await handleQuotaResetRoutesOnDemand(ctx))
|
|
353
367
|
?? (await handleLowQuotaRoutesOnDemand(ctx))
|
|
368
|
+
?? (await handleQuotaPoolRoutesOnDemand(ctx))
|
|
369
|
+
?? (await handleFallbackRoutesOnDemand(ctx))
|
|
354
370
|
?? (await handleWorkflowBudgetRoutesOnDemand(ctx))
|
|
355
371
|
?? (await handleCodexAgentRoleRoutesOnDemand(ctx))
|
|
356
372
|
?? (await handleProtocolRoutesOnDemand(ctx))
|
|
@@ -73,6 +73,7 @@ import {
|
|
|
73
73
|
} from "./gui-session";
|
|
74
74
|
import { hasLocalDesktopSnapshotCapability } from "./local-desktop-snapshot-auth";
|
|
75
75
|
import { hasLocalAccountSwitchCapability } from "./local-account-switch-auth";
|
|
76
|
+
import { authorizationCredential } from "../lib/authorization-scheme";
|
|
76
77
|
export type { GuiSessionBootstrap, GuiSessionRequestContext } from "./gui-session";
|
|
77
78
|
|
|
78
79
|
const LOCAL_READ_REPLAY_LIMIT = 256;
|
|
@@ -545,7 +546,7 @@ function hasGuiPairCapability(
|
|
|
545
546
|
|
|
546
547
|
function requestManagementCredential(req: Request): string | null {
|
|
547
548
|
return req.headers.get("x-opencodex-api-key")?.trim()
|
|
548
|
-
|| req.headers
|
|
549
|
+
|| authorizationCredential(req.headers)
|
|
549
550
|
|| null;
|
|
550
551
|
}
|
|
551
552
|
|
|
@@ -405,22 +405,37 @@ async function runOwnerLookup(file: string, args: string[], timeoutMs: number):
|
|
|
405
405
|
return stdout;
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
+
export type LoopbackOwnerLookupResult = {
|
|
409
|
+
kind: "match" | "mismatch" | "lookup_error" | "deadline";
|
|
410
|
+
elapsedMs: number;
|
|
411
|
+
};
|
|
412
|
+
|
|
408
413
|
export interface LoopbackOwnerLookupIo {
|
|
409
414
|
platform?: NodeJS.Platform;
|
|
415
|
+
nowMs?: () => number;
|
|
416
|
+
scheduleDeadline?: (callback: () => void, timeoutMs: number) => () => void;
|
|
410
417
|
readProc?: (path: string) => Promise<string>;
|
|
411
418
|
listFds?: (path: string) => Promise<string[]>;
|
|
412
419
|
readFdLink?: (path: string) => Promise<string>;
|
|
413
420
|
run?: (file: string, args: string[], timeoutMs: number) => Promise<string>;
|
|
414
421
|
}
|
|
415
422
|
|
|
416
|
-
/**
|
|
417
|
-
|
|
423
|
+
/**
|
|
424
|
+
* Classify fresh IPv4 loopback LISTEN ownership within the bounded lookup deadline.
|
|
425
|
+
* Only match proves the expected PID; mismatch, inspection failure and timeout remain distinct.
|
|
426
|
+
* elapsedMs uses a monotonic clock by default, and no raw OS output is returned.
|
|
427
|
+
*/
|
|
428
|
+
export async function inspectIpv4LoopbackListener(
|
|
418
429
|
port: number,
|
|
419
430
|
expectedPid: number,
|
|
420
431
|
io: LoopbackOwnerLookupIo = {},
|
|
421
|
-
): Promise<
|
|
432
|
+
): Promise<LoopbackOwnerLookupResult> {
|
|
433
|
+
const now = io.nowMs ?? (() => performance.now());
|
|
434
|
+
const startedAt = now();
|
|
435
|
+
const result = (kind: LoopbackOwnerLookupResult["kind"]): LoopbackOwnerLookupResult =>
|
|
436
|
+
({ kind, elapsedMs: Math.max(0, now() - startedAt) });
|
|
422
437
|
if (!Number.isInteger(port) || port < 1 || port > 65535
|
|
423
|
-
|| !Number.isSafeInteger(expectedPid) || expectedPid < 1) return
|
|
438
|
+
|| !Number.isSafeInteger(expectedPid) || expectedPid < 1) return result("mismatch");
|
|
424
439
|
const platform = io.platform ?? process.platform;
|
|
425
440
|
const lookup = async (): Promise<boolean> => {
|
|
426
441
|
if (platform === "linux") {
|
|
@@ -458,13 +473,40 @@ export async function ownsIpv4LoopbackListener(
|
|
|
458
473
|
}
|
|
459
474
|
return false;
|
|
460
475
|
};
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
timer = setTimeout(() => resolve(false), OWNER_LOOKUP_TIMEOUT_MS);
|
|
476
|
+
const scheduleDeadline = io.scheduleDeadline ?? ((callback, timeoutMs) => {
|
|
477
|
+
const timer = setTimeout(callback, timeoutMs);
|
|
464
478
|
timer.unref?.();
|
|
479
|
+
return () => clearTimeout(timer);
|
|
465
480
|
});
|
|
466
|
-
|
|
467
|
-
|
|
481
|
+
let cancelDeadline: (() => void) | undefined;
|
|
482
|
+
const deadline = new Promise<LoopbackOwnerLookupResult["kind"]>(resolve => {
|
|
483
|
+
cancelDeadline = scheduleDeadline(() => resolve("deadline"), OWNER_LOOKUP_TIMEOUT_MS);
|
|
484
|
+
});
|
|
485
|
+
const inspected = lookup().then(
|
|
486
|
+
matches => matches ? "match" as const : "mismatch" as const,
|
|
487
|
+
error => {
|
|
488
|
+
const failure = error as {
|
|
489
|
+
killed?: boolean; signal?: string | null; code?: unknown; stdout?: unknown; stderr?: unknown;
|
|
490
|
+
} | null;
|
|
491
|
+
// lsof exits 1 with no output when no socket matches its selectors.
|
|
492
|
+
if (platform === "darwin" && failure?.code === 1 && !failure.killed
|
|
493
|
+
&& failure.signal == null && failure.stdout === "" && failure.stderr === "") return "mismatch" as const;
|
|
494
|
+
// execFile's timeout kills with SIGTERM; other spawn/exit/buffer errors are lookup failures.
|
|
495
|
+
return failure?.killed === true && failure.signal === "SIGTERM" && failure.code === null
|
|
496
|
+
? "deadline" as const : "lookup_error" as const;
|
|
497
|
+
},
|
|
498
|
+
);
|
|
499
|
+
try { return result(await Promise.race([inspected, deadline])); }
|
|
500
|
+
finally { cancelDeadline?.(); }
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** Boolean compatibility API: only a proven match admits the listener. */
|
|
504
|
+
export async function ownsIpv4LoopbackListener(
|
|
505
|
+
port: number,
|
|
506
|
+
expectedPid: number,
|
|
507
|
+
io: LoopbackOwnerLookupIo = {},
|
|
508
|
+
): Promise<boolean> {
|
|
509
|
+
return (await inspectIpv4LoopbackListener(port, expectedPid, io)).kind === "match";
|
|
468
510
|
}
|
|
469
511
|
|
|
470
512
|
/**
|
|
@@ -172,6 +172,55 @@ export function conversationIdFromResponsesRequest(input: {
|
|
|
172
172
|
);
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Hermes Agent session affinity header for log correlation (#330).
|
|
177
|
+
* Kept separate from runtime/session identities so it cannot affect lanes or routing.
|
|
178
|
+
*/
|
|
179
|
+
export function conversationIdFromHermesHeader(headers: Headers): string | undefined {
|
|
180
|
+
return normalizeLogConversationId(headers.get("x-hermes-conversation-id"));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Validates and normalizes native header candidates in priority order for logging:
|
|
185
|
+
* parent-thread -> session_id -> session-id -> thread-id.
|
|
186
|
+
* Each candidate spelling is evaluated and normalized independently so an empty or
|
|
187
|
+
* whitespace-only candidate (e.g. empty session_id) cannot suppress a subsequent valid candidate.
|
|
188
|
+
*/
|
|
189
|
+
export function nativeLogConversationIdFromHeaders(headers: Headers): string | undefined {
|
|
190
|
+
return (
|
|
191
|
+
normalizeLogConversationId(headers.get("x-codex-parent-thread-id")) ??
|
|
192
|
+
normalizeLogConversationId(headers.get("session_id")) ??
|
|
193
|
+
normalizeLogConversationId(headers.get("session-id")) ??
|
|
194
|
+
normalizeLogConversationId(headers.get("thread-id"))
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The request-log context's conversation slot; structural so callers need not import request-log. */
|
|
199
|
+
export interface LogConversationSlot {
|
|
200
|
+
conversationId?: string;
|
|
201
|
+
conversationIdIsHermesFallback?: boolean;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Record a native log identity. Fills an empty slot or replaces the log-only Hermes fallback,
|
|
206
|
+
* so a native id found late (Responses preparation, a Cursor conversation created mid-turn)
|
|
207
|
+
* still wins. A caller-owned id already in the slot (routed Claude metadata) is kept.
|
|
208
|
+
*/
|
|
209
|
+
export function assignNativeLogConversationId(slot: LogConversationSlot, nativeId: string | undefined): void {
|
|
210
|
+
if (!nativeId || (slot.conversationId && !slot.conversationIdIsHermesFallback)) return;
|
|
211
|
+
slot.conversationId = nativeId;
|
|
212
|
+
delete slot.conversationIdIsHermesFallback;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Fill an empty slot with the log-only Hermes digest, marked so any native id replaces it. */
|
|
216
|
+
export function fillHermesLogConversationFallback(slot: LogConversationSlot, headers: Headers): void {
|
|
217
|
+
if (slot.conversationId) return;
|
|
218
|
+
const hermes = conversationIdFromHermesHeader(headers);
|
|
219
|
+
if (!hermes) return;
|
|
220
|
+
slot.conversationId = hermes;
|
|
221
|
+
slot.conversationIdIsHermesFallback = true;
|
|
222
|
+
}
|
|
223
|
+
|
|
175
224
|
/**
|
|
176
225
|
* Claude Code metadata.user_id only — never the system-hash Desktop fallback.
|
|
177
226
|
* Hashes the raw user_id once (same opacity goal as inbound prompt_cache_key).
|
|
@@ -150,6 +150,8 @@ export interface RequestLogContext extends UpstreamErrorDiagnostics {
|
|
|
150
150
|
lastOutputAt?: number;
|
|
151
151
|
/** Best-effort chat/session correlation for Logs grouping (#330). Opaque; omit when unknown. */
|
|
152
152
|
conversationId?: string;
|
|
153
|
+
/** In-memory only: conversationId holds the log-only Hermes digest, which a native id replaces. */
|
|
154
|
+
conversationIdIsHermesFallback?: boolean;
|
|
153
155
|
surface?: "claude" | "claude-desktop" | "grok";
|
|
154
156
|
/** The matched configured key's id. Set ONLY for admissionKind "configured" —
|
|
155
157
|
* never a sentinel, so a hand-edited entry whose id happens to be "loopback"
|
|
@@ -104,7 +104,7 @@ function findEnvelope(input: unknown): AgentEnvelope | null {
|
|
|
104
104
|
const type = input[itemIndex] && typeof input[itemIndex] === "object"
|
|
105
105
|
? (input[itemIndex] as { type?: unknown }).type
|
|
106
106
|
: undefined;
|
|
107
|
-
if (type !== "compaction_trigger" && type !== "additional_tools") break;
|
|
107
|
+
if (type !== "compaction_trigger" && type !== "additional_tools" && type !== "configuration_update") break;
|
|
108
108
|
itemIndex -= 1;
|
|
109
109
|
}
|
|
110
110
|
|
|
@@ -130,6 +130,7 @@ export function createAnthropicThinkingRecovery(
|
|
|
130
130
|
|| response.status !== "completed" || !Array.isArray(response.output) || response.output.length === 0
|
|
131
131
|
|| !rejectedScope
|
|
132
132
|
|| rejectedScope.clientThreadId !== parsed._reasoningReplayScope?.clientThreadId
|
|
133
|
+
|| rejectedScope.clientPrincipalId !== parsed._reasoningReplayScope?.clientPrincipalId
|
|
133
134
|
|| JSON.stringify(rejectedScope.current) !== JSON.stringify(parsed._reasoningReplayScope?.current)) return;
|
|
134
135
|
remembered = true;
|
|
135
136
|
rememberReasoningReplayOpaqueBlobRejection(rejectedScope, rejectedPayloads);
|
|
@@ -129,13 +129,14 @@ import {
|
|
|
129
129
|
ForwardAdmissionCredentialError,
|
|
130
130
|
contextPrincipalIdOf,
|
|
131
131
|
hasForwardableCodexBearer,
|
|
132
|
+
resolveContextPrincipal,
|
|
132
133
|
validateForwardAdmissionCredential,
|
|
133
134
|
} from "../auth-cors";
|
|
134
135
|
import type { DataPlaneAdmission } from "../auth-cors";
|
|
135
136
|
import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
|
|
136
137
|
import { CODEX_FORWARD_BASE_URL, isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactEndpoint } from "../../providers/openai-tiers";
|
|
137
138
|
import { buildResponsesTransport, copyResponsesClientMetadata } from "../../adapters/openai-responses/transport";
|
|
138
|
-
import { scrubOcxCompactionItems, stripCanonicalOnlyTopLevelFields, stripInternalChatMessageMetadataPassthrough } from "../../adapters/openai-responses/request-strips";
|
|
139
|
+
import { scrubOcxCompactionItems, stripCanonicalOnlyInputItems, stripCanonicalOnlyTopLevelFields, stripInternalChatMessageMetadataPassthrough } from "../../adapters/openai-responses/request-strips";
|
|
139
140
|
import { dropResponsesReasoningInputItems } from "../../adapters/openai-responses/reasoning";
|
|
140
141
|
import { requiresPlaintextReasoningReplay } from "../../adapters/openai-responses/passthrough";
|
|
141
142
|
import { destinationDecodesNativeCompactionBlob, isOpenAiOperatedResponsesDestination } from "../../providers/openai-tiers-destination";
|
|
@@ -1002,7 +1003,12 @@ export async function handleResponsesCompact(
|
|
|
1002
1003
|
cursorConversationId: gatewayReplayParsed._cursorConversationId,
|
|
1003
1004
|
sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
|
|
1004
1005
|
});
|
|
1005
|
-
if (clientThreadId)
|
|
1006
|
+
if (clientThreadId) {
|
|
1007
|
+
gatewayReplayParsed._reasoningReplayScope = {
|
|
1008
|
+
clientThreadId,
|
|
1009
|
+
clientPrincipalId: resolveContextPrincipal(req, config, admission),
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1006
1012
|
}
|
|
1007
1013
|
let compactUrl = gatewayTransport ? `${gatewayTransport.url.replace(/\/+$/, "")}/compact` : `${base}/responses/compact`;
|
|
1008
1014
|
const compactTargetKey = `${route.providerName}|${route.modelId}|compact`;
|
|
@@ -1155,7 +1161,7 @@ export async function handleResponsesCompact(
|
|
|
1155
1161
|
replayBody = stripInternalChatMessageMetadataPassthrough(replayBody) as typeof replayBody;
|
|
1156
1162
|
}
|
|
1157
1163
|
return isOpenAiOperatedResponsesDestination(sendProvider)
|
|
1158
|
-
? replayBody : stripCanonicalOnlyTopLevelFields(replayBody);
|
|
1164
|
+
? replayBody : stripCanonicalOnlyInputItems(stripCanonicalOnlyTopLevelFields(replayBody));
|
|
1159
1165
|
};
|
|
1160
1166
|
const doFetch = (upstreamRecovery?: UpstreamSendRecovery) => fetchWithHeaderTimeout(
|
|
1161
1167
|
compactUrl,
|
|
@@ -1723,7 +1729,9 @@ export async function handleResponsesCompact(
|
|
|
1723
1729
|
// The routed compaction turn is a handoff inside the same logical request, so it draws the
|
|
1724
1730
|
// REMAINDER. Minting here is what let a native attempt spend three sends and the routed
|
|
1725
1731
|
// fallback spend four more.
|
|
1726
|
-
|
|
1732
|
+
// internalReq keeps only FORWARD_HEADERS, so the caller principal is resolved from req.
|
|
1733
|
+
const replayPrincipal = { clientPrincipalId: resolveContextPrincipal(req, config, admission) };
|
|
1734
|
+
const response = await handleResponses(internalReq, config, logCtx, { abortSignal: req.signal, turnAdmissionLease, sendBudget, compactionRecoveryKind: "compaction-v1", compactionRoutingOverride: options.compactionRoutingOverride, replayPrincipal, ...(admission ? { admission } : {}) });
|
|
1727
1735
|
if (req.signal.aborted) {
|
|
1728
1736
|
void response.body?.cancel(req.signal.reason).catch(() => undefined);
|
|
1729
1737
|
return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
|
|
@@ -46,6 +46,7 @@ import { forceRefreshMainAccountToken } from "../../codex/main-account";
|
|
|
46
46
|
import { rebindPoolCreditPolicy } from "../../codex/pool-credit-policy";
|
|
47
47
|
import { safeCallerSessionId } from "../caller-session-identity";
|
|
48
48
|
import { copyResponsesClientMetadata } from "../../adapters/openai-responses/transport";
|
|
49
|
+
import { AgentAssertionUnsupportedError, agentAssertionUnsupportedResponse, authorizationCredential, carryAgentAssertionForCallerBearer, hasAgentAssertionAuthorization } from "../../lib/authorization-scheme";
|
|
49
50
|
|
|
50
51
|
/** Normalize caller aliases only on native egress; preserve ingress and fallback identity. */
|
|
51
52
|
export function withClaudeNativeSession(headers: Headers, provider: OcxProviderConfig, sessionId?: string): Headers {
|
|
@@ -205,6 +206,9 @@ export async function resolveResponsesCodexAuth(
|
|
|
205
206
|
retainAccountForUploadedFiles = false,
|
|
206
207
|
): Promise<ResponsesAuthResolution> {
|
|
207
208
|
let authCtx: CodexAuthContext | undefined;
|
|
209
|
+
// Ingress already refuses assertion_only; a later internal hop must not reach a pool or
|
|
210
|
+
// routed credential with it either, because that would be a silent credential swap.
|
|
211
|
+
if (hasAgentAssertionAuthorization(req.headers)) return { ok: false, response: agentAssertionUnsupportedResponse() };
|
|
208
212
|
try {
|
|
209
213
|
let authInputHeaders = codexRouteCredentialDomainHeaders(
|
|
210
214
|
req,
|
|
@@ -332,7 +336,7 @@ export async function resolveResponsesCodexAuth(
|
|
|
332
336
|
} else {
|
|
333
337
|
// Even adapters that ignore caller auth must not retain a proxy secret for
|
|
334
338
|
// a later internal hop or a future transport change.
|
|
335
|
-
const bearer = headers
|
|
339
|
+
const bearer = authorizationCredential(headers);
|
|
336
340
|
if (bearer && isProxyAdmissionSecret(bearer, config)) {
|
|
337
341
|
headers.delete("authorization");
|
|
338
342
|
headers.delete("chatgpt-account-id");
|
|
@@ -343,6 +347,11 @@ export async function resolveResponsesCodexAuth(
|
|
|
343
347
|
if (options.compactionRecoveryKind === "compaction-v1") {
|
|
344
348
|
copyResponsesClientMetadata(headers, authInputHeaders, route.provider);
|
|
345
349
|
}
|
|
350
|
+
// dual_header: the assertion is first-party only and travels only beside the caller's own
|
|
351
|
+
// unchanged Bearer, compared with the ORIGINAL request (injected credentials do not count).
|
|
352
|
+
if (isCanonicalOpenAiForwardProvider(route.provider) && authCtx?.kind === "main" && !substituteMainCredential) {
|
|
353
|
+
carryAgentAssertionForCallerBearer(headers, req.headers);
|
|
354
|
+
}
|
|
346
355
|
return {
|
|
347
356
|
ok: true,
|
|
348
357
|
authCtx,
|
|
@@ -364,6 +373,7 @@ export async function resolveResponsesCodexAuth(
|
|
|
364
373
|
if (err instanceof ForwardAdmissionCredentialError) {
|
|
365
374
|
return { ok: false, response: formatErrorResponse(401, "authentication_error", err.message) };
|
|
366
375
|
}
|
|
376
|
+
if (err instanceof AgentAssertionUnsupportedError) return { ok: false, response: agentAssertionUnsupportedResponse() };
|
|
367
377
|
const response = mapCodexAuthContextErrorToResponse(err, {
|
|
368
378
|
accountSelector: route.codexAccountNamespace,
|
|
369
379
|
now: Date.now(),
|