Skip to content

Let the AI features go out through a corporate proxy - #185

Merged
Sev7eNup merged 3 commits into
mainfrom
fix/llm-corporate-proxy
Aug 11, 2026
Merged

Let the AI features go out through a corporate proxy#185
Sev7eNup merged 3 commits into
mainfrom
fix/llm-corporate-proxy

Conversation

@Sev7eNup

Copy link
Copy Markdown
Owner

Why

In a network where outbound traffic is only allowed through a proxy, the AI features cannot reach a cloud endpoint at all: the LLM HttpClient hard-codes UseProxy = false, and no configuration key exists to change that. Every call runs into its timeout and surfaces as "LLM endpoint did not respond within {TimeoutSeconds}s", including the Settings "Test connection" button, which shares the same named client.

The fix was written on 2026-08-06 and pushed to claude/llm-endpoint-test-context-wsl22m, but no pull request was ever opened, so it stayed outside main while the symptom kept being reported. This lands it on current main.

What

A global Llm:Proxy block covering every outbound LLM call — both chats, script and workflow generation, the llmQuery activity, and the settings probe.

Key Default Meaning
Mode Off Off = direct, System = the proxy the service account's OS is configured with (incl. its own bypass rules), Custom = the address below
Address "" Proxy URL; required for Custom
BypassList [] Shell globs reached directly; Custom only
Username / Password null Basic auth; password encrypted at rest, plaintext raises a startup hardening warning
UseDefaultCredentials false NTLM/Kerberos with the service account

One block per installation rather than one per profile: the mixed case — cloud model through the proxy, local Ollama direct — is what the bypass list is for, and a single block keeps one handler with one connection pool.

The proxy deliberately does not live in the SocketsHttpHandler. That handler is built once per handler lifetime, so binding the config there would have made the whole Llm settings section restart-required, the way RestApi is. Instead LlmConfiguredProxy implements IWebProxy and resolves IOptionsMonitor per request, which keeps the section hot-reloadable. In Off mode it bypasses every destination, making "no proxy configured" byte-for-byte the direct connection this client made before.

Security trade-off, documented at the type and in the reference: with a proxy in the path the proxy resolves destination DNS, so the connect-time link-local guard only covers the proxy endpoint. The destination stays protected by the literal BaseUrl check that runs on every save and at boot. No mandatory allow-list as restApi has — the LLM BaseUrl is a single Admin-only value, not a per-step URL assembled from trigger payloads.

Proxy rules are validated in LlmProfileValidation, which both AddNodePilotAi and LlmConfigBootValidator run, so a save that is accepted cannot block the next boot. The bypass-glob translation moves to NodePilot.Core.Net.ProxyBypassPattern so the Engine and Ai stacks share it instead of drifting apart.

Landing notes

Only one merge conflict: the IntegrationsSection imports, where main had since added refreshAiCapabilities next to the SectionFormHelpers import the proxy form needs. Both are kept.

One E2E test needed a fix that is not part of the original commit. 38.3 located the LLM Test button with a page-wide .last(). The LLM card renders its heading while the section snapshot is still loading, so that could resolve to SMTP's Test button before the profile form mounts — the test then drove the SMTP probe and timed out. It was already a race on main; the extra proxy form widened it enough to fail a full-file run. The lookup is now scoped to the LLM card.

Verification

  • dotnet build clean; full backend suite green — 5404 tests across six projects.
  • Frontend: tsc --noEmit clean, 2540 vitest tests green, eslint at 0 errors / 13 warnings (the lint:ci cap).
  • Playwright: full suite green, 379 passed / 22 skipped. admin-settings verified against main first to confirm the failure was introduced here and not pre-existing.
  • Regression that matters: Mode: Off still connects directly, covered by LlmConfiguredProxyTests.

The LLM HttpClient hard-coded UseProxy=false, so in networks where outbound
traffic is only allowed through a proxy the AI features could not reach a cloud
endpoint at all — and no configuration key existed to change that.

Adds a global Llm:Proxy block with Mode = Off (default, unchanged behaviour) |
System (the proxy the service account's OS is configured with) | Custom (own
address plus bypass globs), with Basic or Windows-integrated credentials. One
block per installation rather than one per profile: the mixed case — cloud model
through the proxy, local Ollama direct — is what the bypass list is for, and a
single block keeps one handler with one connection pool.

The proxy deliberately does not live in the SocketsHttpHandler. That handler is
built once per handler lifetime, so binding the config there would have made the
whole Llm settings section restart-required, the way RestApi is. Instead
LlmConfiguredProxy implements IWebProxy and resolves IOptionsMonitor per request,
which keeps the section hot-reloadable including the Enabled kill-switch. In Off
mode it bypasses every destination, making "no proxy configured" byte-for-byte
the direct connection this client made before.

Note the security trade-off, documented at the type and in the reference: with a
proxy in the path the proxy resolves destination DNS, so the connect-time
link-local guard only covers the proxy endpoint. The destination stays protected
by the literal BaseUrl check that runs on every save and at boot. No mandatory
allow-list as restApi has — the LLM BaseUrl is a single Admin-only value, not a
per-step URL assembled from trigger payloads.

Proxy rules are validated in LlmProfileValidation, which both AddNodePilotAi and
LlmConfigBootValidator run, so a save that is accepted cannot block the next
boot. The bypass-glob translation moves to NodePilot.Core.Net.ProxyBypassPattern
so the Engine and Ai stacks share it instead of drifting apart.
The LLM HttpClient hard-codes UseProxy=false, so in a network where outbound
traffic is only allowed through a proxy the AI features cannot reach a cloud
endpoint at all and no configuration key exists to change that. The fix was
written on 2026-08-06, pushed, and then never opened as a pull request, so it
sat outside main while the symptom kept being reported.

This merges that work into current main. Everything auto-merged except the
IntegrationsSection imports, where main had since added refreshAiCapabilities
next to the SectionFormHelpers import the proxy form needs; both are kept.
…the proxy

The LLM card renders its heading while the section snapshot is still loading, so
`getByRole('button', {name: /test/}).last()` could resolve to SMTP's Test button
in the window before the profile form mounts — the test then drove the SMTP probe
and timed out waiting for the LLM one. It was already a race on main; the proxy
form widened it enough to fail a full-file run. Scoping the lookup to the LLM
card removes the ordering assumption instead of adding a wait.

Also adds the missing pointer to Llm:Proxy:Mode in the docs-site appsettings
overview, which lists the other opt-in connection settings.
@Sev7eNup
Sev7eNup merged commit 1c776e2 into main Aug 11, 2026
8 checks passed
@Sev7eNup
Sev7eNup deleted the fix/llm-corporate-proxy branch August 11, 2026 19:08
Sev7eNup added a commit that referenced this pull request Aug 11, 2026
Carries the corporate-proxy support for the AI features (#185): Llm:Proxy:Mode
= Off | System | Custom, so an installation whose outbound traffic only leaves
through a proxy can reach a cloud LLM endpoint at all.

Bumped through `npm version --no-git-tag-version` per package rather than a
text replace over the tree, so only the top-level version and its lockfile root
entry move.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant