fix: classify loopback gateway.remote.url targets as local

When gateway.mode=remote is configured with a loopback remote.url
(e.g. ws://127.0.0.1:18789), resolveGatewayTarget now returns undefined
(local) instead of 'remote'. A loopback address is indistinguishable
from a local gateway on a custom port; without a non-loopback URL
proving SSH tunnel usage, classifying it as remote suppresses
deliveryContext in createGatewayTool, causing dropped/misrouted
post-restart wake messages via stale extractDeliveryInfo routing.

The config-based path now uses isNonLoopbackRemoteUrlConfigured
(consistent with the override path) so both paths apply the same
loopback-detection logic.

Fixes: chatgpt-codex-connector CR on PR #34580 (comment 2930418168)
This commit is contained in:
Bryan Marty 2026-03-13 13:45:11 +00:00
parent b588f04a02
commit 8b91aded48
No known key found for this signature in database
2 changed files with 15 additions and 6 deletions

View File

@ -75,6 +75,15 @@ describe("resolveGatewayTarget env URL override classification", () => {
expect(resolveGatewayTarget()).toBeUndefined();
});
it("returns undefined (local) when gateway.mode=remote with a loopback remote.url (no tunnel evidence)", () => {
// A configured loopback remote.url (e.g. ws://127.0.0.1:18789) is indistinguishable
// from a local gateway on a custom port. Without a non-loopback URL proving SSH tunnel
// usage, classify as local so deliveryContext is preserved and post-restart wake
// messages are not misrouted via stale extractDeliveryInfo routing.
setConfig({ gateway: { mode: "remote", remote: { url: "ws://127.0.0.1:18789" } } });
expect(resolveGatewayTarget()).toBeUndefined();
});
it("returns undefined when gateway.mode=remote but gateway.remote.url is empty string", () => {
setConfig({ gateway: { mode: "remote", remote: { url: " " } } });
expect(resolveGatewayTarget()).toBeUndefined();

View File

@ -226,12 +226,12 @@ export function resolveGatewayTarget(opts?: GatewayCallOptions): GatewayOverride
}
}
}
// No env override. When mode=remote with a configured remote URL → truly remote.
// When mode=remote but remote.url is absent, callGateway falls back to local loopback —
// classify that as local (undefined) so deliveryContext is not suppressed.
const remoteUrl =
cfg.gateway?.mode === "remote" ? trimToUndefined(cfg.gateway?.remote?.url) : undefined;
return cfg.gateway?.mode === "remote" && remoteUrl !== undefined ? "remote" : undefined;
// No env override. Classify as "remote" only when mode=remote is configured with a
// non-loopback remote URL. Loopback remote.url (e.g. ws://127.0.0.1:18789) is
// indistinguishable from a local gateway on a custom port; without a non-loopback
// URL proving SSH tunnel usage, treat it as local so deliveryContext is preserved
// and post-restart wake messages are not misrouted.
return isNonLoopbackRemoteUrlConfigured(cfg) ? "remote" : undefined;
}
return validateGatewayUrlOverrideForAgentTools({
cfg,