Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ All notable changes to FoundryGate should be documented here.

The format is intentionally lightweight and human-readable. Group entries by release and focus on user-visible behavior, operational changes, and compatibility notes.

## Unreleased

### Added

- Added conservative response-security headers plus a dashboard CSP for the no-build operator UI
- Added explicit `security` config controls for JSON body size, upload size, and bounded routing-header values
- Added functional API coverage for dashboard headers, JSON request limits, upload limits, and sanitized routing-header behavior

## v0.8.0 - 2026-03-15

### Added
Expand Down
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ FoundryGate is a local OpenAI-compatible router/proxy for OpenClaw and other cli
- Robust fallback behavior: provider errors, timeouts, and connection failures fall through the configured fallback chain.
- Useful observability: `/health` reports provider status, capability coverage, consecutive failures, last error, and average latency.
- Hardened extension seam: request hooks are sanitized, can fail closed, and expose hook errors in dry-run and completion responses.
- Pre-1.0 hardening baseline: response headers are conservative, request headers are sanitized, and JSON/uploads are bounded by explicit security limits.
- Safe database path handling: metrics use `FOUNDRYGATE_DB_PATH`, so the SQLite database does not need to live in the repo checkout.

## Who Is This For?
Expand Down Expand Up @@ -199,6 +200,7 @@ OpenAI-compatible chat completions endpoint.

- `model: "auto"` routes through FoundryGate
- `model: "<provider-id>"` routes directly to that loaded provider
- request body size is bounded by `security.max_json_body_bytes`

For non-streaming responses, FoundryGate also adds these response headers:

Expand Down Expand Up @@ -246,6 +248,7 @@ OpenAI-compatible image editing endpoint.
- `model: "auto"` selects the best loaded provider with `capabilities.image_editing: true`
- `model: "<provider-id>"` routes directly to a loaded image-edit-capable provider
- validates scalar fields such as `prompt`, `n`, and `size` before any provider call
- rejects uploads larger than `security.max_upload_bytes`
- optional image-policy hints can be passed via form field `image_policy`, `metadata.image_policy`, or `X-FoundryGate-Image-Policy`

```bash
Expand Down Expand Up @@ -491,6 +494,37 @@ The ranking logic intentionally prefers providers that both fit the request and
| `GEMINI_BASE_URL` | Overrides the Gemini base URL | Optional |
| `OPENROUTER_BASE_URL` | Overrides the OpenRouter base URL | Optional |

### Security Settings

The runtime also exposes a small `security` block in `config.yaml` for conservative pre-`v1.0` hardening defaults.

Supported fields:

- `response_headers`
- `cache_control`
- `max_json_body_bytes`
- `max_upload_bytes`
- `max_header_value_chars`

Example:

```yaml
security:
response_headers: true
cache_control: "no-store"
max_json_body_bytes: 1048576
max_upload_bytes: 10485760
max_header_value_chars: 160
```

What the current runtime does with it:

- adds conservative response headers such as `X-Content-Type-Options`, `X-Frame-Options`, and `Referrer-Policy`
- sends a dashboard CSP that keeps the no-build UI self-contained
- rejects oversized JSON requests before route resolution
- rejects oversized image uploads before any provider call
- bounds operator- and routing-related header values before they reach metrics, traces, or policy surfaces

### Additional Provider Key Variables Referenced In The Stock Config

The stock `config.yaml` ships many commented provider stanzas. If you uncomment one, its `api_key` field expects one of these variables:
Expand Down
2 changes: 2 additions & 0 deletions RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ This repo does not require a heavy release process. Use lightweight tags plus Gi
7. Use the changelog entry as the release notes, then add any short upgrade notes if needed.
8. Confirm that README plus the relevant docs pages still match the shipped runtime behavior.
9. If packaging or Docker changed shortly before the release, run the publish dry run first.
10. For hardening-heavy releases, keep the API functional tests green alongside unit and config coverage.

## Example

Expand Down Expand Up @@ -57,6 +58,7 @@ The repo also includes [publish-dry-run](./.github/workflows/publish-dry-run.yml
- `v0.6.0` establishes the modality-expansion baseline: image route previews, provider capability coverage, shared image request validation, and image policy presets.
- `v0.7.0` establishes the operations-polish baseline: update alerts, operator events, rollout guardrails, scoped update checks, maintenance windows, and post-update verification hints.
- `v0.8.0` establishes the onboarding baseline: repeatable provider/client rollout helpers, starter templates, delegated-traffic examples, env validation, and shareable onboarding reports.
- `v0.9.0` is the pre-`v1.0` hardening baseline: conservative response headers, bounded request surfaces, stronger functional API coverage, and a full documentation pass over operator-facing behavior.

## Planned Publishing Path

Expand Down
3 changes: 3 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,6 @@ To reduce risk in deployments:
- avoid committing `.env`, database files, SQLite files, logs, or SSH material
- run with the provided `systemd` hardening or an equivalent container/runtime policy
- keep provider API keys scoped to the minimum set of enabled providers
- keep the default response-security headers enabled unless you have an explicit reverse-proxy reason not to
- tune `security.max_json_body_bytes` and `security.max_upload_bytes` to the smallest values that still fit your workloads
- treat `x-foundrygate-*` and `x-openclaw-*` headers as trusted only at the edge you control
7 changes: 7 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ server:
port: 8090
log_level: "info"

security:
response_headers: true
cache_control: "no-store"
max_json_body_bytes: 1048576
max_upload_bytes: 10485760
max_header_value_chars: 160

# ── Provider Definitions ───────────────────────────────────────────────────
#
# Fields:
Expand Down
5 changes: 5 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ The core handles:
- route selection
- fallback order
- timeout and failure handling
- request-size and upload-size guardrails
- response metadata
- metrics and traces

Expand Down Expand Up @@ -86,6 +87,8 @@ This is enough to support:

Request hooks sit beside these caller-aware signals as a narrow extension seam. They can add sanitized request-level hints or profile overrides without giving arbitrary code the ability to mutate the full routing surface.

The pre-`v1.0` hardening baseline also treats caller-controlled headers as bounded inputs. Relevant routing and operator headers are normalized before they influence traces, client tags, or rollout decisions.

## Operational surface

The main operational endpoints are:
Expand All @@ -109,6 +112,8 @@ The main operational endpoints are:

`/api/stats`, `/api/recent`, and `/api/traces` can now be filtered by provider, client profile, client tag, layer, and success state. `/api/operator-events` captures operator-side update checks and helper-driven apply attempts. The dashboard is a thin UI over those same filtered endpoints and persists its active filters in the URL so operators can share one filtered view.

The operational surface now also applies conservative response headers by default. The no-build dashboard ships with a restrictive CSP and frame denial, while JSON and multipart request paths use bounded payload limits so obvious oversize failures are rejected before provider calls.

## Design target

The longer-term design target is to outperform simpler router designs by making routing multi-dimensional instead of mostly keyword- or model-name-driven.
Expand Down
10 changes: 9 additions & 1 deletion docs/FOUNDRYGATE-ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ The foundation that used to be the near-term buildout is largely in place:

This roadmap now shifts from "rename and foundation" to "deepen the gateway plane without bloating it".

`v0.8.x` is the current release line: many-provider and many-client onboarding is being tightened with validation helpers, starter templates, delegated-traffic examples, and shareable onboarding output on top of the already-shipped routing, modality, and ops foundation.
`v0.9.x` is the current release line: the focus now shifts to pre-`v1.0` hardening across request boundaries, functional API coverage, and a full documentation pass on the already-shipped routing, modality, onboarding, and ops foundation.

## Big Picture

Expand Down Expand Up @@ -253,6 +253,14 @@ Primary goals:

This release line should leave `v1.0.0` focused on stability and security gates, not backlog cleanup.

Current `v0.9.x` baseline is aimed at:

- conservative response headers and dashboard CSP defaults
- explicit JSON and multipart size guardrails
- bounded routing and operator header handling
- broader functional API tests around dashboard, routing, and upload surfaces
- documentation updates that make the hardened defaults visible to operators

### `v1.0.0`: stable gateway baseline

Primary goals:
Expand Down
4 changes: 4 additions & 0 deletions docs/INTEGRATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ For a smaller starter snippet without the full alias block, use [examples/opencl

For delegated or many-agent traffic, start from [examples/openclaw-delegated-request.json](./examples/openclaw-delegated-request.json) and keep `x-openclaw-source` stable across sub-agents so traces stay attributable.

Keep delegated/client headers short and stable. The runtime now bounds routing-header values before they reach traces, metrics, and rollout logic.

## n8n

n8n can use FoundryGate as a stable local model gateway.
Expand Down Expand Up @@ -93,6 +95,8 @@ export OPENAI_API_KEY=local

For a reusable shell starter, use [examples/cli-foundrygate-env.sh](./examples/cli-foundrygate-env.sh).

As with other clients, prefer token-like client tags over long free-form values so the bounded header surface remains readable in traces and operator views.

## AI-native app clients

For future app-specific clients, keep the same OpenAI-compatible base URL and add one stable app header before creating multiple custom profiles.
Expand Down
2 changes: 2 additions & 0 deletions docs/ONBOARDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ Examples:
- `X-FoundryGate-Client: n8n`
- `X-FoundryGate-Client: codex`

Keep these tags short and stable. The runtime now bounds routing-header values before they reach traces, client matrices, and rollout decisions.

### 3. Apply a preset or custom profile

Start with:
Expand Down
22 changes: 22 additions & 0 deletions docs/TROUBLESHOOTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,28 @@ request_hooks:
on_error: fail
```

