Skip to content

feat(gateway): read one set of env vars, legacy names as aliases - #1980

Merged
nbouliol merged 3 commits into
feature/prd-1337-create-the-forestadmingateway-package-that-serves-bothfrom
feature/prd-1338-unify-the-standalone-gateway-configuration-on-one-set-of-env
Oct 8, 2026
Merged

nbouliol merged 3 commits into
feature/prd-1337-create-the-forestadmingateway-package-that-serves-bothfrom
feature/prd-1338-unify-the-standalone-gateway-configuration-on-one-set-of-env

Conversation

@nbouliol

@nbouliol nbouliol commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Summary

The standalone Gateway reads one set of variables. Legacy forest-mcp-server / forest-bff names stay readable as aliases; errors name the key the operator actually set.

New name Aliases Read when
PORT MCP_SERVER_PORT, HTTP_PORT always
FOREST_GATEWAY_URL (bare origin) FOREST_MCP_SERVER_URL mcp on
FOREST_AGENT_URL AGENT_URL (only with api) always
FOREST_GATEWAY_ALLOWED_OAUTH_CLIENTS FOREST_MCP_ALLOWED_OAUTH_CLIENTS always, both services
FOREST_GATEWAY_API_* (incl. PUBLIC_URL, must end with P/api) BFF_* (derived from CONFIG_KEYS) api on

Resolution rules (env-aliases.ts):

resolve(name, aliases)
  name set (non-blank, "false" included) → wins; each set alias warned "ignored", never validated
  else aliases set with different values  → boot fails naming both
  else alias set                          → used, warned "legacy name: use <name>"
  warnings name keys, never values
 runGateway(env)
