Skip to content

feat(core): several named proxy base URLs per module (F8) - #690

Open
seanspeaks wants to merge 7 commits into
claude/internal-mcp-07-netlify-providerfrom
claude/internal-mcp-08-proxy-hosts
Open

seanspeaks wants to merge 7 commits into
claude/internal-mcp-07-netlify-providerfrom
claude/internal-mcp-08-proxy-hosts

Conversation

@seanspeaks

@seanspeaks seanspeaks commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Stacked on #686. Today the v2 entity proxy (ADR-052) joins a request path onto one base URL per module. Google's APIs live on several hosts (www.googleapis.com, docs., sheets., analyticsdata., googleads.). One Google entity therefore can't reach Docs, Sheets, GA Data or Ads, even when its token carries the scopes.

This PR lets a module declare several named bases, and lets a proxy request pick one by name.

  • Module config. A module can declare proxy.baseUrls: { default, docs, sheets, … }.
    • Values are https strings, checked at load: no credentials, query or fragment.
    • Names match ^[a-z][a-z0-9-]*$.
    • proxy.baseUrl still works and means { default: baseUrl }. Declaring both is a load error.
  • Request. The proxy request takes an optional base, which is a name and never a URL or host. When omitted it means default.
    • A value shaped like a URL or host, such as https://evil.example, evil.example, ../x or docs:8080, gets 400 INVALID_PROXY_REQUEST.
    • A name the module doesn't declare gets 400 PROXY_UNKNOWN_BASE.
    • Either way nothing goes out, and the host still comes only from the module.
  • Allow rules. Module- and app-level allow rules take an optional base (default default) and match only that base. Redirects are re-checked against the same base.
  • Load errors. The app refuses to start if a rule names a base the module doesn't declare, or if a rule has no base on a module that has no default.
  • No fallback. A declared base never falls back to the requester's host. A legacy baseUrl function that returns nothing now gets 501 PROXY_NOT_CONFIGURED.
  • Single-base modules. The PROXY_NOT_ALLOWED message is byte-identical to before. The base is named only when a module declares more than one. details.base is always present, and the audit record carries the base.
  • Kept in step: the OpenAPI v2 request schema and errors, api-proxy.schema.json, api-module-definition.schema.json, app-definition.schema.json and types/modules/index.d.ts.
  • Docs. docs/architecture-decisions/052-amendment-1-named-proxy-bases.md is a dated amendment. ADR-052 itself lives on adr/052-053-management-api and is not copied here. The base usage is also documented in docs/reference/api-reference.md.

Test plan

  • New use-case suite. It runs the real use case over a real Requester, with only fetch faked, and asserts the outbound URL.
    • base: 'docs' reaches the docs host, and an omitted base reaches default.
    • Allow rules match per base, in both directions.
    • The SSRF set is refused with no fetch.
    • Legacy baseUrl behaves as before.
    • A redirect is checked against the same base.
  • Load errors, both in proxy-bases.test.js and through the real app-definition loader.
  • Schema tests for both definition schemas and the proxy request schema (packages/schemas, 235/235).
  • Mutation checks. 21 mutations killed across the two rounds, plus 12 run by an independent reviewer, with no survivors.
  • Core jest. The same 14 failing suites and 61 failing tests as the base branch, compared by name. These are the existing mongodb-memory-server and environment failures. Total tests rose from 4272 to 4340.
  • eslint: no errors.

Part of the Left Hook internal-MCP work (ADR 0016). frigg-internal will move to this PR's canary so its Google module can reach Sheets, Docs, GA and Ads.


Generated by Claude Code


Note

Medium Risk
Touches the security-sensitive entity proxy (host selection, allow-lists, redirects) and changes query serialization for [] keys, which may break callers that relied on comma-joined values.

Overview
Adds named proxy base URLs so one entity credential can reach multiple upstream hosts (e.g. Google Drive vs Docs) while keeping hosts module-controlled for SSRF safety.

Modules may declare proxy.baseUrls (or keep proxy.baseUrl as { default: … }). Proxy requests accept an optional base name (never a URL); unknown names return 400 PROXY_UNKNOWN_BASE. Module and app allow rules gain optional base and only match that host; redirects are re-checked on the same base. Invalid baseUrls or rules referencing undeclared bases fail at app load via assertProxyConfig (ProxyConfigError).