## Request is rejected as too large

FoundryGate now enforces explicit request-size limits before provider calls.

Check:

- `security.max_json_body_bytes` for JSON endpoints such as `/api/route` and `/v1/chat/completions`
- `security.max_upload_bytes` for multipart uploads such as `/v1/images/edits`

Typical symptoms:

- HTTP `413`
- response type `payload_too_large`

Example:

```yaml
security:
max_json_body_bytes: 1048576
max_upload_bytes: 10485760
```

## Local worker stays unhealthy

For `contract: local-worker`, FoundryGate probes `GET /models`.
Expand Down
59 changes: 54 additions & 5 deletions foundrygate/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1055,6 +1055,40 @@ def _normalize_auto_update(data: dict[str, Any]) -> dict[str, Any]:
return normalized


def _normalize_security(data: dict[str, Any]) -> dict[str, Any]:
"""Validate and normalize runtime security settings."""
raw = data.get("security") or {}
if raw in (None, ""):
raw = {}
if not isinstance(raw, dict):
raise ConfigError("'security' must be a mapping")

normalized = dict(data)
normalized["security"] = {
"response_headers": bool(raw.get("response_headers", True)),
"cache_control": str(raw.get("cache_control", "no-store")).strip() or "no-store",
"max_json_body_bytes": _normalize_positive_int(
raw.get("max_json_body_bytes", 1_048_576),
field_name="security.max_json_body_bytes",
provider_name="runtime",
)
or 1_048_576,
"max_upload_bytes": _normalize_positive_int(
raw.get("max_upload_bytes", 10_485_760),
field_name="security.max_upload_bytes",
provider_name="runtime",
)
or 10_485_760,
"max_header_value_chars": _normalize_positive_int(
raw.get("max_header_value_chars", 160),
field_name="security.max_header_value_chars",
provider_name="runtime",
)
or 160,
}
return normalized


class Config:
"""Holds the parsed and expanded configuration."""

Expand Down Expand Up @@ -1165,6 +1199,19 @@ def auto_update(self) -> dict:
},
)

@property
def security(self) -> dict:
return self._data.get(
"security",
{
"response_headers": True,
"cache_control": "no-store",
"max_json_body_bytes": 1_048_576,
"max_upload_bytes": 10_485_760,
"max_header_value_chars": 160,
},
)

def provider(self, name: str) -> dict | None:
return self.providers.get(name)

Expand All @@ -1190,11 +1237,13 @@ def load_config(path: str | Path | None = None) -> Config:
with path.open() as f:
raw = yaml.safe_load(f)

expanded = _normalize_auto_update(
_normalize_update_check(
_normalize_request_hooks(
_normalize_client_profiles(
_normalize_routing_policies(_normalize_providers(_walk_expand(raw)))
expanded = _normalize_security(
_normalize_auto_update(
_normalize_update_check(
_normalize_request_hooks(
_normalize_client_profiles(
_normalize_routing_policies(_normalize_providers(_walk_expand(raw)))
)
)
)
)
Expand Down
Loading
Loading