-  parseGatewayEnv(env)            # legacy vars warned and ignored
-  buildMcpService
-    missing secret → degraded
-    parseMcpEnv(toMcpEnv(env))
-  buildApiService
-    parseConfig(toBffEnv(env))    # BFF_PUBLIC_URL = FOREST_GATEWAY_URL + P/api
+  parseGatewayEnv(env)            # aliases resolved, mcp/api settings + labels, warnings logged
+  buildMcpService(settings)
+    parseMcpEnv(env, labels)      # invalid value fails even when degraded
+    missing secret → degraded
+  buildApiService(settings)
+    parseConfig(env, labels)
+    buildBff({ labels, name: 'Gateway API', allowedOAuthClients })
+    warn when API sign-in (P/oauth/*?service=api) is off

Also:

  • forest-gateway openapi uses the same resolution: server is FOREST_GATEWAY_API_PUBLIC_URL, else relative P/api.
  • normalizeDomainList(domains, label = 'allowedOAuthClients') (forestadmin-client): the Gateway validates the list at boot, naming its env key.
  • agent-bff exports CONFIG_KEYS, parsePublicUrl; renderOpenApi takes labels.
  • Decided during grilling: AGENT_URL with mcp only → ignored with a warning; FOREST_GATEWAY_URL / FOREST_MCP_SERVER_URL with api only → ignored with a warning.
  • Gateway deps now pin the workspace versions (agent-bff 1.40.0, mcp-server 1.28.0, forestadmin-client 1.43.5): before, packages/gateway/node_modules held 1.39.0 / 1.27.1 from npm, so its tests didn't run against workspace code.

Evidence

  • Before: BFF_PUBLIC_URL, HTTP_PORT, MCP_SERVER_PORT, FOREST_MCP_SERVER_URL ignored with a warning; AGENT_URL/BFF_* passed through raw; errors named tokenTtl.accessTokenSeconds, allowedOAuthClients, BFF_*.
    After: gateway 174/174, agent-bff 2250/2250, mcp-server 1544/1544, forestadmin-client 357/357, lint clean.
    • env-aliases.test.ts: precedence, empty values, shadowed alias not validated, disagreeing aliases, no value in warnings.
    • gateway-env.test.ts: every alias; HTTP_PORT=8080 + MCP_SERVER_PORT=3931 → fails naming both; PORT=8080 + both abc → 8080, two warnings; root BFF_PUBLIC_URL → must end with /api; root BFF_PUBLIC_URL shadowed by a valid new name → boots; FOREST_AGENT_URL with a query → error without the value; legacy MCP allowlist → both services.
    • run-gateway.test.ts: no encryption key → boots, configured.oauth: false, sign-in-off warning; api without agent url → 503 degraded naming FOREST_AGENT_URL; forest-bff config → one warning per alias, key never logged; invalid TTL without FOREST_AUTH_SECRET → fails; invalid CORS entry dropped with warning; uploads off skip the storage module; no FOREST_GATEWAY_URL → healthy + warning; hosted API config (docker-compose.bff.yml keys translated) → healthy, no warning.
    • cli-dispatch.test.ts: openapi announces FOREST_GATEWAY_API_PUBLIC_URL under /forest.

Not covered: the runtime P/api/agent/openapi.json with a public URL (needs a signed token + Forest schema); covered by the openapi command. Staging boot happens with the switch of the hosted API service.

Merge Danger

Door: two-way

Private, unpublished package; shared-package changes are additive (optional label/labels, new exports).

Blast Radius: Gateway-only

forest-bff and forest-mcp-server keep today's names and error messages (default labels unchanged).

Stacked on #1978.

Fixes PRD-1338

🤖 Generated with Claude Code

Note

Split gateway env config into MCP and API settings with legacy names as aliases

  • Reworks Gateway environment parsing to resolve MCP and API settings separately from Gateway-prefixed FOREST_GATEWAY_* variables, keeping old BFF_*, MCP, and listener names as legacy aliases.
  • Adds EnvAliases in env-aliases.ts: the primary name wins, aliases fill gaps, disagreeing aliases throw, and legacy or ignored settings produce warnings without exposing secret values.
  • Service builders in run-gateway.ts now consume the resolved settings; the API service validates its public URL against the configured API base path and logs when API OAuth is off.
  • Configuration error messages now name the actual variable that supplied the value (primary or alias), propagated as labels through renderOpenApi and normalizeDomainList.
  • Behavioral Change: startup rejects malformed or conflicting env aliases instead of silently picking one; AGENT_URL is ignored with a warning when the API service is disabled; MCP settings are validated before degraded-mode secret checks, so previously-bootable invalid configs may now fail; agent URLs must be absolute HTTP(S) without query or fragment.

Changes since #1980 opened

  • Modified parseGatewayEnv to treat whitespace-only values for FOREST_GATEWAY_ALLOWED_OAUTH_CLIENTS and its aliases as set, causing validation to reject them while empty strings remain treated as unset [9c43d06]
  • Extended EnvAliases.resolve to accept ResolveOptions with a blankIsSet boolean option that controls whether whitespace-only values are treated as set versus unset [9c43d06]
  • Added test coverage for the blankIsSet option in EnvAliases.resolve and for whitespace-only OAuth client allowlist behavior in parseGatewayEnv [9c43d06]
  • Added complete Docker image build infrastructure for @forestadmin/gateway including multi-stage Dockerfile, dependency manifest generation, workspace closure validation, vulnerability scanning gate, and smoke testing [76466fa]
  • Added CI/CD workflows for automated Docker image publishing of @forestadmin/gateway triggered by new version tags [76466fa]
  • Added health check probing functionality with port resolution that supports legacy environment variable aliases [76466fa]
  • Removed private flag from @forestadmin/gateway package and pinned zod dependency to exact version in @forestadmin/mcp-server [76466fa]
📊 Macroscope summarized 9c43d06. 2 files reviewed, 1 issue evaluated, 1 issue filtered, 0 comments posted

🗂️ Filtered Issues

packages/gateway/src/gateway-env.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 183: CONFIG_KEYS includes HTTP_PORT, so this loop resolves it again as FOREST_GATEWAY_API_HTTP_PORT with HTTP_PORT as an alias and line 183 restores the legacy value into apiEnv. Consequently PORT=8080, HTTP_PORT=abc correctly selects PORT for the Gateway, but parseConfig(settings.env) later parses the reintroduced HTTP_PORT=abc and aborts startup. This also contradicts the nearby intended gateway.api.env.HTTP_PORT === undefined case. Exclude the listener key from this loop (or clear it after resolving API settings) so a shadowed legacy port is not validated by the API. [ Out of scope (triage) ]

@linear-code

linear-code Bot commented Oct 7, 2026

Copy link
Copy Markdown

PRD-1338

@qltysh

qltysh Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

5 new issues

Tool Category Rule Count
qlty Duplication Found 47 lines of similar code in 2 locations (mass = 226) 3
qlty Structure Function with high complexity (count = 12): generate 1
qlty Structure Function with many parameters (count = 4): resolveApi 1

@qltysh

qltysh Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Qlty


Coverage Impact

This PR will not change total coverage.

Modified Files with Diff Coverage (8)

RatingFile% DiffUncovered Line #s
Coverage rating: A Coverage rating: A
packages/gateway/src/cli-dispatch.ts100.0%
Coverage rating: A Coverage rating: A
packages/gateway/src/gateway-env.ts100.0%
Coverage rating: A Coverage rating: A
...min-client/src/oauth-client-allowlist/normalize-domain-list.ts50.0%33
Coverage rating: A Coverage rating: A
packages/gateway/src/run-gateway.ts100.0%
Coverage rating: A Coverage rating: A
packages/agent-bff/src/index.ts100.0%
Coverage rating: A Coverage rating: A
packages/agent-bff/src/openapi/render-openapi.ts100.0%
New file Coverage rating: A
packages/gateway/src/env-aliases.ts100.0%
New file Coverage rating: A
packages/gateway/src/healthcheck.ts100.0%
Total99.2%
🤖 Increase coverage with AI coding...
In the `feature/prd-1338-unify-the-standalone-gateway-configuration-on-one-set-of-env` branch, add test coverage for this new code:

- `packages/forestadmin-client/src/oauth-client-allowlist/normalize-domain-list.ts` -- Line 33

🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

@Tonours
Tonours self-requested a review October 7, 2026 12:04

@Tonours Tonours left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spec (PRD-1338): conforms.

Comment thread packages/gateway/package.json Outdated
@Tonours

Tonours commented Oct 7, 2026

Copy link
Copy Markdown
Member

Run marker for review 5442140350.

@Tonours Tonours left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All four security triggers verified independently at 18444f1764: the pins resolve to the workspace packages, one allowlist resolution feeds both services with enforcement at authorization and token exchange, and the warning names the setting without its value. The security-request thread is resolved.

FOREST_GATEWAY_* names win; forest-mcp-server and forest-bff names stay
readable with one warning each. Errors name the key the operator set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nbouliol
nbouliol force-pushed the feature/prd-1338-unify-the-standalone-gateway-configuration-on-one-set-of-env branch from 18444f1 to 8b64388 Compare October 7, 2026 13:06
A blank allowlist resolved to no allowlist, accepting any registered client
on both services; forest-mcp-server refused to start on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nbouliol

nbouliol commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

QA in a real environment: 26/26

Setup: Gateway built from this branch, local Forest server (dev), the mabank agent on :3310, a real Gateway API key. Each scenario boots its own forest-gateway on a free port; every run is checked for secret values in the logs (none found).

# Scenario Result
1 forest-bff config (AGENT_URL, HTTP_PORT, BFF_*, BFF_PUBLIC_URL=…/api) + services=api ✅ listens on HTTP_PORT, 6 alias warnings, API key call POST /api/agent/v1/companies/count → 200
2 Root BFF_PUBLIC_URL ✅ boot fails: BFF_PUBLIC_URL must end with /api
3 Root BFF_PUBLIC_URL shadowed by FOREST_GATEWAY_API_PUBLIC_URL=…/api ✅ boots, one is ignored warning
4 Malformed BFF_PUBLIC_URL ✅ error names BFF_PUBLIC_URL
5 MCP_SERVER_PORT + FOREST_MCP_SERVER_URL, services=mcp ✅ listens on it, discovery issuer = that URL
6 FOREST_GATEWAY_URL="" + FOREST_MCP_SERVER_URL ✅ alias wins
7 mcp without public URL ✅ /health 200, warning names FOREST_GATEWAY_URL
8 HTTP_PORT=8080 + MCP_SERVER_PORT=3931 ✅ boot fails naming both
9 PORT + both legacy ports abc ✅ listens on PORT, 2 warnings
10 Only AGENT_URL, both services ✅ API reaches the agent (200), MCP ok
11 FOREST_AGENT_URL + AGENT_URL on a dead port ✅ API reaches the agent (200), AGENT_URL is ignored warning
12 FOREST_AGENT_URL with a query string ✅ boot fails, error names the key, not the value
13 AGENT_URL with mcp only ✅ warning: ignored, use FOREST_AGENT_URL
14 api without agent URL ✅ /health 503, api degraded, warning names FOREST_AGENT_URL
15 No API encryption key ✅ configured.oauth: false, API sign-in (/ai/oauth/*?service=api) is off
16 FOREST_MCP_ALLOWED_OAUTH_CLIENTS with a real registered client (localhost redirect) ✅ claude.ai → 400 invalid_client on MCP and API; localhost → 302 to Forest on both
17 Allowlist https://claude.ai ✅ boot fails naming FOREST_GATEWAY_ALLOWED_OAUTH_CLIENTS
18 Whitespace-only allowlist ✅ boot fails (no domains to allow): fixed in the second commit, see below
19 mcp, invalid TTL, no FOREST_AUTH_SECRET ✅ boot fails naming FOREST_MCP_ACCESS_TOKEN_TTL_SECONDS
20 FOREST_MCP_FILE_UPLOADS=false + missing storage module ✅ boots, MCP ok
21 One invalid FOREST_GATEWAY_API_ALLOWED_ORIGINS entry ✅ boots, entry dropped with warning, valid origin preflight passes
22 FOREST_GATEWAY_API_PUBLIC_URL=https://corp.example/forest/api + base path /forest ✅ runtime /forest/api/agent/openapi.json announces that URL
23 forest-bff / forest-mcp-server with an invalid port, TTL, allowlist ✅ same messages as main

Fix shipped from the security review: a whitespace-only FOREST_GATEWAY_ALLOWED_OAUTH_CLIENTS (or its alias) resolved to no allowlist (fail-open). It now fails the boot like forest-mcp-server did. An empty value stays unset.

Not covered: MCP traffic through the agent (needs a full interactive OAuth sign-in), staging boot (done with the hosted API switch).

@nbouliol
nbouliol added this pull request to stack #1989 October 8, 2026 12:34
@nbouliol
nbouliol removed this pull request from stack #1989 October 8, 2026 12:35
echo "mcp without PORT: HEALTHCHECK healthy on 3931"
stop_container

CONTAINER=$(run_probed \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High docker/smoke-test.sh:163

Both API-only containers omit FOREST_AGENT_URL, so /health returns 503 and wait_health healthy times out; the smoke test then exits before Docker validation can pass. Set FOREST_AGENT_URL in both scenarios, as in the combined-service scenario.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/gateway/docker/smoke-test.sh around line 163:

Both API-only containers omit `FOREST_AGENT_URL`, so `/health` returns 503 and `wait_health healthy` times out; the smoke test then exits before Docker validation can pass. Set `FOREST_AGENT_URL` in both scenarios, as in the combined-service scenario.

echo "is_latest=$IS_LATEST" >> "$GITHUB_OUTPUT"

TAGS="-t $IMAGE:$FULL"
if [ "$IS_LATEST" = "true" ]; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High workflows/docker-publish-gateway.yml:423

An older release can overwrite :latest, the major/minor tags, and the server deployment with its version after a newer release has already published. IS_LATEST is decided before imagetools create, while different versions run concurrently, so the newer tag can appear and publish between the check and write; serialize the mutable-tag decision and write, and ensure notifications use the same protected decision.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @.github/workflows/docker-publish-gateway.yml around line 423:

An older release can overwrite `:latest`, the major/minor tags, and the server deployment with its version after a newer release has already published. `IS_LATEST` is decided before `imagetools create`, while different versions run concurrently, so the newer tag can appear and publish between the check and write; serialize the mutable-tag decision and write, and ensure notifications use the same protected decision.

@nbouliol
nbouliol merged commit 605c738 into feature/prd-1337-create-the-forestadmingateway-package-that-serves-both Oct 8, 2026
65 of 73 checks passed
@nbouliol
nbouliol deleted the feature/prd-1338-unify-the-standalone-gateway-configuration-on-one-set-of-env branch October 8, 2026 13:38
Tonours pushed a commit that referenced this pull request Oct 12, 2026
* feat(gateway): serve the MCP and the API on one port

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ci: lint and test the gateway package

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(gateway): build against the workspace agent-bff and mcp-server

Exact pins fall behind the workspace on each release, since a private
package is skipped by multi-semantic-release, and Yarn then pulls the
published versions instead of linking the local ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(mcp-server): export the listener variables parseMcpEnv reads

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(gateway): follow the listener variable names agent-bff and mcp-server own

A rename on their side now fails the gateway's typecheck, or carries
over through the exported name, instead of silently moving the API's
public url and port.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(agent-bff): export the openapi --output handling for other CLIs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(gateway): reuse agent-bff's openapi --output handling

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(gateway): read one set of env vars, legacy names as aliases (#1980)

* feat(gateway): warn that forest-mcp-server and forest-bff are deprecated (#1990)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

2 participants