Query encoding change: array values under keys ending in [] are sent as repeated query pairs instead of comma-separated (documented as a beta proxy contract change). OpenAPI, JSON schemas, types, and API reference are updated accordingly.

Reviewed by Cursor Bugbot for commit 08cd07e. Bugbot is set up for automated code reviews on this repo. Configure here.

📦 Published PR as canary version: 2.0.0--canary.690.08cd07e.0

✨ Test out this PR locally via:

npm install @friggframework/admin-scripts@2.0.0--canary.690.08cd07e.0
npm install @friggframework/core@2.0.0--canary.690.08cd07e.0
npm install @friggframework/devtools@2.0.0--canary.690.08cd07e.0
npm install @friggframework/eslint-config@2.0.0--canary.690.08cd07e.0
npm install @friggframework/extension-db-credentials@2.0.0--canary.690.08cd07e.0
npm install @friggframework/prettier-config@2.0.0--canary.690.08cd07e.0
npm install @friggframework/provider-aws@2.0.0--canary.690.08cd07e.0
npm install @friggframework/provider-netlify@2.0.0--canary.690.08cd07e.0
npm install @friggframework/schemas@2.0.0--canary.690.08cd07e.0
npm install @friggframework/serverless-plugin@2.0.0--canary.690.08cd07e.0
npm install @friggframework/test@2.0.0--canary.690.08cd07e.0
npm install @friggframework/ui@2.0.0--canary.690.08cd07e.0
# or 
yarn add @friggframework/admin-scripts@2.0.0--canary.690.08cd07e.0
yarn add @friggframework/core@2.0.0--canary.690.08cd07e.0
yarn add @friggframework/devtools@2.0.0--canary.690.08cd07e.0
yarn add @friggframework/eslint-config@2.0.0--canary.690.08cd07e.0
yarn add @friggframework/extension-db-credentials@2.0.0--canary.690.08cd07e.0
yarn add @friggframework/prettier-config@2.0.0--canary.690.08cd07e.0
yarn add @friggframework/provider-aws@2.0.0--canary.690.08cd07e.0
yarn add @friggframework/provider-netlify@2.0.0--canary.690.08cd07e.0
yarn add @friggframework/schemas@2.0.0--canary.690.08cd07e.0
yarn add @friggframework/serverless-plugin@2.0.0--canary.690.08cd07e.0
yarn add @friggframework/test@2.0.0--canary.690.08cd07e.0
yarn add @friggframework/ui@2.0.0--canary.690.08cd07e.0

The entity proxy joined a caller's path onto a single module-declared base
URL, so one Google entity could not reach Docs, Sheets, GA Data or Ads, which
live on other hosts.

A module may now declare proxy.baseUrls (name -> https URL); proxy.baseUrl
still means { default: baseUrl } and declaring both is a load error. The
proxy request takes an optional `base`, a NAME that is shape-checked before
the module is consulted, so a URL or host is refused and the host still comes
only from the module. Unknown names are 400 PROXY_UNKNOWN_BASE. Allow rules
(module and app level) gain an optional `base` and match only their own base;
a rule naming an undeclared base stops the app at load, because a rule that
can never match reads as permission. Threaded through the use case, OpenAPI
v2, the JSON schema, types and docs (ADR-052 amendment, API reference).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
- baseUrls values are strings only (a function could return nothing and fall
  back to the requester's host with the module's token); a declared base
  never falls back to the requester
- the PROXY_NOT_ALLOWED message is unchanged for single-base modules and
  names the base only when the module declares several; details.base stays
- module and app JSON schemas accept baseUrls / allow[].base, with tests
- audit the requested base on PROXY_UNKNOWN_BASE denials; omit the base key
  when not supplied; map an unvalidated definition to 501
- ModuleProxyConfig is wired into the entity type entry's definition type
- replace the copied ADR-052 with a standalone amendment document

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Code review skipped — your organization has no extra usage available to pay for this review.

If your organization's extra usage balance is empty, an organization admin can add extra usage credits at claude.ai/admin-settings/usage. If its monthly spend limit was reached, an admin can raise it on the same page. If neither applies, contact Anthropic support.

