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
4 changes: 2 additions & 2 deletions deploy/render/blueprint-verification.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"blueprints": {
"postgres": {
"blueprint": "render.yaml",
"branch": "main",
"button_url": "https://render.com/deploy?repo=https://github.com/maximhq/bifrost/tree/main",
"branch": "dev",
"button_url": "https://render.com/deploy?repo=https://github.com/maximhq/bifrost/tree/dev",
"last_verified": null,
"verified_release": null
},
Expand Down
15 changes: 13 additions & 2 deletions docs/deployment-guides/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,25 @@ Start with [Bifrost Deployment Requirements](/deployment-guides/runtime-contract
| Azure Kubernetes Service | [AKS](/deployment-guides/platforms/aks) | Deploy the Helm chart to an existing AKS cluster |
| Amazon ECS | [ECS](/deployment-guides/ecs) | Run the Bifrost image as an ECS service |
| Google Cloud Run | [Cloud Run](/deployment-guides/platforms/cloud-run) | Run one Bifrost service with PostgreSQL-backed storage |
| Render | [Render](/deployment-guides/platforms/render) | Run one web service with a free PostgreSQL evaluation database or a durable SQLite disk |
| Railway | [Railway](/deployment-guides/platforms/railway) | Run one Bifrost service with PostgreSQL or a persistent `/app/data` volume |
| Render | [Render](/deployment-guides/platforms/render) | One-click free PostgreSQL evaluation or lowest-cost durable SQLite deployment |
| Railway | [Railway](/deployment-guides/platforms/railway) | One-click PostgreSQL or persistent-volume SQLite deployment |
| Fly.io | [Fly.io](/deployment-guides/fly) | Run one Machine with a Fly Volume or PostgreSQL |
| Terraform | [Terraform module](/deployment-guides/k8s) | Create a supported cloud or Kubernetes deployment from code |
| Docker or a VM | [Docker setup](/quickstart/gateway/setting-up#docker) | Run the image directly with a mounted data directory |

The Kubernetes guides begin with an existing cluster. If you need to create a cluster, each page links to the cloud provider's setup documentation before continuing with the Bifrost installation.

## Hosted one-click choices

The Render and Railway templates all create one authenticated OSS replica, generate stable encryption and administrator secrets, expose `/health`, and reject anonymous inference.

| Platform | PostgreSQL | SQLite |
|---|---|---|
| Render | Free 30-day evaluation with a sleeping web service and PostgreSQL 18 | Starter web service with a 1 GB `/app/data` disk |
| Railway | Bifrost plus private TLS-required PostgreSQL 18 | Bifrost with a persistent `/app/data` volume and secure privilege dropping |

Choose PostgreSQL when application replacement should be independent of Bifrost storage. Choose SQLite for a lower-resource single-instance deployment when brief disk-backed redeploy downtime is acceptable. These templates are not an Enterprise clustering topology.

## Choose where Bifrost stores data

Bifrost stores provider configuration, encrypted credentials, application settings, and request logs. Choose one of these storage models before deploying:
Expand Down
8 changes: 2 additions & 6 deletions docs/deployment-guides/platforms/railway.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,9 @@ The contracts use the container runtime introduced in Bifrost `v1.6.12` and were

### PostgreSQL

<Warning>
The one-click button for this template is not published yet. The checked-in contract records what the template must contain, and nothing in this repository can read back what the live template currently serves; the button is enabled only once the live template has been inspected against that contract and the verification recorded.
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/new/template/blue-dark?utm_medium=integration&utm_source=button&utm_campaign=bifrost)

Until then, reproduce the template manually from `deploy/railway/postgres.template-contract.json`.
</Warning>

Once verified, the Maxim-owned `blue-dark` template provisions Bifrost and PostgreSQL 18. Both stores require TLS, configuration and logs persist in PostgreSQL, and the Bifrost service has no unnecessary volume or root-user override.
The Maxim-owned `blue-dark` template provisions Bifrost and PostgreSQL 18. Both stores require TLS, configuration and logs persist in PostgreSQL, and the Bifrost service has no unnecessary volume or root-user override.

The auditable dashboard contract is checked in at `deploy/railway/postgres.template-contract.json`. Railway templates are published from its dashboard because `railway.json` describes only one service and cannot create this complete multi-service template.

Expand Down
18 changes: 5 additions & 13 deletions docs/deployment-guides/platforms/render.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,19 @@ The Blueprints use the container runtime contract introduced in Bifrost `v1.6.12

### PostgreSQL evaluation

<Warning>
The one-click button for this Blueprint is not published yet. It is enabled only after the Blueprint has been deployed and verified against a qualified `maximhq/bifrost` release.
[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/maximhq/bifrost/tree/dev)

Until then, deploy it manually: in the Render dashboard choose **New → Blueprint**, point it at this repository's `main` branch, and approve the generated `render.yaml`.
</Warning>

Once published, the button is explicitly bound to the `main` branch and its root `render.yaml`. It provisions one Free web service and one private Free PostgreSQL 18 database. Both Bifrost stores use TLS-required PostgreSQL connections, so configuration and logs survive web-service replacement without a Bifrost disk.
This button is explicitly bound to the `dev` branch and its root `render.yaml`. It provisions one Free web service and one private Free PostgreSQL 18 database. Both Bifrost stores use TLS-required PostgreSQL connections, so configuration and logs survive web-service replacement without a Bifrost disk.

Treat this as a 30-day evaluation. Upgrade the database before its expiry date to retain access to the data. An expired Free database has a limited upgrade grace period before Render deletes it. Upgrade the web service as well if you need it to remain awake or need production resources.

### SQLite durable

<Warning>
The one-click button for this Blueprint is not published yet. It is enabled only after the generated `render-sqlite` branch exists and its Blueprint has been deployed and verified against a qualified `maximhq/bifrost` release.

Until then, deploy it manually: copy `deploy/render/render-sqlite.yaml` into your own repository as the root `render.yaml`, then create a Render Blueprint from it.
</Warning>
[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/maximhq/bifrost/tree/render-sqlite)

Once published, the button is explicitly bound to the generated `render-sqlite` branch. It provisions one Starter web service and mounts a 1 GB persistent disk at `/app/data`. Bifrost's SQLite configuration and logs survive restarts and redeploys.
This button is explicitly bound to the generated `render-sqlite` branch. It provisions one Starter web service and mounts a 1 GB persistent disk at `/app/data`. Bifrost's SQLite configuration and logs survive restarts and redeploys.

The canonical Blueprint lives on `main` at `deploy/render/render-sqlite.yaml`; automation publishes an orphan branch containing only the generated root `render.yaml`. Do not edit the generated branch directly.
The canonical Blueprint lives on `dev` at `deploy/render/render-sqlite.yaml`; automation publishes an orphan branch containing only the generated root `render.yaml`. Do not edit the generated branch directly.

## Sign in and create a credential

Expand Down
20 changes: 20 additions & 0 deletions transports/bifrost-http/handlers/session_resolution_review_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package handlers

import (
"testing"

"github.com/maximhq/bifrost/core/schemas"
)

func TestReviewWebSocketAuthContextDropsHarnessSession(t *testing.T) {
ctx, cancel := createBifrostContextFromAuth(nil, &authHeaders{
headers: map[string][]string{
"session-id": {"codex-session"},
"thread-id": {"codex-thread"},
},
})
defer cancel()
if got := ctx.Value(schemas.BifrostContextKeySessionID); got != nil {
t.Fatalf("websocket context unexpectedly retained session ID: %#v", got)
}
}
106 changes: 106 additions & 0 deletions transports/bifrost-http/lib/session_resolution_review_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package lib

import (
"bufio"
"strings"
"testing"

"github.com/maximhq/bifrost/core/schemas"
"github.com/valyala/fasthttp"
)

var reviewSessionIDSink string

func reviewHeaderSet() *fasthttp.RequestHeader {
h := &fasthttp.RequestHeader{}
for i := 0; i < 12; i++ {
h.Set("x-review-header-"+strings.Repeat("a", i+1), "value")
}
return h
}

func BenchmarkReviewResolveSessionIDNoSession(b *testing.B) {
h := reviewHeaderSet()
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
reviewSessionIDSink = ResolveSessionIDFromRequest(h)
}
}

func BenchmarkReviewDirectPeekNoSession(b *testing.B) {
h := reviewHeaderSet()
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
reviewSessionIDSink = string(h.Peek("x-bf-session-id"))
}
}

func reviewResolveSessionIDWithPeek(h *fasthttp.RequestHeader) string {
if value := strings.TrimSpace(string(h.Peek("x-bf-session-id"))); value != "" && len(value) <= schemas.MaxSessionIDLength {
return value
}
for _, name := range schemas.HarnessSessionHeaders {
if value := strings.TrimSpace(string(h.Peek(name))); value != "" && len(value) <= schemas.MaxSessionIDLength {
return value
}
}
return ""
}

func BenchmarkReviewPriorityPeekNoSession(b *testing.B) {
h := reviewHeaderSet()
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
reviewSessionIDSink = reviewResolveSessionIDWithPeek(h)
}
}

func BenchmarkReviewPriorityPeekUnderscoreSession(b *testing.B) {
h := reviewHeaderSet()
h.Set("session_id", "review-session")
if got := reviewResolveSessionIDWithPeek(h); got != "review-session" {
b.Fatalf("underscore header via Peek = %q", got)
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
reviewSessionIDSink = reviewResolveSessionIDWithPeek(h)
}
}

func TestReviewPriorityPeekParsesUnderscoreHeader(t *testing.T) {
var h fasthttp.RequestHeader
raw := "POST /v1/chat/completions HTTP/1.1\r\nHost: localhost\r\nsession_id: raw-session\r\nContent-Length: 0\r\n\r\n"
if err := h.Read(bufio.NewReader(strings.NewReader(raw))); err != nil {
t.Fatal(err)
}
if got := reviewResolveSessionIDWithPeek(&h); got != "raw-session" {
t.Fatalf("parsed underscore header via Peek = %q", got)
}
}

// Recaptured from the PR #6333 review worktree. The original probe asserted
// that an oversized x-bf-session-id was accepted verbatim; the merged form of
// #6333 (2a64316c4) deliberately reversed that, capping every ingestion path at
// schemas.MaxSessionIDLength because session IDs become KV keys and exported
// trace attributes that nothing downstream bounds. This keeps the review's
// question as a live guard for the behavior that actually shipped: the
// oversized value is dropped, and it does not silently fall back to a harness
// header either.
func TestReviewOversizedExplicitSessionIsRejected(t *testing.T) {
oversized := strings.Repeat("x", schemas.MaxSessionIDLength+1)
ctx := &fasthttp.RequestCtx{}
ctx.Request.Header.Set("x-bf-session-id", oversized)
ctx.Request.Header.Set("session-id", "valid-harness-fallback")
if got := ResolveSessionIDFromRequest(&ctx.Request.Header); got != "" {
t.Fatalf("request resolver returned %q, want an oversized explicit session ID to be dropped", got)
}
bifrostCtx, cancel := ConvertToBifrostContext(ctx, testHandlerStore{})
defer cancel()
if got, _ := bifrostCtx.Value(schemas.BifrostContextKeySessionID).(string); got != "" {
t.Fatalf("context resolver returned %q, want an oversized explicit session ID to be dropped", got)
}
}
Loading
Loading