Once extra usage is available, someone with write access to this repository can comment @claude review on this pull request to trigger a review.

@seanspeaks seanspeaks added release Create a release when this pr is merged prerelease This change is available in a prerelease. labels Oct 18, 2026 — with Claude

Copy link
Copy Markdown
Contributor Author

build (22.x) is red, but the failures are not from this PR. The same check fails identically on the base branch, #686 (job 116839383474).

I compared the two runs suite by suite:

  • Same failures. Both runs fail the same 33 suites and the same 119 tests.
  • Where they are. All the failures are in devtools: frigg-cli commands, infrastructure/integration and infrastructure/domains/health, and the management-ui repository and button tests.
  • Nothing new. No suite or test fails here that passes on feat: Netlify provider on the v2 stack (port of #545) (F7) #686.
  • What changed. The only difference is in packages/schemas: one new passing suite (proxy-named-bases.test.js), taking it from 9 suites / 212 tests to 10 / 235.

There is no fix for those devtools failures anywhere in this stack yet, so there is nothing to port into this PR. They predate the internal-MCP stack, and I haven't re-run the job because it would fail the same way.


Generated by Claude Code

The api-reference section on proxy named bases described the rules without
pointing at the amendment that records the decision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
The entity proxy comma-joined every array query value, so APIs that want a
repeated key (Airtable, Fathom: fields[]=a&fields[]=b) were unreachable.

A query key ending in [] whose value is an array of strings is now sent once
per value (fields%5B%5D=a&fields%5B%5D=b; an empty array sends nothing).
Every other array keeps the comma-joined form, in the proxy policy and in
the requester alike.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
@seanspeaks

Copy link
Copy Markdown
Contributor Author

ADR reconciliation needed after Stack A/B — drafted with Claude Code at Sean's request

Severity: must-reconcile (text placement and status; the design keeps ADR-052's SSRF property).

Merge order: #668 (052 with named bases folded in) → #671 → … → #685 → this PR.

…roxy changelog

- Move the ADR-052 named-bases amendment to docs/architecture-decisions/pending/
  and mark it Proposed. ADR-052 on #668 does not describe named bases yet, so
  the text stays until it is folded into ADR-052 §4/§5 there. A 052- file in
  the register would read as a second claim on 052 under the ADR number check
  (ADR-014 amendment) and fails its heading rule.
- State the three "more than one of a module" axes so a later ADR does not
  reuse `base`: several hosts per entity (named bases), several registry
  entries for one module (ADR-059), several configured instances (future).
- api-reference: an "Entity proxy changes" note for the beta proxy: the
  additive optional `base` field, and `[]`-suffixed query keys now sent once
  per array value instead of comma-joined.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Code review skipped — your organization has no extra usage available to pay for this review.

If your organization's extra usage balance is empty, an organization admin can add extra usage credits at claude.ai/admin-settings/usage. If its monthly spend limit was reached, an admin can raise it on the same page. If neither applies, contact Anthropic support.

Once extra usage is available, someone with write access to this repository can comment @claude review on this pull request to trigger a review.

…hedule scope, coreExtensions) into named proxy bases

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6

Copy link
Copy Markdown
Contributor Author

Reply to the ADR-reconciliation review (comment 6276667428), in e76e497f. #686's changes are merged forward in 33ca4351.

The amendment and ADR-052

Three meanings of "more than one of a module". Added to the amendment, and it should carry into ADR-052 when folded:

  • base means several hosts behind one credential and one entity.
  • ADR-059's open question is something else: one module as several named registry entries.
  • The future per-app module-instances ADR is a third thing: several configured instances of one module.
  • Each of the other two needs its own term; neither should reuse base.

frigg validate (ADR-051)

OpenAPI changelog. There's no OpenAPI changelog file, so I added "Entity proxy changes" under "Entity proxy (beta)" in docs/reference/api-reference.md:

  • POST /api/v2/entities/:entityId/proxy gains an optional, additive base (omitted = default) and a new 400 PROXY_UNKNOWN_BASE;
  • []-suffixed query keys are now sent once per value (30e64517).

Tests


Generated by Claude Code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

prerelease This change is available in a prerelease. release Create a release when this pr is merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants